java非阻塞锁_Java并发问题的非阻塞解决方案

转自http://blog.csdn.net/u011277203/article/details/9223545

在并发环境中,对于共享资源通常会采用显式的锁机制(比如synchronized或ReentrantLock)来保证在任意时刻只会有一条线程访问这些变量,并且这些变量的修改对随后获取锁的线程是可见的。无法获取锁的线程会进入阻塞状态,并被JVM和操作系统挂起,在未来某一时刻被调度重新获取锁,挂起和恢复线程会产生很多的系统消耗和较长时间的中断。

线程的切换同时会引起上下文切换,即把当前线程的运行时上下文保存起来,装入新线程的运行时上下文,所以上下文切换并不是免费的,另外被换入的新线程所需要的数据不太可能在CPU Cache中,因此上下文切换会导致Cache Missing激增,新线程开始执行时,性能会相对较低。

在高并发,竞争非常激烈的场景下,显式锁的开销会非常大,将严重影响系统的性能。所以在一些场景下,使用非阻塞方案解决并发问题会显著提升系统性能。

Volatile

Volatile变量是一种比锁更轻量的同步机制,因为它不会产生上下文切换和线程调度。Volatile变量可以保证可见性,即变量被一条线程修改后,其他线程都会读取到最新值,不过volatile变量也有自身的限制:它不支持原子操作,当变量的更新依赖其他变量或自身时(比如i++),volatile不能保证结果的正确性。

最近在项目中需要封装Memcache客户端,实现双机热备和自动切换。这里设置一个变量currentCluster来表示当前使用的是主集群还是备用集群,业务请求根据此变量来判断优先访问的集群,同时有1条守护线程不断轮询2个集群,一旦发现有机器不可用,立刻切换,即修改变量currentCluster的值。所以currentCluster变量会被多条线程读写,且访问非常频繁,不过currentCluster变量的修改并没有依赖其他变量或自身,只需要保证可见性即可,适用与volatile变量的应用场景:

fcf9f57462df9f5f5c777cb45ea23df3.png

由于主存的访问速度相对于CPU的处理速度比较慢,所以CPU通过Cache降低内存延时的成本,编译器和CPU本身会对指令做一些优化,改变指令的执行顺序,在不改变最终结果的前提下,提高CPU Cache的命中率和CPU流水线的执行效率,此过程称为指令重排。当数据不可变或者限制在同一条线程范围内,CPU Cache和指令重排是无害的,但是如果在多核处理器,并发访问共享可变状态的场景下,不同的Cache缓存的数据可能会不一致,共享可变状态的内存操作被重新排序,这些优化会造成程序行为不定,造成共享变量的不可见性。在Java程序中加入volatile关键字可有效解决这些问题。

在C语言中对volatile关键字修饰的共享变量执行写操作的引发2件事情:

1.  将当前处理器缓存行的数据写回到系统内存

2.  这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效

JVM增强了Java中volatile关键字的语义,在访问变量时,会加入内存屏障,使得前后指令不会被重排。因此Java中的volatile关键字可以保证可见性,即共享变量被修改后,其他线程立刻可以读取到最新值。

由于volatile变量在被修改时会将CPUCache中的数据失效掉,而CPU Cache的最小执行单位是Cache Line,所以包含volatile变量的整条Cache Line的数据都会失效。这里需要注意”伪共享”问题,如果volatile变量长度不超过Cache Line,在相邻变量之间需要padding,否则会产生大量Cache Missing。这里对于CPU的细节并不展开讨论,感兴趣的同学可以阅读振辉在4月Rigel技术月刊中发表的文章《优化到 CPU ––java 与 CPU 缓存》

原子操作

上一小节中提到volatile关键字只能保证可见性,如果变量的修改依赖其他变量或自身,则volatile无能为力,此时需要实现原子性,即所有操作是不可分割的,不会被其他线程打断。在现代多核处理器中,都会提供原子指令,比如CAS(compare and swap),该指令有3个操作数:需要操作的内存地址V,预期的原有值oldValue,要写入的新值newValue。使用CAS指令执行更新操作时,如果V上的值和oldValue相同,则原子的V上的值更新为newValue,如果在当前线程读取oldValue之后,其他线程执行了更新操作,则当前线程的CAS指令返回失败。

当多条线程同时试图使用CAS指令更新同一个共享变量时,会有一条线程成功更新变量,而其他线程会失败。由于这里应没有涉及锁的操作,所以失败线程并不会被挂起,也不会阻塞,他们只是被告知这次更新操作失败,可以重试或做其他的事情。下面的一段示例代码是一个计数器的非阻塞实现,在increment的过程中,不断使用CAS操作更新变量,直到成功为止,java.util.concurrent包中的原子实现也是采用类似的机制。

e48cb3703e42e27322d289eec8a7dd14.png

