【Java 多线程】:线程状态 & 线程操纵 & 线程同步

[复制链接]
发表于 2025-9-17 21:10:17 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×


  📃个人主页:island1314
⛺️  接待关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


🚀 媒介

之前我们在这篇博客 【Java多线程】:理解线程创建、特性及背景历程  里面已经讲了多线程的底子内容了,现在就要面临多线程的最大问题了,让我们来看看吧


1. 线程的生命周期及状态转换

在Java中,任何对象都有生命周期,线程也不破例,它也有本身的生命周期。


  • 当 Thread 对象创建完成时,线程的生命周期便开始了
  • 当run()方法中的代码正常执行完毕大概线程抛出一个未捕获的非常(Exception)大概错误(Error)时,线程的生命周期便会竣事。
  • 在线程的整个生命周期中,线程大概处于差别的状态

    •  例如,线程在刚刚创建完成时处于新建状态,线程在执行使命时处于运行状态
    • 在线程的整个生命周期中,其根本状态一共有6种,分别是新建(New)状态、可运行(Runnable)状态、锁壅闭(Blocked)状态、无穷等候(Waiting)状态、计时等候(Timed_Waiting)状态和被克制(Teminated)状态,线程的差别状态表明确线程当前正在举行的活动。

  1. public class ThreadState {
  2.   public static void main(String[] args) {
  3.     for (Thread.State state : Thread.State.values()) {
  4.       System.out.println(state);
  5.    }
  6.  }
  7. }
复制代码
接下来针对线程生命周期中的6种根本状态分别举行具体讲授

(1)新建状态


  • 创建一个线程对象后,该线程对象就处于新建状态
  • 此时还没调用start()方法启动线程,和其他Java对象一样,仅仅由JVM为其分配了内存,没有体现出任何线程的动态特征
(2)可运行状态


  • 可运行状态也称为就绪状态。当线程对象调用了 start() 方法后就进入就绪状态
  • 处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,要得到CPU的使用权并开始运行,还需要等候体系的调度
(3)锁壅闭状态


  • 假如处于可运行状态的线程得到了CPU 的使用权,并开始执行 run() 方法中的线程执行体,则线程处于运行状态。
  • 一个线程启动后,它大概不会不停处于运行状态:

    •  当一个线程试图获取一个对象锁,而该对象锁被其他的线程持偶然,则该线程进入锁壅闭状态;
    • 当该线程持有锁的时间,该线程将酿成可运行状态。

(4)无穷等候状态


  • 一个线程在等候另一个线程执行一个(叫醒)动作时,该线程进入无穷等候状态
  • 线程进入这个状态后是不能主动叫醒的,必须等候另一个线程调用 notify() 大概notifyAll() 方法才可以大概叫醒。
(5)计时等候状态


  • 计时等候状态是具有指定等候时间的线程状态
  • 线程由于调用了计时等候的方法(包罗 Thread.sleep()、Object.wait(),Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil() ),而且指定了等候时间,就处于计时等候状态。这一状态将不停保持到超时大概吸收到叫醒通知。
(6)被克制状态


  • 被克制状态是克制运行的线程的状态
  • 线程由于 run() 方法正常退出而殒命,大概由于没有捕获的非常克制了 run() 方法而竣事执行。
 注意:在步伐中,通过一些操纵,可以使线程在差别状态之间转换


  • 线程状态转化如下:


2. 线程操纵的干系方法

🔥 步伐中的多个线程是并发执行的,某个线程若想执行,就必须得到CPU的使用权。Java 虚拟机会按照特定的机制为步伐中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
在盘算机中,线程调度有两种模型,分别是 分时调度模型 和 抢占式调度模型

  • 分时调度模型:是指让全部的线程轮流得到CPU的使用权,而且匀称分配每个线程占用CPU的时间片
  • 抢占式调度模型:是指让线程池中优先级高的线程优先占用CPU;而对于优先级雷同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取 CPU 的使用权
  • Java 虚拟机默认接纳抢占式调度模型。通常环境下步伐员不需要关心盘算机使用的是哪种调度模型,但在某些特定的需求下需要改变这种模式,由步伐自身控制CPU的调度。本节将围绕线程调度的干系知识举行具体讲授。

