二:并发三大特性

JMM

由来

JMM(Java Memory Model)。不同硬件和不同操作系统在内存上是存在差异的,java为了解决相同代码在不同平台上出现的差异或者问题,使用JMM-Java内存模型来解决这些差异。使并发编程能够跨平台。

Java内存模型是一种抽象的概念,并不真实存在,定义了Java程序在各种平台下对内存访问的机制及规范。

介绍

JMM规定所有的变量都会存储在主内存中。在操作的时候,需要从主内存中复制一份到线程内存中(CPU内存),在线程内部做计算,最后写回主内存中(不一定能写进去)。

在JMM中,所有的共享变量都存储在主内存中,每个线程都是将主内存中的共享变量拷贝一份在副本中,到线程的内存中,修改后,同步到主内存中。

线程之间的共享变量存储在主内存(main memory)中
每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
线程

JMM原子操作图

JMM数据原子操作

具体流程解释:

lock (锁定) : 将主内存变量加锁,表示为线程独占状态,可以被线程进行read
read(读取) :线程从主内存读取数据
load(载入):将上一步线程从主内存中读取的数据,加载到工作内存中
use(使用):从工作内存中读取数据来进行我们所需要的逻辑计算
assign(复制):将计算后的数据赋值到工作内存中
store(存储):将工作内存的数据准备写入主内存
write(写入):将store过去的变量正式写入主内存
unlock(解锁):将主内存的变量解锁,解锁后其他线程可以锁定该变量

原子性

什么是原子性

一个原子性的操作是不可分割、不可被中断的,要不执行成功,要不全部失败。

原子性代码测试(i++)

package com.xqm.juc.threeCharacter;

import java.util.concurrent.CountDownLatch;

public class Test03 {

    private static int count=0;


    private static void incre(){
        try {
            Thread.sleep(10);
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                incre();
            }
        });

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                incre();
            }
        });

        thread.start();
        thread1.start();

        try {
            thread.join();
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 发现每次执行的结果不一样,因为不是原子性的
        System.out.println(count);

    }
}

package com.xqm.juc.threeCharacter;

import java.util.concurrent.CountDownLatch;

public class Test02 {

    private static int count=0;

   private static CountDownLatch countDownLatch=new CountDownLatch(200);

    private static void incre(){
        try {
            Thread.sleep(10);
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            new Thread(() -> {
              incre();
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 发现每次执行的结果不一样,因为不是原子性的
        System.out.println(count);

    }
}

怎么保证原子性

synchronized、CAS、lock锁、ThreadLocal

synchronized

i++操作不是原子性,操作时分为三个步骤:

1.从主内存中获取数据保存在CPU的寄存器中

2.在寄存器中进行+1操作

3.将结果写回主内存中

package com.xqm.juc.threeCharacter;


/**
 *synchronized
 */
public class Test04 {

    private static  int count=0;


    private  static void incre(){
        // 加锁,实现
        synchronized (Test04.class){
            count++;
        }
    }


    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                incre();
            }
        });

        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 返回结果都是200
        System.out.println(count);

    }
}

CAS

compare and swap 。比较和交换,是一条CPU的并发原语,采用的是乐观锁。不是java层面的。

因为是CPU级别的,因此效率比synchronized要高,synchronized是内核级别的。

过程:在修改属性之前,获取属性的值和地址。在本地Thread内存修改完属性的值后,要写入主内存,这时候再次获取原地址上的值,与先前获取的值进行比较,看是否有修改(比较操作),如果没有修改的话,那么就将新值写入主内存(替换操作)。

java中基于Unsafe的类提供了对CAS的操作方法,JVM会帮助实现CAS的汇编指令。

但是:CAS只实现了比较和交换,在获取原值的操作,需要自己去实现。

package com.xqm.juc.threeCharacter;


import java.util.concurrent.atomic.AtomicInteger;

/**
 *synchronized
 */
public class Test05 {

    private static AtomicInteger count=new AtomicInteger(0);



    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                count.incrementAndGet();
            }
        });

        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 返回结果都是200
        System.out.println(count);

    }
}

CAS问题

1.CAS只能保证对一个变量的操作是原子性,无法对多行代码实现原子性

2.CAS可能出现ABA问题(使用版本号来解决),使用AtomicStampedReference,比较版本和原值,用来解决ABA问题

package com.xqm.juc.threeCharacter;

