【java并发编程】--cas和synchronized
java并发编程 --cas和synchronized
- 一、CAS(CompareAndSwap 比较再交换)
- 1.在java代码中的应用
- (1).基于反射方式获取Unsafe对象,实现CAS操作
- (2)调用javaApi实现cas操作(推荐)
- 2.CAS底层实现
- 3.cas操作存在的问题
- 一、synchronized(cas的应用)
- 1.synchronized锁在方法上加锁
- (1)、new的对象作为锁(普通成员方法加synchronized锁,方法是属于对象的,不同对象不存在锁竞争,同一对象存在锁竞争)
- (2)、.class对象锁(静态方法加synchronized锁,方法属于类,所以是类锁,全局只有一把,不同对象存在锁竞争)
- 2.synchronized锁住代码块
- 3.synchronized锁优化
- (1).锁消除
- (2).锁膨胀
- (3).锁升级
- 4.synchronized 底层原理
- (1).java对象组成部分
- MarkWord详解:
- (2).ObjectMonitor对象组成部分:
- (3)锁升级过程原理(锁对象的markword变化)
- Ⅰ.new出来的对象,默认是无锁状态:
- Ⅱ.new出来的对象被作为synchronized锁对象时
- Ⅲ.从偏行锁到轻量级锁
- Ⅳ.轻量级锁到重量级锁
并发编程三大特征:
- 原子性:指一个操作(多条指令)是不可分割的。指一个线程在执行一段指令时,其他线程也需要执行时,必须等待前一个线程执行完才可以执行.
java中保证原子性的方式,CAS、synchronized、ReentrantLock- 可见性: 指一个线程对主线程的修改可以及时的被其他线程看到。即保证了线程读取数据时是从主内存读取的,写出数据时写到了主内存中。
java中保证可见性的操作有volatile关键字,synchronized,Lock锁(本质也是volatile)- 有序性:在java中,.java文件被编译后,会生成多条指令,cpu将回去执行这条指令。cpu在执行时会在不影响结果的前提下对执行进行重排序。在jvm内部,JIT会进行一些优化操作,可能也会导致执行重排序。
在Java中,解决指令重排的方式很简单,可以给涉及到指令重排的属性追加上一个关键字 volatile。
一、CAS(CompareAndSwap 比较再交换)
乐观锁:乐观锁不涉及线程的挂起,不设计用户态与内核态的切换。是一种乐观的思想,认为线程之间不会存在竞争关系,通过(**比较和交换**)来修改内存的变量值。
java中Unsafe类中的方法是基于cas实现的。CompareAndSwap 比较再交换,是基于cpu的原语操作实现的。
他在替换内存中的某个位置的值时,可以保证原子性,会先查看olaValue是否与主存中的值一致,如果一致,就将内存中的数据从olaValue修改为newValue。
在java中提供了一个类为Unsafe,这个类中提供了cas操作,里面的方法是native修饰,在Java层面看到Unsafe的compareAndSwapInt基本就看到头了。public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
1.在java代码中的应用
(1).基于反射方式获取Unsafe对象,实现CAS操作
private Unsafe() {}private static final Unsafe theUnsafe = new Unsafe();
通过反射获取Unsafe类的theUnsafe字段并进行cas操作
仿照AtomicInteger基于cas操作实现自增操作
AtomicInteger自增操作核心代码
VALUE字段:获取value字段的内存偏移量
value字段:用volatile修饰,保证变量可见性,该字段用于AtomicInteger类的进行自增自减等操作的字段
获取value字段的值,通过迭代的cas(比较v的值(value字段)和内存中的值是否一致进行修改)操作进行自增。
public class CompanyTest {private volatile int value;private static Unsafe unsafe = null;public static void main(String[] args) throws NoSuchFieldException, InterruptedException {unsafe = getUnsafe();CompanyTest companyTest = new CompanyTest();long offset = unsafe.objectFieldOffset(CompanyTest.class.getDeclaredField("value"));for (int i = 0; i < 1000; i++) {new Thread(() -> {int oldValue = 0;do {oldValue = unsafe.getInt(companyTest, offset);} while (!unsafe.compareAndSwapInt(companyTest, offset, oldValue, oldValue + 1));}).start();}Thread.sleep(1000);System.out.println(companyTest.value);}private static Unsafe getUnsafe() {Unsafe unsafe = null;try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);unsafe = (Unsafe) theUnsafe.get(null);} catch (Exception e) {throw new RuntimeException(e);}return unsafe;}
}
执行结果:
(2)调用javaApi实现cas操作(推荐)
以AtomicInteger为例:
public static void main(String[] args) throws InterruptedException {AtomicInteger atomicInteger = new AtomicInteger(0);for (int i = 0; i < 1000; i++) {new Thread(() -> {atomicInteger.getAndIncrement();}).start();}Thread.sleep(1000);System.out.println(atomicInteger.get());}
执行结果:
2.CAS底层实现
Unsafe类的CompareAndSwap是通过cmpxchg操作实现的,它是cpu的一条原语.
CPU默认在单核的情况下,不需要追加lock指令,因为单核CPU指令cmpxchg肯定要执行完再去执行其他的指令。但是多核的情况就需要基于lock指令来实现总线锁或者是缓存行粒度的锁。
3.cas操作存在的问题
-
ABA问题:ABA不一定是问题!因为一些操作只存在++,–的操作时,即便出现了ABA问题,也无所谓。
用户A期望从10变为9,用户B期望从10变为9,用户C期望从9变为10,即便先A操作,再C操作,最后B操作,也不影响最终结果。
另外一个维度,毕竟这里存在这用户B拿到的是之前的数据,但是依然操作成功了,在一些特殊的场景下,依然可能会存在问题。所以为了解决这个问题,Java中也提供了一个原子类去解决
核心的方式,就是追加一个版本号,在数据修改的同时,版本号也不停的去更新,操作完,需要保证预期的版本号,和最终的版本号是一致的。 -
自旋次数过多:
发现Atomic中,大量的原子性中CAS都是采用了do-while循环的方式,如果操作不成功,他会一致执行do-while循环,并且调用CAS,此时CPU会一直处于一个忙碌的状态,很浪费CPU资源。
synchronized的处理方案:synchronized提供了轻量级锁的概念,这个概念下,会基于CAS尝试获取锁资源,但是他指定了CAS的次数,如果超过了次数没获取到锁,挂起线程。 -
cas只针对一个属性保证原子性:不过,类似synchronized和ReentrantLock虽然都可以锁住一段代码,但是他们底层都用到了CAS。
一、synchronized(cas的应用)
- synchronized一般用于对方法和同步代码块加锁.
- synchronized锁是基于对象实现的,java中Object对象提供了notify()方法和wait()方法,做锁基本的操作,因此所有对象都可以作为锁.
1.synchronized锁在方法上加锁
(1)、new的对象作为锁(普通成员方法加synchronized锁,方法是属于对象的,不同对象不存在锁竞争,同一对象存在锁竞争)
public static int count = 0;/*** 这个synchronized方法是基于LockTest的对象,作为一把锁*/public synchronized void increment(){LockTest.count++;}/*** 此时调用test.increment()方法时,底层使用的就是test对象作为锁。* @param args*/public static void main(String[] args) {LockTest test = new LockTest();test.increment();}
(2)、.class对象锁(静态方法加synchronized锁,方法属于类,所以是类锁,全局只有一把,不同对象存在锁竞争)
public class LockTest {public static int count = 0;/*** 这个synchronized的static方法是基于LockTest.class,作为一把锁*/public static synchronized void increment(){LockTest.count++;}/*** 此时这种方式,就是基于LockTest.class作为锁,全局就一把* @param args*/public static void main(String[] args) {LockTest.increment();LockTest.increment();}
}
同步方法标记为acc_synchronized
2.synchronized锁住代码块
在使用同步代码块时,需要自己手动的指定用的是什么对象作为这把锁
public class LockTest {public static int count = 0;/*** synchronized (对象),此时这个对象就是当前锁资源*/public static void increment(){synchronized (LockTest.class){count++;}}/*** synchronized (对象),此时这个对象就是当前锁资源*/public static void decrement(){synchronized (LockTest.class){count--;}}
}
执行monitorenter命令,进行加锁,其他线程进行等待,执行monitorexit命令,进行解锁操作.
3.synchronized锁优化
(1).锁消除
锁消除是JIT编译器的一个优化技术,消除不必要的加锁和解锁操作
public void incr(){Object obj = new Object();int j = 0;synchronized (obj) {j++;}
}
上述程序中,锁对象是obj对象,是一个局部变量,每个线程都会创建这个变量,即这个锁是线程私有的,并且锁住的是局部变量,加锁是没有意义的.
此时,会将synchronized锁的加锁和解锁消除掉.
(2).锁膨胀
锁消除是JIT编译器的一个优化技术,对锁的范围进行扩大,避免频繁的加锁和解锁操作。
public static void incr() {for (int i = 0; i < 1000000; i++) {synchronized (LockTest.class) {// 做业务代码~~~}}
}
上述代码中,会将synchronized 锁进行膨胀,膨胀到for循环之外,可以避免加锁1000000次,只加锁一次,完成业务操作。
优化为:
public static void incr() {synchronized (LockTest.class) {for (int i = 0; i < 1000000; i++) {// 做业务代码~~~}}
}
(3).锁升级
synchronized 的锁升级,有点像类似ReentrantLock的加锁操作,先基于cas进行加锁操作,如果拿锁失败,再去排队,排队的过程中可能将线程挂起(用户态和内核态之间的切换)。
synchronized 锁升级有四种状态,无锁/匿名偏向,偏向锁,轻量级锁,重量级锁
无锁:当前对象没有被作为锁对象
偏向锁:当前资源,只有一个线程来进行获取,即就是偏向锁,
- 如果获取锁线程是当前偏向的那个线程,拿锁。
- 如果获取锁线程不是当前偏向的那个线程,如果偏向锁被持有,进行锁升级,如果没有被持有,cas操作获取锁
轻量级锁:会采用自旋锁(cas)的方式进行加锁,即多次cas操作
- 如果cas成功,拿锁
- 如果cas失败,达到一定的阈值,升级为重量级锁
重量级锁:锁对象与Monitor 对象关联.
4.synchronized 底层原理
(1).java对象组成部分
java对象组成:
对象头:
- Mark Word:存储对象hashcode值,gc分代年龄,锁信息等。
- Klass Point:执行对象所属类的元数据的指针,虚拟机通过这个指针来确认对象是那个类的实例。
- 数组长度:如果对象是数组,对象头中这部分存储数组长度。
实例数据:
存放当前对象属性信息和父类属性信息。
填充数据:不是必然存在的,仅仅是占位作用。hotspot vm规定,对象起始地址必须是8字节的整数倍,也就是对象大小必须是8字节的整数倍。如果实例数据没有对齐,就需要通过对齐填充来补全。
MarkWord详解:
MarkWord默认占8字节,64bit位,数组除外,存储长度占32bit位。
锁升级过程中MarkWord变化情况:
无锁状态:存储hashcode值,分代年龄,锁标记001,
偏向锁:存储当前偏向线程的指针,分代年龄,锁标记101,
轻量级锁:存储线程栈中Lock Record指针,锁标记00,
重量级锁:存储ObjectMonitor信息,所标记位10
(2).ObjectMonitor对象组成部分:
ObjectMonitor() {_header = NULL;_count = 0; // 竞争锁的线程个数_waiters = 0, // waitSet中有多个线程处在挂起状态_recursions = 0; // 锁重入的次数_object = NULL;_owner = NULL; // 持有锁的线程_WaitSet = NULL; // 持有锁的线程,执行wait方法后,会扔到这个WaitSet中_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ; // 获取锁资源失败后,线程需要存放到这个单向链表中FreeNext = NULL ;_EntryList = NULL ; // cxq中等待的线程会基于一定的机制,扔到EntryList,同时拿锁失败,也可能到这。_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
当有多个线程访问同一段代码时,首先会进入到_EntryList队列中,当有线程获取锁后,会将_owner设置为当前线程,调用wait()方法,会将_owner设置为null,并将当前线程加入到_WaitSet队列中,再次调用方法时,会进入_EntryList 队列,进行锁竞争。
(3)锁升级过程原理(锁对象的markword变化)
Ⅰ.new出来的对象,默认是无锁状态:
public static void main(String[] args) {// 在Java中,任何对象都可以作为锁,new一个Object,把他当成锁对象Object obj = new Object();// 打印obj的MarkWord信息,发现默认情况下是无锁状态System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
休眠5s中,在进行new对象操作
当前锁标记101,代表偏向锁,当前偏向锁没有被其他线程持有。
上述现象属于偏向锁延迟现象。
1.在jvm启动时,需要先基于classLoader加载大量class对象到堆内存中,加载过程中,涉及synchronized操作.
2.偏向锁在升级到轻量级锁时,需要有偏向锁撤销操作,必须在安全点或者安全区域进行撤销操作,在这个过程中会暂停所有的线程,开销非常大。
安全点:正在执行的线程提供已知的安全状态点,例如,方法跳转,循环跳转,异常跳转。
安全区域:为休眠或阻塞状态的线程,业务线程一直处于休眠状态,这段代码区域,被称为安全区域。
延迟的益处:程序在启动初期暂时禁用偏向锁,让所有锁直接以无锁状态或轻量级锁的模式开始。这样可以避免高竞争的启动阶段,发生大量昂贵且不必要的偏向锁撤销操作,从而提升jvm的整体启动速度。
Ⅱ.new出来的对象被作为synchronized锁对象时
public static void main(String[] args) throws InterruptedException {// 如果在偏向锁延迟的状态下,默认是无锁状态,那对象被当做synchronized锁资源后,是什么状态?Object obj = new Object();// 这里打印肯定是无锁装填System.out.println(ClassLayout.parseInstance(obj).toPrintable());synchronized (obj){// obj锁被main线程持有了,此时是什么状态?// 此时是轻量级锁状态,因为偏向锁延迟处于开启状态,无法跳到偏向锁状态System.out.println(ClassLayout.parseInstance(obj).toPrintable());}}
偏向锁延迟内,直接从无锁升级为偏向锁。
public static void main(String[] args) throws InterruptedException {// 在偏向锁延迟过后,查看一下,对象被作为锁,是否是偏向锁状态?Thread.sleep(5000);Object obj = new Object();// 匿名偏向锁状态System.out.println(ClassLayout.parseInstance(obj).toPrintable());// obj锁,被main线程持有后synchronized (obj){// 打印的是什么状态?System.out.println(ClassLayout.parseInstance(obj).toPrintable());}}
程序启动时先休眠5秒钟,等待偏向锁延迟过后。锁信息为偏行锁101,当前线程通过cas操作,将自己的线程地址信息存储到锁对象的markword中。以后该线程进入和退出这段代码块时,只需要检测锁对象的markword中是否存有存在该线程的地址信息的偏向锁。
Ⅲ.从偏行锁到轻量级锁
public static void main(String[] args) throws InterruptedException {// 尝试看一下从偏向锁到轻量级锁状态,因为CAS次数不多,可能打印不到轻量级锁的情况。Thread.sleep(5000);// obj是匿名偏向Object obj = new Object();Thread t = new Thread(() -> {synchronized (obj) {// main线程持有锁时,启动子线程System.out.println("子线程:" + ClassLayout.parseInstance(obj).toPrintable());}});// main线程持有obj锁资源synchronized (obj){// main线程状态是偏向锁System.out.println("main线程:" + ClassLayout.parseInstance(obj).toPrintable());t.start();}}
当代码执行到同步代码块时,同步对象处于无锁状态(偏行锁撤销),jvm会在当前线程的栈帧中创建一个Lock Record(锁记录)空间。
然后使用cas操作,尝试将对象头的markword更新,并指向Lock Record。
如果成功,则修改markword中的锁信息为00,
失败则检查是否重入,可以直接进入同步代码块,或者修改markword信息为重量级锁。
Ⅳ.轻量级锁到重量级锁
public static void main(String[] args) throws InterruptedException {// 尝试看一下从偏向锁到轻量级锁状态,因为CAS次数不多,可能打印不到轻量级锁的情况。// Thread.sleep(5000);// obj是匿名偏向Object obj = new Object();Thread t1 = new Thread(() -> {synchronized (obj) {System.out.println("子线程t1:" + ClassLayout.parseInstance(obj).toPrintable());}});// main线程持有obj锁资源synchronized (obj){// 偏向锁System.out.println("main线程:" + ClassLayout.parseInstance(obj).toPrintable());// 同时启动t1线程t1.start();// main线程状态是偏向锁System.out.println("启动子线程后,main线程:" + ClassLayout.parseInstance(obj).toPrintable());}
锁竞争激烈时,轻量级锁升级为重量级锁,对象头中的markword指向ObjectMonitor对象,修改锁信息为01,将ObjectMonitor的_owner设置为竞争锁成功的线程,其他线程进入_entrylist等待锁资源被释放.
下面是锁变化的大概流程: