不使用原子类保证 i++ 线程安全
使用原子类保证i++线程安全
什么是CAS
CAS底层原理? UnSafe类+CAS思想[自旋锁]
①. CAS的全称为Compare-And-Swap ,它是一条CPU并发原语,比较工作内存值(预期值)和主物理内存的共享值是否相同,相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。这个过程是原子的
(AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升)
②. CAS并发原语提现在Java语言中就是sun.misc包下的UnSafe类中的各个方法.调用UnSafe类中的CAS方法,JVM会帮我实现 CAS汇编指令. 这是一种完全依赖于硬件 功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题
工作中不建议使用UnSafe类 可能会导致内存混乱
③. 关于unSafe.getAndIncrement()方法的分析
④. 代码展示
/*
* CAS:Compare and swap [比较并交换]
* */
public class AtomicIntegerDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
//true 2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
//false 2019
System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
}
}
CAS如何保证原子性
CAS原理 以及 其底层UnSafe类
解释
一个线程 从主内存中读取并拉取共享变量 v , 并在自己的工作内存形成一个变量副本A 然后对这个A进行操作 ,最终形成一个新值B ,线程将新值B写入主内存之前,会将现在主内存中的变量V 与线程中的变量副本A进行比较,如果相等,则将新值B更新共享变量V的值,如果不相同不进行更新,然后重复上面步骤。
UnSafe类
①. 是CAS的核心类,由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作依赖于UnSafe类的方法
注意:UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
-
②. 变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的
③. 变量value和volatile修饰,保证了多线程之间的可见性
UnSafe类底层汇编源码分析
我们都知道UnSafe 底层调用的方法均为native修饰 所以直接才OpenJdk 查看源码
下载 openJdk
Java Platform, Standard Edition 8 ReferenceImplementations
https://jdk.java.net/java-se-ri/8-MR3
atomicReference.getAndIncrement()原理
getAndIncrement() 其底层是调用 UnSafe的getAndAddInt()
汇编源码分析
cmpxchg 底层
总结
使用AtomicReference类来自定义泛型,来实现原子性
@Getter
@ToString
@AllArgsConstructor
class User
{
String userName;
int age;
}
/**
* @auther zzyy
* @create 2022-02-24 14:50
*/
public class AtomicReferenceDemo
{
public static void main(String[] args)
{
AtomicReference<User> atomicReference = new AtomicReference<>();
User z3 = new User("z3",22);
User li4 = new User("li4",28);
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
}
}
自旋锁与CAS
使用AtomicReference类来实现自旋
package com.bilibili.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 题目:实现一个自旋锁,复习CAS思想
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo
{
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock()
{
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void unLock()
{
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t"+"----task over,unLock...");
}
public static void main(String[] args)
{
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.unLock();
},"A").start();
//暂停500毫秒,线程A先于B启动
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
},"B").start();
}
}
CAS 缺点
①. 循环时间长开销很大
- 我们可以看到getAndInt方法执行时,有个do while
- 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
②. 只能保证一个共享变量的原子性
- 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
- 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
③. ABA问题的产生
(比如一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没问题的)
④. ABA问题解决方案
①. ABA问题解决方案是使用 AtomicStampedReference 每修改一次都会有一个版本号
如何使用AtomicStampedReference
package com.bilibili.juc.cas;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicStampedReference;
@NoArgsConstructor
@AllArgsConstructor
@Data
class Book
{
private int id;
private String bookName;
}
/**
* @auther zzyy
* @create 2022-02-24 17:34
*/
public class AtomicStampedDemo
{
public static void main(String[] args)
{
Book javaBook = new Book(1,"javaBook");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook,1);
System.out.println(stampedReference.getReference()+"\t"+stampedReference.getStamp());
Book mysqlBook = new Book(2,"mysqlBook");
boolean b;
b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
b = stampedReference.compareAndSet(mysqlBook, javaBook, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
}
}
②. 注意:AtomicStampedReference用来解决AtomicInteger中的ABA问题,该demo企图将integer的值从0一直增长到1000,但当integer的值增长到128后,将停止增长。出现该现象有两点原因:
使用int类型而非Integer保存当前值
Interger对-128~127的缓存[这个范围才有效,不在这个范围comareAndSet会一直返回false
首先ABA 如何产生代码如下
public class ABADemo
{
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args)
{
new Thread(() -> {
atomicInteger.compareAndSet(100,101);
try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
atomicInteger.compareAndSet(101,100);
},"t1").start();
new Thread(() -> {
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicInteger.compareAndSet(100, 2022)+"\t"+atomicInteger.get());
},"t2").start();
}
}
解决ABA问题代码如下
package com.bilibili.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @auther zzyy
* @create 2022-02-25 11:40
*/
public class ABADemo
{
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args)
{
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
//暂停500毫秒,保证后面的t4线程初始化拿到的版本号和我一样
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()+"\t"+"2次流水号:"+stampedReference.getStamp());
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+"3次流水号:"+stampedReference.getStamp());
},"t3").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
//暂停1秒钟线程,等待上面的t3线程,发生了ABA问题
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
boolean b = stampedReference.compareAndSet(100, 2022, stamp, stamp + 1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t"+stampedReference.getStamp());
},"t4").start();
}
}