import java.util.concurrent.atomic.AtomicStampedReference;

public class Test06 {

    public static void main(String[] args) {
        AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA", 1);

        // 原来的值是什么
        String oldValue = reference.getReference();
        // 版本号
        int oldVersion = reference.getStamp();
        // 原值-新值-原版本号-新版本号
        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);

        // 修改失败,因为2版本的oldValue是B。可是这里是AAA,返回false
        boolean b1 = reference.compareAndSet(oldValue, "B", 2, 3);
        System.out.println(b);
        System.out.println(b1);
    }

}

CAS自旋时间过长问题:

1.可以指定CAS一共循环多少次,如果超过次数,直接判断失败。(自旋锁、自适应自旋锁)

2.可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回

lock锁

涉及并发比较多时,ReentrantLock比较好,因为synchronized有锁升级。

package com.xqm.juc.threeCharacter;


import java.util.concurrent.locks.ReentrantLock;

/**
 * synchronized
 */
public class Test07 {

    private static int count = 0;

    private static ReentrantLock lock = new ReentrantLock();


    private static void incre() {
        // 加锁,实现
        lock.lock();
        try {
            count++;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
  
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                incre();
            }
        });

        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 返回结果都是200
        System.out.println(count);

    }
}

ThreadLocal

作用:线程隔离

不操作临界资源,

package com.xqm.juc.threeCharacter;


/**
 * ThreadLocal
 */
public class Test08 {

    static ThreadLocal t1=new ThreadLocal();
    static ThreadLocal t2=new ThreadLocal();


    public static void main(String[] args) {
        // 主线程用来存
        t1.set("aaa");
        t2.set("bbb");

        //线程获取不到其他线程的数据
        new Thread(()->{
            // 输入null
            System.out.println(t1.get());
            // 输出null
            System.out.println(t2.get());
        }).start();

        // aaa
        System.out.println(t1);
        // bbb
        System.out.println(t2);
    }

}

ThreadLocal详细解析

  • 每个Thread中都存着一个成员变量,ThreadLocalMap
// Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
  • ThreadLocal本身不存数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap。
  • ThreadLocalMap是基于Entry[]实现的,因为一个线程可能有很多threadLocal,因此底层使用数组存储一个一个的ThreadLocal。
  • 每个线程都有自己的ThreadLocalMap,Entry[]数组中k是ThreadLocal,v是线程的值
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    // entry数组中,key是当前ThreadLocal,value就是线程的值
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
  • ThreadLocalMap的key是一个弱引用,在GC时会被回收

ThreadLocal的内存泄露问题:

如果ThreadLocal引用丢失,key因为弱引用会被GC回收,同时线程没被回收,就会导致value无法被回收和获取,导致内存泄露。

解决方法:在使用完ThreadLocal对象后,即使调用remove方法,移除entry就可以。

image.png

可见性

什么是可见性

可见性问题是出现在CPU中,CPU处理数据太快了,相对于CPU来说,从内存中取数据就很慢 ,因此cpu就提供了L1、L2、L3三级缓存,每次从内存中拿取数据,就会存储到三级缓存,取数据的速度就大大加快。

现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,这就导致每个线程中修改数据只会修改自己的工作内存,没有及时同步到主内存,导致数据不一致的问题。

image.png

可见性测试:

package com.xqm.juc.threeCharacter;


/**
 * 线程可见性问题
 */
public class Test09 {

    private static boolean flag=true;


    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            while (flag) {
                // ...
            }
            System.out.println("Thread1线程结束");
        }, "Thread1");

        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 这里的修改不能即使放到主内存中,导致线程会一直死循环
        flag=false;
        System.out.println("主线程将flag进行修改");

    }
}

怎么保证可见性

volatile、synchronized、final、lock

volatile

volatile是一个java关键字,能够保证线程可见性和禁止指令重排序的问题。

被volatile修饰的变量,会被告诉CPU,对当前变量的操作,不允许使用三级缓存,必须去主内存操作,每次修改完会立刻写到主内存中。

volatile的内存语义

  • volatile变量被写:一旦被修改,JMM会将当前线程对应的CPU缓存立刻刷新到主内存中
  • volatile变量被读:读一个被volatile修饰的变量时,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量

