Java多线程
执键写春秋 352 8

Java多线程

1. 什么是线程?

在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是程序的依次动态执行过程,它需要经历从代码加载、代码执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。一般一个应用就是一个进程,如QQ、微信等,但有些大型应用也会同时拥有多个进程。

线程是比进程还要小的运行单位,一个进程包含多个线程。(线程可以看做一个子程序)

所谓的多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在、同时运行,一个进程中可能包含了多个同时执行的线程。

问题:在只有一个CPU的情况下,如何保证多个程序同时运行呢? 我们把CPU的执行时间,分成很多的小块。每一小块的时间都是固定的,这个小块时间,我们称为“时间片”。时间片的时间可以非常短,例如一毫秒(1ms),当多个程序同时运行时,每个进程随机分配属于它的时间片。这样当一个程序的时间片执行结束后,将CPU转给下一个时间片的程序,这样这些程序就可以轮流运行了。 即通过时间片的轮转,又因时间片非常短,程序间转换时使用者感觉不到,所以就我们认为多个程序是同时运行的。

2. 线程的创建

在Java中要想实现多线程操作有两种手段,一种是继承Thread类,另一种就是要实现Runnable接口。

2.1 Thread类

Thread类是一个线程类,位于java.lang包下。一个类只 要继承了Thread类,此类就称为多线程实现类。在Thread子类中,必须明确地覆写Thread类中的run()方法,此方法为线程的主体。 #### 2.1.1 Thread 构造 |序号|构造方法|说明| |-|-|-| |1|Thread()|Thread()| |2|Thread(String name)|创建一个具有指定名称的线程对象| |3|Thread(Rummable target)|创建一个基于Runnable接口实现类的线程对象| |4|Thread(Runnable target,String name)|创建一个基于Runnable接口实现类,并且具有指定名称的线程对象。|

2.1.2 定义语法

class 类名称 extends Thread{//继承Thread类
    属性...
    方法...
    public void run(){//覆写Thread类中的run()方法,此方法是线程主体
        线程主体;
    }
}

2.1.3 Thread常用方法

序号 方法 说明
1 public void run() 线程相关的代码写在该方法中,需要覆写
2 public void start() 启动线程的方法
3 public static void sleep(long m) c线程休眠m毫秒的方法
4 public void join() 优先执行调用join()方法的线程
#### 2.1.4 继承Thread类实现多线程
```
package person.xsc.practice;
public class ThreadDemo extends Thread{
private String name;
public ThreadDemo(String name) {
this.name=name;
}
public void run() {//覆写Thread类中的run方法
for(int i=0;i<3;++i) {
System.out.println(name+"运行---->"+i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ThreadDemo t1=new ThreadDemo("线程A");
ThreadDemo t2=new ThreadDemo("线程B");
t1.run();
t2.run();
}
}
输出:
线程A运行---->0
线程A运行---->1
线程A运行---->2
线程B运行---->0
线程B运行---->1
线程B运行---->2
```
编译并运行上面代码之后,发现以上程序其实是先执行完t1对象之后再执行t2对象,并不是如线程定义的那样是交错运行的,也就可以肯定这会线程实际上是并没有被我们启动的,还是属于顺序式执行方式。如果想要真正启动线程,就需要调用从Thread类中继承而来的start()方法,具体看下面程序:
```
package person.xsc.practice;
public class ThreadDemo extends Thread{
private String name;
public ThreadDemo(String name) {
this.name=name;
}
public void run() {//覆写Thread类中的run方法
for(int i=0;i<3;++i) {
System.out.println(name+"运行---->"+i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ThreadDemo t1=new ThreadDemo("线程A");
ThreadDemo t2=new ThreadDemo("线程B");
//t1.run();//调用线程主体
//t2.run();
t1.start();//启动线程
t2.start();
}
}
输出:
线程A运行---->0
线程B运行---->0
线程B运行---->1
线程B运行---->2
线程A运行---->1
线程A运行---->2
```
从这段程序的运行结果可以发现,现在两个线程对象是交错运行的,哪个线程抢到了CPU资源,哪个线程就可以运行,这里的输出结果也只是其中的一种结果。
### 2.2 Runnable接口
在Java中也可以通过实现Runnable接口的方式实现多线程,Runnnable接口中只定义了一个抽象方法run()。

