线程安全: 指在多线程对一个共享资源同时进行操作时, 所得到的结果都是一样的

如何保证线程安全

方法: 要保证线程安全, 就必须保证线程同步, 保证线程的可见性,有序性,和原子性

线程同步

线程同步的含义和字面意思相反, 同步其实是线程"排队" 的意思, 就是让线程按照一定的顺序执行, 每一时刻, 只有一个线程, 对共享线程进行操作

可见性

一个线程对共享资源的操作, 所有线程都可以看见

以具体实例来说明 就好比一个线程修改了一个线程, 其他线程立马知道该数据被修改, 即是在线程和主存之间有一个缓存,线程修改数据, 是在缓存中修改, 还需要在主存修改, 而可见性就是立刻在主存中修改,防止其他线程读取时, 发生数据错误

有序性

就是代码的执行执行是有顺序的, 执行的顺序不会发生改变

原子性

顾名思义, 原子是一个不可分的整体, 就是一个代码块, 要么全部执行, 要么全部不执行, 只要其执行, 就无法被任何事物打断

具体方法

volatile 关键字

作用: 保证线程可见性和禁止指令重排序

保证可见性

实现原理: 当一个变量被 volatile 修饰, 一旦其发生改变, 那么根据缓存一致性协议, 其他线程的缓存就会失效,需要重新从内存中获取数据, 就可以保证数据的准确性了, 就好比这个数据修改了, 其他线程缓存失效,就知道它被修改了,就要重新获取, 即可见的含义

不加 volatile 关键字

public class Main2 { // static 静态变量 全局 private static boolean flag = false;

public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        //未停止,不知道已修改
        public void run() {
            while(true) {
                if(flag) {
                    break;
                }
            }
            System.out.println("变量变化");
        };
    }).start();

    //主线程修改flag值
    Thread.sleep(1000);
    flag = true;
}

}

线程先启动,启动后修改变量,未加关键字修饰,子线程不知道其变量值已经发生变化,即不可见,所以死循环无法停止。

加 volatile 关键字

public class Main2 { // static 静态变量 全局 private static volatile boolean flag = false;

public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        //停止,知道已修改
        public void run() {
            while(true) {
                if(flag) {
                    break;
                }
            }
            System.out.println("变量变化");
        };
    }).start();

    //主线程修改flag值
    Thread.sleep(1000);
    flag = true;
}

}

加入关键字,首先先死循环,然后修改其值,知道其修改完成,然后调用,停止死循环,输出文字。可以证明其保证了可见性。

禁止重排序

实现原理:通过jvm给指令的前后加上内存屏障,屏障两边的指令不可以重排序,保证有序。

例子 单例模式

禁止重排序的经典案例就是单例模式的创建过程中的双重检测锁。

public class Main2 { //自己对象,禁止重排序 private volatile static Main2 main = null; //构造方法 private Main2() {

}
//创建自己
public static Main2 getInstance() {
    if(main == null) {
        //类锁,双重检测锁
        synchronized(Main2.class) {
            if(main == null) {
                main = new Main2();
            }
        }
    }
    return main;
}

public static void main(String[] args) {
    Main2 m1 = getInstance();
    Main2 m2 = getInstance();
    Main2 m3 = getInstance();
    System.out.println(m1 == m2);  // true
    System.out.println(m1 == m3);  // true
    System.out.println(m2 == m3);  // true
}

}

可知三个对象,都是同一个对象,内存地址相同。

synchronized关键字

作用:利用线程互斥来实现线程同步,即通过同一时刻只有一个线程可以访问(互斥),来实现线程的原子性,全部执行完,才能换线程执行,线程顺序执行(同步)。

synchronized 最主要的就是保持原子性,保持原子性,最主要的就是线程同步,同步最基本的方法就是加锁,加锁最直接的就是加synchronized关键字。

效率:synchronized 在早期是一把重量级锁,但是随着java发展,如今的效率已经很高。例如i++不是原子操作,它分为三步:1.获取i的值 2.修改i的值 3.将修改的值赋予i 。如果在其外面加入synchronized关键字,保证了每次只有一个线程可以修改i,那么就保证了i++在并发环境下的安全性。保证原子性

上面的双重检测锁也使用了synchronized关键字,加同一个锁的线程同步互斥,里面的代码块在同一时刻,只有一个线程可以访问,所以保证了唯一实例。

防止死锁

原因

两个线程相互请求对方持有的资源,都不释放自己持有的资源,相互阻塞,导致死锁。

后果

至少有两个线程相互阻塞等待对方的资源。

检查死锁

使用jdk工具jconsole查看线程的状态。

解决方法

资源一次性分配

当线程满足条件时释放自己已占有的资源

资源有序分配