并发编程:深入理解synchronized
synchronize的使用场景
线程安全问题:多线程对共享数据状态的访问没有控制
用锁(互斥)来控制对共享数据的访问
synchronized是虚拟机级别提供给我们的同步关键字
synchronized的使用
1、修饰实例方法(锁是当前对象)
2、修饰静态方法(锁是当前类的字节码对象)
3、修饰代码块(锁是括号()中指定的对象)
总结:锁都是锁对象,不管是.class对象还是具体的某个实例对象。当两个线程调用同一把锁的代码时,会串行化执行。
synchronized到底在什么地方加了锁?
存在对象头里。
对象分为三部分:对象头,实例数据,对齐填充(虚拟机要求8字节最小单位)。
那么对象头有哪些信息呢?MarkWord、KlassWord、数组长度;其中Markword记录了锁的信息,如锁标记、偏向锁标记、锁状态、线程ID(偏向锁)还有其他信息如gc标记、分代年龄、hashcode等等。KlassWord主要存储对象的指针相关信息。
synchronized 锁的升级
1.6之前synchronized都是重量级锁,在1.6版本做了大量优化。锁既要保证数据安全性,又要尽量的保证性能。
重量级锁:直接调用操作系统挂起线程(内核态->用户态),重量级锁由重量级锁指针指向的ObjectMonitor实现,监视器对象存储了该对象被获取锁的次数(有加有减)、重入次数、持有锁的线程、阻塞队列、等待队列等。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁:单线程访问,偏向锁记录线程ID为该线程,过来直接执行
轻量级锁:两个线程发生了竞争的时候,会升级成轻量级锁。(当旧的偏向线程被销毁、或者当前没有持有锁,则不会升级,变成无锁)。轻量级锁是自旋等待
重量级锁:自旋次数过多;或者来了第三个线程,则会升级成重量级锁,这时候没拿到锁的线程直接阻塞
锁只可以升级不可以降级,但是偏向级锁可以置为无锁状态。
偏向锁的升级
线程1去查看对象头里是否有线程1ID,假如无锁则CAS替换为线程1,有锁则CAS获取
CAS:乐观锁。CompareAndSwap(value, expect, update),比较expect和value是否一样,假如不一样,则替换失败;假如一样,则成功替换。(比较和替换必须是一个原子操作)
CAS的缺点:存在ABA问题,可以用版本号解决,java里用AtomicStampedReference解决;不断的CAS会消耗CPU比较大
替换成功后,线程1执行,此时来了线程2,线程2 CAS获取偏向锁失败,此时线程2暂停线程1,撤销偏向锁,升级为轻量级锁
轻量级锁的升级
轻量级锁中,两个线程会不断自旋,有锁的时候CAS将MarkWord替换成轻量级锁,替换成功的则持有锁。
线程1会在栈帧中分配Lock Reocrd空间存储MarkWord的拷贝,通过CAS操作 并将对象头MarkWord改成指向Lock reocrd的指针,然后将Lock Reocrd的owner指针指向object mark word。
自旋次数过多或者第三个线程来了会膨胀成重量级锁。
自旋次数:设置自旋次数、自适应自旋(自动调整)
重量级锁
每一个对象都存在一个ObjectMonitor对象。重量级锁通过ObjectMonitor(互斥锁)实现。
该锁直接挂起阻塞的锁。
当线程1获得重量级锁时,线程2会进去监视器的阻塞队列。当线程1释放锁的时候,会唤醒阻塞队列中的线程。