首页 > 编程语言 >JavaEE 初阶(7)——多线程5之线程安全中 -->“死锁”

JavaEE 初阶(7)——多线程5之线程安全中 -->“死锁”

时间:2024-07-27 23:54:51浏览次数:14  
标签:初阶 synchronized locker2 t1 死锁 加锁 线程 多线程

目录

一. 什么是“死锁”

二. 产生死锁的场景 

 场景1:一个线程连续加锁

 场景2:两个线程两把锁

场景3:N个线程M把锁 

三. 产生死锁的四个必要条件(缺一不可)

四. Java 标准库中的线程安全类


一. 什么是“死锁”

并非是 synchronized 就一定线程安全,还要看代码具体咋写。到底是否加 synchronized ,和具体场景直接相关。“无脑加锁”的做法不推荐,锁需要的时候才使用,不需要的时候不要使用,会付出代价(性能)。使用锁,就可能触发阻塞,一旦某个线程阻塞,啥时候能恢复阻塞,继续执行,是不可预期了...(可能需要非常多时间)

因此,synchronized 如果使用不当,就会出现“死锁”

死锁:发生在多个进程或线程在执行过程中,因为竞争资源而造成的一种互相等待的僵持状态,没有任何一个进程或线程可以继续执行下去。简单来说,死锁是指两个或多个进程无限期地等待永远不会发生的条件,导致它们无法继续执行。

二. 产生死锁的场景 

 场景1:一个线程连续加锁
public class DeadLock2 {
    private static int count = 0;

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

运行结果: 

代码分析:

  1. 里面的synchronized要想拿到锁,就需要外面的synchronized释放锁
  2. 外面的synchronized要释放锁,就需要执行到 }
  3. 要想执行到 } 就需要执行完 count++
  4. 但是 count++ 阻塞着呢~

这样,就会一直阻塞等待,造成“死锁”.....

但是,实际运行并没有出现“死锁”现象,这是为什么呢?

因为 synchronized 针对这种情况做了特殊处理~

synchronized 是 “可重入锁”,针对上述一个线程对同一把锁连续加锁做了特殊处理,是Java为了减少程序员写出死锁的概率,引入的特殊机制。同样的代码,换成C++ / Python 就会出现“死锁”。

可重入锁” 的一个主要特性是它允许同一个线程多次获取同一把锁。如果线程已经持有了这把锁,那么它可以再次进入由这把锁保护的代码块,而不会阻塞产生“死锁”。这是通过记录锁的持有者,并引入一个计数器来实现的:

初始情况下,计数器是0  --> 执行到 { 计数器 +1 ,执行到 } 计数器 -1。如果某次 -1 之后,计数器为0了,说明这次就要真正释放锁了。

此处涉及到了 “引入计数” 的思想,后面讲到 JVM 中的垃圾回收机制也会有 

 场景2:两个线程两把锁
public class DeadLock {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("t1 加锁locker1 完成");