为什么要实现Runnable接口,因为基于Java的编程语言规范,如果子类已经继承了一个类,就无法再直接继承Thread类(也就是Java不支持多继承),此时就可以通过实现Runnable接口来创建线程。

2.2.1 语法定义

class 类名称 implements Runnable{
    属性...
    方法...
    public void run(){//覆写Runnable接口中的run()方法,此方法是线程主体
        线程主体;
    }
}     

2.2.2 实现Runnable接口方式实现多线程

package person.xsc.practice;
public class RunnableDemo implements Runnable {
    private String name;
    public RunnableDemo(String name) {
        // TODO Auto-generated constructor stub
        this.name=name;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<3;++i) {
            System.out.println(name+"--->"+i);
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        RunnableDemo r1=new RunnableDemo("线程A");
        Thread thread1=new Thread(r1);
        RunnableDemo r2=new RunnableDemo("线程B");
        Thread thread2=new Thread(r2);
        thread1.start();
        thread2.start();
    }
}
输出:
线程B--->0
线程A--->0
线程A--->1
线程A--->2
线程B--->1
线程B--->2

2.3 Callable 接口

既然有了前面两种实现方式,为什么还需要第三种Callable 接口呢?这是因为前两种方式存在着一种缺陷,观察前两种方式里面覆写的run方法,返回的都是void,也就是说这两种方式都不能返回处理后的结构,而Callable接口的出现可以有效地解决这一问题。

实现Callable接口来创建线程要重写call()方法,call()方法与Runnable接口的run()方法不同,是有返回值的,且允许抛出异常,run()方法的异常只允许在内部处理。且实现Callable接口的类要通过FutureTask包装器才能让Thread调用多线程,FutureTask类实现了Runnable接口,运行Callable任务可拿到一个Future对象。

2.3.1 实现Callable接口方式实现多线程