2.1 线程的优先级 -- priority


🐸 在应用步伐中,假如要对线程举行调度,最直接的方式就是设置线程的优先级


  • 优先级越高的线程得到CPU使用权的机会越大,而优先级越低的线程得到CPU使用权的机会越小。
线程的优先级用1~10的整数体现,数字越大,优先级越高。除了可以直接使用数字体现线程的优先级,还可以使用Thread 类中提供的 3 个静态常量体现线程的优先级
优先级常量功能描述
static int MAX_PRIORITY体现线程的最高优先级,值为 10 
static int MIN_PRIORITY体现线程的最低优先级,值为 1 
static int NORM_PRIORITY体现线程的默认优先级,值为 5 
步伐在运行期间,处于就绪状态的每个线程都有本身的优先级。
例如:主线程具有平常优先级。然而线程的优先级不是固定稳定的,可以通过调用Thread 类的 setPriority(in newPriority)方法举行设置,该方法中的参数 newPriority 吸收的是1~10 的整数大概 Thread类 的 3 个静态常量
下面演示差别优先级的两个线程在步伐中的运行环境
  1. class MaxPriority implements Runnable{
  2.     public void run(){
  3.         for(int i = 0; i < 3; i++){
  4.             System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
  5.         }
  6.     }
  7. }
  8. class MinPriority implements Runnable{
  9.     public void run(){
  10.         for(int i = 0; i < 3; i++){
  11.             System.out.println(Thread.currentThread().getName() + "正在输出: " + i);
  12.         }
  13.     }
  14. }
  15. public class thread_two {
  16.     public static void main(String[] args) {
  17.         // 创建两个线程
  18.         Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
  19.         Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
  20.         minPriority.setPriority(Thread.MIN_PRIORITY);
  21.         maxPriority.setPriority(Thread.MAX_PRIORITY);
  22.         // 开启两个线程
  23.         maxPriority.start();
  24.         minPriority.start();
  25.     }
  26. }
  27. // 运行输出
  28. 优先级较高的线程正在输出: 0
  29. 优先级较低的线程正在输出: 0
  30. 优先级较低的线程正在输出: 1
  31. 优先级较低的线程正在输出: 2
  32. 优先级较高的线程正在输出: 1
  33. 优先级较高的线程正在输出: 2
复制代码


  • 从上面可以看出,优先级较高的maxPriority线程先。运行,它运行完毕后,优先级较低的minPriority 线程才开始运行。所以优先级越高的线程获取CPU时间片的机会越大。
需要注意的是,固然Java 提供了线程优先级,但是这些优先级需要操纵体系的支持。差别的操纵体系对优先级的支持是不一样的,操纵体系中的线程优先级不会和Java中线程优先级逐一对应。


  • 因此,在设计多线程应用步伐时,其功能的实现肯定不能依靠于线程的优先级,而只能把线程优先级作为一种进步步伐服从的本事。
2.2 线程休眠 -- sleep

🦋 线程休眠 指让当火线程停息执行,从运行状态进入壅闭状态,将CPU资源让给其他线程的一种调度方式,可以调用线程的操纵方法 sleep()实现线程休眠,sleep() 方法是 java.lang.Thread 类中界说的静态方法


  • 使用 sleep() 方法时需要指定当火线程体眠的时间,传入一个 long范例的数据作为休眠时间,单元为毫秒,而且恣意一个线程的实例化对象都可以调用该方法。
