Java多线程(三):线程安全问题与解决方法

​一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

1. 什么是线程安全问题

        如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。线程不安全指的是程序在多线程的执行结果不符合预期。

2. 导致线程不安全的因素

2.1 抢占式执行

2.2 多个线程同时修改了同一个变量

public class ThreadDemo16 {
    static class Counter {
        // 变量
        private int number = 0;
        // 循环次数
        private int count;

        public Counter(int count) {
            this.count = count;
        }

        // ++方法
        public int increment() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp++;
            }
            return tmp;
        }

        // --方法
        public int decrement() {
            int tmp = 0;
            for (int i = 0; i < count; i++) {
                tmp--;
            }
            return tmp;
        }

        public int getNumber() {
            return number;
        }
    }

    static int num1 = 0;
    static int num2 = 0;

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);

        Thread thread1 = new Thread(() -> num1 = counter.increment());

        Thread thread2 = new Thread(() -> num2 = counter.decrement());

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("最终结果:" + (num1 + num2));
    }
}
复制代码

运行结果:

​ 

2.3 非原子性操作

 什么是原子性?

        我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

 ⼀条 java 语句不⼀定是原子的,也不一定只是一条指令。

****比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题? 

        如果一个线程正在对一个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大。 

2.4 内存可见性问题

        可见性是指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到。

public class ThreadDemo17 {
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

 运行结果:

可以看到,线程2将flag修改为false,线程1始终未结束执行,这就是内存可见性问题。

2.5 指令重排序

什么是指令重排序?

比如一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

        如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序 。 编译器优化的本质是调整代码的执行顺序,在单线程下没问题,但在多线程下容易出现混乱,从而造成线程安全问题。 

那么有这么多线程不安全问题,该如何解决呢?

3. 解决线程不安全问题

3.1 volatile 解决内存可见性和指令重排序问题

        volatile 可以解决内存可见性指令重排序的问题,代码在写入 volatile 修饰的变量的时候: 

  • 改变线程⼯作内存中volatile变量副本的值;
  • 将改变后的副本的值从⼯作内存刷新到主内存。

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的⼯作内存中;
  • 从⼯作内存中读取volatile变量的副本。

注意 :直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不⼀致的情况,加上 volatile ,强制读写内存,速度虽然慢了,但是数据变得更准确了。

volatile 演示:

public class ThreadDemo17 {
    private volatile static boolean flag = true;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("线程1:开始执行" + LocalDateTime.now());
            while (flag) {
            }
            System.out.println("线程1:结束执行" + LocalDateTime.now());
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2:修改flag = false" + LocalDateTime.now());
            flag = false;
        });
        thread2.start();

    }
}
复制代码

运行结果:

 给之前的代码加上 volatile 之后,线程1接收到了flag的改变,从而结束了执行,解决了内存可见性问题。

volatile 缺点

        volatile 虽然可以解决内存可见性和指令重排序的问题,但是解决不了原子性问题,因此对于 ++ 和 --操作的线程非安全问题依然解决不了,比如以下代码:

public class ThreadDemoVolatile {
    static class Counter {
        // 变量
        private volatile int number = 0;

        // 循环次数
        private final int MAX_COUNT;

        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        // ++ 方法
        public void increase() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        // -- 方法
        public void desc() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }

        public int getNumber() {
            return number;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(100000);
        Thread thread1 = new Thread(counter::increase);
        thread1.start();
        Thread thread2 = new Thread(counter::desc);
        thread2.start();

        // 等待线程执行完成
        thread1.join();
        thread2.join();
        System.out.println("最终结果:" + counter.getNumber());
    }
}
复制代码

3.2 锁(synchronized 和 lock)

3.2.1 synchronized

