Java并发(4)- synchronized与CAS

news2024/11/16 13:06:39

引言

上一篇文章中我们说过,volatile通过lock指令保证了可见性、有序性以及“部分”原子性。但在大部分并发问题中,都需要保证操作的原子性,volatile并不具有该功能,这时就需要通过其他手段来达到线程安全的目的,在Java编程中,我们可以通过锁、synchronized关键字,以及CAS操作来达到线程安全的目的。

synchronized

在Java的并发编程中,保证线程同步最为程序员所熟悉的就是synchronized关键字,synchronized关键字最为方便的地方是他不需要显示的管理锁的释放,极大减少了编程出错的概率。

在Java1.5及以前的版本中,synchronized并不是同步最好的选择,由于并发时频繁的阻塞和唤醒线程,会浪费许多资源在线程状态的切换上,导致了synchronized的并发效率在某些情况下不如ReentrantLock。在Java1.6的版本中,对synchronized进行了许多优化,极大的提高了synchronized的性能。只要synchronized能满足使用环境,建议使用synchronized而不使用ReentrantLock。

synchronized的三种使用方式

1.修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁。
2.修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁。
3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

这三种使用方式大家应该都很熟悉,有一个要注意的地方是对静态方法的修饰可以和实例方法的修饰同时使用,不会阻塞,因为一个是修饰的Class类,一个是修饰的实例对象。下面的例子可以说明这一点:

public class SynchronizedTest {

	public static synchronized void StaticSyncTest() {

		for (int i = 0; i < 3; i++) {
			System.out.println("StaticSyncTest");
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public synchronized void NonStaticSyncTest() {

		for (int i = 0; i < 3; i++) {
			System.out.println("NonStaticSyncTest");
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

public static void main(String[] args) throws InterruptedException {SynchronizedTest synchronizedTest = new SynchronizedTest();new Thread(new Runnable() {
		@Override
		public void run() {
			SynchronizedTest.StaticSyncTest();
		}
	}).start();new Thread(new Runnable() {
		@Override
		public void run() {
			synchronizedTest.NonStaticSyncTest();
		}
	}).start();
}

//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest
//StaticSyncTest
//NonStaticSyncTest 

代码中我们开启了两个线程分别锁定静态方法和实例方法,从打印的输出结果中我们可以看到,这两个线程锁定的是不同对象,可以并发执行。

synchronized的底层原理

我们看一段synchronized关键字经过编译后的字节码:

if (null == instance) { 
	synchronized (DoubleCheck.class) {
		if (null == instance) { 
			instance = new DoubleCheck(); 
		}
	}
} 

可以看到synchronized关键字在同步代码块前后加入了monitorenter和monitorexit这两个指令。monitorenter指令会获取锁对象,如果获取到了锁对象,就将锁计数器加1,未获取到则会阻塞当前线程。monitorexit指令会释放锁对象,同时将锁计数器减1。

JDK1.6对synchronized的优化

JDK1.6对对synchronized的优化主要体现在引入了“偏向锁”和“轻量级锁”的概念,同时synchronized的锁只可升级,不可降级:

这里我不打算详细讲解每种锁的实现,想了解的可以参照《深入理解Java虚拟机》,只简单说下自己的理解。

偏向锁的思想是指如果一个线程获得了锁,那么就从无锁模式进入偏向模式,这一步是通过CAS操作来做的,进入偏向模式的线程每一次访问这个锁的同步代码块时都不需要再进行同步操作,除非有其他线程访问这个锁。

偏向锁提高的是那些带同步但无竞争的代码的性能,也就是说如果你的同步代码块很长时间都是同一个线程访问,偏向锁就会提高效率,因为他减少了重复获取锁和释放锁产生的性能消耗。如果你的同步代码块会频繁的在多个线程之间访问,可以使用参数-XX:-UseBiasedLocking来禁止偏向锁产生,避免在多个锁状态之间切换。

偏向锁优化了只有一个线程进入同步代码块的情况,当多个线程访问锁时偏向锁就升级为了轻量级锁。

轻量级锁的思想是当多个线程进入同步代码块后,多个线程未发生竞争时一直保持轻量级锁,通过CAS来获取锁。如果发生竞争,首先会采用CAS自旋操作来获取锁,自旋在极短时间内发生,有固定的自旋次数,一旦自旋获取失败,则升级为重量级锁。

轻量级锁优化了多个线程进入同步代码块的情况,多个线程未发生竞争时,可以通过CAS获取锁,减少锁状态切换。当多个线程发生竞争时,不是直接阻塞线程,而是通过CAS自旋来尝试获取锁,减少了阻塞线程的概率,这样就提高了synchronized锁的性能。

synchronized的等待唤醒机制

synchronized的等待唤醒是通过notify/notifyAll和wait三个方法来实现的,这三个方法的执行都必须在同步代码块或同步方法中进行,否则将会报错。

wait方法的作用是使当前执行代码的线程进行等待,notify/notifyAll相同,都是通知等待的代码继续执行,notify只通知任一个正在等待的线程,notifyAll通知所有正在等待的线程。wait方法跟sleep不一样,他会释放当前同步代码块的锁,notify在通知任一等待的线程时不会释放锁,只有在当前同步代码块执行完成之后才会释放锁。下面的代码可以说明这一点:

public static void main(String[] args) throws InterruptedException {waitThread();notifyThread();
}

private static Object lockObject = new Object();
	
private static void waitThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "wait-before");try {TimeUnit.SECONDS.sleep(2);lockObject.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "after-wait");}}},"waitthread");watiThread.start();
}