如下:
  1. public static void main(String[] args) {
  2.     Thread t = new Thread(() ->{
  3.         while (true) {
  4.             System.out.println("Hello thread");
  5.             try {
  6.                 Thread.sleep(1000);
  7.             } catch (InterruptedException e) {
  8.                 e.printStackTrace();
  9.             }
  10.         }
  11.     });
  12.     t.start();
  13.     while (true) {
  14.         System.out.println("Hello main");
  15.         try {
  16.             Thread.sleep(1000);
  17.         } catch (InterruptedException e) {
  18.             e.printStackTrace();
  19.         }
  20.     }
  21. }
复制代码
注意:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠竣事之后,线程就会返回 就绪状态,而不是立刻开始执行,
2.3 线程插队 -- join

🦌 线程插队 指将某个线程插入当火线程中,由两个线程瓜代执行酿成两个线程顺序执行,即一个线程执行完毕之后再执行第二个线程,可以通过调用线程对象的 join() 方法实现线程插队。
假设有两个线程——线程甲和线程乙。


  • 线程甲在执行到某个时间点的时间调用线程乙的 join() 方法,则体现从当前时间点开始CPU资源被线程乙独占,线程甲进入壅闭状态;
  • 直到线程乙执行完毕,线程甲才进入就绪状态,等候获取CPU资源后进入运行状态继续执行。
下面通过一个案例演示 join()方法在步伐中的使用,如下:
  1. class JoinRunnable implements Runnable{
  2.     public void run(){
  3.         for(int i = 1; i <= 3; i++){
  4.             System.out.println(Thread.currentThread().getName() + "输出:" + i);
  5.         }
  6.     }
  7. }
  8. public class thread_join {
  9.     public static void main(String[] args) throws InterruptedException{
  10.         Thread thread = new Thread(new JoinRunnable(), "Thead");
  11.         thread.start();
  12.         for(int i =1; i <= 5; i++) {
  13.             System.out.println(Thread.currentThread().getName() + "输出: " + i);
  14.             if (i == 2) thread.join();
  15.         }
  16.     }
  17. }
  18. // 输出:
  19. main输出: 1
  20. main输出: 2
  21. Thead输出:1
  22. Thead输出:2
  23. Thead输出:3
  24. main输出: 3
  25. main输出: 4
  26. main输出: 5
复制代码
Thread 类不但提供了无参数的线程插队方法 join() ,还提供了带偶然间参数的线程插队方法 join(long millis) 


  • 当执行带偶然间参数的 join(long millis)   方法举行线程插队时,必须等候插入的线程指定时间过后才会继续执行其他线程。
同样是完成线程归并的操纵,join()  和 join(long millis)   还是有区别的。

  • join() 体现在被调用线程执行完成之后才气执行其他线程
  • join(long millis)  则体现被调用线程执行 milis 毫秒之后,无论是否执行完毕,其他线程都可以和它夺取CPU资源。
方法 阐明功能
public void join() x等候线程竣事
public void join(long millis)等候线程竣事,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度
  1. package NoteBook;
  2. class JoinRunnable implements Runnable{
  3.     public void run(){
  4.         for(int i = 1; i <= 3; i++){
  5.             try {
  6.                 Thread.currentThread().sleep(1000);
  7.             } catch (InterruptedException e) {
  8.                 throw new RuntimeException(e);
  9.             }
  10.             System.out.println(Thread.currentThread().getName() + "输出:" + i);
  11.         }
  12.     }
  13. }
  14. public class thread_join {
  15.     public static void main(String[] args) throws InterruptedException{
  16.         Thread thread = new Thread(new JoinRunnable(), "Thead");
  17.         thread.start();
  18.         for(int i =1; i <= 5; i++) {
  19.             System.out.println(Thread.currentThread().getName() + "输出: " + i);
  20.             if (i == 2) thread.join(2000);
  21.         }
  22.     }
  23. }
  24. // 输出
  25. main输出: 1
  26. main输出: 2
  27. Thead输出:1
  28. main输出: 3
  29. Thead输出:2
  30. main输出: 4
  31. main输出: 5
  32. Thead输出:3
