史上最全Java多线程面试题及答案
写在开头:
锁,锁住的是什么?
- “锁”就是做一个标记, 表示该对象资源已被占用。要说的话,就是锁住了对象。
- synchronized只是一个加锁的动作;
1.实例方法的话,锁实例对象
2.类方法锁类对应的Class对象
3.代码块也是锁指定的对象
1.多线程有什么用?
1)发挥多核CPU的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
2)防止阻塞
从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
2.创建线程的方式
1、继承Thread类
看jdk源码可以发现,Thread类其实是实现了Runnable接口的一个实例,继承Thread类后需要重写run方法并通过start方法启动线程。
继承Thread类耦合性太强了,因为java只能单继承,所以不利于扩展。
2、实现Runnable接口
通过实现Runnable接口并重写run方法,并把Runnable实例传给Thread对象,Thread的start方法调用run方法再通过调用Runnable实例的run方法启动线程。
所以如果一个类继承了另外一个父类,此时要实现多线程就不能通过继承Thread的类实现。
3、实现Callable接口
通过实现Callable接口并重写call方法,并把Callable实例传给FutureTask对象,再把FutureTask对象传给Thread对象。它与Thread、Runnable最大的不同是Callable能返回一个异步处理的结果Future对象并能抛出异常,而其他两种不能。
3.start()方法和run()方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
4.Runnable接口和Callable接口的区别
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
5.CyclicBarrier和CountDownLatch的区别
两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier是控制多个线程执行到某一个点停止,等待所有线程都到达后一起执行
- CountDownLatch是控制多线程执行完后,主线程再继续执行
6.volatile关键字的作用
理解volatile关键字的作用的前提是要理解Java内存模型,这里就不讲Java内存模型了
1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据。
2)代码底层执行不像我们看到的高级语言----Java程序这么简单,它的执行是Java代码-->字节码-->根据字节码执行对应的C/C++代码-->C/C++代码被编译成汇编语言-->和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率。
volatile关键字的使用场景:java volatile关键字解惑
volatile为什么不能保证线程安全:
这样如果有一个变量i = 0用volatile修饰,两个线程对其进行i++操作,如果线程1从内存中读取i=0进了缓存,然后把数据读入寄存器,之后时间片用完了,然后线程2也从内存中读取i进缓存,因为线程1还未执行写操作,内存屏障是插入在写操作之后的指令,意味着还未触发这个指令,所以缓存行是不会失效的。然后线程2执行完毕,内存中i=1,然后线程1又开始执行,然后将数据写回缓存再写回内存,结果还是1。如何使用volatile关键字保证线程安全?
要想在多线程下安全的修改一个共享变量,保证可见性和有序性的同时,也要保证原子性,就是一个操作是不可分的,必须是连续的,要么成功,要么失败,例如i++这种操作就不是连续的,它包含以下几个操作:
1:读:首先读取i
2:改:将i进行++
3:写:然后写修改后的值写入
要保证线程安全,就需要满足 可见性、有序性、原子性
下面我们看volatile结合CAS如何保证线程安全的:
首先:
1:volatile能保证可见性
2:volatile能保证有序性
CAS是compare and swap的简写,从字面上理解就是比较和交换:比较当前值与我拿到的值是否相同,相同则将修改后的值写入,不相同则失败。(CAS是通过硬件命令保证了原子性)
参考:CAS操作确保原子性
因此volatile和CAS结合是可以保证线程安全的,java并发包下面的AtomicInteger等都是用此原理保证线程安全的
volatile和CAS是并发包的基石。
什么是线程安全?
又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
- 1)不可变
final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用 - 2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
8、Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。
9.一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个对象的锁,那么这个对象锁会被立即释放
10.线程间通信如何实现?
线程间通信有三种方式:
- 1.共享变量
(1)volatile关键字:两个线程同时监听一个变量,当这个变量被一个线程修改时,另一个线程可以感知并执行响应的处理。AtomicInteger 和volatile类似
(2)使用Object类的wait() 和 notify() 方法 - 2.消息传递
(1)PipedInputStream、PipedOutputStream,这里用流在两个线程间通信,但是Java中的Stream是单向的,所以要在两个线程中分别建了一个input和output -
- 利用BlockingQueue
java线程间通讯的几种方式
11.sleep方法和wait方法有什么区别
sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的锁,sleep方法不会放弃这个对象的锁,wait方法会放弃这个对象的锁
12.生产者消费者模型的作用是什么
这个问题很理论,但是很重要:
1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约
13.ThreadLocal有什么用
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了
Java ThreadLocal
14.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
15.wait()方法和notify()/notifyAll()方法在放弃对象锁时有什么区别
wait()方法和notify()/notifyAll()方法在放弃对象锁的时候的区别在于:
- wait()方法立即释放对象监视器,
- notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
15.5 wait/notify/notifyAll、await/signal/signalAll有什么区别?
- wait/notify/notifyAll是Object的方法
- await/signal/signalAll是Condition的方法
await(), signal(),signalAll() 的功用和 wait(), notify(), notifyAll() 基本相同, 区别是, 基于 Condition 的 await(), signal(), signalAll() 使得我们可以在同一个锁的代码块内, 优雅地实现基于多个条件的线程间挂起与唤醒操作。
16.为什么要使用线程池
避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
核心参数:
1.corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。
2.maximumPoolSize:最大线程池大小。
3.keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。
4.unit:销毁时间单位。
5.workQueue:存储等待执行线程的工作队列。
6.threadFactory:创建线程的工厂,一般用默认即可。
7.handler:当工作队列、线程池全已满时执行拒绝策略;(1)丢弃任务并抛出RejectedExecutionException异常(2)丢弃任务但是不抛出异常(3)丢弃队列最前面的任务(4)由调用线程处理该任务执行过程
- 初始化线程池,此时是没有线程被创建的
- 如果任务来了,线程池会创建新线程执行任务,直到corePoolSize。
- 如果继续来任务,就会暂时把任务存储到工作队列workQueue中等待执行。
- 如果工作队列workQueue也满了,且当线程数小于最大线程池数时,就会创建新线程来处理;而线程数大于等于最大线程池数时就会执行拒绝策略。
17. 怎么检测一个线程是否持有对象监视器
某个线程是否持有对象锁:
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的锁被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着"某条线程"指的是当前线程。
18.synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的是对象头中mark word+jvm的monitorEnter,monitorExit。
18.5 可重入锁是什么意思?
可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
- 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
- 当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。
syncronized 和ReentrantLock都是可重入锁。
18.6 synchronized可重入的原理是什么?
- 每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;
- 当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;
- 而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
19.ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的数量,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。
ConcurrentHashMap解析
20.ReadWriteLock是什么
ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,
读锁是共享的,写锁是独占的,
读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
21.FutureTask是什么
FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
22.Linux环境下如何查找哪个线程使用CPU最长
法1:top -a
法2:
打印出所有线程的统计信息
这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
(1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过
(2)top -H -p pid,顺序不能改变
这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号。
使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。
最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
23.Java编程写一个会导致死锁的程序
第一次看到这个题目,觉得这是一个非常好的问题。很多人都知道死锁是怎么一回事儿:线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了,问一下怎么写一个死锁的程序就不知道了,这种情况说白了就是不懂什么是死锁,懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的。
真正理解什么是死锁,这个问题其实不难,几个步骤:
1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁
3)线程2的run()方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1持有,线程2肯定是要等待线程1释放lock1的对象锁的
这样,线程1"睡觉"睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。代码就不写了,占的篇幅有点多,Java多线程7:死锁这篇文章里面有,就是上面步骤的代码实现。
死锁案例
//其实实现很简单,两个线程互相等待对方持有的锁
public class DeadLockTest {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) throws Exception{
new Thread(){
@Override
public void run() {
synchronized (lock1){
System.out.println("thread1 get lock1");
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println("thread2 get lock2");
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (lock2){
System.out.println("thread2 get lock2");
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println("thread2 get lock1");
}
}
}
}.start();
}
}
24.怎么唤醒一个阻塞的线程
- suspend与resume
Java废弃 suspend() 去挂起线程的原因,是因为 suspend() 在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行 resume() 方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。
但是,如果 resume() 操作出现在 suspend() 之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。而且,对于被挂起的线程,它的线程状态居然还是 Runnable。 - wait与notify
wait与notify必须配合synchronized使用,因为调用之前必须持有锁,wait会立即释放锁,notify则是同步块执行完了才释放 - await与singal
Condition类提供,而Condition对象由new ReentLock().newCondition()获得,与wait和notify相同,因为使用Lock锁后无法使用wait方法 - park与unpark
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程任意位置让线程阻塞。和Thread.suspenf()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出IException异常。可以唤醒指定线程。
24.5 线程的几个状态:
- 1.new, 即线程刚刚创建,而并未执行
- 2.可运行(runnable),意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。
- 3.Dead,从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用 stop()令其死掉,但会产生一个违例——属于Error的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2 则是坚决反对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。
- 4.阻塞(Blocked),线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何CPU时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。为什么会阻塞呢,原因有:
(1)调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
(2)用suspend()暂停了线程的执行。除非线程收到 resume()消息,否则不会返回“可运行”状态。
(3)用wait()暂停了线程的执行。除非线程收到 nofify()或者notifyAll()消息,否则不会变成“可运行”
(4)线程正在等候一些 IO(输入输出)操作完成。
(5)线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。
java线程四种状态
25.不可变对象对多线程有什么帮助
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
26.什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
27.如果你提交任务时,线程池队列已满,这时会发生什么
这里区分一下:
1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
28.Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
29.Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
30.什么是自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
31.什么是Java内存模型
Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:
1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
2)定义了几个原子操作,用于操作主内存和工作内存中的变量
3)定义了volatile变量的使用规则
4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
32.什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。更多CAS详情请点击
33.什么是乐观锁和悲观锁
1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
34.什么是AQS
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
35.单例模式的线程安全性
老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
1)饿汉式单例模式的写法:线程安全
2)懒汉式单例模式的写法:非线程安全
3)双检锁单例模式的写法:线程安全
36.Semaphore有什么作用
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
37.Hashtable的size()方法中明明只有一条语句"return count",为什么还要做同步?
这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?
关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性
2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,线程就切换了。
38.线程类的构造方法、静态块是被哪个线程调用的
这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
39.同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁-->解锁的次数,有效地提升了代码执行的效率。
40 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。