1 创建线程的方式及实现
ans:(1) 继承Thread类,覆盖run()方法,通过start()方法启动线程
(2) 实现Runnable/Callable接口,实现run()方法,通过start()方法启动线程
继承Thread类的问题在于,如果用户定义的任务类已经有父类存在,就不能再继承Thread,Java不允许多继承。使用Runnable/Callable接口就能避免这个问题,也能将任务与运行解耦。
每个任务都启动一个线程比较浪费,通常的做法是使用线程池,将任务交给线程池进行处理。
2 sleep() 、join()、yield()有什么区别
ans: sleep() 将使任务暂停给定的时间;
join() 表示当前线程挂起,直到目标线程结束,也可以指定等待超时时间;
sleep() 和 yield() 在线程等待期间并不会释放持有的锁。
yield() 将当前线程的CPU使用权让出,表示自己的任务已经完成,使线程重新回到可执行状态。只能使同优先级或更高优先级的线程有执行的机会。
参考:http://www.cnblogs.com/yhc20091116/p/4317338.html
3 AQS 同步队列
ans: AQS是AbstractQueuedSynchronizer, 一个用来构建锁和同步工具的框架。它内部保存一个int类型的state变量,其含义由子类根据自己的场景决定。AQS主要负责维护这个state变量和一个阻塞线程的等待队列,以及提供一些判断处理方法,有独占和共享两种模式。使用AQS可以按需要实现以下五个方法:
tryAcquire:独占模式下尝试获取资源。如果这个方法返回失败,则将线程放入阻塞队列,直到其他线程发出释放信号。
tryRelease:独占模式下释放资源
tryAcquireShared:共享模式下尝试获取资源。如果方法返回失败,则将线程放入阻塞队列。
tryReleaseShared:共享模式下释放资源。
isHeldExclusively:该线程是否正在独占资源。只有用到condition才需要去实现它。
4 CountDownLatch 原理
ans: CountDownLatch是一个计数器,可以用它设置一个初始值,调用await()方法的线程都将阻塞,直到计数器值为0,可以使用countDown()方法对计数每次减一。典型的应用场景是 将一个任务分级为多个子任务,并等待它们全部完成后主线程返回。
CountDownLatch 内部定义了一个类Sync继承AQS,初始化时将计数传递给AQS,作为state值。线程调用await()方法时,会尝试获取共享锁,由CountDownLatch 实现tryAcquireShared()方法,获取state值并判断是否为0(即所有任务已完成),如果不为0则将线程加入阻塞队列。
而 countDown()方法实际就是释放共享锁(state减1),当计数=0时开始唤醒阻塞的节点,因为节点都是共享类型的,所有阻塞的节点都会被通知到。
5 CyclicBarrier 原理
ans: CyclicBarrier是一个可以重复使用的等待屏障工具,它能够使一组互相等待直到到达共同的屏障点。常用于线程组,可以在等待线程释放后重用。它支持一个可选的Runnable回调,在最后一个线程完成后,屏障释放前执行。
如果参与的某个线程由于中断、超时、或失败而离开屏障,那么其他等待中的线程也会抛出BrokenBarrierException异常停止等待,继续运行。
CyclicBarrier 内部保存一个可重入锁、参与线程数量的计数 和一个表示批次的对象Generation,用它来区分每个批次。
每个批次开始的时候,线程调用await()方法,CyclicBarrier 先用可重入锁加锁,以保证只有一个线程此时能进行以下操作:
(1)检查当前批次是否已经被破坏,或线程已被中断。如果是,中断该批次,线程抛出异常。
(2)计数减1,如果此时计数=0,表示所有线程都已就位,那么唤醒所有线程,开启以下个批次。
(3)如果此时计数>0,则将当前线程阻塞,直到失败或超时异常抛出,或唤醒条件达成。
6 CountDownLatch 与 CyclicBarrier 区别
ans: CountDownLatch 不可重复使用,使用一个继承AQS的内部类来维护计数和等待队列。
CyclicBarrier 可重复使用,自己维护计数,使用ReentrantLock维护等待队列。
7 Semaphore 原理
ans: Semaphore 是计数信号量,常用于限制访问资源的线程数量。每当线程需要访问一个资源,就要向Semaphore 请求一个许可,当线程处理完成后,就向Semaphore返回这个许可。如果请求许可时未成功,线程就将阻塞等待。Semaphore 初始化时可以选择两种模式:公平模式和非公平模式,默认为非公平模式,所谓公平,指的是保证线程获得许可的顺序与访问顺序相同。
Semaphore 内部定义了一个类Sync继承AQS,初始化时将计数传递给AQS,作为state值。
线程调用acquire()方法请求信号量许可,Semaphore会检查当前资源是否能够满足请求,如果是就更新资源数量并返回, 如果不能则将当前线程加入阻塞队列。
线程处理完成,调用release()方法释放资源,如果有正在等待的线程,就从队列中取出唤醒。
8 Exchanger 原理
ans: Exchanger 是交换器,用于在两个线程之间交换各自的数据,适合用于生产者-消费者场景。
API比较简单,只有一个exchange()方法,线程将自己需要交换的数据传入,如果另一个线程已经调用了exchange()并且也传入了自己的数据,那么方法会立刻返回;如果是第一个到达的线程,就先阻塞等待,需要交换的对象到达后被唤醒。
其内部定义了一个类Participant,实际是一个ThreadLocal,用于保存一个数据对象和一个要交换的槽位。Exchanger有两种数据交换的方式,当并发量低的时候,内部采用“单槽位交换”;并发量高的时候会采用“多槽位交换”。
“单槽位交换” 场景下,只需要检查单槽位交换节点是否为空,如果为空就表示自己是第一个到达的,如果不为空说明两个线程都已到达,获取节点的数据,并将自己的数据赋给节点的槽位,使另一个线程能够获得数据。
“多槽位交换”场景下,根据当前线程的数据携带结点Node中的index字段计算出命中的槽位。如果槽位被占用,说明已经有线程先到了,之后的处理和单槽位交换一样;如果槽位为null,说明当前线程是先到的,就占用槽位,然后线程等待。
参考:https://segmentfault.com/a/1190000015963932
9 ThreadLocal 原理,ThreadLocal为什么会出现OOM,出现的深层次原理
ans: ThreadLocal 是线程本地变量,ThreadLocal表面上看是一个共享变量,实际它会在每个线程保存一个本线程专用的副本,这样一来就解决了多线程情况下需要变量保存不同数据的问题。
Thread类保存了一个成员变量ThreadLocalMap,它是ThreadLocal的内部类,这就是实际存储变量副本的位置,key 是 ThreadLocal的弱引用,value 是真正需要存储的数据。当我们需要对变量进行set()或get()操作时,ThreadLocal就会从当前线程中获取ThreadLocalMap,利用这个变量来操作。
由于将ThreadLocal的弱引用作为key,一旦ThreadLocal变量没有外部强引用,那么GC时必然会被回收,这样ThreadLocalMap中就会出现key=null的entry,无法访问它的value,如果线程迟迟不结束,那么这些value会一直被ThreadLocalMap引用,不能被回收,造成内存泄漏。大量线程存在这样的情况下,就可能出现OOM。
为了防止出现这个问题,Java官方建议一旦不再使用ThreadLocal变量,就立刻调用remove()方法,清除数据。
参考:https://blog.csdn.net/GoGleTech/article/details/78318712
10 讲讲线程池的实现原理
ans:Java的线程成ThreadPoolExecutor主要负责接收用户提交的任务,管理线程资源,以及线程调度。
ThreadPoolExecutor 一共定义了四种状态:
(1)当创建线程池后,初始时,线程池处于RUNNING状态;
(2)如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
(3)如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
(4)当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
对于用户提交的任务:
(1)线程池会首先检查当前线程数是否小于核心池大小,如果小于,那么创建新的线程来执行任务;
(2)否则,如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列(BlockingQueue),放入队列后需要再次检查线程池状态,如果已关闭就需要拒绝处理;但如果当前正在运行的线程数=0,就要新建线程执行任务。
(3)如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,执行reject()方法进行任务拒绝处理。
用户可以自定义核心池大小、线程池最大数量、线程等待时间、线程工厂、拒绝策略 这些参数。不过官方推荐使用Executors创建线程池,这个类定义了三类线程池:见11问。
11 线程池的几种方式
ans: newCachedThreadPool() 创建一个可缓存空闲线程的线程池。如果当前有可以直接使用的空闲线程,就直接将任务赋给它;如果没有就新建一个线程添加到线程池中。如果一个线程60秒未被使用,线程池将会回收它。这类线程池通过对线程的重复使用提高性能。
newFixedThreadPool() 创建一个线程数固定的线程池,每提交一个任务就创建一个线程,直到到达设置的限额。
SingleThreadExecutor() 创建一个仅有一个线程的线程池,一般用在需要顺序执行的场景。
newScheduledThreadPool() 创建一个固定线程数的线程池,以延迟或定时的方式来执行任务。
12 线程的生命周期
线程状态转换.png
新建new:创建后尚未启动
运行runnable:start() 启动之后,包括等待CPU时间片的Ready状态和正在运行的running状态。
无期限等待waiting:此时线程必须等待被唤醒,否则不会分配CPU时间片,如wait() 、join() 方法。
期限等待 timed waiting:此时线程也不会被分配CPU时间片,但经过一段时间它们会自动被系统唤醒。如sleep(), wait(timeout), join(timeout)
阻塞 blocked:线程被阻塞,可能是在等待一个锁或者资源
结束 terminated:线程已经终止,结束运行。
参考:《深入理解JVM》12.4.3
13 静态变量、实例变量、局部变量线程安全吗,为什么?
ans:静态变量属于类成员,存放于方法区,能被所有实例共享,也就是说能够被多个线程观察到,不是线程安全的。
实例变量属于对象成员,存放在堆中,各对象之间不是共享的,是线程安全的。但单例模式的实例变量除外。
局部变量作用域仅限于方法或语句块内,保存在栈上,线程间不会共享,是线程安全的。
14 corePoolSize maximumPoolSize 有什么区别
ans:core = 线程池基本大小,在不超时情况下,保持活跃的最少线程数
max = 可以创建的最大线程数
当我们向 ThreadPoolTaskExecutor 提交新任务时,如果正在运行的线程少于 corePoolSize 线程,即使池中有空闲线程,或者如果正在运行的线程少于 maxPoolSize 且由 queueCapacity 定义的队列已满,它也会创建一个新线程。
15 线程池有哪些参数
ans:corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 线程空闲时间
BlockingQueue<Runnable> 等待队列
ThreadFactory 线程工厂
RejectedExecutionHandler 任务拒绝处理器
网友评论