复制代码
在上面可以看到,当main线程执行到 i = 2 时,thread线程插队,优先于main线程执行。


  • thread线程 插队是通过调用 join(3000) 方法实现的。
  • 从插队开始 thread线程 独占CPU资源,执行 2000ms之后,main线程继续与thread线程抢占资源。由于 thread线程 每次执行会休眠 1000ms
  • 所以看到的效果是在执行了两次 thread线程 之后,main线程再次进人就绪状态,抢占CPU资源。
     

2.4 线程让步 -- yield

🦁 线程让步 是指在某个特定的时间点,让线程停息抢占CPU资源的举动,即从运行状态或就绪状态转到壅闭状态,从而将CPU资源让给其他线程使用。


  • 这相称于实际生存中地铁列队进站,轮到你进站时,你让其他人先辈了,把这次进站的机会让给其他人。
  • 但是这并不意味着你放弃列队,你只是在某个时间点做了一次让步,过了这个时间点,你依然要举行列队。
  • 线程的让步也是云云:

    • 假如线程甲和线程乙在瓜代执行
    • 在某个时间点线程甲做出让步,让线程乙占用了CPU资源,执行其业务逻辑
    • 线程乙执行完毕之后,线程甲会再次进人就绪状态,夺取CPU资源

下面通过一个案例演示yield()方法在步伐来实现线程让步,如下:
  1. class YieldThread extends Thread{
  2.    public YieldThread(String name){
  3.        super(name);
  4.    }
  5.     public void run() {
  6.         for(int i = 1; i < 5; i++){
  7.             System.out.println(Thread.currentThread().getName() + "---" + i);
  8.             if(i == 2){
  9.                 System.out.print("线程让步: ");
  10.                 Thread.yield();
  11.             }
  12.         }
  13.     }
  14. }
  15. public class thread_yield {
  16.     public static void main(String[] args) {
  17.         Thread thread1 = new YieldThread("thread1");
  18.         Thread thread2 = new YieldThread("thread2");
  19.         thread1.start();
  20.         thread2.start();
  21.     }
  22. }
  23. // 运行结果如下:(答案不固定)
  24. thread1---1
  25. thread1---2
  26. 线程让步: thread2---1
  27. thread2---2
  28. 线程让步: thread1---3
  29. thread2---3
  30. thread2---4
  31. thread1---4
复制代码


  • 从上面可以看出,当线程 thread1 输出2以后,会做出让步,线程 thread2 得到执行权;同样,线程thread2输出2以后,也会做出让步,线程thread1得到执行权。
小提示:yield()方法的毛病


  • 通过 yield() 方法可以实现线程让步,让当前正在运行的线程失去CPU使用权,让体系的调度器重新调度一次
  • 由于Java虚拟机默认接纳抢占式调度模型,全部线程都会再次抢占CPU资源使用权,所以在执行线程让步后并不能保证立刻执行其他线程,CPU大概会有一段空余时间。
2.5 线程中断 -- interuppt 

这里介绍的线程中断是指在线程执行过程中通过手动操纵克制该线程


  • 例如,当用户在执行一次操纵时,由于网络问题导致耽误,则对应的线程对象就不停处于运行状态。假如用户渴望竣事这个操纵,即克制该线程,就要使用线程中断机制了。
在Java中执行线程中断有如下两个常用方法:
  1. public void interrupt()。
  2. public boolean isInterrupted()
复制代码


  • 当一个线程对象调用 interrupt() 方法时,体现中断当火线程对象。每个线程对象都通过一个标志位来判定当前是否为中断状态。
  • isInterrupted()  方法 就是用来获取当火线程对象的标志位的。

    • 该方法有true和false两个返回值。

      • true体现打扫了标志位,当火线程对象已经中断
      • false体现没有打扫标志位,当前对象没有中断
      • 当一个线程对象处于差别的状态时,中断机制也是差别的