利用CAS指令除了能够实现简单数据类型的原子操作(比如java.util.concurrent包中的AtomicInteger,AtomicLong)外,还能实现复杂数据类型的原子操作。实现复杂数据类型的非阻塞算法的关键在于如何在维护数据一致性时,将原子更新的范围限定在一个简单变量上。比如一个栈,每个元素Node(value, next)只会指向一个其他的元素,并且每个元素也只会被一个其他的元素指向。对于push方法,会创建一个新节点指向栈顶元素top,并使用java.util.concurrent.atomic.AtomicReference 的CAS操作尝试替换top元素,如果top没有被其他线程修改,则替换成功,否则重新获取top元素,再次尝试替换,知道成功为止。

74cb4afbf71ecbf87e5ba59c40162441.png

自旋锁

当程序中需要保证多个资源或变量的一致性,无法将更新范围限定在一个变量上时,必须使用显式的锁机制,比如synchronized关键字或ReentrantLock,但如上文所述,由于会产生阻塞,这种显式锁机制的开销比较大,尤其是在高并发场景下。这里介绍一种非阻塞的显式锁机制——自旋锁。

f34415a0860b90a0cccc84b12ba072f8.png

自旋锁采用java.util.concurret包提供的AtomicBoolean类表示锁的状态,false表示没有其他线程获取当前锁,true表示当前锁已被其他线程获取。当有多条线程同时访问lock()方法,试图获取锁时,只有一条线程可以成功,其他线程会停留在while(state.get()){}循环中,只有当活动线程调用unlock()方法释放锁时,才会有另一条线程跳出while(state.get()){}循环,因为unlock方法将state设置为false。由于这里没有获取到锁的线程并没有被阻塞,所以不会有阻塞相关的开销。

一条线程成功获取锁后,所有的非活动线程都会不停的循环,竞争会非常激烈,造成CPU资源的浪费。所以可以引入让步锁的机制降低CPU的开销。

f63baff91259887abb6d357b75b45278.png

当线程获取锁失败时,会调用backoff()方法时sleep一段时间,避免多条线程同时循环,并且每条线程恢复的时间不一样,减少了竞争,降低了CPU的开销。这里让步锁的sleep时长设定非常关键,如果太短,效果不明显,如果太长,会降低系统的吞吐量。根据同步块的预计运行时长来设置比较合理。

总结

在实际应用场景中,为避免阻塞带来的开销,使用非阻塞方案解决并发问题是非常有必要的。当更新范围可以限定在一个变量上时,可以使用volatile关键字或原子操作。如果需要保证多个资源或变量的一致性,则可以考虑自旋锁,不过对于同步块执行时间较长或执行时间长度差距较大的场景,并不适合使用自旋锁,因为很难避免CPU的过度开销,所以这种场景下,不妨直接使用synchronized关键字或ReentrantLock。

事实上Synchronized关键字和ReentrantLock都在不同程度上实现了自旋锁,在竞争开始时,会先尝试自旋,如果能够获取锁,则直接返回,否则进入阻塞状态。不过这里的自旋时长并不可控,如果已经确定同步块会执行比较快(一般来说没有IO和复杂计算),直接使用自旋锁效果会更佳。关于synchronized关键字和ReentrantLock的内部实现原理,后面会专门写文章详细讨论。

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

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

相关文章

Java垃圾收集器:G1GC何时将CMS强制退出?

在针对JDK 9(2017/4/4)提出的JEP中 , Mark Reinhold写道JEP 291 (“弃用并发标记扫描(CMS)垃圾收集器”)是“已被放入“建议的在讨论和审查后,由所有者将其定位为目标”。 如果JEP 2…

模块简介/模块的导入/模块的查找顺序/绝对导入和相对导入/软件开发目录规范...

一.模块的简介 什么是模块: 模块就是一系列功能的结合体 模块的三种来源: 1.内置的 2.第三方的  3.自定义的 模块的四种表现形式: 1.使用python编写的py文件(也就意味着py文件也可以称之为模块:一个py文件也可以称之为一个模块)(***)    2.已被编译为共享库或DLL的C或C扩展…

java dll 乱码_java调用c++ dll出现中文乱码 | 学步园

最近的开发用到了使用java调用本机动态连接库的功能,将文件路径通过java调用C代码对文件进行操作。在调用中如果路径中包含有中文字符就会出现问题,程序运行就会中止。下面用一个小例子,来说明记录下解决的方法。java中传入一个字符串&#x…

每日一问:Android 滑动冲突,你们都是怎样处理的

坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过。 在 Android 开发中,滑动冲突总是我们一个无法避免的话题。而对于解决方案却是众说纷纭。比…

java多线程 cpu分配_java多线程总结(转载)

Java 多线程编程总结-------------------------------------------------------------------------------------------------下面的内容是很早之前写的,内容不够充实,而且是基于Java1.4的内容,Java5之后,线程并发部分扩展了相当多…

