07. 线程的安全性分析

线程的使用带来了很是多的便利同时,也给咱们带来了不少困扰java

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

截图

1、线程安全问题的本质

一、原子性缓存

二、可见性安全

三、有序性架构

截图

一、CPU 增长了告诉缓存,均衡与内存的速度差别app

二、操做系统增长进程、线程、以及分时复用 CPU,均衡 CPU 与 I/O 设备的速度差别函数

三、编译程序优化指令的执行顺序,使得可以更加合理的利用缓存工具

截图

截图

public class AtomicDemo{
    public static int count=0;
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++; //count++ (只会由一个线程来执行)
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        Thread.sleep(4000);
        System.out.println("result:"+count);
    }
}

截图

一、Java的内存模型

JMM优化

​ Java内存模型是一种抽象结构,它提供了合理的禁用缓存以及禁止重排序的方法来解决可见性,有序性问题this

截图

截图

截图

二、可见性,有序性的解决方案

一、Volatile、synchronized、final 关键字

二、Happens-Before 原则

1)synchronized

锁的范围

​ 一、对于普通同步方法,锁是当前实例对象。

​ 二、对于静态同步方法,锁是当前类的Class对象。

​ 三、对于同步方法块,锁是 Synchonized 括号里配置的对象。

//对象锁(同一对象生效),锁的是this
public class SyncDemo {
    //对象锁
    public synchronized void demo(){}
    public static void main(String[] args) {
        SyncDemo syncDemo1 = new SyncDemo();
        SyncDemo syncDemo2 = new SyncDemo();
        //没法实现两个线程的互斥
        new Thread(()->{
            syncDemo.demo();
        }).start();
        new Thread(()->{ 
//            syncDemo1.demo();
            syncDemo.demo();//BLOCKED状态
        }).start();
    }
}
//静态同步方法(类级别的锁),锁的是 SyncDemo.class
public class SyncDemo {
    //静态同步方法,
    public synchronized static void demo(){

    }
    public static void main(String[] args) {
        SyncDemo syncDemo = new SyncDemo();
        SyncDemo syncDemo1 = new SyncDemo();
        //若是synchronized为静态方法,那么下面两个线程会互存在斥
        new Thread(()->{
            syncDemo.demo();
        }).start();
        new Thread(()->{
            syncDemo1.demo();
        }).start();
    }
}
//同步方法块,能够是对象锁,也能够是类级别锁,可是范围是可控的。
public class SyncDemo {
    
    public void demo(){
        //TODO
        synchronized (this){//对象锁
        }
        //TODO
    }
    public void demo1(){
        //TODO
        synchronized (SyncDemo.class){//类级别的锁
        }
        //TODO
    }
    
}

一、以上出现的锁就只有两把,一个是对象锁,一个是类级别锁。

二、synchronized方法块中锁的范围是可控的

截图

synchronized 的优化

​ 一、自适应自旋锁

​ 二、引入偏向锁、轻量级锁

​ 三、锁消除、锁粗化

2)volatile

volatile 是能够用来解决可见性和有序性问题的

Lock指令的做用

​ 一、将当前处理器缓存行的数据写回到系统内存

​ 二、这个写回内存的操做会使在其余CPU里缓存了该内存地址的数据无效

什么状况下须要用到volatile

​ 当存在多个线程对同一个共享变量进行修改的时候,须要增长volatile,保证数据修改的实时可见

public class VisableDemo {
    //volatile解决[可见性]
    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("begin start thread");
        Thread.sleep(1000);
        stop=true; //主线程中修改stop的值
    }
}
//volatile 解决 [有序性]
value = 3;
void exeToCPU0(){
    value = 10;
    isFinsh = true;
}
void exeToCPU1(){
   	if(isFinsh){
        assert value == 10;
    }
}
/*
CPU层面的内存屏障,(X86架构里面提供了三种内存屏障)
一、Store Barrier[写],强制全部在 store 屏障指令以前的 store 指令,都在该 store 屏障指令执行以前被执行,并把 store 缓冲区的数据都刷到 CPU 缓存
二、Load Barrier[读],强制全部在 load 屏障指令以后的 load 指令,都在该 load 屏障指令执行以后被执行,而且一直等到 load 缓冲区被该 CPU 读完才能执行以后 load 指令
三、Full Barrier[全],复合了 load 和 store 屏障的功能
*/
value = 3;
void exeToCPU0(){
    value = 10;
    storebarrier();//写屏障,让value 与 isFinsh 没法指令重排
    isFinsh = true;
}
void exeToCPU1(){
   	if(isFinsh){
        loadbarrier();//读屏障,确保value必定是10[最新值]
        assert value == 10;
    }
}

