目录
三、原子类利用CAS实现单个变量无锁线程安全,底层是如何做到的?
3.2 Java中通过Unsafe类操作cpu底层原语实现硬件保证
前置
讲解CAS之前,先要知道一下原子类
也就是java.util.concurrent.atomic包下的这些类,几乎都是CAS思想的落地实现!
它们可以保证单个变量上的无锁线程安全。
没有CAS之前
多线程环境下不使用原子类,如何保证线程安全?
是通过加锁实现的。
可以看到,到目前为止,我们都是通过加锁实现并发线程安全的。当然我们也可以使用上volatile关键字,保证可见性和有序性。提高并发量。但是前面也提到了,volatile变量不适合参与到依赖当前值的运算。也即volatile不能满足原子性,所以也必须使用锁。
但是synchronized属于重量级的锁,加锁操作它会严重影响到并发量。
那么我们不使用重量级锁,如何做呢?
CAS
下面这个例子:
使用原子类,直接就可以保证单个变量的线程安全,它类似我们的乐观锁思想。
一、什么是CAS
即Compare and Swap,比较并交换。是实现并发算法时常用到的一种技术。
它包含了三个操作数:
- 内存位置
- 预期原值
- 更新值
执行CAS操作的时候,将内存位置的值与预期原值比较。
如果匹配,那么处理器会自动将该位置的值更新为新值。
如果不匹配,处理器不做任何处理,多个线程同时执行CAS操作只有一个会成功!
举个例子:
并发的A、B、C三个线程,要来操作主物理内存中的某个变量的值5。进行++操作。
A线程先将主物理内存的值5读取到,作为它的预期原值。然后进行++操作,得到更新值6。
接着就要进行比较并交换了那主物理内存的值和当前的预期原值进行比较。
如果相同则修改成功。
但是在这个,线程B和线程C抢先完成了这两轮CAS。
线程B通过CAS完成后,将主物理内存的值改为了6。
线程C通过CAS完成后,将主物理内存的值改为了7。
此时线程A进行比较的时候,发现主物理内存的值为7,和它的预期原值5不匹配。
于是线程A开始重试。再次将主物理内存的7写入它的预期原值,然后再++操作得到更新值8。然后又继续进行比较。
如果比较的时候,其他线程没有操作,那么它就更新成功。
否则就继续重试。这种重试的行为就叫自旋。其实底层就是不断的循环判断。
二、代码演示
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(
"比较并交换是否成功? " +
atomicInteger.compareAndSet(5, 2022) +
" 当前的值为:" +
atomicInteger.get()
);
System.out.println(
"比较并交换是否成功? " +
atomicInteger.compareAndSet(5, 2022) +
" 当前的值为:" +
atomicInteger.get()
);
}
}
可见,预期原值一定要和主物理内存的值一致才会修改成功。
那么它底层是如何做到的呢?
三、原子类利用CAS实现单个变量无锁线程安全,底层是如何做到的?
3.1 硬件保证
CAS是JDK提供的非阻塞原子性操作。它通过硬件保证了比较-更新的原子性。
它直接是CPU的原子指令,不会造成所谓的数据不一致问题。
换句话说,CAS的原子性实际上是CPU实现独占的。
3.2 Java中通过Unsafe类操作cpu底层原语实现硬件保证
总结
原子类之所以能实现单个变量无锁线程安全,靠的就是CAS思想。(也即比较并交换)
而这种思想落地实现靠的就是UnSafe类,对cpu原语级别的汇编操作。直接是执行的CPU的原子指令,不会造成数据不一致的问题。
但是工作中不会直接使用UnSafe类,它容易导致内存混乱。
四、原子引用AtomicReference
我们前面提到的这些原子类,通过看Java的api发现,并没有提供多少原子类。主要就是原子整型、原子布尔类型这些。
假如我们要想自定义实现怎么办?比如AtomicBook、AtomicOrder
那么可以通过AtomicReference类来自定义原子类。
代码演示
import com.hssy.sqldemo.entity.User;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<>();
User user1 = new User();
user1.setId(2);
user1.setName("高启强");
user1.setAge(35);
User user2 = new User();
user2.setId(7);
user2.setName("陈书婷");
user2.setAge(32);
atomicReference.set(user1);
System.out.println(
"比较并交换是否成功? " +
atomicReference.compareAndSet(user1,user2) +
" 当前的值为: " +
atomicReference.get()
);
System.out.println(
"比较并交换是否成功? " +
atomicReference.compareAndSet(user1,user2) +
" 当前的值为: " +
atomicReference.get()
);
}
}
五、自旋锁
前面讲CAS理论的时候就讲到了自旋。
比较并交换的值如果不匹配,要么停止,要么就自旋。
例如下面这个原子类的自增的方法就是自旋。
自己实现一个自旋锁
题目:
并发两个线程,A线程抢占锁,执行代码5s之后,释放锁。然后B线程获取到锁,执行B线程的任务。
要求:不能使用synchronized,lock这些重量级锁。
上代码:
其实就是使用CAS思想。利用原子引用,自己造一个原子类。
package com.hssy.sqldemo.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class MyLock {
private AtomicReference<Thread> atomicReference = new AtomicReference();
public void lock(){
Thread currentThread = Thread.currentThread();
while (!atomicReference.compareAndSet(null,currentThread)){
}
}
public void unlock(){
Thread currentThread = Thread.currentThread();
atomicReference.compareAndSet(currentThread,null);
}
}
class MyLockTest{
public static void main(String[] args) {
MyLock myLock = new MyLock();
new Thread(()->{
myLock.lock();
System.out.println(Thread.currentThread().getName()+ "线程拿到了锁,预计五秒后执行完成");
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+ "线程执行任务完成");
myLock.unlock();
System.out.println(Thread.currentThread().getName()+ "线程解锁了");
},"A").start();
// 睡300ms,保证线程A先于线程B启动,更好看到效果
try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
new Thread(()->{
myLock.lock();
System.out.println(Thread.currentThread().getName()+ "线程拿到了锁");
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+ "线程执行任务完成");
myLock.unlock();
System.out.println(Thread.currentThread().getName()+ "线程解锁了");
},"B").start();
}
}
六、CAS是缺点
CAS的好处,到这里应该清楚了。就是避免了synchronized这样重量级的锁!
那CAS的缺点也很明显:
- 循环时间如果长了,可能给CPU带来很大的开销
- 引来了ABA问题
七、CAS缺点之ABA问题
7.1 ABA问题模拟
package com.hssy.sqldemo.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(59);
new Thread(()->{
atomicInteger.compareAndSet(59,100);
// 模拟线程A犹豫了一下
try {TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}
// 线程A又把数据改回去了
atomicInteger.compareAndSet(100,59);
},"A").start();
new Thread(()->{
// 为了保证A线程先执行完成,先稍微停一下
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(
atomicInteger.compareAndSet(59,10086) +
"\t" +
atomicInteger.get()
);
},"B").start();
}
}
我们发现线程B修改成功了,但是我们不知道线程B之前有人搞了小动作,线程A偷偷操作了然后又改回去。
7.2 解决ABA问题
比较要加上版本号一块比较。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"线程,首次版本号:" + stamp);
// 先暂停500ms,保证线程B进来,拿到的初始值和初始版本号一样
try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"线程,二次版本号:" + stampedReference.getStamp());
// 线程A操作完了,又改回去了
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"线程,三次版本号:" + stampedReference.getStamp());
},"A").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"线程,首次版本号:" + stamp);
// 为了保证A线程先执行完成,先稍微停一下
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(
stampedReference.compareAndSet(100,2023,stamp,stamp+1) +
"\t" +
stampedReference.getReference() +
"\t" +
stampedReference.getStamp()
);
},"A").start();
}
}