Java面试题基础篇(三)

线程

22、创建线程的方式及实现
继承Thread类:
定义Thread的子类,重写run()方法,通过start()进行启动线程。
实现Runnable接口:
创建Runnable接口的实现类的实例,并用这个实例作为Thread的target来创建Thread对象,通用通过start()启动线程。
实现Callable接口
Callable接口提供了一个call()方法作为线程执行体,call()方法可以有返回值,可以声明抛出异常。实现方式:创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例。使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。使用FutureTask对象作为Thread对象的target创建并启动线程。调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

23、sleep(),wait(),yield()和join()方法的区别
sleep():
sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
wait():
wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。 除了使用notify()和notifyAll()方法,还可以使用带毫秒参数的wait(long timeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。 此外,wait(),notify()及notifyAll()只能在synchronized语句中使用,但是如果使用的是ReenTrantLock实现同步,该如何达到这三个方法的效果呢?解决方法是使用ReenTrantLock.newCondition()获取一个Condition类对象,然后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。
yield():
yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
join():
join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行。

24、说说 CountDownLatch 原理

CountDownLatch概述:
CountDownLatch是一个用来控制并发的常见的工具(CountDownLatch countDownLatch = new CountDownLatch(2)),它允许一个线程等待其他线程执行到某一操作后不再阻塞。CountDownLatch的构造函数中使用的int型变量的意思是需要等待多少个操作的完成。这里是2所以需要等到调用了两次countDown()方法之后主线程的await()方法才会返回。这意味着如果我们错误的估计了需要等待的操作的个数或者在某个应该调用countDown()方法的地方忘记了调用那么将意味着await()方法将永远的阻塞下去。
CountDownLatch实现原理:
CountDownLatch实际上是使用计数器的方式去控制的,初始化CountDownLatch时传入一个int变量,每当调用countDownt()方法的时候就使得这个变量减1,而对于await()这个方法则取判断这个int变量的值是否为0,是则表示所有的操作均已经完成,否则继续等待。

25、说说 CyclicBarrier 原理
CyclicBarrier概述:
CyclicBarrier让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障,屏障才会打开,所有被拦截的线程继续执行,线程通过await()方法进入屏障,然后当前线程被阻塞。
CyclicBarrier实现原理:
CyclicBarrier在内部定义了一个Lock对象,每当一个线程调用CyclicBarrier的await()方法时,将拦截的线程数加1,然后判断拦截的线程数是否等于初始化的线程数,如果不是,进入Lock对象的条件队列等待,如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁,接着先从await方法返回,再从CyclicBarrier的await方法返回。

CyclicBarrier主要用于一组线程之间的相互等待,而CountDownLatch一般用于一组线程等待另一组些线程。


26、说说 Semaphore 原理
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,保证合理的使用公共资源。线程通过acquire方法获取信号量的许可,当信号量中没有可用的许可时,线程阻塞,直到有可用的线程许可为止。线程可以通过release方法释放它持有的信号量许可。

27、说说 Exchanger 原理
Exchanger概述:
Exchanger一般用于两个工作线程之间交换数据,它对外提供的方法是同步的,用于成对出现的线程之间交换数据。一个线程达到Exchanger的调用点时,如果它的伙伴线程在此前已经调用了此方法,那么它的伙伴线程会被唤醒并与之进行对象交换然后各自返回。如果它的伙伴还没有到达交换点,那么当前线程会被挂起,直到伙伴线程到达完成数据的交换或者当前线程被中断抛出中断异常又或者等候超时抛出超时异常。
Exchanger实现原理:
我们假定一个空的栈(Stack),栈顶(Top)当然是没有元素的。同时我们假定一个数据结构Node,包含一个要交换的元素E和一个要填充的“洞”Node。这时线程T1携带节点node1进入栈(cas_push),当然这是CAS操作,这样栈顶就不为空了。线程T2携带节点node2进入栈,发现栈里面已经有元素了node1,同时发现node1的hold(Node)为空,于是将自己(node2)填充到node1的hold中(cas_fill)。然后将元素node1从栈中弹出(cas_take)。这样线程T1就得到了node1.hold.item也就是node2的元素e2,线程T2就得到了node1.item也就是e1,从而达到了交换的目的。


28、说说 CountDownLatch 与 CyclicBarrier 区别
CyclicBarrier主要用于一组线程之间的相互等待,而CountDownLatch一般用于一组线程等待另一组些线程。


29、ThreadLocal 原理分析
ThreadLocal概述:
ThreadLocal类用来提供线程内部的局部变量,这些变量在多线程环境下访问时能保证各个线程里变量相对独立于其他线程。ThreadLocal为每一个线程创建一个单独的变量副本,提供保持对象的方法和避免参数传递的复杂性。
ThreadLocal实现原理:
ThreadLocal可以看作一个容器,容器中存放着属于当前线程的变量,ThreadLocal提供方法对变量进行操作。在ThraadLocal类中有一个静态的内部类ThreadLocalMap,用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key就是当前的ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。
内存泄漏问题:
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收. 
  所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。


30、讲讲线程池的实现原理
线程池的优点:
线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多的内存导服务器崩溃。
线程池的实现原理:
(1)判断线程池中的核心线程是否都在执行任务,如果不是(还有核心线程没有被创建或者核心线程空闲)则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,则进入第二个流程。
(2)线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列中,如果工作队列已满进入下个流程。
(3)判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,直到线程池达到最大线程数,则交给饱和策略来处理这个任务。
饱和策略 RejectedExecutionHandler:
当线程池中的线程达到最大线程数,说明线程处于饱和状态,那么必须对新提交的任务采用特殊的策略进行处理。线程池默认的策略是AbortPolicy,表示无法处理新的任务而抛出异常。java中提供四种策略,AbortPolicy:直接抛出异常;CallerRunsPolicy:只用调用所在的线程运行任务;DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务;DiscardPolicy:不处理,丢弃掉。


31、线程池的几种方式
newFixedThreadPool(int nThreads):
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
newSingleThreadExecutor()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程的最大数量为Interger. MAX_VALUE,如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
newSingleThreadExecutor()
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool()
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。


32、线程的声明周期
新建:
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
就绪:
当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行:
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞:
当处于运行状态的线程失去所占用资源之后,便进入阻塞状态。
死亡:
线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。


锁机制

33、说说线程安全问题
线程安全概念:
类或者对象在多线程并发的场景下,能够保存程序的逻辑是可以被接受的而且不是被扰乱的,能够保证业务逻辑处理不出问题。
判断线程是否安全的方法:
(1)程序是否运行在多线程环境下。
(2)多线程是否会共享一个资源并且对这个共享资源有读和写操作。
如何解决线程安全问题:
(1)将对象设置为无状态的。
(2)使用局部变量
(3)对象不得不使用属性时,考虑用ThreadLocal类包装,包装后的属性就是线程安全的,但是各线程修改的属性不被共享。
(4)使用线程同步技术(synchronized和lock)将读写的共享资源代码块锁上,让多线程调用这段代码的时候按顺序来访问。


34、volatile 实现原理
volatile概述:
如果一个变量被volatile修饰,则java可以确保所有的线程看到这个变量是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新。对volatile变量的单次读/写操作可以保证原子型,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
volatile使用:
(1)防止重排序,实例化一个对象可以分为三个步骤:分配内存空间,初始化对象,将内存空间的地址赋值给对应的引用。但由于操作系统可以对指令进行重排,所以上面的过程也有可能变成如下过程:分配内存空间,将内存空间的地址赋值给对应的引用,初始化对象。如果是上述这个流程的话就有可能将未初始化的对象暴漏出来,从而导致不可预测的结果。因此为了防止这个过程的重排序,需要将变量使用v
(2)实现可见性,可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题。
(3)volatile能保证对单次读/写的原子性,因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
volatile的原理:
(1)可见性实现,线程本身并不直接与主内存进行交互,而是通过线程的工作内存来完成相应的操作,这也是导致线程间数据不可见的本质原因。对volatile变量的写操作与普通变量的主要区别有两点:修改volatile变量时会强制将修改后的值刷新到主内存中;修改volatile变量后会导致其他线程工作内存中对应的变量失效,因此,再读取该变量的时候就需要重新读取主内存中的值。
(2)有序性实现,a happen-before b,表示a所做的任何操作对b是可见的,在java中对volatile变量的写操作 happen-before后续的读操作。(volatile规则)。java的重排序分为编译器重排序和处理器重排序,为保证volatile的有序性,JMM会对volatile变量限制这两种类型的重排序。
(3)为了实现volatile可见性和happen-befor的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

35、synchronize 实现原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
(1)普通同步方法,锁是当前实例对象
(2)普通同步方法,锁是当前实例对象
(3)同步方法块,锁是括号里面的对象


36、synchronized 与 lock 的区别
(1)存在层次,synchronized是java关键字,在jvm层面,Lock是一个类。
(2)锁的释放,synchronized获取锁的线程执行完同步代码释放锁,如果在执行过程中出现异常,jvm会让线程释放锁。Lock需要手动释放锁,不然容易造成线程死锁。
(3)锁的获取,synchronized的线程获取锁,其他线程会一直等待,直到获取锁的线程释放锁。Lock有多个获取锁的方式,可以尝试获取锁,不用一直等待。
(4)锁状态,synchronized无法判断锁状态,Lock可以判断锁状态。
(5)锁类型,synchronized可重入 不可中断 非公平,Lock可重入 可判断 可公平(两者皆可)。
(6)性能,synchronized少量同步,Lock大量同步。


37、CAS 乐观锁
乐观锁的核心算法是CAS(比较并交换),它涉及到三个操作数,内存值,预期值,新值,当且仅当预期值和内存值相等时才将内存值修改为新值。


38、ABA 问题
CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被修改过,但是这个判断逻辑并不严谨,假如内存值原本是A,后来被一条线程更改为B,最后又被另一条线程更改为A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值