截图

本质上来讲:volatile 其实是 经过内存屏障来防止指令重排序 以及 禁止 cpu 高速缓存来解决可见性问题

#Lock 指令,它本意上是禁止高速缓存解决可见性问题,但实际上在这里,它表示的是一种内存屏障的功能。也就是说针对当前的硬件环境, JMM 层面采用 Lock 指令做为内存屏障来解决可见性问题。

3)final 关键字

final 在 Java中是一个保留的关键字,能够声明成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了

final域 与 线程安全 有什么关系?

对于 final 域,编译器 和 处理器 要遵照两个重排序规则

​ 1)在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。

​ 2)初次读一个包含 final 域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。

//写 final 域得重排序规则
public class FinalExample{
    int i;	//普通变量
    final int j;	//final变量
    static FinalExample obj;
    public FinalExample(){	//构造函数
        i = 1;	//写普通域
        j = 2;	//写 final 域
    }
    public static void writer(){//写线程 A 执行
        obj = new FinalExample();
    }
    public static void reader(){	//读线程 B 执行
        FinalExample object = obj;	//都对象引用
        int a = object.i;	//读普通域
        int b = object.j;	//读 fianl 域
    }
}

写final域重排序规则

​ 一、JMM禁止编译器把final域的写重排序到构造函数以外。

​ 二、编译器会在 final 域的写以后,构造函数 return 以前,插入一个 StoreStore 屏障。这个屏障禁止处理器把final域的写重排序到构造函数以外

截图

读域的重排序规则

​ 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操做,编译器会在读final域操做的前面插入一个 LoadLoad 屏障。

截图

截图

截图

三、Happens-Before模型

什么是Happens-Before?

​ Happens-Before 是一种可见性规则,它表达的含义是前面一个操做的结果对后续操做是可见的。

​ A Happens-Before B ➡ A 得执行结果对 B 可见

6种 Happens-Before 规则 [ 天生知足可见性规则 ]

​ 一、程序顺序规则

​ 二、监视器锁规则

​ 三、Volatile 变量规则

​ 四、传递性

​ 五、start() 规则

​ 六、Join() 规则

1)程序顺序规则

/*
as-if-serial 
	单线程中无论怎么重排序,获得的执行结果是不能改变的
	编译器/处理器都要遵照,所以不会对存在数据依赖关系的语句进行重排序
*/
class VolatileExample{
    int x = 0;
    volatile boolean v = false;
    public void write(){
        x = 42;
        v = true;
    }
    public void reader(){
        if(v == true){
            //这里x会是多少?
            
        }
    }
}

2)监视器锁规则

对一个锁的解锁 Happens-Before 于后续对这个锁的枷锁

synchronized(this){//此处自动枷锁
    //x 是共享变量,初始值 = 10
    if(this.x < 12){
        this.x = 12;
    }
}//此处自动解锁

3)volatile 变量规则

对一个 volatile 域的写,Happens-Before 于任意后续对这个 volatile 域的读。

其实是经过内存屏障来实现的

4)传递性规则

若是 A happens-before B,且 B happens-before C,那么 A happens-before C

class VolatileExample{
    int x = 0;
    volatile boolean v = false;
    public void write(){
        x = 42;
        v = true;
    }
    public void reader(){
        if(v == true){
            //这里x会是多少?
            
        }
    }
}

5)start() 规则

若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操做happens-before于线程B中的任意操做

Thread B = new Thread(()->{
   //主线程调用 B.start()以前
   //全部对共享变量的修改,此处皆可见
   //此例中,var == 77
});
//此处对共享变量 var 修改
var = 77;
//主线程启动子线程
B.start();

6)join() 规则

若是线程A执行操做 ThreadB.join() 并成功返回,那么 线程B 中的任意操做 happens-before 于 线程A 从 ThreadB.join() 操做成功返回