private static void notifyThread() {Thread watiThread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lockObject) {System.out.println(Thread.currentThread().getName() + "notify-before");lockObject.notify();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();} System.out.println(Thread.currentThread().getName() + "after-notify");}}},"notifythread");watiThread.start();
}

//waitthreadwait-before
//notifythreadnotify-before
//notifythreadafter-notify
//waitthreadafter-wait 

代码中notify线程通知之后wait线程并没有马上启动,还需要notity线程执行完同步代码块释放锁之后wait线程才开始执行。

CAS

在synchronized的优化过程中我们看到大量使用了CAS操作,CAS全称Compare And Set(或Compare And Swap),CAS包含三个操作数:内存位置(V)、原值(A)、新值(B)。简单来说CAS操作就是一个虚拟机实现的原子操作,这个原子操作的功能就是将旧值(A)替换为新值(B),如果旧值(A)未被改变,则替换成功,如果旧值(A)已经被改变则替换失败。

可以通过AtomicInteger类的自增代码来说明这个问题,当不使用同步时下面这段代码很多时候不能得到预期值10000,因为noncasi[0]++不是原子操作。

private static void IntegerTest() throws InterruptedException {final Integer[] noncasi = new Integer[]{ 0 };for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {noncasi[0]++;}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(noncasi[0]);
}

//7889 

当使用AtomicInteger的getAndIncrement方法来实现自增之后相当于将casi.getAndIncrement()操作变成了原子操作:

private static void AtomicIntegerTest() throws InterruptedException {AtomicInteger casi = new AtomicInteger();casi.set(0);for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 1000; j++) {casi.getAndIncrement();}}});thread.start();}while (Thread.activeCount() > 2) {Thread.sleep(10);}System.out.println(casi.get());
}

//10000 

当然也可以通过synchronized关键字来达到目的,但CAS操作不需要加锁解锁以及切换线程状态,效率更高。

再来看看casi.getAndIncrement()具体做了什么,在JDK1.8之前getAndIncrement是这样实现的(类似incrementAndGet):

private volatile int value;

public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;}
} 

通过compareAndSet将变量自增,如果自增成功则完成操作,如果自增不成功,则自旋进行下一次自增,由于value变量是volatile修饰的,通过volatile的可见性,每次get()都能获取到最新值,这样就保证了自增操作每次自旋一定次数之后一定会成功。

JDK1.8中则直接将getAndAddInt方法直接封装成了原子性的操作,更加方便使用。

public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
} 

