Java多线程和高并发总结(基础篇)

一、进程的概念?

进程可以理解为一个应用程序执行的实例(比如在windows下打开Word就启动了一个进程),进程是资源分配的最小单位,每个进程都有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。进程主要有数据、程序和程序控制块(PCB)组成,其中PCB是系统感知进程存在的唯一标志。

二、线程的概念?

线程是进程中的一个执行单元,一个进程中可以启动多个线程,并且一个进程中的多个线程可以共享此进程中的所用资源。每个线程都有自己独立的运行时桟和程序计数器,线程是CPU调度的最小单位

三、并发和并行的概念?

  • 并发(concurrent):单核CPU 下,操作系统通过任务调度器,将CPU的时间片分给不同的线程使用,只是由于CPU的切换速度非常快(Windows下一个最小的时间片是15ms),让用户看上去是同步执行的,实际上还是串性执行的。这种线程轮流使用CPU的方法叫并发。
  • 并行(parallel):多个cpu或者多台机器同时处理任务,是真正意义上的同时执行。

四、创建和启动线程

Java中创建线程的方法本质上只有两种,那就是继承Thread类和实现Runnable接口两种,其余的方式都是他俩的变种。

1、继承java.lang.Thread

通过继承Thread类来创建并启动多线程的步骤如下:

  • 定义Thread类的子类,并重写Thread的run()方法,该run()方法的方法体就代表了线程要完成的任务,因此把run()方法称为线程执行体。

  • 创建Thread类的实例及创建线程对象。

  • 调用线程对象的satrt()方法来启动该线程。

public class ThreadTest {

    public static void main(String[] args) {
        Task task = new Task();
        task.setName("test-thread");
        task.start();
    }

    static class Task extends Thread{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }

}
2、实现java.lang.Runnable接口

实现Runnable接口来创建并启动多线程的步骤如下。

  • 定义Runnable接口的实现类,实现Runnable接口的run()方法,该run()方法的方法体同样是该线程执行体。

  • 创建Runnable接口实现类的实例,并以此实例作为Thread的构造方法的参数来创建Thread对象。

  • 调用线程对象的start()方法来启动该线程。

Java 8 以前的写法:

public class RunnableTest{
    
    public static void main(String[] args){
        Thread t=new Thread(new Task(),"test-thread");
        t.start();
    }
    
    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        } 
    }
    
    
}

Java 8以后可以用lambda简化代码

public class RunnableTest{
    
    public static void main(String[] args){
        
        Thread t=new Thread(()->{
            System.out.println(Thread.currentThread().getName());
        },"test-thread");
        t.start();   
    }
}
3、实现java.util.concurrent.Callable<V>接口

​ 在很多的Java书籍或者博客上都写的Java中第三种创建线程的方式是实现jdk1.5开始提供的Callable接口。的确,从形式上看,它确实是一种新的方式,我们只需要实现Callable接口的call()方法,而且call方法还可以有返回值,还可以抛出异常,功能非常牛逼!并且在jdk1.5中提供了Future接口来表示call()方法的返回值,并且提供了一个FutureTask实现类。但是,从本质上看,FutureTask类直接实现了RunnableTask接口,间接实现了Runnable和Future接口。因此,这就相当于还是一个Runnable的实现类,而且打开源码可以发现(在FutureTask的第255行开始)确实是这样:

因此,这种实现方式只是使用Runnable的特例,本质还是在使用Runnable实现创建一个新线程。不过话说回来,虽然是特例,但是我们还是有必要掌握如何使用Callable来启动新线程的。

创建并启动有返回值的线程的步骤如下。

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。在创建Callable接口实现类的实例。

  • 使用FutureTask类来包装Callable接口对象,该FutureTask对象,封装了该Callable对象的call()方法的返回值。

  • 使用FutureTask类对象作为Thread对象的构造函数参数创建并且启动新线程。

  • 调用FutureTask对象的get()方法,获得子线程执行结束后的返回值。

package top.easyblog;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author :huangxin
 * @modified :
 * @since :2020/04/01 00:49
 */
public class CallableTest {

