一. 并发编程问题的源头-原子性、可见性、有序性

1.如何理解线程安全

当多个线程访问某个对象时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为.那么就称这个类是线程安全的.
mark

2.可见性问题的源头

mark
如图所示,线程A操作CPU1上的缓存,线程B操作CPU2上的缓存,线程A对变量X的操作对于线程B而言是不可见的.
可见性示例代码:
public class VisableDemo {
    //stop对于main线程而言是不具有可见性的 ,因此下面线程并不能通过stop来停止
    public static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int i = 0;
            while (!stop){
                i++;
            }
            System.out.println("Result:"+i++);
        });
        thread.start();
        System.out.println("线程开始执行");
        Thread.sleep(1000);//使线程休眠阻塞 切换到mian的主线程
        stop = true;//主线程中修改stop
    }

}

3.原子性问题的源头:

CPU对运行中线程的调度切换会带来原子性问题:
mark
public class AtomicDemo {

    //定义一个成员变量
    public static int count = 0;

    //定义一个方法
    public static  void incr(){
        try {
            //使线程睡眠1毫秒,这样可以模拟出线程来回调度切换
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        //构建1000个线程来调用incr方法
        for (int i = 0; i < 1000 ; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        //设置睡眠,因为线程是异步的,这样所有线程都能执行完毕main主线程才结束
        Thread.sleep(4000);
        //最终的执行结果是<=1000 并且可能每一次都不一样
        System.out.println(count);
    }
}
mark
(1)上段代码因为设置了睡眠1毫秒,所以在运行时CPU会进行调度切换,而如果刚好在线程A读取到Count的值 0 ,在未进行count++操作前线程阻塞CPU切换至线程B,此时count的值还未被线程A操作,线程B便拿到Count=0 并进行Count++操作,操作完毕后Count=1,线程B执行完毕,CPU重新切回线程A,A线程此前拿到的Count值为0,继续操作Count++,执行后的结果为Count=0,便操作成了结果的不正确性,这就是原子性问题.

总结:导致以上问题的根本性原因是硬件层面的CPU缓存和重排序.

3.有序性问题的根源

mark
JAVA 源代码到最终执行的阶段会经过多种重排序,这些重排序的优化会导致多线程情况下出现结果的不正确性.
mark
CPUA和CPUB的代码按照正常理解执行结果是 X = 2 y = 1 ,但在实际的执行过程中,因为重排序的原因,A1 A2 B1 B2 的先后执行顺序会有变化,在多线程情况下又有可见性问题,因此最终结果是不一定的,处理器允许执行后得到结果 x=y=0.

二.JAVA内存模型 Java Memory Model

JAVA内存模型(JMM)是一种抽象结构,它提供了合理的禁用缓存以及禁止重排序的方法来解决可见性、有序性问题
mark
java内存模型并不是通过直接禁用硬件的重排序和缓存来解决多线程的问题,而是提供了一些关键等在软件层面来解决多线程的安全问题.

1.可见性、有序性的解决方案

Volatile 、synchronized 、 final 关键字;

Happens-Before原则;

2.同步关键字 synchronized

(1).synchronized 的作用
可以解决可见性、原子性 、有序性问题;
(2).synchronized 锁的范围
对于普通同步方法.锁是当前实例对象;
对于静态同步方法,锁是当前类的Class对象;
对于同步方法快,锁是synchronized 括号里配置的对象
package com.yzf.demo.demo.Thread;

/**
 * Created by 于占峰 on 2020/3/31/031.
 * ##### 1.synchronized 的作用
 * ######     可以解决可见性、原子性 、有序性问题;
 * ##### 2.synchronized 锁的范围
 * ######     对于普通同步方法.锁是当前实例对象;
 * ######     对于静态同步方法,锁是当前类的Class对象;
 * ######     对于同步方法快,锁是synchronized 括号里配置的对象
 */
public class synchronizedDemo {

    //方法修饰 对象锁 同一个对象有有效
    public synchronized void demo() {

    }

    /**
     * 代码块的好处  只包裹当前存在线程安全问题的代码 括号内指定要锁定的对象或class类
     * 只对括号内的代码有效 也可以实现对象锁 和 类级别的锁
     */
    public void demoMethod() {
        synchronized (this) {

        }
    }

    //锁的是class类
    public void demoMethod2() {
        synchronized (synchronizedDemo.class) {

        }
    }

    //静态方法锁 类级别的锁
    public synchronized static void syncDemo() {

    }

    public static void main(String[] args) {
        //对象锁只在同一对象下有效 ,下面代码因为调用的是两个对象,因此锁无效
        synchronizedDemo syncDemo = new synchronizedDemo();
        synchronizedDemo syncDemo2 = new synchronizedDemo();
        new Thread(() -> {
            syncDemo.demo();
        }).start();
        new Thread(() -> {
            syncDemo2.demo();
        }).start();

        //静态方法锁 synchronized修饰的静态方法 锁住的是类 下段代码锁有效
        new Thread(() -> {
            syncDemo.syncDemo();
        }).start();
        new Thread(() -> {
            syncDemo2.syncDemo();
        }).start();


    }
}
(3).synchronized 的本质
mark
(4).synchronized的一些优化
自适应自旋锁;
引入偏向锁、轻量级锁;
锁消除、锁粗话;

3.volatile关键字

volatile关键字 用来实现可见性和有序性

public class VisableDemo {
    //stop对于main线程而言是不具有可见性的 ,因此下面线程并不能通过stop来停止
    //public static boolean stop = false;
    //加入volatile字段 使stop具有可见性
    public volatile static  boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int i = 0;
            while (!stop){
                i++;
            }
            System.out.println("Result:"+i++);
        });
        thread.start();
        System.out.println("线程开始执行");
        Thread.sleep(1000);//使线程休眠阻塞 切换到mian的主线程
        stop = true;//主线程中修改stop
    }

}

