线程安全

众所周知,Java是多线程的。但是,Java对多线程的支持其实是一把双刃剑。一旦涉及到多个线程操作共享资源的情况时,处理不好就可能产生线程安全问题。线程安全性可能是非常复杂的,在没有充足的同步的情况下,多个线程中的操作执行顺序是不可预测的。
Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。
Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里不再详细介绍JMM及锁的其他相关知识。但是我们要讨论一个问题,那就是锁到底是不是有利无弊的?

锁存在的问题

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

悲观锁机制存在以下问题:
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。与锁相比,volatile变量是一个更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile不能解决原子性问题,因此当一个变量依赖旧值时就不能使用volatile变量。因此对于同步最终还是要回到锁机制上来。

乐观锁

乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS)。

CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

Java对CAS的支持

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable {  

        private volatile int value;  

    public final int get() {
return value;
} public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
} public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}

在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。这样在获取变量的值的时候才能直接读取。然后来看看++i是怎么做到的。
getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。

ABA问题

CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

总结

Java中的线程安全问题至关重要,要想保证线程安全,就需要锁机制。锁机制包含两种:乐观锁与悲观锁。悲观锁是独占锁,阻塞锁。乐观锁是非独占锁,非阻塞锁。有一种乐观锁的实现方式就是CAS ,这种算法在JDK 1.5中引入的java.util.concurrent中有广泛应用。但是值得注意的是这种算法会存在ABA问题。

乐观锁的一种实现方式—CAS的更多相关文章

  1. 多线程深入:乐观锁与悲观锁以及乐观锁的一种实现方式-CAS(转)

    原文:https://www.cnblogs.com/qjjazry/p/6581568.html 首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每 ...

  2. Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...

  3. 乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

    首先介绍一些乐观锁与悲观锁: 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.传统的关系型数据库里边就用到了很 ...

  4. [数据库事务与锁]详解八:底理解数据库事务乐观锁的一种实现方式——CAS

    注明: 本文转载自http://www.hollischuang.com/archives/1537 在深入理解乐观锁与悲观锁一文中我们介绍过锁.本文在这篇文章的基础上,深入分析一下乐观锁的实现机制, ...

  5. 乐观锁的一种实现方式——CAS

    在java里面,synchronized关键字就是一种悲观锁,因为在加上锁之后,只有当前线程可以操作变量,其他线程只有等待. CAS操作是一种乐观锁,它假设数据不会产生冲突,而是在提交的时候再进行版本 ...

  6. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

  7. 分布式锁的三种实现方式 数据库、redis、zookeeper

    版权声明: https://blog.csdn.net/wuzhiwei549/article/details/80692278 一.为什么要使用分布式锁 我们在开发应用的时候,如果需要对某一个共享变 ...

  8. 2020-05-24:ZK分布式锁有几种实现方式?各自的优缺点是什么?

    福哥答案2020-05-24: Zk分布式锁有两种实现方式一种比较简单,应对并发量不是很大的情况.获得锁:创建一个临时节点,比如/lock,如果成功获得锁,如果失败没获得锁,返回false释放锁:删除 ...

  9. redis分布式锁的几种实现方式,以及Redisson的配置和使用

    最近在开发中涉及到了多个客户端的对redis的某个key同时进行增删的问题.这里就会涉及一个问题:锁 先举例在分布式系统中不加锁会出现问题: redis中存放了某个用户的账户余额 ,例如100 (用户 ...

随机推荐

  1. Python-Flask实现基金自选网站

    代码地址如下:http://www.demodashi.com/demo/14734.html 项目介绍 本项目的基金数据来自天天基金网,但该网站用户体验较差,内容冗余,故自己实现一个轻量级网站,从8 ...

  2. 【laravel5.* + 钉钉实现WEB第三方登录】 使用redis 作为持久化存储

    1.去钉钉开发者平台>自助者工具,创建扫码登录授权应用,填写名称.描述.授权页面logo地址(这个图片最后会出现在用户扫码设备中,建议使用压缩图片减少用户加载时间).回调域名(一般都是写一个子域 ...

  3. ios中摄像头和图片调用

    推荐文章 http://www.xuanyusong.com/archives/1493 http://blog.csdn.net/ryantang03/article/details/7830996

  4. (原)torch7中指定可见的GPU

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/7418694.html 参考网址: https://gitter.im/torch/torch7/arc ...

  5. 致Play Framework开发者们的一封信

    2012/04/06  导读:3月中旬,Play Framework 2.0 正式版发布了.2.0 版本的主要新特性:内置对 Java 和 Scala 的支持.完全异步编程模型.侧重于类型安全.强大的 ...

  6. 向大家介绍15个漂亮的Ubuntu GDM主题

    没事向大家介绍几个Ubuntu GDM主题,希望大家喜欢,这些Ubuntu GDM主题是我找了很久的…… "GNOME Display Manager允许用户轻松的设定登录界面主题.网上有大 ...

  7. 开发只懂 AFN ?搞定 NSURLSession 才是硬道理

    由于傲娇的苹果在 iOS9 之后已经放弃了 NSURLConnection,所以在现在的实际开发中,除了大家常见的 AFN 框架,一般使用的是 iOS7 之后推出的 NSURLSession,作为一名 ...

  8. React(0.13) hello world

    <!DOCTYPE html> <html> <head> <title>React JS</title> <script src=& ...

  9. JSP内置对象之WEB安全性及config对象

    一.WEB-INF的安全性是最高的. 在Java EE的标准中,Web目录中的WEB-INF是必须存在的,而且此文件夹的安全性是最高的,在各个程序的开发中,基本上都将一些配置信息保存在此文件夹中.在定 ...

  10. C++的坑真的多吗?

    先说明一下,我不希望本文变成语言争论贴.希望下面的文章能让我们客观理性地了解C++这个语言.(另,我觉得技术争论不要停留在非黑即白的二元价值观上,这样争论无非就是比谁的嗓门大,比哪一方的观点强,毫无价 ...