    public static void main(String[] args) {
        /**
         * FutureTask实现了Runnable接口,因此FutureTask也就相当于是一个Runnable实现类
         */
        FutureTask<Integer> ft = new FutureTask<Integer>(new Task());
        Thread thread = new Thread(ft, "callable-test");
        thread.start();
        try {
            //FutureTask的get()方法会阻塞
            System.out.println(ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    static class Task implements Callable<Integer> {

        /**
         * 实现call()方法
         *
         * @return
         * @throws Exception
         */
        public Integer call() throws Exception {
            int ret = 0;
            for (int i = 0; i < 100; i++) {
                ret += i & (i - 1);
            }
            return ret;
        }
    }

}
4、使用线程池

这块内容比较多而且非常重要,感兴趣的同学可以参考我的博文:高并发编程之线程池ThreadPoolExecutor详解

五、线程的上下文切换(Thread Context Switch)

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在用完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

当发生以下情况时,会发生线程上下文切换:

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己代用了sleep()、yield()、wait()、join()、park()….

六、Java中线程的5种状态

Java中的线程有5种转态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)

  • (1)New:当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
  • (2)Runnable:线程被创建后,其他线程调用了线程的start()方法之后该线程处于这个状态。该状态的线程处于可执行线程池中,就绪状态的线程表示有权力去获取CPU时间片了,CPU时间片就是执行权。当线程拿到CPU时间片后就会马上执行run()方法,这时线程就进入了运行状态。
  • (3)Running:线程获取到CPU时间片后线程执行任务的状态
  • (4)Blocked:阻塞状态是由于当前执行中的线程因为某种原因放弃CPU使用权,暂时停止运行的状态。处于阻塞状态的线程必须再次切换到Runnable才能再次获取到CPU时间片。阻塞的类型可以分为三种:
  • 等待阻塞:运行中的线程执行了wait()方法,JVM会把该线程放入等待队列(waiting queue)
  • 同步阻塞:运行中的线程尝试获取一个对象的对象锁时,当发现这把锁正在被其他线程使用,那么JVM就会把该线程放入锁池(lock pool )
  • 其他阻塞:运行中的线程调用了sleep()、join()方法或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • (5)Dead:当执行中的线程执行完线程任务或执行过程中发生了Error或Exception线程都会死亡。

下图更详细的展示了Java中一个线程的生命周期:

七、Java中如何停止线程的方法

如何停止线程是Java并发面试中的常见问题,这里总结一下。

答题思路

  • 停止线程的正确方式是使用中断
  • 想停止线程需要停止方,被停止方,被停止方的子方法相互配合
  • 扩展到常见的错误停止线程方法:已被废弃的stop/suspend,无法唤醒阻塞线程的volatile标记位方式
1、正确的方式:使用interrupt()安全的终止程序(推荐)

关于使用interruput()方法来终止线程,最佳的说明文档就是javadoc了。在这个方法的javadoc上说明了在使用interruput()的三种情况:

  • (1)如果当前线程处于阻塞状态:比如调用了sleep()、wait()以及重载方法、join()以及重载方法或者其他操作让当前线程进入到阻塞状态,此时调用interrupt(),那么它的中断状态会被清除为false并且会收到一个InterruptedException异常

> 比如一个线程调用了wait()方法后处于阻塞状态,此时别处调用interrupt()后会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。

  • (2)如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时,线程的中断标记会被设置为true,并且它会立即从选择操作中返回。

  • (3)线程未处于阻塞状态,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”

第一个方法是通过循环不断判断自身是否产生了中断:

public class Demo1 implements Runnable {

    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted()) {
            if (num % 10000 == 0) {
                System.out.println(num);
            }
            num++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Demo1());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

}

在上面的代码中,我们在循环条件中不断判断线程本身是否产生了中断,如果产生了中断就不再打印。

还有一个方法是通过java内定的机制响应中断:当线程调用sleep(),wait()方法后进入阻塞后,如果线程在阻塞的过程中被中断了,那么线程会捕获或抛出一个中断异常,我们可以根据这个中断异常去控制线程的停止。具体代码如下:

public class Demo3 implements Runnable {
    @Override
    public void run() {
        int num = 0;
        try {
            while(num < Integer.MAX_VALUE / 2){
                if(num % 100 == 0){
                    System.out.println(num);
                }
                num++;
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            //捕获中断异常,在本代码中,出现中断异常后将退出循环
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Demo3());
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}
2、各方配合才能完美停止

在上面的两段代码中已经可以看到,想通过中断停止线程是个需要多方配合。上面已经演示了中断方和被中断方的配合,下面考虑更多的情况:假如要被停止的线程正在执行某个子方法,这个时候该如何处理中断?

有两个办法:第一个是把中断传递给父方法,第二个是重新设置当前线程为中断

第一个例子:在子方法中把中断异常上抛给父方法,然后在父方法中处理中断

public class Demo4 implements Runnable{

    @Override
    public void run() {
        try{
            //在父方法中捕获中断异常
            while(true){
                System.out.println("go");
                throwInterrupt();
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("检测到中断,保存错误日志");
        }
    }

    private void throwInterrupt() throws InterruptedException {//把中断上传给父方法
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Demo4());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

第二个例子:在子方法中捕获中断异常,但是捕获以后当前线程的中断控制位将被清除,父方法执行时将无法感知中断。所以此时在子方法中重新设置中断,这样父方法就可以通过对中断控制位的判断来处理中断

public class Demo5 implements Runnable{

  @Override
  public void run() {
    while(!Thread.currentThread().isInterrupted()){
	 //每次循环判断中断控制位
      System.out.println("go");
      throwInterrupt();
    }
    System.out.println("检测到了中断,循环打印退出");
  }

  private void throwInterrupt(){
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
	  //重新设置中断
      Thread.currentThread().interrupt();
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Demo5());
    thread.start();
    Thread.sleep(1000);
    thread.interrupt();
  }
}
3、常见错误停止线程例子

这里介绍两种常见的错误,先说比较好理解的一种,也就是开头所说的,在外部直接调用Thread类的stop()方法把运行中的线程停止掉。这种暴力的方法很有可能造成脏数据,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

public class Demo6 implements Runnable{
  /**
   * 模拟指挥军队,以一个连队为单位领取武器,一共有5个连队,一个连队10个人
   */
  @Override
  public void run() {
    for(int i = 0; i < 5; i++){
      System.out.println("第" + (i + 1) + "个连队开始领取武器");
      for(int j = 0; j < 10; j++){
        System.out.println("第" + (j + 1) + "个士兵领取武器");
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println("第" + (i + 1) + "个连队领取武器完毕");
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Demo6());
    thread.start();
    Thread.sleep(2500);
    thread.stop();
  }
}

在上面的例子中,我们模拟军队发放武器,规定一个连为一个单位,每个连有10个人。当我们直接从外部通过stop方法停止武器发放后。很有可能某个连队正处于发放武器的过程中,导致部分士兵没有领到武器。

这就好比在生产环境中,银行以10笔转账为一个单位进行转账,如果线程在转账的中途被突然停止,那么很可能会造成脏数据。

另外一个常见错误就是:通过volatile关键字停止线程。具体来说就是通过volatile关键字定义一个变量,通过判断变量来停止线程。这个方法表面上是没问题的,我们先看这个表面的例子

public class Demo7 implements Runnable {

  private static volatile boolean canceled = false;

  @Override
  public void run() {
    int num = 0;
    while(num <= Integer.MAX_VALUE / 2 && !canceled){
      if(num % 100 == 0){
        System.out.println(num + "是100的倍数");
      }
      num++;
    }
    System.out.println("退出");
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Demo7());
    thread.start();
    Thread.sleep(1000);
    canceled = true;
  }
}

上面的代码可以正常执行,但是这个方法有一个潜在的大漏洞,就是若线程进入了阻塞状态,我们将不能通过修改volatile变量来停止线程,看下面的生产者消费者例子:

/**
 * 通过生产者消费者模式演示volatile的局限性,volatile不能唤醒已经阻塞的线程
 * 生产者生产速度很快,消费者消费速度很慢,通过阻塞队列存储商品
 */
public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);//1s足够让生产者把阻塞队列塞满

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(storage.take() + "被消费");
            Thread.sleep(100);//让消费者消费慢一点,给生产者生产的时间
        }