下面通过案例来演示差别生命周期状态下的线程中断
  1. public class thread_interrupt {
  2.     public static void main(String[] args){
  3.         // 情况一:实例化线程,但是未启动
  4.         Thread t = new Thread();
  5.         t.interrupt();
  6.         System.out.println("未启动线程是否中断 --- " + t.isInterrupted());
  7.         // 情况二:线程运行
  8.         Thread thread = new Thread(new Runnable() {
  9.             @Override
  10.             public void run() {
  11.                 for(int i = 0; i < 5; i++){
  12.                     if(i == 2) {
  13.                         Thread.currentThread().interrupt();
  14.                         System.out.println("thread 线程是否中断 ---" + Thread.currentThread().isInterrupted());
  15.                     }
  16.                 }
  17.             }
  18.         });
  19.         thread.start();
  20.     }
  21. }
  22. // 运行
  23. 未启动线程是否中断 --- true
  24. thread 线程是否中断 ---true
复制代码
2.6 补充

🥝 Thread 的常见构造方法

方法 阐明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【相识】Thread(ThreadGroup group,
Runnable target)
线程可以被用来分组管理,分好的组即为线程组,这
个现在我们相识即可
  1. Thread t1 = new Thread();
  2. Thread t2 = new Thread(new MyRunnable());
  3. Thread t3 = new Thread("这是我的名字");
  4. Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
复制代码
🥝 Thread 的常见构造属性

有些属性之前已经讲过,我们这里只是做个小结
属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否背景线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()


  • ID 是线程的唯一标识,差别线程不会重复
  • 名称是各种调试工具用到
  • 状态体现线程当前所处的一个环境
  • 优先级高的线程理论上来说更容易被调度到
  • 关于背景线程,需要记着一点:JVM会在一个历程的全部非背景线程竣过后,才会竣事运行。
  • 是否存活,即简朴的理解,为 run 方法是否运行竣事了

3. 线程同步


💫 之前讲过,多线程的并发执行可以进步步伐的服从。但是,当多个线程访问共享资源时,也会引发一些安全问题。


  • 例如,当统计一个班的门生数目时,假如有门生进收支出,则很难统计正确。为相识决如许的问题,需要实现多线程的同步,即限定共享资源在同一时刻只能被一个线程访问。本节将具体讲授线程同步的干系知识。
3.1 线程安全

还记得我们之前学的 join嘛,下面我们来演示一个例子,如下:
  1. public class Test {
  2.     private static int cnt = 0;
  3.     public static void main(String[] args) throws  InterruptedException{
  4.         Object locker = new Object();
  5.         Thread t1 = new Thread(() ->{
  6.             // 对 count 变量自增 5w 次
  7.             for(int i = 0; i < 50000; i++)  cnt++;            
  8.         });
  9.         Thread t2 = new Thread(() ->{
  10.             // 对 count 变量自增 5w 次
  11.             for(int i = 0; i < 50000; i++) cnt++;
  12.         });
  13.         t1.start();
  14.         t2.start();
  15.         // 如果没有这个 join,肯定不行,线程还没有自增完,就开始打印,就会导致打印的 cnt 为0
  16.         t1.join(); //线程随机调度,如果将 t1.join 放在 start 之后,就会使得调度混乱
  17.         t2.join();
  18.         // 预期结果应该是 10 w
  19.         System.out.println("cnt最后自增的结果是: " + cnt);
  20.     }
  21. }
复制代码
运行效果如下:
​

最后效果并不是我们想象的 10 w,因此就会用到 synchronized 的关键字,我们来学习一下吧

3.2 同步代码块

🐅 通过上面可以相识到,线程安全问题其实就是由多个线程同时处置处罚共享资源所导致的。


  • 要想解决上面的线程安全问题,必须保证在任何时刻都只能有一个线程访问共享资源
  • 为了实现多个线程处置处罚同一个资源,在Java中提供了同步机制
  • 当多个线程使用同一个共享资源时,可以将处置处罚共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。
使用synchronized关键字创建同步代码块的语法格式如下:
  1. synchronized(lock){
  2.     处理共享资源的代码块
  3. }
