Java多线程安全

1. 什么是线程安全

在解释什么是线程安全之前,我们先来看一个线程不安全的场景:

public class Test {
    
    private static  long n = 0;
    private static  long count = 1_000_000_000L;

    static class Add extends Thread{
        @Override
        public void run() {
            for(int i = 0; i < count; i++){
                n++;
            }
        }
    }

    static class Sub extends Thread{
        @Override
        public void run() {
            for(int i = 0; i < count; i++){
                n--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread add = new Add();
        Thread sub = new Sub();
        add.start();
        sub.start();

        add.join();
        sub.join();

        System.out.println(n);
    }
}

我们预想的结果应该是0,但是并不是这样。实际结果则是一个随机值。

java 线程安全什么意思 java中什么是线程安全_Test

java 线程安全什么意思 java中什么是线程安全_线程安全_02


那么什么是线程安全呢?

  • 线程安全:通俗的来讲就是可以100%运行正确的就是线程安全。

2. 为什么会出现线程不安全?

  • 出现了数据共享:
    再上一个场景中线程add和线程sub共享了变量n和count。
  • 对共享数据进行了修改。
    同样上一个场景中线程add和线程sub都修改了变量n。

3.线程不安全的三种情况

  • 破环了原子性
    比如刚才我们看到的 n++,其实是由三步操作组成的:
  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了即原子性遭到了破环,结果就可能是错误的,第一个场景不安全就是因为破环了原子性。

  • 内存不可见
  • 主内存-工作内存

为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是内存呢可见性问题。

  • 场景演示
public class 内存不可见场景 {

    private  static boolean running =true;

    static  class  Test extends  Thread{
        @Override
        public void run() {
            int n = 0;
            while (running){
                n++;
            }
            System.out.println(n);
        }
    }

    public static void main(String[] args) {
        Thread t3 = new Test();
        t3.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("随便输入什么结束线程");
        scanner.nextLine();

        System.out.println(running);
        running = false;
        System.out.println(running);

        System.out.println(t3.getState());

    }
}

java 线程安全什么意思 java中什么是线程安全_System_03

当获取输入后将running的状态改为false,按理来说线程将终止,但线程的状态依然是RUNNABLE,这时就出现了内存不可见。虽然主内存中改变了running状态,但t3的工作内存并不知道。

  • 代码重排序
    线程的有序性是指:在线程内部,所有的操作都是有序执行的,而在线程之间,因为工作内存和主内存同步的延迟,操作是乱序执行的,发生了代码重排序。
    比如创建一个新的对象的执行过程是:(1)new在堆上开辟空间(2)调用构造方法(3)给引用赋值。在单线程的情况下代码重排序的前提是没有副作用,但在多线程情况下并不能保证。

4.线程安全机制

synchronized

  • 语法
  • 作为方法修饰符存在
  • 作为语句块出现
public class SynchronizedDemo {
    
    public  synchronized  static  void  fun(){
        
    };

    public synchronized  void  start(){
        
    };

    public void  method(){
        Object o = new Object();
        synchronized(o){
            //引用不为空,否则抛出NullPointerException异常
        }
    }
}
  • 作用
  • 主要保证原子性:互斥,抢的是同一对象的锁(monitor lock )
  • 一定程度保证内存可见性:当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中;当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

对刚开始的案例进行改造,再来观察效果。

public class Test {
    private static  long n = 0;
    private static  long count = 1_000_000_000L;

    static class Add extends Thread{
        private Object lock;

        Add(Object lock) {
            this.lock = lock;
        }
        @Override
        public synchronized void run() {
            for(int i = 0; i < count; i++){
                synchronized (lock) {
                    n++;
                }
            }
        }
    }

    static class Sub extends Thread{
        private Object lock;

        Sub(Object lock) {
            this.lock = lock;
        }
        @Override
        public synchronized void run() {
            for(int i = 0; i < count; i++){
                synchronized (lock) {
                    n--;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test lock = new Test();
        Thread add = new Add(lock);
        Thread sub = new Sub(lock);
        add.start();
        sub.start();

        add.join();
        sub.join();

        System.out.println(n);
    }


}

java 线程安全什么意思 java中什么是线程安全_System_04


输出和我们预想的值一样。

volatile

  • 语法
  • 修饰属性/静态属性
  • 作用
  • 主要保证内存可见性
  • 限制重排序:对象的初始化
public class 内存不可见场景 {

    private  volatile static  boolean running =true;

    static  class  Test extends  Thread{
        @Override
        public void run() {
            int n = 0;
            while (running){
                n++;
            }
            System.out.println(n);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t3 = new Test();
        t3.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("随便输入什么结束线程");
        scanner.nextLine();

        System.out.println(running);
        running = false;
        System.out.println(running);

        while (true) {
            System.out.println(t3.getState());
            TimeUnit.SECONDS.sleep(1);
        }

    }
}

java 线程安全什么意思 java中什么是线程安全_System_05


可以看到t3线程接受到了running状态的改变,线程终止。