深入理解synchronized
加锁原理
synchronized (a){} 锁住的就是()里面的对象,多个线程对同一个对象操作时,就会形成互斥效果,如果是操作两个不同的对象,那么就不会受synchronized影响。
public class SynchronizedDemo { public static void main(String[] args) { SynchronizedDemo s = new SynchronizedDemo(); Integer a = 1; Integer b = 2; new Thread(()->{ s.sync(a); }).start(); new Thread(()->{ s.sync(b); //s.sync(a); }).start(); } public void sync(Integer a){ synchronized (a){ System.out.println("线程:"+Thread.currentThread().getName()+" 获取到变量"+a); try { Thread.sleep(8000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
在jdk中,synchronized同步是基于Monitor对象实现的,它里面主要有两个指令:
monitorenter: 插入到同步代码块的开始位置
monitorexit: 插入到同步代码块结束的位置
它们对应着JMM模型8大原子操作的lock与unlock,lock获取锁后把对象加载到工作内存,数据操作完之后重新赋值到主内存,最后unlock解锁。JVM需要保证每一个monitorenter都有一个monitorexit与之对应。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行 异常的monitorexit指令。通过monitor里的加锁计数器可以实现可重入的加锁。 Monitor(监视器锁)是依赖操作系统的Mutex Lock(互斥锁)实现的,需要向内核申请资源,此时cpu将由用户态转换为内核态,它是一个低性能重量级锁。
synchronized优化
jdk1.6之后就对这个synchronized锁进行了各种优化,是基于 jvm 通过 cas 设置头信息来做的。如适应性自旋锁、轻量级锁和偏向锁,并默认开启偏向锁。从 无锁—>偏向锁—>轻量级锁—>重量级锁 ,锁升级的这个过程是不可逆的。被加锁的对象 jvm中为它定义了一种对应的数据结构,通过判断数据结构的对象头就知道目前是什么锁状态。例如通过倒数第三个bit的值 0/1 就知道目前是无锁还是偏向锁了。
三种锁的区别
偏向锁:仅有一个线程进入临界区(主要用于不存在锁竞争,而是一个线程多次获得锁时,为的使线程获取锁使用最小的代价(因为只需要修改获取锁的线程id就好了))
轻量级锁:多个线程交替进入临界区(当其他线程尝试竞争偏向锁时,会升级为轻量锁)
重量级锁:多个线程同时进入临界区
锁的升级过程
1. 无锁:此时还没有线程获取所得资源
2. 获取偏向锁:第一个线程获取到锁就会将前面的23个bit位修改为自己线程的id,将无锁升级为偏向锁。以后该线程进入或退出同步块只需要看对象头的MarkWord的值就好了
3. 升级轻量锁:偏向锁采用了一种等到竞争出现才释放锁的机制。此时另一个线程尝试获取锁,发现锁里的线程id并不是自己的,等之前线程执行完同步块后就会释放锁,将对象头重的Mark Word替换为指向锁记录的指针,然后其升级为轻量锁。
4. 若刚才将对象头重的Mark Word替换为指向锁记录的指针失败,则会自旋(循环等待)来获取锁,此时若有另一个线程同时竞争,锁会升级为重量级锁。
。