synchronized 基本用法:

  1. 修饰静态方法

    public class ThreadSynchronized {
    
        private static int number = 0;
    
        static class Counter {
            // 循环次数
            private static final int count = 100000;
    
            // ++方法
            public synchronized static void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized static void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(Counter::increase);
            thread1.start();
            Thread thread2 = new Thread(Counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("执行结果:" + number);
        }
    }
    复制代码

    ​​

  2. 修饰普通⽅法

    public class ThreadSynchronized2 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // ++方法
            public synchronized void increase() {
                for (int i = 0; i < count; i++) {
                    number++;
                }
            }
    
            // --方法
            public synchronized void desc() {
                for (int i = 0; i < count; i++) {
                    number--;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            // 等待线程执行完毕
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

  3. 修饰代码块

    public class ThreadSynchronized3 {
        private static int number = 0;
    
        static class Counter {
            private static final int count = 100000;
    
            // 自定义锁对象
            final Object myLock = new Object();
    
            // ++方法
            public void increase() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number++;
                    }
                }
            }
    
            // --方法
            public void desc() {
                for (int i = 0; i < count; i++) {
                    synchronized (myLock) {
                        number--;
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread1 = new Thread(counter::increase);
            thread1.start();
            Thread thread2 = new Thread(counter::desc);
            thread2.start();
    
            thread1.join();
            thread2.join();
            System.out.println("最终结果:" + number);
        }
    }
    复制代码

    ​​

synchronized 特性:

1. 互斥。synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会 阻塞等待。

  • 进入 synchronized 修饰的代码块, 相当于 加锁,
  • 退出 synchronized 修饰的代码块, 相当于 解锁。

2. 刷新内存。 synchronized 的⼯作过程: 

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到⼯作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

​3. 可重入。 synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。

public class ThreadSynchronized4 {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized4.class) {
            System.out.println("主线程得到锁");
            synchronized (ThreadSynchronized4.class) {
                System.out.println("主线程再次得到锁");
            }
        }
    }
}
复制代码

 

注意 :

  1.  加同一把锁。

    public class ThreadSynchronized6 {
        private static final int count = 100000;
        static int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Object obj = new Object();
            Object obj2 = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (obj) {
                    for (int i = 0; i < count; i++) {
                        num++;
                    }
                }
            }, "线程1");
            t1.start();
            Thread t2 = new Thread(() -> {
                synchronized (obj2) {
                    for (int i = 0; i < count; i++) {
                        num--;
                    }
                }
            }, "线程2");
            t2.start();
            t1.join();
            t2.join();
            System.out.println("最终执⾏结果:" + num);
        }
    }
    复制代码

  2. 实例类可以使用 this,静态类使用 xxx.class。

  3. synchronized用的锁是存在Java对象头的:


  4. 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分工作.

  5. 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

3.3.2 ****Lock

Lock 基本用法:

public class ThreadLock {
    public static void main(String[] args) {
        // 1.创建锁对象
        Lock lock = new ReentrantLock();
        // 2.加锁
        lock.lock();
        try {
            // 业务代码
            System.out.println("hello");
        } finally {
            lock.unlock();
        }
    }
}
复制代码

Lock 注意事项:

  • lock() 一定要放在 try 之前,或者 try 的首行,否则会导致两个问题:
    1.如果放在 try 里面,因为 try 代码中的异常导致加锁失败,还会执行 finally 释放锁的操作。
    2.unlock 异常会覆盖 try 里面的业务异常,增加排查错误的难度。
  • unlock() 一定要放在 finally 里面。

Lock 公平锁和非公平锁 :

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

通过源码可以看到 Lock 默认创建的是非公平锁,传入 true 会创建公平锁。

synchronized VS Lock:

  • Lock 更灵活,有更多的方法。
  • 锁类型不同。Lock 默认是非公平锁,但也可以指定为公平锁;synchronized 只能为非公平锁。
  • 调用 lock()方法和 synchronized 线程等待锁状态不同,lock()方法会变成 WAITTING; synchronized 会变成 BLOCKED。
  • synchronized 是JVM 提供的锁,它是自动进行加锁和释放锁操作的,而Lock 需要开发者自己进行加锁和释放锁操作。
  • synchronized 可以修饰 方法(静态方法 / 普通方法)和代码块, 而Lock 只能修饰代码。

猜你喜欢

转载自juejin.im/post/7088642481381703688