Java基础之死锁与线程通信

250 阅读4分钟

回顾

在上一篇 Java基础之线程安全 介绍了怎么处理线程安全问题,主要的思路就是使用线程同步的方法,线程A执行时获取 "锁",然后对共享数据进行操作,操作完毕后退出,线程B获取锁对共享数据进行操作。

我们看到此时此时线程B必须等线程A对共享数据操作完毕后才能对共享数据进行操作。此时虽然性能有所下降,但是保证了线程安全和数据准确性。我们在多线程编程要时刻注意线程安全的问题。且注意:

  • 操作共享数据代码块的判定
  • 锁对象的选择(锁需要是同一个)

死锁

我们先前都是讲的都是单个锁的问题,但是当锁有多个时,会不会出现什么其他问题呢? 例如

public class TestMain {
    static StringBuffer sb1= new StringBuffer();
    static StringBuffer sb2= new StringBuffer();
    public static void main(String[] args) {
        new Thread(){
            public  void  run(){
                synchronized (sb1){
                    try {
                        Thread.currentThread().sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("A");
                    synchronized (sb2){
                        sb2.append("B");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
        new Thread(){
            public  void  run(){
                synchronized (sb2){
                    try {
                        Thread.currentThread().sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("C");
                    synchronized (sb1){
                        sb2.append("D");
                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
    }
}

我们创建了两个线程(匿名方式),且有两个锁,执行这段代码,你会发现控制台卡住了,如下图。

此时因为都在等待对方的锁,故称为死锁。

我们在编码实践中因尽量避免以上问题,在现实情况下多个锁不可避免时,我们可以设定锁的优先级别 例如A>B 任何线程都需要先获得A 才能获得B,此时避免死锁。更糟糕都情况下,我们可以kill一个线程使得目标锁释放,使得程序继续运行。

线程通信

Java基础之多线程编程 讲解了多个关于Thread类的方法,回顾下

Thread.currentThread() //获取当前线程
//以下都是线程实例上的方法
setName(); //设置线程名字
getName(); //获取线程名字
yield();//显示释放cpu的执行权  
join();//在一个线程执行中调用另一个目标线程的join方法,意味着立马执行目标线程,且执行完毕才回到原线程
isAlive();//判断当前线程是否还存活
sleep();//显示的让线程睡眠
setPriority() //设置当前线程的优先级
getPriority()//获取当前线程的优先级

此时我们补充三个Object类下关于线程的三个方法

wait();//令当前线程挂起放弃cpu且放弃同步资源
notify();//唤醒正在排队等待同步资源的线程,其中优先级最高的结束等待
notifyAll();//唤醒正在排队的等待资源的所有线程。

可见wait方法与notify notifyAll是对应的操作:那么我们可以利用这三个方法使得线程交替执行,即线程相互通信。

注:Object中在三个方法只能在synchronized方法或者同步代码块中执行,否则会抛异常

案例

使两个线程打印1到100 且两个线程交替打印 代码实现:(两个线程打印1到100 没有交替打印版本)

class PrintNum implements Runnable{
   int num=1;
   @Override
   public void run() {
       while (true){
           synchronized (this) {
               if(num<=100){
                   try {
                       Thread.currentThread().sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+":"+num);
                   num++;
               }else {
                   break;
               }
           }
       }
   }
}

PrintNum printNum=new PrintNum();
Thread t1=new Thread(printNum);
Thread t2=new Thread(printNum);
t1.setName("A");
t2.setName("B");
t1.start();
t2.start();

利用notify await 实现交替打印代码

class PrintNum implements Runnable{
   int num=1;
   @Override
   public void run() {
       while (true){
           synchronized (this) {
               notify();
               if(num<=100){
                   try {
                       Thread.currentThread().sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName()+":"+num);
                   num++;
               }else {
                   break;
               }
               try {
                   wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }
   }
}

控制台打印如下

解释下:当第一个线程A进入时notify执行,但此时没有等待状的但线程,打印1后,await(),线程A进入等待状态且释放同步锁,线程B开始执行,进入synchronized代码块内,此时已经获取了锁,notify()后,唤醒了线程A,但是锁没释放,打印2后,await()进入等待状态,然后线程A获得锁资源,又进入同步代码块执行,依次下去边交替打印。仿佛两个线程之间实现了相互通信。

总结

本篇主要讲解了 死锁和线程通信两个概念

  • 什么是死锁问题,应该如何避免
  • 线程通信中Object类中notify notifyAll await的准确含义和使用,应特别区分sleep与await区别,其中sleep不释放锁资源,而await释放锁资源。

喜欢本文的朋友们,欢迎长按下图关注订阅号"我的编程笔记",收看更多精彩内容~~