线程安全问题

线程安全问题

1. 什么是线程安全

线程是cpu随机调度, 抢占式执行的,  这就导致程序的结果和预期不同, 我们把这样的问题叫做线程安全问题

 

 

例子:

class Demo19 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {

                count++;

            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {

                count++;

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

 

我们期望程序并发执行,且最后打印 10 0000

但是, 上述代码3次程序结果随机, 这样期望和结果不同的问题, 就是线程安全问题

 

2. 导致线程安全问题的原因

原因1  ->  线程是抢占式执行

 

为什么, 上述程序3次结果随机 ?

首先, count++, 这段代码会编译成3个指令

1. 把count在内存的值读取到寄存器

2. 寄存器中值 + 1

3. 把寄存器中值, 写回内存

 

 

有可能是一个cpu核心并发执行, 这一刻执行线程1, 下一刻执行线程2

也有可能是二个cpu核心并行执行

但是无论哪种情况都会出问题, 因为线程是抢占式执行的

 

看下面的例子, 进行理解

正确的执行顺序

 

错误的执行顺序, 这只是一种可能性

总结:  由于线程是抢占式执行, 这就导致指令执行的顺序随机 -> 结果也就随机了

 

原因2 -> 两个线程针对同一个变量进行修改

查看代码
 class Demo19 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {

                count++;

            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {

                count++;

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

 

原因3 -> 针对同一个变量进行修改的操作不是 "原子的" ( 一条指令 )

count++ -> 3条指令

 

原因4 -> 内存可见性导致的线程安全问题

查看代码
 import java.util.Scanner;

class Demo23 {
    private static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ;      // 循环体中, 啥都没有.

                // 无其他操作 -> 输入1 -> 线程不终止
            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

t1线程不终止,  原因 ?

这是内存可见性 ( 编译器优化导致的问题 )

count == 0, 这段代码分为2个指令, load, cmp

由于load操作比cmp慢很多, 所以为了提升速度, 编译器做了优化 -> 只执行一次load指令, 之后就不执行了把count的值放到寄存器中, 每次cmp指令就拿寄存器中的值比较

所以, 当t2线程改变count的值, t1线程不执行load指令, t1线程也就不会终止

 

 

 

 

为什么写个打印就能终止线程了

查看代码
class Demo22 {

    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
           while (count == 0) {
               System.out.println("t1");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("t1线程执行结束");
        });


        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数: ");
            count = scanner.nextInt();
            System.out.println("t2线程执行结束");
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
   ;
    }
}

现在, t1线程涉及3个指令 load, cmp, I/O,  由于I/O比load慢很多, I/O不能优化

所以, 编译器就不会优化load指令了

编译器是否做优化, 这个不好说

如何彻底解决内存可见性问题 ?

用关键字volatile,提示编译器不做优化

查看代码
 import java.util.Scanner;

class Demo23 {
    private static volatile int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // count == 0, 不会进行优化
            while (count == 0) {
                ;      // 循环体中, 啥都没有.

            }
            System.out.println("t1 执行结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数: ");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

 

 

 

 

 

原因5 -> 指令重排序

 

3. 如何解决线程安全问题

针对原因1 -> 没办法解决, 线程抢占式执行是底层写死的

 

针对原因2 -> 可以解决线程安全问题,  但是不好

 

针对原因3 -> 最好的解决方法, 把针对变量的修改的操作, 搞成 "原子的"  

就是把count++的3条指令 load -> add -> save "打包成一条指令 ", 这样线程之间就算是抢占式执行, 也能得到正确的结果 

 

"把多条指令打包成一条指令",  这样的操作是通过锁实现的

 

1. 在 java中, 锁就是任意一个对象

2. 锁有两个核心操作 -> 加锁、解锁

3.  一个线程拿到了锁对象加锁之后, 另一个线程如果也尝试拿这同一把锁进行加锁, 那么这个线程就会阻塞等待 -> 锁竞争 / 锁冲突

 

代码例子:

查看代码
 class Demo19 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        // 先创建出一个对象, 使用这个对象作为锁
        Object locker1 = new Object();
        // Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // t1线程对 locker1锁对象 进行加锁
                synchronized (locker1) {
                    count++;
                }
                // 对 locker1锁对象 解锁
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // t2 线程也是对 locker1这个锁对象进行加锁
                // 如果t1线程已经拿到了锁对象, 那么t2线程会阻塞等待
                // t1线程对locker1 解锁 -> t2线程再对锁尝试加锁
                synchronized (locker1) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

"把多条指令打包成一条指令", 并不是把多条指令变成一条指令,而是通过锁引入阻塞等待, 让一部分代码串行执行, 一部分代码并发执行

比如上述代码中, synchnized关键字中的代码就是串行执行, 其他for循环代码都是并发执行

 

写法2

查看代码
class Counter {
    private int count = 0;
    
    public void add() {
          count++;
        /*synchronized (this) {
            count++;
        }*/
    }

    public int get() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter.get());
    }
}

 

写法3:

synchronized 修饰普通方法, 针对this对象加锁

查看代码
 package demo2;

class Counter {
    private int count = 0;

    // 简化写法: 一进入add()函数, 就尝试对this对象 进行加锁
    synchronized public void add() {
        count++;
    }

    public int get() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter.get());
    }
}

 