        System.out.println("消费者消费完毕");
        //让生产者停止生产(实际情况是不行的,因为此时生产者处于阻塞状态,volatile不能唤醒阻塞状态的线程)
        producer.canceled = true;

    }
}

class Producer implements Runnable {

    public volatile boolean canceled = false;

    private BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num < Integer.MAX_VALUE / 2 && !canceled) {
                if (num % 100 == 0) {
                    this.storage.put(num);
                    System.out.println(num + "是100的倍数,已经被放入仓库");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者停止生产");
        }
    }
}

class Consumer {
    private BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        return Math.random() < 0.95 ? true : false;
    }
}

上面的例子运行后会发现生产线程一直不能停止,因为他处于阻塞状态,当消费者线程退出后,没有任何东西能唤醒生产者线程。

这种错误用中断就很好解决:

/**
 * 通过生产者消费者模式演示volatile的局限性,volatile不能唤醒已经阻塞的线程
 * 生产者生产速度很快,消费者消费速度很慢,通过阻塞队列存储商品
 */
public class Demo8 {
  public static void main(String[] args) throws InterruptedException {
    ArrayBlockingQueue storage = new ArrayBlockingQueue(10);

    Producer producer = new Producer(storage);
    Thread producerThread = new Thread(producer);
    producerThread.start();
    Thread.sleep(1000);//1s足够让生产者把阻塞队列塞满

    Consumer consumer = new Consumer(storage);
    while(consumer.needMoreNums()){
      System.out.println(storage.take() + "被消费");
      Thread.sleep(100);//让消费者消费慢一点,给生产者生产的时间
    }

    System.out.println("消费者消费完毕");
    producerThread.interrupt();
  }
}

