![44d57197e1c618d602455ad47cbdd4ea.png](https://i-blog.csdnimg.cn/blog_migrate/13cdbe477a914d3bfd749b7eb8a74a0b.jpeg)
前言
jdk1.5之前所有的数据同步都是靠着sync那把OS锁,他的效率一直被人所不喜,为了提高效率,号称无锁的自旋锁横空出世,其原理就是依靠CPU原语不可被打断的特性,在发生值改变的时候先对比其期望值与真实值,从而判断出读值与赋值两个操作中间有没有其他线程修改变量,以此保证原子性。接下来让我们一点点来揭开他的面纱。
点关注不迷路,请搜索《杂讲java》微信公众号,上面会持续更新更多技术分享文章!!
正文
什么是CAS?
CAS:Compare and Swap,即先比较再交换。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
CAS原理(无锁优化 自旋)
CAS是一种无锁算法,不再需要从用户态转到内核态,跟操作系统去申请OS锁,而是在用户态,凭借着CPU原语中途不能被打断的特性,达到锁的效果。CAS这把自旋锁有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则重试或者回执失败。CAS比较与交换的伪代码可以表示为:
![fc3d2d3edff1f3f80804804da988d144.png](https://i-blog.csdnimg.cn/blog_migrate/251e223a130bec0ed2f8a655ab01b16d.png)
流程示意图如下:
![583f427a570b73127784fe7e2e5e07a5.png](https://i-blog.csdnimg.cn/blog_migrate/8e4678e17b4c12da415f2c7ad613d69d.jpeg)
问:CAS如何避免ABA问题?
答:最常见最简单的办法是可以给值添加一个版本号,虽然由A到B,B再到A,但是操作版本往后推进了两个版本,对比的时候连版本号一起检查,便会触发重试或者回执失败。
思考1:
线程a和线程b将变量vari从1递增两次,如何避免第一个线程赋值的时间处于第二个线程判断之后造成的同步失败?如图:
![b4b0fa9b8ec2c9e6c492e9cdc1e0bd79.png](https://i-blog.csdnimg.cn/blog_migrate/0eab75e97bfb1842a7f34c187c96190d.jpeg)
CAS语法为cpu原语支持,所以中途不会被打断,保证了不会出现上面的问题,即判断是否一致和赋值之间不会有其他的操作。
思考2:
CAS修改的变量是int等基础数据不会出现问题,如果是Object呢(引用数据),是否会出现问题?
不建议用引用数据类型进行CAS同步数据。同样是一个对象,可能属性变量已经修改了。
CAS算法在JDK中的应用示例
在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。Java 1.8中AtomicInteger.incrementAndGet()的实现源码如下:
![691b1e16b111a7031b4f92b19214429d.png](https://i-blog.csdnimg.cn/blog_migrate/102e9be82908ce68fd4957e950294620.png)
![246a0c2ed3c6cb61ede72efd208388a4.png](https://i-blog.csdnimg.cn/blog_migrate/7060390e12773176ded6d1721e006160.png)
注释:
cas底层是由Unsafe实现的,直接操作内存,1.9版本之后已经关闭,只能内部使用,外部可以调到,但是会报错
基于CAS的一些锁用法以及原理
1.AtomicXXX:
![ac171dd9685906cf20de7a0d0c3a85d0.png](https://i-blog.csdnimg.cn/blog_migrate/239fe8b66c76c9505e99d061ed0d2f8d.jpeg)
result:
![8d089209739c4922125eefd42f851a98.png](https://i-blog.csdnimg.cn/blog_migrate/5d9561c57a74f1a91a9f770adb0b972d.jpeg)
java.util.concurrent.atomic中的AtomicXXX都是java为了保证原子性开发的利器,如上图在10个并发的情况下,每个线程递增1000,递增使用AtomicInteger.incrementAndGet(),使得最后的结果是一致的。如果使用sync锁的情况下,必须给count++加锁,count++是非原子的。
2.ReentrantLock->可重入锁
其使用以及和sync的比较:
- lock.lock():
lock.unlock() 放在finally里面,保证会运行,否则出现异常之后就会发生死锁,sync自动释放锁。 - lock.tryLock():尝试获得锁,拿不到就放弃(返回boolean),sync拿不到锁就进入等待队列。
但是在解锁的时候需要判断一下锁定状态 - lock.lockInterruptibly();可以在调用t2.interrupt()方法的时候做出响应。
- 公平锁:
sync只有非公平锁
ReenTrantLock:true公平锁 false非公平 默认非公平锁
注释:
在公平锁中,每一次的tryAcquire都会检查队列中是否仍有前驱的元素,如果有那么等待,通过这种方式来保证先来先服务的原则;而非公平锁,首先是检查并设置锁的状态,这种方式会出现即使队列中有等待的线程,但是新的线程仍然会与排队线程中的对头线程竞争(但是排队的线程是先来先服务的),所以新的线程可能会抢占已经在排队的线程的锁,这样就无法保证先来先服务,但是已经等待的线程们是仍然保证先来先服务的,所以总结一下公平锁和非公平锁的区别:
- 公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
- 非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
2.CountDownLatch:发令枪,用法和join一样,使用方法请查看项1。
3.栅栏CyclicBarrier
用法如下:
![d0aa55c429e7f38ad685663ad4a1e2a9.png](https://i-blog.csdnimg.cn/blog_migrate/d3f8efd29c91defc084e0d0c0f5ce5d5.jpeg)
result:
![cfba78ff81426651ca9672cd3ee5ea4d.png](https://i-blog.csdnimg.cn/blog_migrate/30ba2a3b764902c41f935e354183cb7d.jpeg)
CyclicBarrier vs CountDownLatch:
CountDownLatch更加灵活一点,一个线程可以调用多次计数,但是CyclicBarrier只是计算线程数的
4.Phaser:阶段,在遗传算法的时候可以用
5.ReadWriteLock:
- 读锁:共享锁,读线程获取锁可以允许其他读线程一起读,只拒绝写线程,写线程等待
- 写锁:排他锁,写线程获取锁不允许其他线程来,读和写操作都不允许,防止产生脏数据。
读写锁的引入主要目的是优化速度,读锁可以共享,写锁为了防止脏读,设置为排他锁。为了对比速度做了一个demo,代码分两部分,第一部分为锁的使用,第二部分为锁的选择,代码如下:
![2754f48e42b12441b435fd218745b1c0.png](https://i-blog.csdnimg.cn/blog_migrate/58c5379900fefd27583d2110c55a64bf.jpeg)
锁的选择:
![7ce4116d59ced140881c9ce940e86727.png](https://i-blog.csdnimg.cn/blog_migrate/c0a687ca045ce9dd9a8d183c1a86ccbc.jpeg)
结果如下:
当使用读写锁的时候,读操作同时进行,写操作单独进行,当时用ReentrantLock的时候,读操作和写操作都是单独进行,有兴趣的可以找我要源码,自己运行一下。
扩展:
StampedLock:ReadWriteLock的升级状态,Java 8引入的新的读写锁,前面介绍的ReadWriteLock可以解决多线程同时读,但只有一个线程能写的问题。
如果我们深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。
StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
注释:
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
6.Semaphore:信号灯。
设置线程同时有多少线程可以使用,就像厕所里的坑一样,无论多少人等待,只能有特定的人数上厕所。
代码示例如下:
![c7d37826e597f251bfb300bd128d5974.png](https://i-blog.csdnimg.cn/blog_migrate/324bcfbfba2c845a6a8c3ab7d5557a92.jpeg)
7.Exchanger:线程两两交换数据,通信(只是两个线程之间)
Exchanger是线程间通信的一种方式,两个线程之间阻塞的进行数据交换,演示代码如下:
![5a5b3674098ed12a651f4c47be38202c.png](https://i-blog.csdnimg.cn/blog_migrate/6ea525e87281f32760e301bcecc36184.jpeg)
result:
![cead79431ae91c6c8d3705f91af5eaf1.png](https://i-blog.csdnimg.cn/blog_migrate/349314b154fd0729f206f57e6eace8ad.png)
8.LockSupport:
lockSupport是对线程的一个控制,优点:
- 不用加锁再进而控制线程的阻塞与唤醒
- 当多个线程阻塞的时候,可以唤醒指定的某个线程,这个在通过锁控制的时候是比较麻烦的。
演示代码如下:
![f841a884fabc8628c96ea1007ba73936.png](https://i-blog.csdnimg.cn/blog_migrate/91daab63c66cd1529fd32b4df771430e.jpeg)
result:
![7864668580e7af7db857191f6adbddd8.png](https://i-blog.csdnimg.cn/blog_migrate/60e398fd6b7df184c066691d86e24f88.png)
9.LongAdder:
分段锁(线程比较多比较有优势)同样是cas操作,这个保证变量原子性的关键字设计的初衷是为秒杀等并发高,吞吐大的场景准备的。同样对于AtomicInteger、LongAdder甚至使用sync这把锁的选型的时候,按照场景需求不同,用户并发量不同,需要根据压测做出最优的选择。
![a9b033e7580e4bfecaf4535532a3368c.png](https://i-blog.csdnimg.cn/blog_migrate/4e772547972efd503a616a2bc8262cad.png)
点关注不迷路,请搜索《杂讲java》微信公众号,上面会持续更新更多技术分享文章!!