线程开始执行
Result:-708282811
加入了volatile关键字,在底层的字节指令中会加入Lock指令;
Lock指令的作用:
将当前处理器缓存行的数据写回到系统内存;
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效;
也就是禁用了缓存;
什么情况下需要用到volatile
当存在多个线程对同一个共享变量进行修改的时候,需要增加volatile,保证数据修改的实时可见
volatile总结:
volatitle实际上是通过内存屏障来防止指令重排序以及禁止CPU高速缓存来解决可见性问题
而Lock指令,它本意上是禁止高速缓存解决可见性问题,但实际上它表示的是一种内存屏障的功能,针对不同的硬件环境,JMM会采用不同的指令作为内存屏障来解决可见性问题.

4.final关键字

final在JAVA中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量.一旦将引用声明作final,将不能修改这个引用.
对于final域,编译器和处理器要遵循两个重排序规则:
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序;
写final域的重排序规则:
JMM禁止编译器把final域的写 重排序到构造函数外;
编译器会在final域的写之后,构造函数return之前.插入一个StoreStore屏障.这个屏障禁止处理器把final域的写重排序到函数之外;
mark
可能执行的情况:
mark
读final域的重排序规则:
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作,编译器会在读final域操作的前面插入一个LoadLoad屏障;
mark
溢出带来的重排序问题:
mark
可能出现的情况:
mark

5.Happens-Before规则

Happens-Before是一种可见性规则,它表达的含义是前面一个操作的结果对后续操作是可见的;
(1)6种Happens-Before规则
程序顺序规则;
监视器锁规则;
Volatitle变量规则;
传递性;
start()规则;
Join()规则;
程序顺序规则;
as-id-serial规则,在单线程中,不管怎么重排序,单线程的执行结果都是一定的,为了遵守规则,编译器和处理器不会对存在数据依赖关系的程序重排序.
class volatitleExample{
    int x =0;
    volatile boolean v = false;
    public void writer(){
        x = 42;
        v = true;
    }
    public void reader(){
        if(v == true){
            //x = 42
        }
    }
}
监视器锁规则:
对一个锁的解锁 Happens-Before 于后续对这个锁加锁;
class syncclassDemo{
    int x =0;
    volatile boolean v = false;

