Java多线程(4)---死锁和Synchronized加锁流程

简介: Java多线程(4)---死锁和Synchronized加锁流程

前言

       在多线程的讲解当中,我们可以知道synchronized是加锁操作,让两个线程发生互斥效果,在代码中使用synchronized关键字来实现锁的获取和释放。如果是刚刚接触多线程的人,我希望你可以从第一章多线程开始学习:http://t.csdn.cn/0vEhY


一.synchronized

1.1概念

      Synchronized是Java中内置的锁机制,用于实现线程同步。它可以通过在代码中使用synchronized关键字来实现锁的获取和释放。Synchronized关键字可以用在方法上或者代码块中。当一个线程执行到synchronized修饰的代码块时,它会尝试获取锁,如果锁没有被其他线程占用,则获取成功,执行代码块中的内容。如果锁已经被其他线程占用,则该线程会进入等待状态,直到获取到锁才能继续执行。

1.2Synchronized是什么锁?

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

注:需要使用公平锁,建议使用ReentrantLock来实现。ReentrantLock提供了公平锁和非公平锁两种模式,通过构造函数的参数来指定锁的模式。

1.3Synchronized加锁工作过程

       对于锁资源只有一个或者两个线程交替竞争的,仍然需要使用系统调用,无疑对CPU资源是极大的消耗。因此,在jdk1.6针对Synchronized加锁进行优化。按对锁的竞争程度划分成:无锁,偏向锁,轻量级锁,重量级锁。简单而言就是从无锁-->重量级锁。

无锁

当你添加了锁时,如果编译器认为不需要加锁,会自动删除,因此便是无锁

偏向锁

偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.

如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.

注:相当于做个标记,相当于 "延迟加锁" . 能不加锁就不加锁, 尽量避免不必要的加锁开销.

轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

此处的轻量级锁就是通过 CAS 来实现

  1. 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  2. 如果更新成功, 则认为加锁成功如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

注:此处的自旋锁不会一种持续进行,而是达到一定的时间/重试次数, 就不再自旋了.

重量级锁

       如果锁竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex .

具体流程:

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
  • 这个线程, 尝试重新获取锁

1.4其他优化操作

       我们额外补充2个编译器对锁的优化操作。锁消除和锁粗化

锁消除

       代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

StringBuffer tq02 = new StringBuffer();
tq02.append("a");
tq02.append("b");
tq02.append("c");
tq02.append("d");

每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加

锁解锁操作是没有必要的, 白白浪费了一些资源开销.因此将锁给优化了。

锁粗化

锁的粗化每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加是根据锁的粒度:粗和细

实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.但可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁.

 

二.死锁

2.1什么是死锁

       死锁是指在多进程系统中,每个进程都在等待某个资源,而该资源又被其他进程占用,导致所有进程都无法继续执行的状态。

例如:A、B、C、D和E去上厕所,A进入厕所并且锁门,B.C.D等待,可是A刚刚进入厕所,因为特殊的原因,凭空转移到了外面,A就得重新排队,可是门还是锁着的啊,因此导致了死锁。

2.2死锁的几个经典场景

经典场有:

  1. 一个线程,一把锁
  2. 两个线程,两把锁
  3. 多个线程,多把锁

1.一个线程,一把锁

      一个线程连续被同一个加锁两次,如果是不可重入锁,那么会是死锁。

解析:我去上厕所,我把厕所门锁住,再通过厕所的窗户出去,然后再来上厕所,发现厕所锁住了,就耐心等待,却没想过这是自己锁的。

代码实现:

public class Counter {
   void increase() {       
     synchronize(this){
       increase()   //可以理解为翻窗逃走,第二次加锁时,是锁了的
    }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
                counter.increase();
        });
            t1.start();
    }
}

2.两个线程,两把锁

       线程1先获取锁A,再尝试获取锁B,同时,线程2先获取锁B,再尝试获取锁A,此时两个线程就会互相僵住,谁都获取不到对方持有的锁。

解析:我在汽车里,车钥匙在我妻子手上,我出不来,我妻子在房间里,房间钥匙在我手上,我妻子也出不来,导致双方被锁,导致死锁。

代码示例:

public class Test {
    public static void main(String[] args) {
        //2个锁对象
        Object lockerA = new Object();
        Object lockerB = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1尝试获取锁A");
            synchronized (lockerA){
                System.out.println("t1获取到锁A");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1尝试获取锁B");
                synchronized (lockerB){
                    System.out.println("t1获取到锁B");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2尝试获取锁B");
            synchronized (lockerB){
                System.out.println("t2获取到锁B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2尝试获取锁A");
                synchronized (lockerA){
                    System.out.println("t2获取到锁A");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3.多个线程,多把锁

       很明显啊,两把锁,两个线程也有问题,更何况是多把锁啊,在这方面最经典的是"哲学家就餐问题"。

如图:火柴人是哲学家、红线是筷子,每一个哲学家的左右都有一根筷子。规定,当有一根哲学家饿了,会先拿起左边的筷子,然后再拿右边的筷子,吃完了就放下筷子。

造成死锁问题:每一个哲学家都饿了,然后都拿起了左边的筷子,可是当拿右边的筷子时,发现有其他人在使用,所以导致阻塞,然后一直等待别人吃饱放下筷子,可是每个人都在等待。

2.3死锁产生的条件

死锁产生需要四个条件:

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

 

2.4如何解决死锁

       想没有死锁,那么我们可以从死锁产生的条件入手,只有破坏其他一条就可以了。 互斥使用和不可抢占是锁的基本特性,因此无法干预,但是请求和保持,也不可能改变,因为这是代码执行逻辑。因此只有循环等待,我们可以打破

       为了解决死锁问题,可以采取预防、避免、检测和解除四种方法。

预防:通过设置某些限制条件,以防止死锁的发生。

避免:系统在分配资源时根据资源的使用情况提前作出预测,从而避免死锁的发生。

检测:允许系统在运行过程中产生锁,但系统中有相应的管理模块可以及时检测出已经产生的死锁,并精确地确定与死锁有关的进程和资源,然后采取适当措施清除系统中已经产生的死锁。


解除:当发现有进程死锁后,立即解脱它从死锁状态中出来。常用的方法包括剥夺资源和撤销进程。剥夺资源是从其他进程中剥夺足够数量的资源给死锁进程,以解除死锁状态。撤销进程可以直接撤销死锁进程或撤销代价最小的进程,直至有足够的资源可用,从而消除死锁状态

目录
相关文章
|
27天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
157 60
【Java并发】【线程池】带你从0-1入门线程池
|
20天前
|
存储 架构师 安全
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
锁状态bits1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode001偏向锁线程ID101轻量级锁指向栈中锁记录的指针000重量级锁指向互斥量的指针010尼恩提示,讲完 如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等优化手段 , 可以得到 120分了。如减少锁粒度、锁粗化、关闭偏向锁(-XX:-UseBiasedLocking)等‌。JVM锁的膨胀、锁的内存结构变化相关的面试题,是非常常见的面试题。也是核心面试题。
深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(图解+史上最全)
|
16天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
66 23
|
23天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
93 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
1月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
135 14
|
Java
java并发编程:死锁代码示例
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
176 0
|
Java
一个简单的Java死锁示例(转)
在实际编程中,要尽量避免出现死锁的情况,但是让你故意写一个死锁的程序时似乎也不太简单(有公司会出这样的面试题),以下是一个简单的死锁例子,程序说明都写着类的注释里了,有点罗嗦,但是应该也还是表述清楚了的。
820 0
|
1月前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
61 13
|
1月前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。