复制代码
表明:


  • 在上面的代码中,lock是一个锁对象,它是同步代码块的关键,相称于为同步代码加锁
  • 当某个线程执行同步代码块时,其他线程将无法执行同步代码块,进入壅闭状态
  • 当火线程执行完同步代码块后,再与其他线程重新劫掠CPU的执行权,抢到CPU执行权的线程将进入同步代码块,执行此中的代码。
  • 以此循环往复,直到共享资源被处置处罚完为止
 需要加锁来保证为  10 w,因此代码修改如下:

注意: 


  • 同步代码块中的锁对象可以是恣意范例的对象,但多个线程共享的锁对象必须是同一个。
  • “恣意”:说的是共享锁对象的范例。
  • 锁对象的创建代码不能放到 run()方法中,否则每个线程运行到 run() 方法时都会创建一个新对象,如许,每个线程都会有一个差别的锁,每个锁都有本身的标志位,线程之间便不能产生同步的效果。

3.3 同步方法

🐻 同步代码块可以有用解决线程安全问题,当把共享资源的操纵放在同步代码块中时,便为这些操纵加了同步锁。
synchronized 关键字除了修饰代码块,同样可以修饰方法,被synchronized 关键字修饰的方法称为同步方法。


  • 同步方法和同步代码块一样,在同一时刻只答应一个线程调用同步方法
synchronized 关键字修饰方法的语法格式如下:
  1. synchronized 返回值类型 方法名([参数列表]) {}
复制代码
代码修改如下:
  1. class Counter{
  2.     public int count;
  3.     synchronized public void increase(){
  4.         count++;
  5.     }
  6.     public void increase2(){ //上下方法等价,上面是下面方法的简化
  7.         synchronized (this){
  8.             count++;
  9.         }
  10.     }
  11. }
  12. public class Demo14 {
  13.     public static void main(String[] args) throws InterruptedException{
  14.         Counter counter = new Counter();
  15.         Thread t1 = new Thread(() ->{
  16.             for(int i = 0; i < 50000; i++){
  17.                 counter.increase();
  18.             }
  19.         });
  20.         Thread t2 = new Thread(() ->{
  21.             for(int i = 0; i < 50000; i++){
  22.                 counter.increase();
  23.             }
  24.         });
  25.         t1.start();
  26.         t2.start();
  27.         t1.join();
  28.         t2.join();
  29.         System.out.println(counter.count);
  30.     }
  31. }
复制代码
这个时间也实现了和同步代码块一样的效果。
多学一招:同步方法的锁


  • 读者大概会有如许的疑问:同步代码块的锁是本身界说的恣意范例的对象,那么同步方法是否也存在锁?假如有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是调用该方法的当前对象,也就是this指向的对象。如许做的利益是,同步方法被全部线程所共享,方法所属的对象相对于全部线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行同步方法时,其他线程就不能进入该方法中,直到当火线程执行完同步方法为止,从而到达线程同步的效果。
  • 偶然间需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时间读者就会有一个疑问:假如不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?Java中静态方法的锁是该方法地点类的class对象,class对象在装载该类时主动创建,该对象可以直接用“类名.class”的方式获取,
  • 同步代码块和同步方法解决多线程问题有利益也有毛病。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一个线程执行。但是线程在执行同步代码时每次都会判定锁的状态,非常斲丧体系资源,服从较低。
3.4 死锁问题

有如许一个场景:一个中国人和一个美国人在一起用饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始辩说不休:


  • 中国人:“你先给我筷子,我再给你刀叉!”
  • 美国人:“你先给我刀叉,我再给你筷子!”
