Java的锁机制:synchronized和CAS详解
作者:我是坑货
提到Java的知识点一定会有多线程,JDK版本不断的更迭很多新的概念和方法也都响应提出,但是多线程和线程安全一直是一个重要的关注点。比如说我们一入门就学习的synchronized怎么个实现和原理,还有总是被提到的CAS是啥,他和synchronized关系是啥?这里大概会让你对这些东西有一个认识。
一 为什么要用锁
我们使用多线程肯定是为了提高效率,压榨硬件的性能提高效率,假设多一个线程相当于多一个人干活,但是有时候人多了就不是很好管理,可能出现问题。
比如我现在搞一个多线程的demo,我的本意是每个线程都高呼“ZPNB!”,我写下了如下的代码。
public class ThreadDemo implements Runnable{ @Test public void testThread() { System.out.println("大声告诉我:"); ThreadDemo demo = new ThreadDemo(); Thread threadOne = new Thread(demo,"张三:ZPNB"); Thread threadTwo = new Thread(demo,"李四:ZPNB"); Thread threadThree = new Thread(demo,"王二麻子:ZPNB"); Thread threadFour = new Thread(demo,"赵四:ZPNB"); threadOne.start(); threadTwo.start(); threadThree.start(); threadFour.start(); } @Override public void run() { // synchronized (this){ for( int i = 0; i < 10 ;i++ ){ try { System.out.println(Thread.currentThread().getName()); //这里设置0是因为Junit的限制你设置长了,他就执行一段时间就不执行了 Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } } // } }
没有加锁的情况是这样的,看起来很乱我希望他们每个人都喊十遍然后下一个人,显然下面的结果不满意
大声告诉我: 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 张三:ZPNB 李四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB 赵四:ZPNB 王二麻子:ZPNB
但是如果我把synchronized的注释取消就变成了我想要的依次每人喊十遍
大声告诉我: 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 张三:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 赵四:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 王二麻子:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB 李四:ZPNB
这就突出了锁的重要性,我们希望有些线程能按照我们希望的一个顺序依次来执行,而不是先到先得的。
二 synchronized怎么实现的
实际上每一个对象实际都拥有一个叫做监视器monitor的东西,线程只有获得了这个监视器才能才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,具体过程如下图:
那如果没获取到监视器怎么办,有个同步队列的东西,你没得到监视器就等一等,等上一个获取监视器的exit推出监视器你再根据队列顺序去再获取,当然可能在这个再获取的过程碰到一个“新来的”没进队列直接跟你抢,你还没抢过,那你就还要重复之前的等待过程。
其实这里还涉及一个锁的“happen before”的概念(“ A hapen-bfore B,那么 A 的结果对 B 是可见的”),就是上一个线程如果对某些值有改写,后一个应该在这个基础上改写的原则,假设一个计算程序,值都改了,新的线程你还在拿原先的值再去计算是不对的,应该是在新的值上面再去做操作,这样多线程协作才有实际意义。
以下是关于synchronized作用范围(基本是实际对象或者是类对象,如果你是类对象的话,那你new多少个实例对象还是被锁的。)
三 CAS来者何人
CAS突然这个概念出来作为线程安全的一个实现方式出现,那它和synchronized是一个什么样的关系呢?
实际二者应该是同级的概念,大家都是锁,synchronized是悲观锁,基本就是来一个线程就是锁起来,阻塞同步的。认为任何操作都有可能是冲突,所以按照最坏的情况来处理,线程竞争阻塞了就阻塞,阻塞结束了就唤醒阻塞的进程。
CAS就是compare and swap ,不是直接锁起来,大概意思就是:
CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
CAS对于线程竞争冲突的情况相对来说就温柔一些,他会有自己的重试机制,就是这次不行我等一会再去看看,而不是直接阻塞挂起再唤醒的状态,这样太耗费时间了。
在Java.util,ConCurrent包里面很多都是用CAS来处理同步的问题,而不是直接来个synchronized来修饰。
四synchronized和CAS孰优孰劣
实际上现在来看,还真不好说,因为在CAS的方案提出,实际上synchronized也是不断的进步的。不能说CAS一定比synchronized好。
比如说CAS也会有自己的问题,最主要的有:ABA,自旋时间过长和只能保证一个共享变量的原子操作,虽然说都要相关的解决方案:
(1)ABA就是两个线程第一个线程将最开始的A值改成B再改成A,第二个线程接手直接CAS,会得不到之前的转换的过程,解决方式跟数据库一样加一个版本号1A 2B 3C解决。
(2)自旋时间过长就是线程竞争冲突,不停地重试,实际是一个循环操作,这个循环可能要等好长时间,导致所谓的自旋时间过长。
(3)只能操作一个共享原子,就让这个原子变成一个对象,把要共享的都塞进去。
synchronized自身也在不断地优化自身,甚至也借鉴了CAS的思想在1.6里面。为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
偏向锁(通过线程ID来看对象头和栈帧里面查找线程ID(记录的线程ID就是偏向的线程ID),有就获取没有就尝试CAS设置自己为偏向的线程)
具体如下:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
轻量级锁
(替换锁的指针替换成就获得锁,替换不成就自旋循环去找机会替换)
具体如下:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
重量级锁
(monitor监视器锁的实现,最重的一步,因为涉及到用户态和系统态切换。)
重量级锁是依赖对象内部的monitor锁来实现。当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,需要从用户态转换到内核态,而转换状态是需要消耗很多时间。
这么看来synchronized并不是那么不堪,未必你用CAS实现的就一定在某些环境比synchronized这个“元老”强。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!