package person.xsc.practice;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableDemo implements Callable<String> {
    private String name;
    public CallableDemo(String name) {
        this.name=name;
    }
    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        return name+"------"+new Date();
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
         CallableDemo c1=new CallableDemo("线程A");
         FutureTask<String> result = new FutureTask<String>(c1);
         Thread thread = new Thread(result);
         thread.start();
         try {
            System.out.println("返回的结果是:"+result.get());
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
输出:
返回的结果是:线程A------Fri May 14 13:05:24 CST 2021

3. 线程的状态与生命周期

1、新建状态(New):在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可以采用Thread类的构造方法来实现,例如“Thread thread=new Thread();"。 2、可运行状态 (就绪状态) 调用start方法就可以启动线程。当线程启动时,就进入就绪状态,此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。 3、正在运行状态: 获得cpu允许后(Running),此时线程进入了运行状态。此时,自动调用该线程对象的run方法。 4、阻塞状态(Blocked):一个正在执行的线程在某些特殊情况下,如被认为挂起或者需要执行耗时的输入/输出操作时,会让CPU暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用sleep()\wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。 5、终止状态(Dead):线程调用stop方法时或者run方法执行结束后,线程就处于死亡状态。处于死亡状态的线程不具备继续运行的能力。 Java多线程

3.1 sleep()方法使用

3.1.1 语法

Thread类为睡眠线程提供了两种方法:

  • public static void sleep(long miliseconds)throws InterruptedException

  • public static void sleep(long miliseconds, int nanos)throws InterruptedException

    3.1.2 作用

    Thread类的sleep()方法用于在指定的时间内睡眠线程。即让线程暂时停止执行,同时,这个暂时停止执行的时间由我们指定,时间一到,线程就会恢复正常执行。

    3.1.3 方法示例
  • 提示:调用sleep方法时,需要处理异常【捕获中断异常】。*

    package person.xsc.practice;
    public class SleepDemo extends Thread {
       public void run() {
           for (int i = 1; i < 5; i++) {
               try {
                   Thread.sleep(500);
               } catch (InterruptedException e) {
                   System.out.println(e);
               }
               System.out.println(i);
           }
       }
       public static void main(String args[]) {
           SleepDemo t1 = new SleepDemo();
           SleepDemo t2 = new SleepDemo();
           t1.start();
           t2.start();
       }
    }

    3.2 join()方法使用

    3.2.1 语法

    Thread类为加入线程提供了两种方法:

  • public final void join()

  • public final void join(long millis)

    3.2.2 作用

    join()的作用是 使所属的线程对象正常执行 run() 方法中的任务, 而使当前线程进行无限期(或指定时间)的阻塞, 等待方法join所属线程销毁后再继续执行当前线程后续的代码。

    3.2.3 方法示例
  • 提示:join()方法是最终方法,它不能够被重写。*

    package person.xsc.practice;
    public class JoinDemo extends Thread{
       private String name;
       public JoinDemo(String name) {
           this.name=name;
       }
       @Override
       public void run(){
           for(int i=0;i<100;i++){
               System.out.println(name + ":" + i);
           }
       }
       public static void main(String[] args) {
           // TODO Auto-generated method stub
           JoinDemo j1=new JoinDemo("线程A");
           JoinDemo j2=new JoinDemo("线程B");
           j1.start();
           try {
               j1.join(1);
               j2.start();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }    
       }
    }

    3.3 wait()方法使用

    调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或者被中断后才会返回。需要注意的是,在调用wait方法后会释放对象的锁,因此wait方法一般被用于同步方法或者同步代码块中。

    3.4 yield()方法使用

    该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。(该方法会让当前线程释放CPU执行时间片,与其他线程一起重新竞争CPU时间片。)

    package person.xsc.practice;
    public class MyThread implements Runnable {
       @Override
       public void run() {
           // TODO Auto-generated method stub
           for(int i=0;i<5;++i) {
    
               System.out.println(Thread.currentThread().getName()+"运行,i="+i);
               if(i==3) {
                   System.out.print(Thread.currentThread().getName()+"礼让"+'\t');
                   Thread.currentThread().yield();
               }
           }
       }
       public static void main(String args[] ) {
           MyThread my=new MyThread();
           Thread my1=new Thread(my,"线程A");
           Thread my2=new Thread(my,"线程B");
           my1.start();
           my2.start();
       }
    }
    输出:
    线程A运行,i=0
    线程B运行,i=0
    线程A运行,i=1
    线程B运行,i=1
    线程A运行,i=2
    线程B运行,i=2
    线程B运行,i=3
    线程B礼让    线程A运行,i=3
    线程A礼让    线程B运行,i=4
    线程A运行,i=4

    上面的程序,每当满足条件i=3的时候,就会先将本线程暂停,然后让给其他线程先执行。

    3.5 interrupt()方法使用

    interrupt()方法用于中断线程。

如果任何线程处于休眠或等待状态(即调用sleep()或wait()),那么使用interrupt()方法,可以通过抛出InterruptedException来中断线程执行。

如果线程未处于休眠或等待状态,则调用interrupt()方法将执行正常行为,并且不会中断线程,但会将中断标志设置为true。

这里要特别注意:线程本身不会因为调用了该方法而改变状态(阻塞、终止等)。

package person.xsc.practice;
public class MyThread implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("1、进入run方法");
        try {
            Thread.sleep(10000);//休眠10秒
            System.out.println("2、完成休眠");
        }catch(Exception e) {
            System.out.println("3、休眠被终止");
            return;
        }
            System.out.println("4、run方法执行结束");
    }
    public static void main(String args[] ) {
        MyThread my=new MyThread();
        Thread my2=new Thread(my,"线程A");
        my2.start();
        try {
            Thread.sleep(2000);//休眠2秒再继续中断
        }catch(Exception e) {
            e.printStackTrace();
        }
        my2.interrupt();//中断线程执行


    }
}
输出:
1、进入run方法
3、休眠被终止

3.6 notify()方法使用

该方法用于唤醒在此对象监视器上等待的一个线程,如果所有的线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。

3.7 取得和设置线程名称

