前言:

JUC编程基础性阶段总结第一篇:关于JMM内存模型,volatile底层原理,原子类得底层原理,与源码解读。

JUC(java.util.concurrent)

  • 进程和线程
    • 进程:后台运行得程序(一个打开的软件,就是进程)
    • 线程;轻量级的进程,一个进程包含多个线程(一个软件内,同时运行的模块,就是线程)
  • 并发和并行
    • 并发:同时访问某个东西,就是并发
    • 并行:一起做某些事情,就是并行

一.JMM

JMM时Java内存模型,是一种抽象得概念,实际上并不存在,实际上他描述得是一种规范,定义了程序中各个变量(包括实例字段,静态字段和构成数组对象得元素)得访问方式

1. JMM关于同步得规定

  1. 线程解锁前,必须把共享变量得值刷新回主内存
  2. 线程解锁前,必须读取主内存得最新值,到自己得工作内存
  3. 加锁和解锁是同一把锁

解释:由于JVM运行程序得实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,每个内存是线程私有的数据区域,而Java内存模型中规定所有的变量都存储在主内存,主内存是内存共享得内存区域,所有线程都可访问,但线程对变量的操作(读取赋值)必须在自己的工作内存中进行,首先要将变量从主内存拷贝一份副本到自己的工作内存空间,然后对变量进行操作,操作后要将变量写回主内存。因此不同得线程间无法访问对方得工作内存空间,线程间的通信必须通过主内存来实现

2. JMM的三大特性

  1. 原子性
    • 一个操作是不可分割的,不能执行一半就不执行了
  2. 可见性
    • 某个线程对变量进行修改,会立即通知其他线程更新变量值
  3. 有序性
    • 指令有序,不会造成指令重排

二.Volatile

Volatile时Java虚拟机提供的轻量级同步机制(丐版synchoized)

  • 保证可见性
  • 不保证原子性
  • 防止指令重排

1.可见性

class MyData{
int number=0;
//volatile int number=0;

AtomicInteger atomicInteger=new AtomicInteger();
public void setTo60(){
this.number=60;
}

//此时number前面已经加了volatile,但是不保证原子性
public void addPlusPlus(){
number++;
}

public void addAtomic(){
atomicInteger.getAndIncrement();
}
}

//volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
private static void volatileVisibilityDemo() {
System.out.println("可见性测试");
MyData myData=new MyData();//资源类
//启动一个线程操作共享数据
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try {TimeUnit.SECONDS.sleep(3);myData.setTo60();
System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();}
},"AAA").start();
while (myData.number==0){
//main线程持有共享数据的拷贝,一直为0
}
System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number);
}
可见性测试
AAA come in
AAA update number value: 60

虽然一个线程将number改成了60,但是main线程中的工作内存中的值仍为0

对number进行volatile修饰,运行结果如下

AAA	 come in
AAA update number value: 60
main mission is over. main get number value: 6

不难发现一个线程对number修改,会立刻刷新到主内存上,通知其他线程值修改

2.原子性

volatile不能保证操作得原子性,这是因为,比如一条number++操作,将它进行反编译,会形成三条指令

getfield        //读
iconst_1 //++常量1
iadd //加操作
putfield //写操作

一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

如何解决原子性问题?

  1. 对addPlus()方法加锁
  2. 使用java.util.concurrent.AtomicInteger

测试

private static void atomicDemo() {
System.out.println("原子性测试");
MyData myData=new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);
System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger);
}

结果

原子性测试
main int type finally number value: 17542
main AtomicInteger type finally number value: 20000

3.Volatile可见性与有序性原理

​ volatile关键字之所以可以保证可见性与有序性是依靠于内存屏障实现的,对于可见性来说,通过lock前缀指令使当前处理器缓存行的数据写回到系统内存,同时这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。对于有序性来说,JVM是通过在对volatile变量的写指令后插入写屏障保证在写屏障之前对共享变量的改动都会刷新到主内存中,而读屏障是在对volatile变量的读指令之前插入的,保证了在读取到的volatile变量是主内存中的最新数据。写屏障会保证在写屏障之前的代码不会排在写屏障之后,读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。

4.happen-before规则

​ happen-before规定了对共享变量的写操作对其他线程的读操作可见,它是可见性与有序性的一套规则总结,但JMM并不能保证一个线程对共享变量的写排在读操作之后

  • 线程解锁m前对共享变量的写,对解锁后其他线程对m加锁对共享变量的读

    static int x;
    static Object m = new Object();
    new Thread(()->{
    synchronized(m) {
    x = 10;
    }
    },"t1").start();
    new Thread(()->{
    synchronized(m) {
    System.out.println(x);
    }
    },"t2").start();
  • 线程对volatile共享变量的写,对其他线程对共享变量的读可见

    volatile static int x;
    new Thread(()->{
    x = 10;
    },"t1").start();
    new Thread(()->{
    System.out.println(x);
    },"t2").start();
  • 线程开始前对共享变量的写,对线程开始后对该共享变量的读可见

    static int x; x = 10;
    new Thread(()->{
    System.out.println(x);
    },"t2").start();
  • 线程结束前对该共享变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)

    static int x;
    Thread t1 = new Thread(()->{
    x = 10;
    },"t1");
    t1.start();
    t1.join();
    System.out.println(x);

三.DCL单例

public class SingletonDemo {
// private static SingletonDemo singletonDemo=null;
private static volatile SingletonDemo singletonDemo=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
}
//DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
public static SingletonDemo getInstance(){
if (singletonDemo==null){
synchronized (SingletonDemo.class){
if (singletonDemo==null){
singletonDemo=new SingletonDemo();
}
}
}
return singletonDemo;
}

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i+1)).start();
}
}
}

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

instance=new SingletonDemo();可以大致分为三步

memory = allocate();     //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址

其中2,3没有数据依赖关系,可能发生指令重排,有可能instance不为null但是还没有初始化,解决方法就是对singletondemo对象添加上volatile关键字,禁止指令重排

四. CAS

CAS是指Compare And Swap比较并交换,是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data : "+ atomicInteger.get());
//修改失败
System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get());
}
}

第一次修改,期望值为5,主内存也为5,修改成功,为2019。第二次修改,期望值为5,主内存为2019,修改失败。

查看AtomicInteger.getAndIncrement()方法,发现其没有加synchronized也实现了同步。这是为什么?

CAS底层原理

AtomicInteger内部维护了volatile int valueprivate static final Unsafe unsafe两个比较重要的参数。

public final int getAndIncrement(){
return unsafe.getAndAddInt(this,valueOffset,1);
}

AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

public final int getAnddAddInt(Object var1,long var2,int var4){
int var5;
do{
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}

这个方法的var1和var2,就是根据对象偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

CAS缺点

CAS实际上是一种自旋锁,

  1. 一直循环,开销比较大。
  2. 只能保证一个变量的原子操作,多个变量依然要加锁。
  3. 引出了ABA问题

五.ABA问题

所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。 有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用。

AtomicReference

AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。

User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false

AtomicStampedReference

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);