Java并发系列之一:底层原理
1. 前言
2. volatile
1. 在并发编程中,volatile和synchronized同等重要,它是轻量的synchronized,在多处理器开发中保证了共享变量的“可见性”,即线程A修改了共享变量,线程B能读到这个值
2. volatile使用得当的话,执行成本更低,因为它不会引起上下文的切换和调度
3. Java编程语言,允许线程访问共享变量,但是为了保证共享变量能够被准确一致性的更新,线程应该确保通过排他锁单独的获取这个变量,此外Java提供了volatile可以保证所有线程看到的变量的值是一致的
2.1 CPU术语
2.2 实现原理
一个变量被声明为volatile,生成的Lock前缀汇编指令在多核处理器下,会引发两件事情
1. 将当前处理器缓存行的数据写回到系统内存
2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
为了提高处理速度,处理器不直接和内存通信,而是首先将内存的数据读取到缓存(L1,L2..)后再操作,这样就有一个问题,操作完不知道何时会写回到内存
如果对声明了volatile的变量进行写操作,JVM首先向处理器发送一条Lock前缀指令,将这个变量所在缓存行的数据写回到系统内存,那么对导致另外一个问题
如果其他处理器缓存的值还是旧值,再次执行时同样会出现问题,因此在多处理器下,为了保证各个处理器缓存数据的一致性,就必须实现缓存一致性协议
缓存一致性机会会阻止同时修改2个以上处理器缓存的内存区域数据
每个处理器通过嗅探系统总线的数据来检查自己的缓存数据是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会置为无效
同时当处理器对这个数据再次进行操作的时候,会重新从系统内存中把数据读取到处理器缓存中,从而实现缓存一致性(通过MESI控制协议实现)
3.对象头
通常在java中一个对象主要包含三部分:
1. 对象头:主要包含GC的状态、类型、类的模板信息(地址)、synchronization状态等
2. 实例数据:程序代码中定义的各种类型的字段内容
3. 对齐数据:对象的大小必须是 8 字节的整数倍,此项根据情况而定,若对象头和实例数据大小正好是8的倍数,则不需要对齐数据,否则大小就是8的差数
3.1 Java代码实例
3.1.1 引入maven依赖
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
3.1.2 示例代码
public class ClassHead { public static void main(String[] args) { Student student = new Student(10, "jack"); System.out.println(student.hashCode()); System.out.println(ClassLayout.parseInstance(student).toPrintable()); } } class Student { private int age;//4个字节 private String name; //4个字节 public Student(int age, String name) { this.age = age; this.name = name; } }
3.1.3 执行分析
4. synchronized
在并发编程中不得不提synchronized,很多人称它为重量级锁,但是随着Java SE 1.6+版本对其的优化,有些情况下,sychronized变的不是那么重了
Java中任何对象都可以作为一个锁,具体的表现形式有以下三种:
1. 对于普通方法,锁是当前实例对象
2. 对于静态同步方法,锁是当前类的Class对象
3. 对于同步方法块,锁是Sychronized括号里配置的对象
5. JVM层面锁的优化
JDK1.6+以后,JVM出现了多种锁的优化技术,例如轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除等,这一系列手段的目的只有一个
线程间更高效的解决竞争问题,提升程序的执行效率。JVM通过引入轻量级锁和偏向锁来减少重量级锁的使用,按照状态划分有四种状态锁
无状态锁、偏向锁、轻量级锁、重量级锁,这四种状态锁随着竞争可以升级,但是不能降级;无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁
5.1 偏向锁
JVM引入偏向锁是因为大多数情况下,锁虽然存在这多线程的竞争,但是总是由同一个线程多次获得,因此为了降低线程获取锁的代价,引入了偏向锁;
如上图所示,当线程请求到锁对象时,biased_lock=1,锁状态标志位为01,表示当前是偏向锁,然后使用CAS操作,将对象头Mark Word中记录线程ID
以后该线程在进出同步代码块的时候,不需要CAS操作,只需要看一下对象头的Mark Word中,是否存在指向当前线程的偏向锁,如果存在,直接获得锁;
但是,如果此时有另外一条线程竞争锁,那么偏向锁升级为轻量级锁状态;如下图:线程1为偏向锁初始化过程、线程2为偏向锁撤销过程
5.1.1 锁的获取
5.1.2 关闭偏向锁
Java6 & Java7默认开启偏向锁,如果程序中的锁通常都处于竞争状态,可以通过JVM参数-XX:+UseBiasedLocking=false来关闭偏向锁
1. IDEA开发工具:RUN AS -->RUN Configurations-->Arguments -- >VM Arguments
2. Linux:java -XX:+UseBiasedLocking=false -jar xxx.jar &
5.1.3 优缺点
与轻量级锁的区别:轻量级锁是在无竞争的情况下使用CAS操作来代替互斥量的使用,从而实现同步;而偏向锁是在无竞争的情况下完全取消同步
作用:偏向锁是为了消除无竞争情况下的同步原语,进一步提升程序性能
优点:偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竞争,那偏向锁就是多余的
5.2 轻量级锁
核心思想:轻量级锁将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁
如果线程获得轻量级锁成功,则可以顺利进入临界区
如果获取轻量级锁失败,则表示其它线程先抢到了锁,那么线程的锁请求就会膨胀为重量级锁
前提:轻量级锁比重量级锁性能更高的前提是,在轻量级锁被占用的整个同步周期内,不存在其他线程的竞争
若在该过程中一旦有其他线程竞争,那么就会膨胀成重量级锁,从而除了使用互斥量以外,还额外发生了CAS操作,因此更慢!
轻量级锁与重量级锁的比较:
重量级锁是一种悲观锁,它认为总是有多条线程要竞争锁,所以它每次处理共享数据时,不管是否真的有线程在竞争锁,都会使用互斥同步保证线程安全;
轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,不使用互斥同步,而是使用CAS操作来获得锁,这样能减少互斥同步中『互斥量』的性能开销
6. 原子操作
6.1 处理器的原子操作
处理器提供总线锁定和缓存锁定两个机制保证复杂内存操作的原子性,有两种情况不可以用缓存锁定
1. 被操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行时,处理器会调用总线锁定
2. 有些处理器不支持缓存锁定
6.2 Java的原子操作
总的来说,Java实现原子操作的方式有两种:锁和循环CAS
CAS的三大问题
1. ABA问题,可以通过AtomicStampedReference解决,参考6.2.2
2.循环时间长,开销大
3. 只能保证一个共享变量的原子性,参考6.2.3
6.2.1 CAS代码
public class CAS { private AtomicInteger atomicI = new AtomicInteger(0); private int i; public static void main(String[] args) { final CAS cas = new CAS(); List<Thread> ts = new ArrayList<>(600); long start = System.currentTimeMillis(); for (int j = 0; j < 100; j++) { Thread thread = new Thread(() -> { for (int i = 0; i < 10000; i++) { cas.count(); cas.safeCount(); } }); ts.add(thread); } ts.forEach(Thread::start); ts.forEach(thread -> { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(cas.i); System.out.println(cas.atomicI.get()); System.out.println(System.currentTimeMillis() - start); }
//线程安全的自增 private void safeCount() { for (; ; ) { int i = atomicI.get(); boolean suc = atomicI.compareAndSet(i, ++i); if (suc) { break; } } } private void count() { i++; } }
6.2.2 AtomicReference
public class AtomicReferenceDemo { public static void main(String[] args) throws InterruptedException { Student student = new Student("jack", 20); /** * 现在有两个线程对student进行修改 * 线程1将name修改为jack1,age+1 * 线程2将name修改为jack2,age+2 * 我们认为的结果有两种 * 如果线程1先执行,中间状态为 name=jack1,age=21,结果状态为 name=jack2,age=23 * 如果线程2先执行,中间状态为 name=jack2,age=22,结果状态为 name=jack1,age=23 * * 但是实际上线程任务里面的打印的可能出现如下情况: * name=jack1,age=23 * name=jack1,age=23 * * 因为student是共享变量 */ Thread thread1 = new Thread(() -> { student.setName("jack1"); student.setAge(student.getAge() + 1); System.out.println(" --->"+student); }); Thread thread2 = new Thread(() -> { student.setName("jack2"); student.setAge(student.getAge() + 2); System.out.println("thread2--->"+student); }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(student); } @Data @AllArgsConstructor @ToString @NoArgsConstructor public static class Student { private String name; private Integer age; } }