在Thread类中可以通过getName()方法取得线程的名称,通过setName()方法设置线程名称。

线程名称设置一般在启动线程前设置,但也允许为已经运行的线程设置名称。

提示:如果没有设置名称,系统会为其自动分配名称。名称格式为Thread-Xx.

package person.xsc.practice;
public class MyThread implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<3;++i) {
            System.out.println(Thread.currentThread().getName()+"运行,i="+i);
        }
    }
    public static void main(String args[] ) {
        MyThread my=new MyThread();
        Thread my1=new Thread(my);
        Thread my2=new Thread(my,"线程A");
        Thread my3=new Thread(my);
        my3.setName("线程B");
        my1.start();
        my2.start();
        my3.start();
    }
}
输出:
线程B运行,i=0
Thread-0运行,i=0
Thread-0运行,i=1
线程A运行,i=0
Thread-0运行,i=2
线程B运行,i=1
线程B运行,i=2
线程A运行,i=1
线程A运行,i=2

3.8 判断线程是否启动

通过Thread类中的start()方法通知CPU这个线程已经准备好启动,然后等待CPU分配资源,运行此线程。在Java中可以使用isAlive()方法来测试线程是否已经启动而且仍然在运行。

package person.xsc.practice;
public class MyThread implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<3;++i) {
            System.out.println(Thread.currentThread().getName()+"运行,i="+i);
        }
    }
    public static void main(String args[] ) {
        MyThread my=new MyThread();
        Thread my2=new Thread(my,"线程A");
        System.out.println("线程开始执行之前的状态---》"+my2.isAlive());
        my2.start();
        System.out.println("线程开始执行之后的状态---》"+my2.isAlive());
        for(int i=0;i<20;++i) {
            System.out.println("main运行---》"+i);
        }
        System.out.println("代码执行之后的状态---》"+my2.isAlive());

    }
}
输出:
(其中一种线程存活结果)
线程开始执行之前的状态---》false
线程开始执行之后的状态---》true
main运行---》0
main运行---》1
main运行---》2
main运行---》3
main运行---》4
main运行---》5
main运行---》6
main运行---》7
main运行---》8
main运行---》9
main运行---》10
main运行---》11
main运行---》12
main运行---》13
main运行---》14
main运行---》15
main运行---》16
main运行---》17
main运行---》18
main运行---》19
代码执行之后的状态---》true
线程A运行,i=0
线程A运行,i=1
线程A运行,i=2
----------------------------------------------
(其中一种线程不存活的结果)
线程开始执行之前的状态---》false
线程开始执行之后的状态---》true
main运行---》0
main运行---》1
main运行---》2
main运行---》3
main运行---》4
main运行---》5
main运行---》6
main运行---》7
main运行---》8
线程A运行,i=0
线程A运行,i=1
main运行---》9
main运行---》10
main运行---》11
main运行---》12
main运行---》13
main运行---》14
线程A运行,i=2
main运行---》15
main运行---》16
main运行---》17
main运行---》18
main运行---》19
代码执行之后的状态---》false

以上结果是不确定的,所以主线程有可能先执行完,那么此时其他线程不会受到影响,最后可能线程已经不存活,但也有可能继续存活。

4. 线程优先级

  1. 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
  2. 可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级。
