volatile
volatile是轻量级的synchronized,它可以在并发中保证共享变量的可见性
原理
volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在的缓存行写回到系统内存中。 为了保证缓存一致性,每个处理器会通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己 缓存行对应的内存地址被修改了,就会将当前处理器的缓存行失效,当处理器要对这个数据进行操作时,就会重新从系统内存 中把数据读取到缓存中。
Lock前缀指令会引起处理器缓存回写到内存。
一个处理器到缓存回写到内存回导致其他处理器缓存失效。(处理器使用嗅探技术保证它到内部缓存、系统内存和其他处理器 缓存到数据在总线上保持一致)
synchronized
介绍
synchronized用到锁是存储在对象头里面的,对象头由 MarkWord、类型指针、数组长度(对象为数组时)。
markword(表格中空白格子为向左合并)
锁状态 | 25bit | 4bit | 1bit | 2bit |
---|---|---|---|---|
无锁 | 对象hashCode | 对象分代年龄 | 0 | 01 |
轻量级锁 | 指向栈中指针 | 00 | ||
重量级锁 | 指向互斥量(重量级锁)指针 | 10 | ||
GC标记 | 空 | 11 | ||
偏向锁 | 线程ID(23bit) Epoch(2bit) | 对象分代年龄 | 1 | 01 |
Monitor类型对象,重量级锁状态下,MarkWork里指针指向的对象。
synchronized
用来修饰方法(静态方法、实例方法)、代码块 synchronized加锁是指竞争获取对象头MarkWord重量级锁下指向Monitor类型对象,jdk1.6后进行了锁优化
原理
jdk1.6前,进入synchronized修饰的方法或代码块前要先获取重量级锁(对象头里面指向Monitor类型的对象)
静态方法 获取类的Class对象对应的Monitor对象
实例方法 获取类的实例对象对应的Monitor对象
代码块 修饰的代码块自己指定
synchronized修饰的代码块,编译阶段回在方法前后生成monitorenter、monitorexit指令
每个对象都有一个Monitor对象,线程通过执行monitorenter指令获取Monitor对象的拥有权。
如果拥有当前Monitor对象的线程数为0,则执行_count++,当前线程称为Monitor对象的拥有者
如果当前线程已经拥有此Monitor对象,则将_count++
如果其他线程有此Monitor对象,则当前线程阻塞直到Monitor计数_count==0,然后重新竞争获取锁
获取重量锁
当线程执行到monitorenter指令,会进入ObjectMonitor对象的_EntryList队列,通过CAS会将_owner指针指向当前线程,同时_count++,
当前线程执行monitorexit指令,会释放持有的Monitor对象,并将_owner置为null同时_count–
如果调用wait(),同上,但是会进入_WaitSet队列,等待被唤醒。(看到没:wait状态的线程在唤醒之后,还得需要获取锁④,然后执行完毕)
锁优化:_owner指向当前线程调用的函数涉及到了特权指令Mutex Lock导致用户态线程和内核态线程之间进行切换,切换过程影响效率
获取偏向锁
原因:大部分情况下不会存在线程竞争,而且只会有同一个线程进入临界区,为了减少同一线程获取锁带来的消耗,所以当进入临界区前不会先去获取重量锁,而是先获取偏向锁。
膨胀成轻量级锁:偏向锁主要是为了解决同一个线程进入临界区,当有超过一个线程竞争偏向锁,就会膨胀为轻量级锁。
获取偏向锁过程: 先判断是否能开启偏向锁,如果可以 => 将偏向锁偏向线程ID用CAS(相对于轻量级锁获取和释放都需要CAS操作费时,偏向锁只有这一次)修改为当前线程ID。
获取轻量锁
原因:在多个线程都会尝试进入临界区的情况下,多个线程只会交替进入临界区,不会存在锁竞争,为了减少重量级锁系统调用造成的消耗。
膨胀成重量级锁:当多个线程同一时间都尝试获取锁,则会膨胀为重量级锁。
获取轻量级锁获取过程: 如果当前无锁并且不可偏向,会尝试获取轻量级锁,将MarkWord拷贝到当前线程的栈帧中的LockRecord,然后通过CAS更新MarkWord内容为指向当前线程LockRecord的指针。
和偏向锁的区别:偏向锁是同一个线程多次获取锁,轻量级锁是多个线程交替获取锁。相同点是假定都不存在锁竞争。
自旋锁
原因: 还是大部分情况下,线程持有锁的时间很短,当一个线程获取锁了以后,其他线程尝试获取锁就会进入阻塞状态,挂起->恢复都需要在用户态和内核态之间进行切换。此时如果让后来的线程进行自旋一段时间(for循环),在获取锁,可能就会获取,也就避免了转入内核态。 JDK1.6引入了自适应的自旋锁,即根据具体情况结合前面旋转的次数决定此次需要旋转的次数。
优点:如果线程占用锁的时间比较短则自旋操作很有效,避免进入内核态
缺点:如果线程占用锁的时间比较长则自旋操作白白耗费CPU资源,倒不如挂起。
参考:java并发编程艺术