写法4:

synchronized 修饰静态方法, 针对 该类的类对象加锁

查看代码
package demo2;
class Counter {
    private static int count = 0;

    public static void func() {
        // Counter.class 表示当前类对象, 一个类只能有一个类对象
        synchronized (Counter.class) {
            count++;
        }
    }

    public int get() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.func();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.func();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter.get());
    }
}

 

 

4. 可重入锁

每个锁对象会记录当前是哪个线程, 正在持有锁 (哪个线程已经对锁对象加锁了)

在针对锁对象, 加锁之前会先进行判断, 如果是同一个线程, 放行 —— 可重入锁

如果是不同线程, 则正常阻塞 

查看代码

class Counter2 {

    private int count = 0;

    void add() {

        // 按道理, counter2这个锁已经加锁了, 这里会阻塞等待
        // 但是java jvm 对synchronized关键字这里做了特殊处理
        // 如果当前线程已经对该锁对象加锁了, 就直接放行
        synchronized (this) {
            count++;
        }
    }

    int get() {
        return count;
    }
}

class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = new Counter2();
        //Counter2 counter1 = new Counter2();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter2.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter2) {
                    counter2.add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter2.get());
    }
}

 

 

5. 死锁

一个死锁的例子:

查看代码
package demo2;


class Demo22 {
    public static void main(String[] args) throws InterruptedException {

        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    // 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // ... t1线程阻塞
                synchronized (locker2) {
                    System.out.println("t1 获取了两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // ... t2线程 阻塞
                synchronized (locker1) {
                    System.out.println("t2 获取到两把锁");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

 

如何解决死锁 ?

产生的死锁4个必要条件, 少了一个就解决死锁了

 

针对条件3, 解决死锁

查看代码
class Demo22 {
    public static void main(String[] args) throws InterruptedException {

        Object locker1 = new Object();
        Object locker2 = new Object();


        // 约定加锁顺序 t1: locker1 加锁 -> locker2 加锁 -> locker2 解锁 -> locker1 解锁
        // 约定加锁顺序 t2: locker1 加锁  -> locker2 加锁 -> locker2 解锁 -> locker1 解锁
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    // 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {
                    System.out.println("t1 获取了两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {
                    System.out.println("t2 获取到两把锁");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

 

针对条件4, 解决死锁

查看代码
 package demo2;


class Demo22 {
    public static void main(String[] args) throws InterruptedException {

        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    // 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {
                    System.out.println("t1 获取了两把锁");
                }
            }


        });

        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {
                    System.out.println("t2 获取到两把锁");
                }
            }

        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

 

6. wait、notify

什么是线程饿死 ? 这里有问题

假设这样一种情况, 某个线程频繁的加解/锁, 但是该线程又能很快获取锁 , 这就导致其他线程拿不到锁, 进而阻塞等待

这种情况, 就可以用wait让该线程主动阻塞, 让其他线程继续执行

 

wait() -> 解锁 + 阻塞等待 ,sleep() -> 阻塞等待 

notify() -> 随机唤醒一个, 由于调用 锁对象.wait() 陷入阻塞的线程

 

例子1 :

重要 ! -> 里面有很多重要的细节

查看代码
class Demo26 {
    public static void main(String[] args) throws InterruptedException {

        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 等待之前");
                try {
                    // 解锁 -> t1 线程阻塞等待
                    locker.wait();
                    // notify把这里唤醒阻塞之后, 不会立即执行
                    // 只有t1线程重新加上锁了之后, 再继续执行
                    // Runnable -> Waiting (wait) -> Runnable(notify) -> Blocked (由于锁导致的阻塞, 得再加上锁, 才能继续执行)
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            synchronized (locker) {
                System.out.println("输入: ");
                scanner.next();
                // 唤醒 -> 由于locker这个锁对象, 导致阻塞的线程t1
                System.out.println("唤醒t1线程");
                locker.notify();
            }
        });

        t1.start();
        t2.start();

    }
}

 

例子2:

查看代码
class Demo26 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();
 
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 等待之后");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2 等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 等待之后");
            }
        });
        Thread t3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t3 通知之前");
 
                // notify() 随机唤醒等待的线程
                // 只能唤醒一个等待线程
                locker.notify();
 
                // 唤醒全部等待线程
                locker.notifyAll();
                System.out.println("t3 通知之后");
            }
        });
 
        t1.start();
        t2.start();
        Thread.sleep(100);
        t3.start();
    }
}

 

posted @ 2024-04-12 14:59  qyx1  阅读(24)  评论(0)    收藏  举报