序号 定义 描述 表示的常量
1 MAX_PRIORITY 线程的最高优先级 10
2 MIN_PRIORITY 线程的最低优先级 1
3 NORM_PRIORITY 线程的默认优先级 5
### 4.1 设置与获取优先级
➢ public int getPriority() 获取优先级的方法
➢ public void setPriority(int newPriority) 设置优先级的方法
### 4.2 测试线程优先级
```
package person.xsc.practice;
public class MyThread implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<5;++i) {
try {
Thread.sleep(500);
}catch(Exception e) {
return ;
}
System.out.println(Thread.currentThread().getName()+"运行,i="+i);
}
}
public static void main(String args[] ) {
MyThread my=new MyThread();
Thread my1=new Thread(my,"线程A");
Thread my2=new Thread(my,"线程B");
Thread my3=new Thread(my,"线程C");
Thread my4=new Thread(my,"线程D");
my1.setPriority(Thread.MIN_PRIORITY);
my2.setPriority(Thread.MAX_PRIORITY);
my3.setPriority(Thread.NORM_PRIORITY);
my4.setPriority(8);
my1.start();
my2.start();
my3.start();
my4.start();
}
}
输出:【其中一种结果】
线程B运行,i=0 -----》线程B最高优先级10
线程D运行,i=0 -----》线程D优先级8
线程C运行,i=0 -----》线程C默认优先级5
线程A运行,i=0 -----》线程A最低优先级1
线程B运行,i=1
线程D运行,i=1
线程C运行,i=1
线程A运行,i=1
线程C运行,i=2
线程A运行,i=2
线程D运行,i=2
线程B运行,i=2
线程C运行,i=3
线程D运行,i=3
线程B运行,i=3
线程A运行,i=3
线程B运行,i=4
线程C运行,i=4
线程D运行,i=4
线程A运行,i=4
```
### 4.3 提示
- 不是线程的优先级越高,线程就先执行。优先级越高表示CPU分配给该线程的时间片越多,执行时间就多。优先级越低表示CPU分配给该线程的时间片越少,执行时间就少。具体实际还是看CPU的调度。线程运行的结果在不同电脑上也可能会不同。
  • 主方法的优先级是NORM_PRIORITY。下面可以通过代码来确认一下到底是不是默认优先级5:

    public static void main(String args[] ) {
          System.out.println("主方法的优先级是:"+Thread.currentThread().getPriority());
    }
    输出:
    主方法的优先级是:5

    5. 线程同步

    一个多线程程序如果通过Runnable接口实现的,就意味着类中的属性将被多个线程共享,那么这样一来就会产生一个问题,即多个线程要操作同一个资源就会出现资源同步问题。打个比方,一个卖票程序,如果多个线程同时操作时就有可能出现出票为负数的情况。下面通过代码来演示一下:

    package person.xsc.practice;
    public class MyThread implements Runnable {
      private int ticket=5;
      @Override
      public void run() {
          // TODO Auto-generated method stub
          for(int i=0;i<100;++i) {
              if(ticket>0) {
                  try {
                      Thread.sleep(300);
                  }catch(Exception e) {
                      e.printStackTrace();
                  }
                  System.out.println("出票,此时票数ticket="+ticket--);
              }
          }
      }
      public static void main(String args[] ) {
          MyThread my=new MyThread();
          Thread my1=new Thread(my,"线程A");
          Thread my2=new Thread(my,"线程B");
          Thread my3=new Thread(my,"线程C");
          my1.start();
          my2.start();
          my3.start();
    
      }
    }
    输出:
    出票,此时票数ticket=5
    出票,此时票数ticket=5
    出票,此时票数ticket=4
    出票,此时票数ticket=3
    出票,此时票数ticket=2
    出票,此时票数ticket=3
    出票,此时票数ticket=0
    出票,此时票数ticket=1
    出票,此时票数ticket=-1    ---------》此时票数为负数

    从上面的操作代码来看,执行的操作步骤一是先判断票数是否大于0,大于0表示还可以卖;然后步骤二如果票数大于0,就将票卖出。出现上面这种情况是因为在步骤一、二之间加入了延迟操作,这就有可能一个线程还没有对票数进行减的操作,其他线程就在他之前就把票卖出去了。

如果想要解决这样的问题,就需要用到同步。所谓的同步就是指多个操作在同一时间内只能有一个线程进行,线程排好队,其他线程要等这个执行完才可以继续执行。

5.1 使用同步解决问题的两种方式

5.1.1 第一种:同步代码块

package person.xsc.practice;
public class MyThread implements Runnable {
    private int ticket=5;
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;++i) {
            synchronized(this) {//设置需要同步的操作
                if(ticket>0) {
                    try {
                        Thread.sleep(300);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"出票,此时票数ticket="+ticket--);
                }
            }
        }
    }
    public static void main(String args[] ) {
        MyThread my=new MyThread();
        Thread my1=new Thread(my,"线程A");
        Thread my2=new Thread(my,"线程B");
        Thread my3=new Thread(my,"线程C");
        my1.start();
        my2.start();
        my3.start();

    }
}
输出:
线程A出票,此时票数ticket=5
线程A出票,此时票数ticket=4
线程C出票,此时票数ticket=3
线程B出票,此时票数ticket=2
线程C出票,此时票数ticket=1