class Producer implements Runnable{

  private BlockingQueue storage;

  public Producer(BlockingQueue storage) {
    this.storage = storage;
  }

  @Override
  public void run() {
    int num = 0;
    try{
      while(num < Integer.MAX_VALUE / 2 && !Thread.currentThread().isInterrupted()){
        if(num % 100 == 0){
          this.storage.put(num);
          System.out.println(num + "是100的倍数,已经被放入仓库");
        }
        num++;
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }finally {
      System.out.println("生产者停止生产");
    }
  }
}

class Consumer{
  private BlockingQueue storage;

  public Consumer(BlockingQueue storage) {
    this.storage = storage;
  }

  public boolean needMoreNums(){
    return Math.random() < 0.95 ? true : false;
  }
}

八、等待/通知机制

1、什么是等待/通知机制?

多个线程之间也可以实现通信,原因就是多个线程共同刚 访问同一个变量。但是这种通信机制不是 “等待/通知”,两个线程完全是主动地读取一个共享变量。简单的说,等待/通知机制就是一个【线程A】等待,一个【线程B】通知(线程A可以不用再等待了)。比如生产者和消费者模型,消费者等待生产者生产资源,这是等待,生产者生产好资源通知等待的消费者去消费,这是通知。

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在java.lang.Object类中

wait()     //代用该方法的线程会进入到WAITING状态,并且当前线程会被放置到等待队列中,只有等待另外线程的唤醒或被中断返回,调用wait()方法后线程会释放对象锁,同时该方法必须在同步方法或同步块中使用,否则会抛出IllegalMonitorStateException异常。
wait(long)     //超时等待一段时间,时间单位是ms,如果没有被唤醒就超时返回
wait(long,int)   //更精细的超时等待,精确到ns
notify()        //唤醒一个在等待对象锁的其他线程,如果有多个线程在等待该对象锁,那么会由线程规划器随机唤醒一个线程,此方法也必须在同步方法或同步块中使用,否则会抛出IllegalMonitorStateException异常。
notifyAll()     //唤醒所有等待此对象锁的线程 
    
注意!notify()或notifyAll()在调用之后,等待线程不会立即从WAITING状态立即变为RUNNING状态,而是需要等到调动notify()或notifyAll()的方法释放对象锁之后才会从WAITING状态返回。
2、等待/通知机制的经典范式(模板)

(1)等待方(消费者)需遵循如下原则:

  • 获取对象锁
  • 如果条件不满足,那么调用对象的wait()方法,被通知后仍然要检查条件
  • 条件满足则执行对应逻辑
synchronized(对象){
   while(条件不满足){
       对象.wait();
   } 
   对应的逻辑;
}    

(2)通知方(生产者)需遵循如下原则:

  • 获得对象锁
  • 改变条件
  • 通知所有等待该对象锁的线程
synchronized(对象){
    改变条件;
    对象.notifyAll();
}
3、使用等待/通知机制实现生产者和消费者模型

详细的生产者消费者模式的实现可以参考我的另一篇文章Java并发编程实践

资源Resourcs.java

package top.easyblog.wait;

/**
 * 生产者——消费者模型:资源
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/01 18:21
 */
public class Resources {

    //模拟资源的数量
    private int num;

    //资源的最大允许数存放量
    private static final int MAX_NUM = 10;


