Java的锁机制:synchronized和CAS详解
作者:我是坑货 发布时间:2023-03-18 16:04:00
目录
一 为什么要用锁
二 synchronized怎么实现的
三 CAS来者何人
四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这个“元老”强。
来源:https://blog.csdn.net/FeiChangWuRao/article/details/120489390


猜你喜欢
- 多层嵌套JSON类型数据解析简单来说:“key”:“value&rdqu
- 本文实例分析了JAVA反射机制。分享给大家供大家参考,具体如下:反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感
- 前言近期一直在忙项目,我也是打工仔。不多说,我们开始玩一玩seata。什么都不说,我们按照惯例,先上一个图(图里不规范的使用请忽略):简单一
- @ConfigurationProperties加载外部配置@ConfigurationProperties可以将外部配置文件(比如appl
- 本文实例讲述了C#导出数据到Excel文件的方法。分享给大家供大家参考。具体实现方法如下:/// <summary>/// 导出
- 权限提升方法:一种方法:1、在AndroidManifest.xml中的manifest节点中添加android:sharedUserId=
- 本文实例讲述了Java使用JDBC连接postgresql数据库。分享给大家供大家参考,具体如下:package tool;import j
- 1、什么是ThreadLocal变量ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thr
- package cn.liangjintang.httpproxy;import java.io.BufferedReader;import
- Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new
- 使用ProcessBuilder踩到的坑最近使用ProcessBuilder执行命令,命令内容正确,但始终报错命令实行失败,是因为不熟悉Pr
- 今天给大家讲讲有关自定义对话框的相关内容,前面两篇都在在利用系统提供的函数来实现对话框,但局限性太大,当我们想自己定义视图的时候,就不能利用
- 使用CachePut注解,该方法每次都会执行,会清除对应的key值得缓存(或者更新),分为以下两种情况:如果返回值null,下次进行该key
- 前言.如何设置设置使用的地方1.设置类注释模板代码/*** @author: lujie* @create: $date$* @descri
- 在C#中,一共有38个常用的运用符,根据它们所执行运算的特点和它们的优先级,为了便于记忆,我将它们归为七个等级:1、单元运算符和括号。2、常
- C#事件标准命名规则一些开源代码的事件命名很混乱,以此文章用作本人以后工作的参考。事件的名称事件始终是指某个操作,这个操作可能正在发生,也可
- 本文实例为大家分享了C#实现飞行棋的具体代码,供大家参考,具体内容如下基于Winform框架写的不足之处请大佬指教using System;
- 本文实例讲述了C#画笔Pen绘制光滑模式曲线的方法。分享给大家供大家参考。具体实现方法如下:using System;using Syste
- java 中二分法查找的应用实例二分查找的前提是:数组有序 注意:mid的动态变化,否则出错!!! 实例代码:publ
- 在Android串口通信:基本知识梳理的基础上,我结合我项目中使用串口的实例,进行总结;Android使用jni直接进行串口设备的读写网上已