从程序运行发现,以上代码将取值和修改值操作进行了同步,就不会再出现出票为负数的情况。

5.1.2 第二种:同步方法

除了第一种将需要的代码设置成同步代码块外,还可以将其单独写成一个同步方法,具体如下:

package person.xsc.practice;
public class MyThread implements Runnable {
    private int ticket=5;
    public synchronized void sell() {
        if(ticket>0) {
            try {
                Thread.sleep(300);
            }catch(Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"出票,此时票数ticket="+ticket--);
        }
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i=0;i<100;++i) {
            this.sell();
        }
    }
    public static void main(String args[] ) {
        MyThread my=new MyThread();
        Thread my1=new Thread(my,"线程A");
        Thread my2=new Thread(my,"线程B");
        Thread my3=new Thread(my,"线程C");
        my1.start();
        my2.start();
        my3.start();

    }
}
输出:
线程A出票,此时票数ticket=5
线程A出票,此时票数ticket=4
线程C出票,此时票数ticket=3
线程B出票,此时票数ticket=2
线程C出票,此时票数ticket=1

可以发现以上代码完成了与之前同步代码块同样的功能。

6. 线程死锁

所谓的线程死锁就是指两个线程都在等待对方完成,造成了程序的停滞,一般程序的死锁都是在程序运行时出现的。例如,现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。下面通过这个案例来演示一下死锁出现的情况:

package person.xsc.practice;
class Zhangsan{ // 定义张三类
    public void say(){ //定义say()方法
        System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
    }
    public void get(){ //定义得到的方法
        System.out.println("张三得到画了。") ;
    }
}
class Lisi{ // 定义李四类
    public void say(){
        System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
    }
    public void get(){
        System.out.println("李四得到书了。") ;
    }
}
public class ThreadDeadLock implements Runnable{
    private static Zhangsan zs = new Zhangsan() ;       // 实例化static型对象
    private static Lisi ls = new Lisi() ;       // 实例化static型对象
    private boolean flag = false ;  // 声明标志位,判断那个先说话
    public void run(){  // 覆写run()方法
        if(flag){//用于判断哪个对象先执行
            synchronized(zs){   // 同步张三
                zs.say() ;
                try{
                    Thread.sleep(500) ;
                }catch(InterruptedException e){
                    e.printStackTrace() ;
                }
                synchronized(ls){//只有当李四的线程执行完张三线程才会调用得到方法
                    zs.get() ;
                }
            }
        }else{
            synchronized(ls){
                ls.say() ;
                try{
                    Thread.sleep(500) ;
                }catch(InterruptedException e){
                    e.printStackTrace() ;
                }
                synchronized(zs){//只有当张三的线程执行完李四线程才会调用得到方法
                    ls.get() ;
                }
            }
        }
    }
    public static void main(String args[]){
        ThreadDeadLock t1 = new ThreadDeadLock() ;      // 控制张三
        ThreadDeadLock t2 = new ThreadDeadLock() ;      // 控制李四
        t1.flag = true ;//张三先执行
        t2.flag = false ;
        Thread thA = new Thread(t1) ;
        Thread thB = new Thread(t2) ;
        thA.start() ;
        thB.start() ;
    }
};
输出:
李四对张三说:“你给我书,我就把画给你”
张三对李四说:“你给我画,我就把书给你。”

这时候,只有当李四的线程执行完张三线程才会调用得到方法,同样,只有当张三的线程执行完李四线程才会调用得到方法。这样,程序就无法向下继续执行,从而造成了死锁的现象。

7. 线程问题拓展

Java程序每次运行至少启动几个线程? 答:至少启动两个线程。一个是main线程,另一个是垃圾收集线程。

评论区

索引目录