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

目录

  • [Day 7:多线程(5)](#Day 7:多线程(5))
    • [1. 死锁](#1. 死锁)
    • [2. 死锁场景](#2. 死锁场景)
    • [3. 场景二:两个线程,两把锁](#3. 场景二:两个线程,两把锁)
    • [4. 场景三:N个线程,M把锁](#4. 场景三:N个线程,M把锁)
    • [5. 避免死锁问题](#5. 避免死锁问题)
    • [6. 内存可见性问题](#6. 内存可见性问题)

Day 7:多线程(5)

回顾synchronized

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

1. 死锁

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

上述线程t2的代码相当于

java 复制代码
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
        synchronized (counter2) {
            synchronized (counter2){
                 count++;
            }
        }
    }
});

假设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,才能出示健康码

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

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

上述代码:

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

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

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

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

哲学家就餐问题

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

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

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

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

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

5. 避免死锁问题

死锁的四个必要条件

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

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

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

6. 内存可见性问题

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

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

相关推荐
懒羊羊不懒@15 分钟前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss27318 分钟前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父30 分钟前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛1 小时前
Spring面试
java·spring·面试
Java中文社群1 小时前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme1 小时前
java.nio 包详解
java·python·nio
零千叶1 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
白云千载尽1 小时前
leetcode 912.排序数组
算法·leetcode·职场和发展
哆啦刘小洋2 小时前
Tips:预封装约束的状态定义
算法
代码充电宝2 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表