效果可想而知,两个人都吃不成饭。
这个例子中的中国人和美国人相称于差别的线程,筷子和刀又就相称于锁。两个线程在运行时都在等候对方的锁,如许便造成了步伐的停滞,这种征象称为 死锁
  1. public class DeadLocker {
  2.     private static Object locker1 = new Object();
  3.     private static Object locker2 = new Object();
  4.     // 死锁案例:
  5.     public static void main(String[] args) {
  6.         Thread t1 = new Thread(() ->{
  7.             synchronized (locker1){
  8.                 // 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
  9.                 try{
  10.                     Thread.sleep(1000);
  11.                 } catch (InterruptedException e){
  12.                     e.printStackTrace();
  13.                 }
  14.                 synchronized (locker2){
  15.                     System.out.println("t1 加锁成功");
  16.                 }
  17.             }
  18.         });
  19.         Thread t2 = new Thread(() ->{
  20.             synchronized (locker2){
  21.                 try{
  22.                     Thread.sleep(1000);
  23.                 } catch (InterruptedException e){
  24.                     e.printStackTrace();
  25.                 }
  26.                 synchronized (locker1){
  27.                     System.out.println("t2 加锁成功");
  28.                 }
  29.             }
  30.         });
  31.         t1.start();
  32.         t2.start();
  33.     }
  34. }
复制代码
运行输出如下:

缘故因由:两个线程都需要对方占用的资源,但是都无法开释本身的锁,于是两个线程都处于挂起状态,因此造成了上面看到的死锁
解决办法如下:
  1. public class DeadLocker {
  2.     private static Object locker1 = new Object();
  3.     private static Object locker2 = new Object();
  4.     // 解决死锁的方法:
  5.     // 方式一:不构成嵌套即可
  6.     public static void main(String[] args) {
  7.         Thread t1 = new Thread(() ->{
  8.             synchronized (locker1){
  9.                 // 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
  10.                 try{
  11.                     Thread.sleep(1000);
  12.                 } catch (InterruptedException e){
  13.                     e.printStackTrace();
  14.                 }
  15.             }
  16.             synchronized (locker2){
  17.                 System.out.println("t1 加锁成功");
  18.             }
  19.         });
  20.         Thread t2 = new Thread(() ->{
  21.             synchronized (locker2){
  22.                 try{
  23.                     Thread.sleep(1000);
  24.                 } catch (InterruptedException e){
  25.                     e.printStackTrace();
  26.                 }
  27.             }
  28.             synchronized (locker1){
  29.                 System.out.println("t2 加锁成功");
  30.             }
  31.         });
  32.         t1.start();
  33.         t2.start();
  34.     }
  35.     //方式二:约定先用 locker1,后用locker2
  36.     public static void main3(String[] args) {
  37.         Thread t1 = new Thread(() ->{
  38.             synchronized (locker1){
  39.                 // 此处的 sleep 很重要,要确保 t1 和 t2 都分别拿到一把锁之后再进行后续操作
  40.                 try{
  41.                     Thread.sleep(1000);
  42.                 } catch (InterruptedException e){
  43.                     e.printStackTrace();
  44.                 }
  45.                 synchronized (locker2){
  46.                     System.out.println("t1 加锁成功");
  47.                 }
  48.             }
  49.         });
  50.         Thread t2 = new Thread(() ->{
  51.             synchronized (locker1){
  52.                 try{
  53.                     Thread.sleep(1000);
  54.                 } catch (InterruptedException e){
  55.                     e.printStackTrace();
  56.                 }
  57.                 synchronized (locker2){
  58.                     System.out.println("t2 加锁成功");
  59.                 }
  60.             }
  61.         });
  62.         t1.start();
  63.         t2.start();
  64.     }
  65. }
复制代码
解决死锁的方法重要有以下几种:

  • 制止死锁:通过粉碎死锁的须要条件之一来防备死锁

    • 互斥条件:答应历程同时访问某些资源,但并非全部资源都可以被同时访问,因此这种方法在实际应用中大概有限。
    • ”不可抢占条件:答应历程强行从占据者那里夺取某些资源,即当一个历程申请不到新的资源时,必须开释所占据的资源,之后再重新申请。
    • 循环等候条件:通过制止存在一个历程等候序列(如P1等候P2,P2等候P3,….,Pn等候P1),可以有用制止死锁。

  • 检测与解除死锁:答应体系进入死锁状态,但通过检测息争除来解决

    • 检测死锁:通过检测体系状态,辨认出死锁的历程
    • 解除死锁:通过克制或回滚死锁历程来解除死锁。