Thread B = new Thread(()->{
    //此处对共享变量var 修改
    var = 66
});
//例如此处对共享变量修改
//则这个修改结果对线程B可见
//主线程启动子线程
B.start();
B.join();
//子线程全部对共享变量的修改
//在主线程调用B.join()以后皆可见
//此例中,var == 66

四、原子类 Atomic

原子性问题的解决方案

​ synchronized、Lock

​ J.U.C 包下的 Atomic 类 [ 无锁工具的典范 ]

import java.util.concurrent.atomic.AtomicInteger;
//atomic 保证原子性
public class AtomicDemo{
//    public static int count=0;
    private static AtomicInteger atomicInteger=new AtomicInteger(0);
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//        count++; //count++ (只会由一个线程来执行)
        atomicInteger.incrementAndGet();
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(AtomicDemo::incr).start();
        }
        Thread.sleep(4000);
//        System.out.println("result:"+count);
        System.out.println("result:"+atomicInteger.get());
    }
}

Atomic 实现原理

​ 一、Unsafe 类

​ 二、CAS

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

    return var5;
}

Atomic 分类

​ 一、原子更新基本类型

​ 二、原子更新数组

​ 三、原子更新引用类型

​ 四、原子更新字段类

五、ThreadLocal 的使用和原理

public class ThreadLocalDemo {

    private static Integer num=0;

    public static final ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //初始值
        }
    };
    public static final ThreadLocal<Integer> local1=new ThreadLocal<Integer>();
    public static void main(String[] args) {
        Thread[] threads=new Thread[5];
        //但愿每一个线程都拿到的是0
        for (int i = 0; i < 5; i++) {
            threads[i]=new Thread(()->{
//                num+=5;
                int num=local.get(); //拿到初始值
                local1.get();
                num+=5;
                local.set(num);
                System.out.println(Thread.currentThread().getName()+"->"+num);
            },"Thread-"+i);
        }
        for(Thread thread:threads){
            thread.start();
        }
    }
}
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

截图

2、如何安全发布对象

一、发布对象

发布的意思是使一个对象可以被当前范围以外的代码所使用

public static HashSet<Person> persons;
public void init(){
    persons = new HashSet<Person>();
}
//不安全发布
private String[] states = {'a','b','c','d'};
//发布出去一个
public String[] getStates(){
    return states;
}
public static void main(String[] args){
    App unSafePub = new App();
    System.out.println("Init array is: "+Arrays.toString(unSafePub.getStates()));
    unSafePub.getStates()[0] = "Mic!";
    System.out.println("After modify.the array is: "+ Arrays.toString(unSafePub.getStates()));
    
}

二、对象溢出

也成为对象的逃逸,一种错误的发布,当一个对象尚未构造完成时,就使它被其余线程所见

截图

截图

三、安全发布对象

一、在静态初始化函数中初始化一个对象引用

二、将对象的引用保存到volatile类型的域或者AtomicReference对象中(利用volatile happen-before规则)

三、将对象的引用保存到某个正确构造对象的final类型域中(初始化安全性)

四、将对象的引用保存到一个由锁保护的域中(读写都上锁)

//静态初始化
public class StaticDemo {
    private StaticDemo(){}
    private static StaticDemo instance=new StaticDemo();
    public static StaticDemo getInstance(){
        return instance;
    }
}
//final域
public class FinalDemo {
    private final Map<String,String> states;
    public FinalDemo(){
        states=new HashMap<>();
        states.put("mic","mic");
    }
}
//volatile
public class VolatileSyncDemo {
    private VolatileSyncDemo(){}
     //DCL问题[加 volatile]
    private volatile static VolatileSyncDemo instance=null;
    public static VolatileSyncDemo getInstance(){
        if(instance==null){
            synchronized(VolatileSyncDemo.class) {
                if(instance==null) {
                    instance = new VolatileSyncDemo();
                }
            }
        }
        return instance;
    }
    /**
     * instance = new VolatileSyncDemo();
     * ->
     * 1. memory=allocate()
     * 2. 
     * 3. instance=memory
     *	  ctorInstance(memory)
     *
     * 1.3.2 (不完整实例)
     */
}
相关文章
相关标签/搜索