java设计模式迭代器模式_迭代器模式和Java

java设计模式迭代器模式大家好,在本文中,我们将检查Iterator Pattern 。 我知道你们中许多人已经使用过一种设计模式,但是也许您没有意识到它是模式,或者不知道它的巨大价值。 根据《 Head First Design 》一书: 迭代…

笔记_SQLite入门

1、SQLite是什么? QLite没有单独的服务器进程。 它直接读取和写入普通磁盘文件。 具有多个表,索引,触发器和视图的完整SQL数据库包含在单个磁盘文件中。2、SQLite语法 区分大小写: SQLite不区分大小写。但是,有一些区分…

java线程提交_从Java线程到线程池

线程模型线程模型分为两类,用户级线程(ULT)和内核级线程(KLT)用户级线程(ULT):user level threads,系统内核对ULT无感知,线程的创建和调度都由用户级APP进程管理;即APP自行管理的线程,就是用户级线程内核级…

使用Spring Boot自动发布和监视API

如果您正在沿着微服务风格的架构前进,那么您将需要接受的一个租户就是自动化。 这种架构风格介绍了许多活动部件。 如果成功,您的环境将具有大量服务API,企业可以将其用于应用程序开发和集成。 这意味着必须有一种方法可以发现可用的API文档…

mysql导入导出

导出1.将数据库mydb导出到e:\mysql\mydb.sql文件中:打开开始->运行->输入cmd 进入命令行模式c:\>mysqldump -h localhost -u root -p mydb >e:\mysql\mydb.sql然后输入密码,等待一会导出就成功了,可以到目标文件中检查是否成功。…

mysql 半同步关闭_MySQL的半同步模式配置

1、什么是半同步?在有一台主服务器、多台从服务器的情况下,主服务器只会等待一台从服务器同步数据。2、为什么要使用半同步?在使用同步模式时,数据的写速度太慢。在使用异步模式时,可能会造成从服务器上的现在存储的数…

GIS集成技术之二:数据集成

一。数据驱动。疑惑:ado.net作为一种通用的数据技术,适用与各种格式的文件各种格式的数据库,为一种通用的编程模型。GIS也提出数据集成:毫无疑问,不陷于某击中数据库。因此,我觉得GIS的中心是数据驱动&…

mysql中的replication_mysql中replication的相关问题总结

首先,要使用mysql中的replication,则需要配置master的my.cnf。例如:复制代码 代码示例:server-id 1log_bin mysql-bin#log_bin /var/log/mysql/mysql-bin.logexpire_logs_days 10max_binlog_size 100M#binlog_do_db include_database_na…

rest spring_带有Spring的REST的ETag

rest spring1.概述 本文将重点介绍ETag – Spring支持,RESTful API的集成测试以及带有curl的使用场景。 这是关于使用Spring 3.1和Spring Security 3.1和基于Java的配置来建立安全的RESTful Web服务的系列文章的第9篇。 REST with Spring系列: 第1部分 …

MySQL最早版本源码_MySQL旧版本升级为新版本

一:从版本3.23升级到4.104.10新特性:支持事务处理和存储过程升级过程的注意事项:1:升级采用直接复制安装文件的方法,简单实用,但要注意相关的配置更改; 2:直接复制备份的数据库不能通过复制的方法恢复&…

关于国产科学软件的思考(转)

转自 http://blog.pfan.cn/eMath/24264.html 作者 爱数学 http://blog.pfan.cn/eMath 原文如下: 这一段一直在问自己这个问题:“在我们常用的科学计算软件当中,有多少国产的?”,在记忆里面搜索了很久,似乎…

每个私有静态方法都是新类的候选人

您是否有私有的静态方法来帮助您将算法分解为更小的部分? 我做。 每次编写新方法时,我都会意识到它可以是一个新类。 当然,我不会从所有课程中选修课程,但这是目标。 私有静态方法不可重用,而类则可重用,这…

448. Find All Numbers Disappeared in an Array

一、题目 1、审题  2、分析 一个整形数组!元素值为 1~size; 出现的元素次数为 1~2 次。 求出所有 1~size 中没有出现的整数! 二、解答 1、思路 由于元素值都为 1~size之间。所以可以将数组下标 index 进行联系。 ① 遍历数组,将出现的元素值…

枚举:如何正确使用name()和toString()方法

两种方法之间的区别? Java枚举有两种方法来检索枚举常量的值: name()和.toString() 。 toString()方法调用name()方法,该方法返回枚举常量的字符串…

gradle maven_Gradle – Maven的观点

gradle maven正如我博客的读者所知道的, 我有点像Maven迷 。 我从2007年8月左右开始使用Maven,从没有回过头。 但是,与其他所有情况一样,“变化是唯一的常数”。 现在这个领域还有其他参与者,Gradle似乎是最有前途的。…