3.5 重入锁

🦋 重入锁(ReentrantLock)的作用雷同于synchronized 关键字,synchronized 关键字是通过Java虚拟机实现的,而重入锁通过JDK实现。


  • 重入锁是指可以给同一个资源添加多个锁,而且开释锁的方式与synchronized也差别。
  • synchronized的锁在线程执行完毕之后主动开释。
  • ReentrantLock的锁必须手动开释。
重入锁的使用格式如下:
  1. private ReentrantLock reentrantLock = new ReentrantLock();
  2. //加锁
  3. reentrantLock.lock();
  4. //需要锁的数据
  5. //释放锁
复制代码
修改后的代码如下: 
  1. package NoteBook;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. class Counter {
  4.     public int count;
  5.     // 使用 ReentrantLock 代替 synchronized
  6.     private final ReentrantLock lock = new ReentrantLock();
  7.     public void increase() {
  8.         // 使用 lock 进行加锁
  9.         lock.lock();
  10.         try {
  11.             count++;
  12.         } finally {
  13.             lock.unlock(); // 保证释放锁
  14.         }
  15.     }
  16.     // 静态方法使用 ReentrantLock 来代替 synchronized
  17.     private static final ReentrantLock staticLock = new ReentrantLock();
  18.     public static void increase3() {
  19.         // 使用静态锁来替代 synchronized
  20.         staticLock.lock();
  21.         try {
  22.             // 静态方法的同步内容
  23.         } finally {
  24.             staticLock.unlock(); // 保证释放静态锁
  25.         }
  26.     }
  27.     public static void increase4() {
  28.         // 使用锁住 Counter.class 对象来代替 synchronized
  29.         staticLock.lock();
  30.         try {
  31.             // 静态方法的同步内容
  32.         } finally {
  33.             staticLock.unlock(); // 保证释放锁
  34.         }
  35.     }
  36. }
  37. public class thread_reentrant {
  38.     public static void main(String[] args) throws InterruptedException {
  39.         Counter counter = new Counter();
  40.         // 创建两个线程,分别执行 increase 方法
  41.         Thread t1 = new Thread(() -> {
  42.             for (int i = 0; i < 50000; i++) {
  43.                 counter.increase();
  44.             }
  45.         });
  46.         Thread t2 = new Thread(() -> {
  47.             for (int i = 0; i < 50000; i++) {
  48.                 counter.increase();
  49.             }
  50.         });
  51.         t1.start();
  52.         t2.start();
  53.         t1.join();
  54.         t2.join();
  55.         System.out.println(counter.count); // 输出最终结果
  56.     }
  57. }
复制代码
从上面可以看出, 运行效果 和 使用 synchronized 效果划一。假如需要在此底子添加几把锁,只需要调用 lock() 方法即可。 需要注意的是,使用重入锁是加了几把锁就必须开释几把锁,否则会使线程处于壅闭状态
为什么 ReentrantLock 更灵活?



  • ReentrantLock 是可重入的,这意味着同一线程可以多次得到同一个锁而不会发存亡锁。
  • ReentrantLock 提供了更丰富的功能,比如实验锁定 (tryLock()),锁定带有超时的 (lock(long time, TimeUnit unit)) 等,这对于一些复杂的并发控制逻辑黑白常有用的。
  • 与 synchronized 相比,ReentrantLock 更实用于需要在多个线程之间共享更复杂的控制逻辑时。
结论:

通过将 synchronized 更换为 ReentrantLock,我们实现了与原来代码雷同的线程安全效果,只不外是显式控制加锁和开释锁的过程。如许做可以进步代码的可读性和灵活性,尤其在复杂并发操纵中,ReentrantLock 提供了更多控制选项。




回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表