    /**
     * 从资源池中取走资源
     */
    public synchronized void remove() {
        if (num > 0) {
            num--;
            System.out.println("消费者" + Thread.currentThread().getName() + "消耗一件资源," + "当前资源有" + num + "个");
            //通知生产者生产资源
            notifyAll();
        } else {
            try {
                //没有资源,消费者进入等待状态
                wait();
                System.out.println("消费者" + Thread.currentThread().getName() + "线程进入等待状态");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 向资源池中添加资源
     */
    public synchronized void add() {
        if (num < MAX_NUM) {
            num++;
            System.out.println("生产者" + Thread.currentThread().getName() + "+生产一个资源,当前资源有" + num + "个");
            //通知消费者消费
            notifyAll();
        } else {
            try {
                //资源生产数量过多,让生产者进入等待状态,并释放锁
                wait();
                System.out.println(Thread.currentThread().getName() + "线程进入等待");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

生产者Producer.java

package top.easyblog.wait;

/**
 * 生产者——消费者模型:生产者
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/01 18:11
 */
public class Producer implements Runnable {

    private Resources resources;

    public Producer(Resources resources) {
        this.resources = resources;
    }


    /**
     * 生产者每1s生产一个资源
     */
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resources.add();
        }
    }
}

消费者Customer.java

package top.easyblog.wait;

/**
 * 生产者——消费者模型:消费者
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/01 18:34
 */
public class Customer implements Runnable {

    private Resources resources;

    public Customer(Resources resources) {
        this.resources = resources;
    }

    /**
     * 消费者每2s消费一个资源
     */
    public void run() {
        while (true) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resources.remove();
        }
    }
}

九、sleep() 与 与 wait() 区别

  1. wait()方法定义在Object类中,作用于所有对象;sleep()方法定义在Thread类中,作用于当前线程
  2. wait()方法只能在同步块或者同步方法中调用;sleep()可以在任何地方调用
  3. 最主要的区别:调用 wait()方法后,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态;而sleep()方法调用后不会释放锁资源,如果sleep()是在同步上下文中调用的,那么其他线程是无法进入到当前同步块或者同步方法中的

十、什么是线程死锁?如何避免死锁?

1、什么是线程死锁?

多线程或多进程提高了系统的资源利用率以及系统的处理能力,但是这也同样会带来新的问题——死锁。所谓的死锁是指多个线程因为竞争资源而造成的互相等待的僵局,如果没有外力干预,这些进程都将无法向前推进

如下图所示,线程 A 持有锁1,线程 B 持有锁2,他们同时都想申请对方的资源,但是有无法获取到,所以这两个线程就会因互相等待而进入死锁状态。

2、面试官:你给我写一个死锁
package top.easyblog;

/**
 * 死锁测试
 *
 * @author :huangxin
 * @modified :
 * @since :2020/04/01 23:15
 */
public class DeadLock {

    private static final Object resources1 = new Object();   //资源1
    private static final Object resources2 = new Object();   //资源2

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            synchronized (resources1) {
                System.out.println(Thread.currentThread().getName() + "获得了resources1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "等待获取resources2");
                synchronized (resources2) {
                    System.out.println(Thread.currentThread().getName() + "获得了resources2");
                }
            }
        }, "worker1");

        Thread t2 = new Thread(() -> {
            synchronized (resources2) {
                System.out.println(Thread.currentThread().getName() + "获得了resources2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "等待获取resources1");
                synchronized (resources1) {
                    System.out.println(Thread.currentThread().getName() + "获得了resources1");
                }
            }
        }, "worker2");


        t1.start();
        t2.start();

    }

}

执行结果:

3、如果避免死锁?

在谈如何避免之前先了解一下产生死锁的4个必要条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

这四个条件是产生线程死锁的必要条件,缺一不可,因此避免死锁就可以破坏掉其中一个条件即可。

(1)破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

(2)破坏请求与保持条件

一次性申请所有的资源。

(3)破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

(4)破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

4、使用工具定位程序中的死锁

Java提供了两个工具可以分析程序中是否有死锁发生,一个是jps命令配合jstack;另一个是jconsole工具。下面我来介绍一下如何使用这两个工具分析程序中的死锁。

(1)使用jps命令配合jstack
首先使用jps命令查看运行中的Java线程

然后使用jstack 线程Id查看具体的线程信息

如果发生了死锁还会给出死锁信息以及死锁发生的代码行数,非常方便

(2)使用jconsole工具
jconsol是一个图形化的检测工具,不仅可以检测线程,还可以分析内存,功能更加强大!!!
在cmd命令行中输入jconsole,等待图形化工具启动,启动完成后连接上目标程序后,点击线程--->检测死锁,之后如果程序中发生了死锁就会被列举出来。

检测死锁的结果

留言区

还能输入500个字符