其实加了volatile修饰符,就是告诉CPU禁止使用三级缓存,变量在被汇编的时候,会追加一个lock前缀,CPU执行指令时,如果带有lock前缀,CPU会做两件事:一是将当前处理器缓存行的数据写回到主内存;二是写回的数据,在其他CPU缓存中是无效的。

package com.xqm.juc.threeCharacter;


/**
 * 线程可见性问题
 */
public class Test09 {

    // 加上volatile就可以解决可见性问题
    private volatile static boolean flag=true;
  
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            while (flag) {
                // ...
            }
            System.out.println("Thread1线程结束");
        }, "Thread1");

        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 这里的修改不能即使放到主内存中,导致线程会一直死循环
        flag=false;
        System.out.println("主线程将flag进行修改");

    }
}

synchronized

synchronized除了解决原子性的问题,还能解决可见性的问题。使用的是synchronized内存语义。

如果涉及到synchronized的同步代码块或同步方法,获取到锁资源后,内部涉及到的变量从CPU缓存中移除,从主内存中拿取数据。在释放锁资源后,会立即将缓存中的数据同步到主内存中。

package com.xqm.juc.threeCharacter;


/**
 * 线程可见性问题,使用synchronized
 */
public class Test10 {

    private  static boolean flag=true;

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            while (flag) {
		// 在拿到锁的一瞬间,会重新取得flag
                synchronized (Test10.class){
                    // ...
                }
            }
            System.out.println("Thread1线程结束");
        }, "Thread1");

        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag=false;
        System.out.println("主线程将flag进行修改");

    }
}

final

final和volatile不能同时修饰一个属性。

被final修饰的

lock

lock锁也能保证线程可见性,但是实现方式和synchronized不同,synchronized是基于内存语义,而lock锁是基于volatile实现的。

lock锁内部在获得锁和释放锁时,会对一个有volatile修饰的state变量进行加一和减一 的操作

package com.xqm.juc.threeCharacter;


import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程可见性问题,使用synchronized
 */
public class Test11 {

    private  static boolean flag=true;
    private static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            while (flag) {
                lock.lock();
                try {
                    // 业务代码
                }finally {
                    lock.unlock();
                }
            }
            System.out.println("Thread1线程结束");
        }, "Thread1");

        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 这里的修改不能即使放到主内存中,导致线程会一直死循环
        flag=false;
        System.out.println("主线程将flag进行修改");

    }
}

有序性

什么是有序性

.java文件会被编译、加载之后,交由机器来执行。在执行引擎中,为了提升执行效率,在不影响结果的前提下,会对指令进行重排序,因此java的程序时乱序的。

多线程执行乱序的指令会出现问题。

验证有序性:x和y会出现等于0的情况

package com.xqm.juc.threeCharacter;


/**
 *
 */
public class Test13 {

    static int a,b,x,y;

    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            a=0;
            b=0;
            x=0;
            y=0;

            Thread thread = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread thread1 = new Thread(() -> {
                b = 1;
                y = a;
            });

            thread.start();
            thread1.start();
  
            try {
                thread.join();
                thread1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            if (x==0 && y==0){
                System.out.println("第"+i+"次出现x=0 & y=0");
            }


        }
    }
}

第1707159次出现x=0 & y=0
第1719422次出现x=0 & y=0
第1726500次出现x=0 & y=0

DCL(double check lock)来解决创建单例时的有序性问题:

package com.xqm.juc.threeCharacter;


/**
 *  DCL double check lock
 */
public class Test14 {

    private  static Test14 instance;
  
    public static Test14 getInstance(){
        if (instance==null){
            synchronized (Test14.class){
                if (instance==null){
                    // 开辟空间--初始化--指针链接地址
                    instance=new Test14();
                }
            }
        }
        return instance;
    }

}

怎么保证有序性

as-if-serial、happens-before、volatile

as-if-serial

as-if-serial语义:需要保证单线程的程序执行结果是不变的,如果存在依赖的关系,也不可以做指令重排。

// 这种情况就不允许做指令重排
int i=0;
i++;

编译器、runtime和处理器都必须遵守as-if-serial语义

happen-before

  1. 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
  2. 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
  3. volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操
  4. happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
  5. 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
  6. 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
  7. 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
  8. 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

JMM遵守hassen-before规则来保证可见性问题。

JMM只有不出现上述八种规则的情况下,才不会触发指令重排的效果

volatile

使用volatile修饰属性,能够禁止指令重排序

内存屏障:可以将内存屏障看成一条指令