    public void demo(){
        synchronized (this){//此处自动加锁
            if(this.x < 12){
                this.x = 12;
            }
        }//此处自动解锁
    }
}
volatile变量规则:
对于一个volatitle域的写, Happens-Before 于任意后续对这个volatile的域读;
传递性规则:
如果A Happens-Before B ,且B Happens-Before C,那么A Happens-Before C;
start()规则:
如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作Happens-Before于线程B中的任意操作;
    public static void main(String[] args) {
        int i = 66;
        Thread B = new Thread(()->{
            /**
             * 主线程调用B.start()之前
             * 所有对共享变量的修改,处处皆可见
             */

        });
        i = 76;
        //main主线程启动子线程B
        B.start();
    }
join()规则:
如果线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作Happens-Before于线程A从ThredB.hoin()操作成功返回;
int var = 1;
public  void demoMethod() throws InterruptedException {
    Thread B = new Thread(()->{
        //此处共享变量var修改
        var = 2;
    });
    //例如此处对共享变量修改
    var = 66;
    //则这个修改结果对线程B可见
    //主线程启动子线程
    B.start();
    B.join();
    //子线程所有对共享变量的修改
    //在主线程调用B.join()之后皆可见
}

6.原子类Atomic-无锁工具的典范

使用Atomic解决原子性问题
public class AtomicDemo1 {
    //定义一个成员变量
    //public static int count = 0;

    //定义一个AtomicIntger 可以保证原子性
    private static AtomicInteger count = new AtomicInteger(0);
    //定义一个方法
    public static  void incr(){
        try {
            //使线程睡眠1毫秒,这样可以模拟出线程来回调度切换
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //count++;(只会由一个线程来执行)
        count.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        //构建1000个线程来调用incr方法
        for (int i = 0; i < 1000 ; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        //设置睡眠,因为线程是异步的,这样所有线程都能执行完毕main主线程才结束
        //Thread.sleep(4000);
        //最终的执行结果一定是1000
        System.out.println(count.get());
Atomic的实现原理:
Unsafe类:
CAS:
原子递增的实现源代码:
    private static final long valueOffset;

    static {
        try {
            //valueOffset拿到的是AtomicInteger的value在内存中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //Unsafe.incrementAndGet
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

       //unsafe.getAndAddInt
        public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
            //compareAndSwapInt 采用了乐观锁
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

7.ThredLocal的使用和原理

多线程下线程循序的问题:
public class ThredLocalDemo {

    private static  Integer sum = 0;

    public static void main(String[] args) {
        //定义五个线程
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5 ; i++) {
            threads[i] = new Thread(()->{
                sum+=5;
                System.out.println(Thread.currentThread().getName()+"--->"+sum);
            },"Thred-"+i);
        }
        //遍历运行每个线程
        for (Thread thread:threads){
            thread.start();
        }
    }
}

Thred-0--->5
Thred-2--->10
Thred-1--->15
Thred-3--->20
Thred-4--->25
因为线程的先后运行顺序不同,每个线程拿到的变量值随着先后顺序变化而不一定,存在相互干扰,数据的不正确性;
使用ThredLocal让每个线程是相互独立,互不干扰:
public class ThredLocalDemo {

    private static  Integer sum = 0;

    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        //重写initialValue方法
        protected Integer initialValue(){
            return 0;//初始值
        }
    };

    public static void main(String[] args) {
        //定义五个线程
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5 ; i++) {
            threads[i] = new Thread(()->{
                //sum+=5;
                //拿到初始值
                int num = local.get();
                num+=5;
                local.set(num);
                System.out.println(Thread.currentThread().getName()+"--->"+sum);
            },"Thred-"+i);
        }
        //遍历运行每个线程
        for (Thread thread:threads){
            thread.start();
        }
    }
}

Thred-0--->5
Thred-1--->5
Thred-2--->5
Thred-3--->5
Thred-4--->5