当前位置:   article > 正文

JavaEE初阶Day 7:多线程(5)

JavaEE初阶Day 7:多线程(5)

Day 7:多线程(5)

回顾synchronized

  • synchronized带有(),填写锁对象,锁对象存在的意义,只是起到“身份标识”效果
  • 两个线程是否是针对同一个对象加锁,如果是,就可能产生阻塞/锁竞争/锁冲突
  • synchronized{},进入代码块,就相当于加锁操作,出了代码块,就相当于解锁操作
  • 修饰普通方法,相当于针对this加锁,修饰静态方法,相当于针对类对象加锁

1. 死锁

package thread;

class Counter2 {
    private int count = 0;

    void add() {
        synchronized (this) {
            count++;
        }
    }

    int get() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2 = 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();
                }
            }
        });
        t2.start();
        t1.start();
        
        t2.join();
        t1.join();
        
        System.out.println("count = " + counter2.get());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

上述线程t2的代码相当于

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
        synchronized (counter2) {
            synchronized (counter2){
                 count++;
            }
        }
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

假设t2先启动(t1先不考虑),t2第一次加锁,肯定能加锁成功,当t2尝试第二次加锁的时候,此时counter2变量,属于已经被锁定的状态了,针对一个已经被锁定的对象加锁,就会出现阻塞等待,阻塞到对象被解锁为止

  • 要想获得到第二层的锁,就要执行完第一层的代码块
  • 要想执行完第一层代码块,就需要先获取到第二层的锁

这种情况下,就叫“死锁”

但是在实际上述过程中,对于synchronized是不适用的,synchronized上述代码是不会出现死锁的,但是如果是C++/Pyhton的锁就会出现死锁

  • synchronized在内部进行了特殊处理(JVM)
  • 每个锁对象里,会记录当前是哪个线程持有了这个锁,当针对这个对象加锁操作时,就会先判定一下,当前尝试加锁的线程,是否是持有同一锁的线程,如果不是,就阻塞,如果是,直接放行
  • 这种机制称为**“可重入锁”**,目的是为了避免程序员粗心大意,搞出死锁

注意:当加了多层锁的时候,代码执行到哪里要真正进行解锁呢

一定是在遇到最外层的},那么,如何确定遇到的}是最外层的,运行时,给锁对象里也维护一个计数器(int n),每次遇到{,n++(只有第一次才真正加锁),当遇到}就n–,当n减到0了,才真正解锁

2. 死锁场景

死锁有三种比较典型的场景

(1)场景一:锁是不可重入锁,并且一个线程针对一个锁对象,连续加锁两次,通过引入可重入锁,可以解决上述问题

(2)场景二:两个线程,两把锁

(3)场景三:N个线程,M把锁

3. 场景二:两个线程,两把锁

有线程1和线程2,以及锁A和锁B,现在线程1和2都需要获取到锁A和锁B(拿到锁A之后,不释放A,继续获取锁B),即先让两个线程分别拿到一把锁,然后去尝试获取对方的锁

举个例子:健康码崩了,程序员回到公司修复bug,被保安拦住了

  • 保安:出示健康码,才能进公司
  • 程序员:我得进公司修复bug,才能出示健康码

类似于:家钥匙锁车里了,车钥匙锁家里了

package thread;

public 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 {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

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

        Thread t2 = new Thread(()->{
            synchronized (locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

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

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

上述代码:

  • t1尝试针对locker2加锁,就会阻塞等待,等待t2释放locker2
  • t2尝试针对locker1加锁,也会阻塞等待,等待t1释放locker1

在这里插入图片描述

当遇到死锁问题,可以通过上述调用栈+状态进行定位

4. 场景三:N个线程,M把锁

随着线程数目/锁的个数增加,此时情况就更复杂了,更容易出现死锁

哲学家就餐问题

现在桌子上均匀摆放有5根筷子,总共有5位哲学家,也就是说每位哲学家左右两边各一双筷子,每一位哲学家要做的事情就是放下筷子或者拿起左右两根筷子,但是每个哲学家什么时候放下筷子,什么时候拿起左右两根筷子是不确定的(抢占式执行)

如果出现下列极端情况,就相当于死锁了

  • 同一时刻,所有的哲学家都拿起左边的筷子,那么此时所有的哲学家都无法拿起右手的筷子
  • 假如哲学家都是比较固执的人,不能拿起两双筷子,就绝对不会放下手里的筷子

上述就是非常典型的死锁情况

死锁是非常严重的问题:死锁会使线程被卡住,没办法继续工作了,而且死锁这种bug,往往都是概率性出现

5. 避免死锁问题

死锁的四个必要条件

  • 锁具有互斥特性:这个是锁的基本特性,一个线程拿到锁之后,其他线程就得阻塞等待
  • 锁不可抢占(不可被剥夺):锁的基本特点,一个线程拿到锁之后,除非自己主动释放锁,否则别人抢不走
  • 请求和保持:属于代码结构层面,一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁
  • 循环等待:属于代码结构层面,多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A

必要条件缺一不可,任何一个死锁的场景,都必须同时具备上述四点

当代码中,确实需要用到多个线程获取多把锁,一定要记得约定好加锁的顺序,就可以有效避免死锁了

6. 内存可见性问题

package thread;

import java.util.Scanner;

public class Demo23 {

    private static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

上述代码,当t2线程读到一个不为0的整数的时候,预期t1就会结束循环,但是结果并非如此

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/431014
推荐阅读
相关标签
  

闽ICP备14008679号