Java Multithreading (3): Thread Safety Issues and Solutions

​Create a writing habit together! This is the 8th day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

1. What is thread safety

        If the result of the code running in the multi-threaded environment is in line with our expectations, that is, the result should be in the single-threaded environment, the program is said to be thread-safe . Thread unsafe means that the execution result of a program in multiple threads does not meet expectations.

2. Factors that cause thread insecurity

2.1 Preemptive execution

 

2.2 Multiple threads modify the same variable at the same time

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));
    }
}
复制代码

operation result:

​ 

 

2.3 Non-atomic operations

 What is atomicity?

        We think of a piece of code as a room, and each thread is the person who wants to enter the room. If there is no mechanism to ensure that after A enters the room, it has not come out; can B also enter the room and interrupt A's privacy in the room. This is not atomic.

 A java statement is not necessarily atomic, nor is it necessarily just an instruction.

****For example, the n++ we just saw is actually composed of three steps:

  1. Read data from memory to CPU
  2. make data update
  3. write data back to the CPU

What problems does not guarantee atomicity bring to multithreading? 

        If a thread is operating on a variable, and other threads intervene in the middle, if the operation is interrupted, the result may be wrong. This is also closely related to the preemptive scheduling of threads. If the thread is not "preemptive", even if there is no atomicity, it is not a big problem. 

2.4 Memory Visibility Issues

        Visibility means that changes to a shared variable value by a thread can be seen by other threads in a timely manner.

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();

    }
}
复制代码

 operation result:

It can be seen that thread 2 modifies the flag to false, and thread 1 never ends its execution, which is the memory visibility problem.

 

2.5 Instruction Reordering

What is instruction reordering?

For example a piece of code like this:

  1. Go to the front desk and remove the U disk
  2. Go to the classroom and write a 10-minute homework
  3. Go to the front desk to pick up the courier

        如果是在单线程情况下,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

Basic usage of 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 Notes:

  • lock() must be placed before the try, or the first line of the try, otherwise it will cause two problems:
    1. If it is placed in the try, the lock will fail due to an exception in the try code, and the operation of finally releasing the lock will also be performed. .
    2. The unlock exception will cover the business exception in the try, making it more difficult to troubleshoot errors.
  • unlock() must be placed inside finally.

Lock fair lock and unfair lock:

  • Fair lock : Multiple threads acquire locks in the order in which they apply for locks. Threads will directly enter the queue to queue, and the first place in the queue can always get the lock.
  • Unfair lock : When multiple threads acquire a lock, they will try to acquire it directly. If they cannot acquire it, they will enter the waiting queue. If they can acquire it, they will acquire the lock directly.

From the source code, you can see that Lock creates an unfair lock by default. Passing in true will create a fair lock.

synchronized VS Lock:

  • Lock is more flexible and has more methods.
  • The lock types are different. Lock is an unfair lock by default, but it can also be specified as a fair lock; synchronized can only be an unfair lock.
  • Calling the lock() method is different from the synchronized thread waiting for the lock state, the lock() method will become WAITTING; synchronized will become BLOCKED.
  • Synchronized is a lock provided by JVM, which automatically locks and releases locks, while Lock requires developers to lock and release locks by themselves.
  • Synchronized can modify methods (static methods/normal methods) and code blocks, while Lock can only modify code.

Guess you like

Origin juejin.im/post/7088642481381703688