Synchronized的特性
synchronized 是java中的关键字,主要用于加锁,来实现同步,具体实现形式有以下三种:
1.对普通方法加锁,锁是当前实例对象
2.对静态方法加锁,锁是当前类的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
3.对对象实例加锁,锁是Synchronized括号里对象实例。
synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 可以用来实现对临界资源的同步互斥访问 ,是可重入的。其可重入最大的作用是避免死锁,如:子类同步方法调用了父类的同步方法,如果没有可重入的特性,就会发生死锁。
Synchronized的同步实现
Synchronized的锁的实现是基于JVM进入和退出monitor对象来实现的。
monitor对象有2个关键指令monitorenter和monitorexit,具体就是在编译过程中把monitorenter指令插入到同步代码块开始位置,把monitorexit指令插入退出或异常的位置。
线程执行同步代码块的时首先需要获取锁,即尝试获取monitor对象的所有权,具体过程如下:
首先线程执行指令monitorenter指令尝试获取monitor的所有权,如果monitor的进入数为0,则线程进入进入monitor,并且把进入修改为1,当前线程就持有了锁;如果线程进入monitor时,发现进入数大于0,则会判断monitor的持有者是否为当前线程,如果是当前线程则表示只是重新进入,则对monitor的进入数+1;如果monitor的所有权不是被当前线程持有,线程进入阻塞状态,直到monitor的进入数等于0,再次重新获取monitor的所有权。
对象头
java中每个对象都包含三部分:对象头、实例数据、对齐填充(非必须)。synchronized使用的锁是存在对象的对象头中,对象头主要包含了2部分数据,Mark Word(存储对象的hashCode、分代年龄和锁标记位)、Class Metadata Address(存储对象类型数据的指针)。
锁的升级和对比
synchronized在JDK 1.6以后引入锁的状态级别,从低到高为:无锁、偏向锁、轻量级锁、重量级锁;
锁可以根据竞争进行粗话升级,但是不能降级。
偏向锁
当一个线程访问同步代码块获取锁时,会在对象头和栈帧中的锁记录中存储锁偏向的线程id,以后该线程再次进入和退出同步代码块时不需要信息CAS操作来加锁或解锁。
首先执行monitorenter命令尝试获取锁,查询Mark Word中存储的线程id是否为当前线程id,如果为当前线程id,则直接执行同步代码,如果不是当前线程id,则判断是否开启了偏向锁;
当偏向锁标识为1是,使用CAS竞争锁,竞争成功,尝试把Mark Word中的偏向锁指向自己;竞争失败,表示当前有多个线程在竞争锁,当到达全局安全点时,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
偏向锁的机制:竞争时才会释放锁,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点是上没有正在执行的代码)
轻量锁
轻量锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为DisplacedMark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。