一)多线程执行代码是如何实现的
假设有三个线程A、B、C,CPU通过给这三个线程分配时间片,时间片就是每个线程的执行时间,时间片是由CPU通过算法循环分配的。当A执行完一个时间片后切换到C去执行,C也执行完一个时间片后再切换到B或A去执行,并不能保证会切换到哪个线程去执行,但会保证已生成的每个线程都得到执行,而在切换线程时会保存上一个线程执行任务的状态,以便切换回去时继续执行。
二)volatile的应用
volatile是一个轻量级的synchronized,它使得共享变量是“可见性的”,“可见性”是指当一个线程修改了共享变量的值时,另外一个线程可以读取到这个修改的值。使用恰当的volatile变量修饰符比使用synchronized有着更低的执行成本,因为它不会引起线程的上下文切换和调度。
对volatile变量的修改操作,CPU会做些什么?
为了保证处理速度,CPU并不会直接和内存进行通信,而是将内存中的数据读取到内部缓存中,即CPU高速缓存。当一个线程对被volatile修饰的变量进行修改操作后,JVM会向CPU发送一条指令,将当前CPU缓存行(CPU高速缓存中可分配的最小单位)中数据写回到内存中(这一操作是原子性的,当这一操作在进行时,其它线程无法干预)。
而在多CPU下,其它CPU还缓存着这个变量的旧值,那么继续操作这个旧值依然会出问题。因此多CPU的情况下是实现了缓存一致性协议(同时该协议会阻止两个及以上CPU修改内存区域数据的操作),其它CPU通过嗅探总线上传播的数据检查自身缓存行中的变量值是否过期(缓存行中的数据内存地址是否被修改),若发现过期则将缓存这个变量的缓存行标记为无效状态,而当这个CPU再去操作这个变量时,会去内存中获取该变量最新的值并缓存。
什么是原子性操作?
不依赖于其它的连续性不可间断地操作。
如以下伪代码:
1 /** 2 * 以下为一段伪代码 3 */ 4 5 public int number; 6 7 System.out.println(number++); 8 9 public volatile int number2; 10 11 System.out.println(number2++);
未被定义为volatile的变量number执行自增运算时,会经历这三个步骤:获取当前值、将当前值加1、再将加1后的值给到变量。单线程模式下自然是没有什么问题的,但多线程下问题就严重了,可能线程一获取到number的值后切换到了线程二执行,而线程二执行完了自增步骤后又切换回了线程一继续执行,number初始值为0,那经历了上述场景后值又是多少呢?答案是1,咋一看使用多线程number应该会自增到2才对,而其实当线程二执行完后再执行线程一时,由于线程一之前获取到number的值为0,则后续操作还是对这个0执行的。
但使用了volatile关键字后就不一样了,再切换回线程一执行时它会发现这个变量值已经过期需要获取最新的值,最终执行完毕后number的结果就变为了2。
三)synchronized的实现原理与运用
三种表现形式:
普通同步方法,锁是当前实例对象;
静态同步方法,锁是当前类的class对象;
同步代码块,锁是括号里配置的对象;
synchronized在JVM中的实现:
JVM是通过进入和退出Monitor(监听器)对象来实现同步代码块的,其依赖于monitorenter(进入监听器)和monitorexit(退出监听器)指令。在Java文件被编译后,monitorenter会被插入到同步代码块开始处,monitorexit会被插入到方法结束处和异常处,任何对象都有一个monitor与之关联,当且一个monitor被持有后它将被锁定,即当一个线程执行到monitorenter指令时,会尝试获取对象的monitor所有权(尝试持有它,即获得对象锁),若发现无法持有(被锁定),则等待其释放。
四)避免死锁的常见方法
①避免一个线程同时获取多个锁,因为尝试获取多个锁时可能与另外的线程互相想要获取对方占有的锁,这就会导致死锁。
②尝试使用定时锁,使用lock.tryLock来替代内部锁,因为这种锁有一个timeout,达到规定时间会自动释放掉占有的锁,有效避免了死锁的出现。