Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
volatile借助Java内存模型保证所有线程能够看到最新的值。(内存可见性)
实现原理:
将带有volatile变量操作的Java代码转换成汇编代码后,可以看到多了个lock前缀指令(X86平台CPU指令)。这个lock指令是关键,在多核处理器下实现两个重要操作:
1.将当前处理器缓存行的数据写回到系统内存。
2.这个写回内存的操作会使其他处理器里缓存该内存地址的数据失效
如果了解计算机组成原理,可以知道CPU为了提高处理速度,不和内存直接进行交互,而是使用Cache(高速缓存,通过缓存数据交互速度和内存不是一个数量级,而同时Cache的存储容量也很小)。
从内存将数据读到缓存后,CPU进行一系列数据操作,而操作完成时间是不可知的。而JVM对带有volatile变量进行写操作时,会发送Lock前缀指令,将数据从缓存行写入到内存。写入内存还不够,因为其他线程的缓存行中数据还是旧的,Lock指令可以让其他CPU通过监听在总线上的数据,检查自己的缓存数据是否过期,如果缓存行的地址和总线上的地址相同,则将缓存行失效,下次该线程对这个数据操作时,会重新从内存中读取,更新到缓存行。
2.Synchronized
Synchronized也是经常用到的,它给人的印象一般是”重量级锁”。在JDK1.6后,对Synchronized进行了一系列优化,引入了偏向锁和轻量级锁,对锁的存储结构和升级过程。有效减少获得锁和释放锁带来的性能消耗。
Synchronized同步基础:
1.普通同步方法,锁是当前实例对象。 public synchronized void test(){…}
2.静态同步方法,锁是当前类的Class对象。public static synchronized void test(…){}
3.对于同步方法块,锁是Synchronized括号中里配置的对象。synchronized(instance){…}
用javap反编译class文件,可以看到Synchronized用的是monitorenter和monitorexit实现加锁。一个monitorenter必须要有monitorexit与之对应,所以同步方法会在异常处和方法返回处加入monitorexit指令。
3: monitorenter //注意此处,进入同步方法 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit //注意此处,退出同步方法
3.Java对象头
Synchronized用到的锁存在Java对象头里,若对象非数组类型,用32bit存储(2个字宽,32虚拟机一个字宽为4字节,一个字节8bit)
MarkWord存储和锁相关的信息:
锁有四个等级: 无锁->偏向锁->轻量级锁->重量级锁。如果存在竞争,就会不断升级,但不会降级。
1.偏向锁
多数情况下,锁不会存在竞争,而是同一个线程多次获得。当某个线程访问同步块代码时,会将锁对象和栈帧中的锁记里存储锁偏向的线程ID,以后线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比对一下对象头中的MarkWord里的线程ID,如果一致则表示线程获得锁。若不一致,再继续测试偏向锁的标识是否为1:如果没有设置(无锁状态),用CAS(Compare and Swap)竞争锁;如果设置了,尝试使用CAS将对象头的偏向锁指向当前线程。
当有另一个线程尝试竞争锁时,持有偏向锁的线程才会释放锁。需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
Java 6,7默认开启偏向锁,可以通过JVM的参数-XX:-UsebiasedLocking=false关闭
2.轻量级锁
(1)加锁
锁记录存储在栈桢,会将对象头的MarkWord复制到锁记录。线程在执行同步块时,会尝试用CAS将对象头的MarkWord替换为指向锁记录的指针,若成功,获得锁;失败表示其他线程竞争锁,当前线程尝试使用自旋获取锁。
(2)解锁
类似于加锁反向操作,会将锁记录复制会对象头的MarkWord。若成功,表示操作过程中没有竞争发生;若失败,存在竞争,锁会膨胀成重量级锁。
如下图:
当膨胀到重量级锁时,不会再通过自选获得锁(自旋时线程处于活动状态,会消耗CPU),而是将线程阻塞,获得锁的线程执行完后会释放重量级锁,此时唤醒因为锁阻塞的线程,进行新一轮的竞争。
3.其他锁概念
自旋锁:
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
public class SpinLock { private AtomicReference sign =new AtomicReference<>(); public void lock(){
Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){
}
} public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}
使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。
当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
锁的优缺点对比:
优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程使用自旋会消耗CPU追求响应时间,锁占用时间很短重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,锁占用时间较长
想透彻理解多线程并发编程可以参考下面的思维导图:获取免费的架构学习资料可以加群:777158424 .里面有分布式,微服务,性能优化,spring mybatis tomcat源码分析等资料免费分享。还有下图的多线程并发学习资源哦~全部免费可以获取。