CAS操作是实现Java并发包的基石,他理解起来比较简单但同时也非常重要。Java并发包就是在CAS操作和volatile基础上建立的,下图中列举了J.U.C包中的部分类支撑图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/150798.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

因子图--isam相关内容总结

编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;平方根法--通过平方根的方法&#xff0c;发现矩阵新增加的变量都会出现在最后一行。Givens旋转方法求解Ax…

Linux环境下,java调用C/C++动态库

目录 一、环境准备 1、安装gcc/g 2、下载jdk库并配置运行环境 二、配合Java程序创建C程序的动态库 1、生成要求清单 2、交给C 去实现 (1) 接口函数实现 (2) 创建动态库 (3) 检查动态库是否正常链接 3、测试&#xff1a;Java程序调用C动态库 一、环境准备 既然是同时…

百万级数据以Excel形式导出

(1).主要考虑到两个方面&#xff0c;第一个方面是内存溢出问题&#xff0c;所以选用阿里的EasyExcel因为它对POI进行了封装功能强大&#xff1b;第二个方面是由于excel版本导致Sheet存储容量不一样&#xff0c;cexcel2003(.xls)每个Sheet只能存6万多条数据而cexcel2007(xlsx)能…

【自学Python】Python布尔型(bool)

Python布尔型(bool) Python布尔型(bool)教程 Python 布尔类型也叫 bool 类型&#xff0c;Python 布尔类型取值为 True 和 False。Python bool 类型的 True 表示条件为真&#xff0c; False 表示条件为假。 Python布尔型(bool) Python 中的布尔类型可以当做 整数 来对待&…

LeetCode 287. 寻找重复数难度中等2004

&#x1f308;&#x1f308;&#x1f604;&#x1f604;欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 287. 寻找重复数难度中等2004&#xff0c;做好准备了么&#xff0c;那么开始吧。&#x1f332;&#x1f332;&#x1f434;&#x1f434;一、题目名称LeetCo…

怎么在Gitee(码云)上传一个项目(一分钟)

目录怎么在Gitee&#xff08;码云&#xff09;上传一个项目1、工具1.1、Git1.2、新建仓库2、上传流程3、回答上传项目流程中的几个疑问&#xff1f;怎么在Gitee&#xff08;码云&#xff09;上传一个项目 1、工具 1.1、Git 在Git官网或者利用镜像下载符合自己电脑操作系统版…

小众企业在选购低代码平台时需要注意什么

编者按&#xff1a;企业个性化定制需求如何实现&#xff1f;本文介绍了小众企业在选择低代码平台需要注意的点&#xff0c;帮助企业选出更合适得的软件平台。关键词&#xff1a;源码交付&#xff0c;数据整合&#xff0c;前后端分离&#xff0c;私有化部署&#xff0c;安全技术…

IB生物笔记:细胞学说

国际学校生物老师从0开始解读IB生物&#xff0c;感兴趣的同学记得收藏哦~IB生物分为SL(standard level)和HL(higher level)SL有6个topic∶细胞生物&#xff0c;分子生物&#xff0c;遗传学&#xff0c;生态学&#xff0c;物种进化以及多样性和人体生理。HL除了上述6个topic外还…

【pat】出租

下面是新浪微博上曾经很火的一张图&#xff1a;一时间网上一片求救声&#xff0c;急问这个怎么破。其实这段代码很简单&#xff0c;index数组就是arr数组的下标&#xff0c;index[0]2 对应 arr[2]1&#xff0c;index[1]0 对应 arr[0]8&#xff0c;index[2]3 对应 arr[3]0&#…

年底清货刻不容缓!3天清空库存的服装老板是怎么做到的?

春节气氛越来越浓厚&#xff0c; 服装人的心情却是越来越着急——眼看着要过年了&#xff0c;气候也马上回暖了&#xff0c;我店里的库存可咋办&#xff1f;做服装零售的老板都知道&#xff0c;大部分时候压垮服装店的不是淡季、不是租金、不是人工费&#xff0c;而是库存积压。…

Java IO流 - 对象序列化和对象反序列化