                //这里的sleep是为了让t1和t2都先拿到自己锁,然后再拿对方的锁
                //如果没有sleep执行顺序就不可控
                //可能出现某个线程一口气拿到两把锁,另一个线程还没执行,无法构造出死锁.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1 加锁locker2 完成");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized(locker2){
                System.out.println("t2 加锁locker2 完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(locker1){
                    System.out.println("t2 加锁locker1 完成");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

运行结果:

“死锁”场景描述:

  1. t1 线程先对 locker1 加锁,t2 线程先对 locker2 加锁   
  2. t1 线程在不释放 locker1 的情况下,对 locker2 加锁;同时,t2 线程在不释放 locker2 的情况下,对 locker1 加锁

这样,就会造成“循环依赖”的效果,产生“死锁”。

形象的比喻:疫情期间,一码通又寄了.......程序员来到公司楼下,被保安拦住了。
保安:请出示一码通
程序员:我得上楼修了bug,才能出示一码通
保安:你得出示一码通,才能上楼

通过上述例子,“死锁”往往会出现“依赖循环”。针对这种“死锁”情况,“可重入锁”机制就无能为力了....


 

通过观察 jconsole,我们发现此时 t1 和 t2 线程都处于 BLOCKED 阻塞状态。

解决办法:t1 线程可以先释放 locker1,再请求 locker2

场景3:N个线程M把锁 

经典模型:哲学家就餐问题

 现在共有 5个 哲学家,桌子上有 5根筷子。5个哲学家要吃桌子上的面条。 

 此时,每根筷子都被哲学家左手拿起来了,他们的右手都拿不到筷子了.....由于哲学家 非常固执,当他吃不到面条的时候,也绝对不会放下左手的筷子.....这样,每一个人都吃不到面条了,只能循环等待....


“哲学家”相当于“线程”,“筷子”相当于“锁”。如果线程1拿A锁,线程2拿B锁,线程3拿C锁,线程4拿D锁,线程5拿E锁。这时候,5个线程再同时请求等待旁边的锁,就会造成“死锁”的局面....

但是,上述情况是由代码结构造成的,可以通过一些方法来避免。


 比如, 必须先针对编号小的锁加锁,后针对编号大的锁加锁。每个哲学家必须先拿起编号小的筷子 ,后拿起编号大的筷子。同一时刻,拿起第一根筷子

在编写代码时,可以给锁编号:1,2,3.....N。规定所有线程在加锁的时候,都必须按照一定的顺序来加锁(比如,必须先针对编号小的锁加锁,后针对编号大的锁加锁)

三. 产生死锁的四个必要条件(缺一不可)

a. 互斥条件(锁的基本特性):同一把锁 锁住的代码块 一次只能由一个线程执行(基本特性无能为力)

b. 不可被抢占条件(锁的基本特性):线程1 拿到了锁A,如果线程1 不主动释放锁A,线程2就不能把锁A抢过来(基本特性无能为力)

c. 持有和等待条件(代码结构):线程1 在持有 A锁 的情况下(持有),去拿B锁(等待);线程2 持有 B锁 的情况下(持有),又去拿A锁(等待)。(解决办法:线程1 可以先释放A锁,再请求B锁)

d. 循环等待 / 循环依赖 / 环路等待 条件(代码结构):每个线程至少持有一个把锁,并等待获取下一个线程所持有的锁,构成一个循环等待链。(解决办法:给锁编号,并约定要按照一定的顺序加锁)

四. Java 标准库中的线程安全类

 Java标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施,因此,可以避免很多隐形加锁情况,防止产生“死锁”

ArrayList,Queue,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder......

但是还有一些是线程安全的,使用了一些锁机制来控制,内置了synchronized,这些类是不推荐使用的,甚至 jdk 未来版本,会把这几个东西删掉....

Vector,HashTable,Stack,StringBuffer....

例如:StringBuffer中  

标签:初阶,synchronized,locker2,t1,死锁,加锁,线程,多线程
From: https://blog.csdn.net/2301_80243321/article/details/140673484

相关文章

  • JavaEE 初阶(9)——多线程7之 wait 和 notify
    目录一.监视器锁与监视器二.wait()  三.notify()和notifyAll()3.1notify() 3.2notifyAll()3.3wait等待 和sleep休眠的对比(面试题)wait(等待)/notify(通知)由于线程在操作系统上是“随机调度,抢占式执行”的,因此线程之间执行的先后顺序难以预知。但是......
  • 学习c语言第十五天(初阶测评)
    选择题1.下列程序输出结果为672.下列程序输出结果为 死循环打印3.i和j的值分别为什么 214.k的终值是什么905.输出结果是什么 16.正确的是    C7.C语言规定main函数位置    C8.不正确的是    D9.正确的是     c ......
  • C语言初阶(6)
    1.函数递归定义程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可......
  • 【C++第九章】初阶模板
    C++模板初阶模板介绍......
  • 多线程实现阻塞队列
    今天面试被问到了,多线程实现阻塞队列,记录一下。1importjava.util.LinkedList;2importjava.util.Queue;3importjava.util.concurrent.locks.Condition;4importjava.util.concurrent.locks.ReentrantLock;56publicclassFixedSizeBlockingQueue<T>{7......
  • RT-Thread多线程
    RT-Thread启动流程分析线程的状态初始状态:线程刚开始创建,还没开始运行时就处于,初始状态。就绪状态:在就绪状态下,线程按照优先级排队,等待被执行。运行状态:线程正在运行,在单核系统中,只有rt_thread_self()函数返回的线程处于运行状态,但多核系统下,运行的线程不止一个。......
  • C++多线程基本使用方式
    一、线程创建        创建线程的函数  thread t(函数名f,函数f的参数) 或者 用lambda表达式代码:#include<iostream>#include<thread>#include<vector>usingnamespacestd;voidoutput(stringinput,inta){ cout<<input<<endl; cout<<a......
  • 【C++初阶】vector
    【C++初阶】vector......
  • 分布式集群与多线程高并发
     后台数据的处理语言有很多,Java是对前端采集的数据的一种比较常见的开发语言。互联网移动客户端的用户量特别大,大量的数据处理需求应运而生。可移动嵌入式设备的表现形式很多,如PC端,手机移动端,智能手表,Google眼镜等。Server2client的互联网开发模式比较常见,有一种新的数......
  • java多线程把数据迁移到不同数据库中
    publicvoidsync_table_test_thread()throwsSQLException,InterruptedException{    longstart=System.currentTimeMillis();    SimpleDateFormatformat=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");    //获取要迁移oracle表数据库......