文章目录序列化对象对象序列化对象反序列化序列化对象 对象序列化 对象序列化概念: 作用&#xff1a;以内存为基准&#xff0c;把内存中的对象存储到磁盘文件中去&#xff0c;称为对象序列化。 使用到的流是对象字节输出流&#xff1a;ObjectOutputStream ObjectOutputStream构…

Navicat远程连接ubuntu数据库

关于这个问题&#xff0c;我看许多博主也给出了解答&#xff0c;但作者此次遇到的问题也是略微特殊&#xff0c;希望其他人遇到同样的问题能得到启发。 首先是MySQL的安装&#xff0c;我是用的是Xubuntu系统&#xff0c;按照ubuntu系统安装MySQL的步骤即可。如下&#xff1a; …

关系运算符shell

关系运算符只支持数字&#xff0c;不支持字符串&#xff0c;除非字符串的值是数字。下表列出了常用的关系运算符&#xff0c;假定变量 a 为 10&#xff0c;变量 b 为 20&#xff1a;运算符说明举例-eq检测两个数是否相等&#xff0c;相等返回 true。[ $a -eq $b ] 返回 false。…

Mask RCNN网络源码解读(Ⅲ) --- FCN网络

目录 1.FCN网络简介 2.FCN-32S 3.FCN-16S 4.FCN-8S 5.膨胀卷积&#xff08;空洞卷积&#xff09; 6.pytorch中FCN的实现 6.1 代码地址 1.FCN网络简介 首个端对端的针对像素级预测的全卷积网络。全卷积网络即将分类网络中的全连接层全部替换为卷积层。 从数据可以看出…

微服务自动化管理【etcd快速集群】

概述 1.etcd构建自身高可用集群主要有三种形式&#xff1a; 静态发现 预先已知etcd集群中有哪些节点&#xff0c;在启动时通过–initial-cluster参数直接指定好etcd的各个节点地址 etcd动态发现 静态配置前提是在搭建集群之前已经提前知道各节点的信息&#xff0c;而实际应用中…

使用VS Code创建第一个Flutter工程

一、前言 你可以使用任意文本编辑器&#xff0c;再结合命令行工具来开发 Flutter 应用。然而&#xff0c;我们推荐使用本文接下来介绍的编辑器插件以获取更好的开发体验。这些插件提供了代码补全、代码高亮、widget 辅助编辑的功能&#xff0c;以及为项目的运行和调试提供支持…

【Denial-of-Service on FPGA-based Cloud Infrastructures论文笔记】

基于FPGA的云基础设施拒绝服务摘要引言背景FPGA TechnologyFPGA设计的实现用户设计在AWS的注册用户设计在AWS上的部署基于FPGA的系统攻击与对策客户端的FPGA使用基于云的fpga的使用信息泄露错误注入攻击对策云FPGA上的拒绝服务攻击攻击模型AWS FPGA安全架构AWS EC2 F1强力锤击攻…

@Validated+循环依赖报错

目录 测试demo 回顾下三级缓存 循环依赖bean实例化初始化过程​​​​​​​ 源码解读 实例化myZmTest1 myZmTest1依赖myZmTest2&#xff0c;实例化2去注入 myZmTest2依赖myZmTest1&#xff0c;获取myZmTest1去注入 获取到myZmTest1&#xff0c;继续myZmTest2初始化 m…

TCP协议的三次握手与四次挥手(附带常见面试题)

目录 一.TCP/UDP的区别 二.三次握手 三.四次挥手 四.常见面试题 一.TCP/UDP的区别 TCP&#xff1a;TCP协议是面向连接&#xff0c;连接稳定可靠适用于网络连接连接较高的场景&#xff0c;可以准确无误的把数据传递给对方&#xff0c;但传输速度上可能会存在一定的延迟。TCP…

Leetcode.32 最长有效括号

题目链接 Leetcode.32 最长有效括号 题目描述 给你一个只包含 (和 )的字符串&#xff0c;找出最长有效&#xff08;格式正确且连续&#xff09;括号子串的长度。 示例 1&#xff1a; 输入&#xff1a;s “(()” 输出&#xff1a;2 解释&#xff1a;最长有效括号子串是 “()”…