【Java多线程】并发编程不得不知的重要概念(持续更新)

本文深入探讨Java多线程编程,涵盖进程与线程的区别、使用场景、线程创建、并发与并行概念、线程同步、死锁、线程调度策略、过期方法风险等内容,旨在帮助开发者理解并有效利用Java多线程提高程序效率。
摘要由CSDN通过智能技术生成

一.对java多线程的认识

  1. 如果在一个进程中同时运行了多个线程,且每个线程间相互独立,用来完成不同的工作,则称之为多线程
  2. 每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量
  3. 线程与线程之间存在争夺关系,相互争夺cpu资源竞相执行相关对象的代码段。
  4. 多个线程同时执行可能会造成线程安全问题(线程之间同时拥有一个变量,且发生了修改),为了避免这个问题,需要同步锁来使一个线程执行时,其他线程会等待这个线程执行完毕才执行

1.进程和线程的区别

区别进程线程
根本区别进程是作为资源分配的单位,一个程序就是一个进程线程是cpu调度和执行的单位
开销每个进程都有独立的代码和数据空间(进程上下文),进 程间的切换会有较大的开销。线程可以看成时轻量级的进程,同一类线程共享代码数据空间,每个线程有独立的运行栈 和程序计数器(PC),CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
所处环境操作系统中能同时运行多个任务(程序)同一程序(进程)中有多个顺序流同时执行
内存分配系统在运行的时候会为每个进程分配独立的内存区域,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段除了CPU之外,不会为线程分配内存,线程是共享进程中的数据的,使用相同的地址空间(线程所使用的资源是它所属的进程的资源)
包含关系没有线程的进程是可以被看作单线程的,如果一个进 程内拥有多个线程,则执行过程不是一条线的,而是 多条线(线程)共同完成的。线程是进程的一部分,所以线程有的时候被称 为是轻权进程或者轻量级进程。
通信方式同一进程下的线程共享全局变量、静态变量等数据,线程之间的通信方便 ,如何处理好同步与互斥是编写多线程程序的难点进程之间的通信需要以通信的方式(IPC)进行
健壮性一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间多线程程序只要有一个线程死掉,整个进程也死掉
进程互斥与同步的区别

2.多线程的使用场景

  1. 常见的浏览器Web服务(现在web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
  2. servlet多线程
  3. FTP下载,多线程操作文件
  4. 数据库用到的多线程
  5. 分布式计算
  6. Tomcat:tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
  7. 后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
  8. 自动作业处理:比如定期备份日志、定期备份数据库
  9. 异步处理:如发微博、记录日志
  10. 页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
  11. 数据库的数据分析(待分析的数据太多),数据迁移
  12. 多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
  13. 桌面应用开发,一个费时的计算开个线程,前台加个进度条显示
  14. JavaSwing编程(Java桌面应用程序开发)

3.使用是不是很多线程就能提高效率呢?

  • 不一定的。因为程序中上下文的切换开销也很重要,如果创建了太多的线程,CPU花费在上下文的切换的时间将多于执行程序的时间!这会降低程序执行效率的。所以有效利用多线程的关键是理解程序是并发执行而不是串行执行的。

4.JVM默认启动的线程

  1. 主线程 (Main方法)
  2. GC
  3. 异常

5.线程是如何创建起来的

  1. 进程仅仅是一个容器,包含了线程运行中所需要的数据结构等信息。

  2. 一个进程创建时,操作系统会创建一个线程,这就是主线程,而其他的子线程,却要主线程的代码来创建,也就是由程序员来创建。

  3. 当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的 主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少都有一个主线程, 主线程的重要性体现在两方面:1.是产生其他子线程的线程;2.通常它必须最后完成执行比如执行各种关闭动作

6.什么是串行、并发、并行

  1. 串行:一个线程执行到底,相当于单线程。
  2. 并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在外人看来就像是多个线程同时执行。
  3. 并行:多个线程在不同的cpu中同时执行。

7.并发与并行的区别

  1. 并发严格的说不是同时执行多个线程,只是线程交替执行且速度很快,相当于同时执行
  2. 而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。

8. 三高

  • 高可用:数据不能出错
  • 高性能: 体验要好,不能等太久
  • 高并发: 同时操作,有竞争的乐趣

9.单线程和多线程

  • 单线程:程序中只存在一个线程,实际上主方法就是一个主线程。
  • 多线程:多线程是指在同一程序中有多个顺序流在执行。 简单的说就是在一个程序中有多个任务运行。

10.线程分类

对于线程的分类,我们可以简单划分为:

  1. 主线程 (每个进程只有一个主线程)

    • 主线程: main方法
  2. 子线程: 子线程中可以简单划分为:

    • 非守护线程,即用户线程

      • 非守护线程/用户线程: 通常异步处理一些业务或逻辑
    • 守护线程

      • 守护线程: 主要是指在进程中,为主线程提供一种通用服务的线程。
        比如 gc线程,因为主线程一旦结束或者销毁,守护线程没有了守护对象,也将同步进行结束或销毁。

Java 线程分为守护线程(DaemonThread) 和 用户线程(UserThread)两类

  • 通常情况下,我们使用Thread 创建的线程在默认情况下都属于用户线程, 当在启动线程之前, 执行thread.setDaemon(true)时, 线程会变成守护线程
  • 其实在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是会影响虚拟机的退出(程序的终止)。
  • 当jvm中只剩下守护线程时,虚拟机会退出,及程序终止;而当jvm中至少拥有一个用户线程时,jvm都不会退出。

由此可以得到:

  • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。
  • 用户线程是独立存在的,不会因为其他用户线程退出而退出。
  • 默认情况下启动的线程是用户线程,在线程执行前通过setDaemon(true)将线程设置成守护线程,
    • 在守护线程中启动的子线程也是守护线程
    • 守护线程不建议进行写操作, 因为守护进程随时可能结束

守护线程适用场景

  • 针对于守护线程的特点,笔者认为,java 守护线程通常可用于开发一些为其它用户线程服务的功能。比如说心跳检测事件监听等。Java 中最有名的守护进程当属GC(垃圾回收)

测试程序退出

  1. 注释thread.setDaemon(true)时, 线程为用户线程, 程序进行无线循环, 程序不终止。
    public class TestThread {
        public static void main(String[] args) {
            AnsyTask ansyTask = new AnsyTask();
            
            Thread thread = new Thread(ansyTask);
    
            // 设置线程为守护线程
            // thread.setDaemon(true);
    
            // 启动线程
            thread.start();
    
            System.out.println("main thread done");
        }
    }
    
    class AnsyTask implements Runnable{
        @Override
        public void run() {
            while (true){
                System.out.println(LocalDateTime.now() + "-hello,thread");
            }
        }
    }
    
    在这里插入图片描述
    2. 设置thread.setDaemon(true)后, 线程变成守护线程(守护线程), 程序直接终止, 仅输出一行信息"main thread done"。 因为程序执行完system 语句之后, main 程序作为唯一的一个用户线程执行结束了, jvm 中只剩下一个守护进程,所以jvm 便退出了。

测试守护线程中创建新的线程

  • 测试会发现, 默认情况下, 守护线程创建的子线程依然是守护线程,用户创建的守护线程依然是用户线程。也可以在创建子线程时通过setDaemon()方法修改.
    public class TestThread {
        public static void main(String[] args) throws InterruptedException {
            AnsyTask ansyTask = new AnsyTask();
    
            Thread thread = new Thread(ansyTask);
    
            // 设置线程为守护线程
            thread.setDaemon(true);
    
            // 启动线程
            thread.start();
    
            // 给守护线程点儿执行时间
            Thread.sleep(1000l);
        }
    }
    
    class AnsyTask implements Runnable{
        @Override
        public void run() {
            Thread thread = new Thread("subThread");
            
            System.out.println(thread.getName() + " is daemon:" + thread.isDaemon());
    
        }
    }
    
    在这里插入图片描述

二.JAVA实现多线程

1.实现多线程的3种方式:

  1. 继承java.lang.Thread类,并且重写它的run方法(线程体),将线程的执行主体放在其中;
  2. 实现java.lang.Runnable接口,重写它的run方法(线程体),并将线程的执行主体放在其中;
  3. 实现java.util.concurrent.Callable<T>接口,重写它的call方法(线程体),并通过Futrue获取 call 方法的返回值

2.线程的创建

  1. 在java中负责线程的这个功能是Java.lang.Thread这个类
  2. 每个线程都是通过某个特定Thread对象的run 方法,来完成其操作的, run() 称为”线程体”, run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。
  3. 通过调用Thread类的start()方法来启动一个线程
  4. 一般来说,我们在对线程进行创建的时候,一般是继承Thread类实现Runnable 接口。其实还有一种方式是实现 Callable与Future接口, 类似于Runnable接口,但是就功能上来说更为强大一些,也就是被执行之后,可以拿到返回值。

推荐单线程的时候使用继承 Thread 类方式创建,多线线程的时候使用Runnable、Callable 接口的方式来创建创建线程

  1. 继承 Thread 类创建的线程,编写最为简单,可以直接使用Thread类中的方法,比如休眠直接就可以使用sleep方法,而不必在前面加个Thread;获取当前线程Id,只需调用getId就行,而不必使用Thread.currentThread().getId() 这么一长串的代码。但是使用Thread 类创建的线程,也有其局限性。比如资源不能共享,无法放入线程池中等等。

  2. 使用Runnable、Callable接口的方式创建的线程,可以增强代码的复用性,并且可以避免单继承的局限性,可以和线程池完美结合。但是也有不好的,就是写起来不太方便,使用其中的方法不够简介```。

3.线程的启动

  1. 新建的线程不会自动开始运行,必须通过start( )方法启动
  2. 执行线程必须调用start()加入调度器中, 但是不一定立即执行,由系统安排调度器安排cpu时间片调用线程体()
  3. 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行
  4. Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产生新线程时,程序是单线程的
Thread类与静态代理模式
  1. 真实角色
  2. 代理角色 代理角色要持有真实角色的引用
  3. 二者实现相同接口
  4. Thread类就是使用静态代理模式来实现线程的 , Thread类就是一个写好的静态代理(代理角色)
    • 创建接口
    • 创建代理角色并实现接口,代理类中设置真实角色的引用,并通过构造方法传入,然后通过真实角色的引用调用真实角色的方法,然后在方法前后对方法进行处理。
    • 创建真实角色并实现接口
/**
 * 代理角色
 */
public class ProxyRole implements  MyInterface {
   private  RealRole realRole;

   public ProxyRole(RealRole realRole){
       this.realRole = realRole;
   }

   public void before(){
       System.out.println("向对方问好");
   }

   public void after(){
       System.out.println("再见");
   }

    @Override
    public void talk() {
        before();
        realRole.talk();
        after();
    }
}

/**
 * 共同接口
 */
interface  MyInterface{
    void talk();//谈话
}

/**
 * 真实角色
 */
class RealRole implements  MyInterface {

    @Override
    public void talk() {
        System.out.println("正在谈话中。。。。");
    }
}

class TestMain {
    public static void main(String[] args) {
        //创建真实角色
        RealRole realRole = new RealRole();
        //创建代理角色 + 传入真实角色的引用
        ProxyRole proxyRole = new ProxyRole(realRole);
        //由代理角色代理真实角色调用真实方法
        proxyRole.talk();
    }
}

4.继承Thread

描述:
 创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例

public class RunMain {
    public static void main(String[] args) {
        //调用run 方法相当于普通方法的调用
        Rabbit rabbit  =new Rabbit();
        Tortoise tortoise = new Tortoise();
        //调用start()方法会将线程加入
        rabbit.start();
        tortoise.start();

        //默认启动的线程
        //1.主线程  2.GC  3.异常
        for (int i = 1;i<1000 ;i++ ) {
            System.out.println("主线程走了"+i+"步");
        }
    }
}

/**
 * 兔子
 * 1.继承Thread类  +  重写 run()(线程体)
 * 2.创建子类对象 + 调用start()
 */
class Rabbit extends  Thread {
    /**
     * run方法称为线程体
     */
    @Override
    public void run() {
        for (int i = 1; i<= 1000 ; i++) {
            System.out.println("兔子跑了"+i+"步");
        }
    }
}

class Tortoise  extends  Thread{
    @Override
    public void run() {
        //线程体
        for (int i = 1;i<1000 ;i++) {
            System.out.println("乌龟跑了"+i+"步  ");
        }
    }
}

5.实现Runable接口 + 重写run()

推荐使用Runnable接口创建线程

  1. 避免单继承的局限性
  2. 便于共享资源

实现方式

  1. 实现Runable接口 + 重写run() ——真实角色类
  2. 启动多线程,使用静态代理
    • 创建真实角色(实现Runnable接口,实现run()方法-线程体)
    • 创建代理角色(Thread类实例) + 真实角色引用(将真实角色实例传入Thead)
    • 调用start(),启动线程
/**
 * 继承Runnable接口实现Run()方法
 * 并发:(当一份资源多个使用的时候,可能会产生线程安全问题)
 */
class Web12306 implements  Runnable{
    private int num = 50;
    @Override
    public void run() {
        while (true){
            if(num<=0){
                break;//跳出循环
            }
            System.out.println(Thread.currentThread().getName()+"抢到了票"+(--num));
        }
    }
}

class TestMain{
    public static void main(String[] args) {
        // 真实角色(一份资源)
        Web12306 web = new Web12306 ();

        // 代理角色(多个代理使用)
        Thread thread1 = new Thread(web,"张三");
        Thread thread2 = new Thread(web,"李四");
        Thread thread3 = new Thread(web,"王五");

        //启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

实例2

class RunnableDemo implements Runnable {
   private Thread t;
   private String threadName;
   
   RunnableDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // Let the thread sleep for a while.
            Thread.sleep(50);9
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}

public class TestThread {
   public static void main(String args[]) {
      RunnableDemo R1 = new RunnableDemo( "Thread-1");
      R1.start();
      RunnableDemo R2 = new RunnableDemo( "Thread-2");
      R2.start();
   }   
}
//Thread也可以进行类似的拓展重写,一模一样

3.Thread和Runnable创建方式的比较

  1. 继承Thread类方式的多线程(Thread也是实现Runable接口,队Runnable接口进行了一些封装)
    • 优势:可以直接使用Thread类中的方法,编写简单
    • 劣势:如果已经有了父类,就不能用这种方法 或者 如果继承了Thread就不能其他类了
  2. 实现Runnable接口方式的多线程
    • 优势:避免类单继承的局限,一个类可以继承多个接口(接口是可以多实现的) ,实现Runnable接口方式要通用一些。

    • 劣势:编程方式稍微复杂,不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法。

4.java.util.concurrent.callable(高级并发编程 JUC-并发领域)

  1. 与Runnable相比, Callable功能更强大些

    • 方法不同
    • 可以有返回值,支持泛型的返回值
    • 可以抛出异常
    • 需要借助FutureTask,比如获取返回结果
  2. Future接口

    • 可以对具体Runnable、Callable任务的执行结果进行取消查询是否完成获取结果等。
    • FutrueTask是Futrue接口的唯一的实现类
    • FutureTask同时实现Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为 Future得到Callable的返回值
  3. 通过 Callable 和 Future 创建线程

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

    2. 创建 Callable 实现类的实例,使用 FutureTask类来包装 Callable 对象,该FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

    3. 使用 FutureTask对象作为 Thread 对象的 target 创建并启动新线程。

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

    /**
     * 使用Callable创建线程
     */
    public class TestCallable {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            // 从线程池中获取线程
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            Race tortoise = new Race("乌龟", 1000L);
            Race rabbit = new Race("兔子", 5000L);
    
            // 执行并获取值保存到Future中
            Future<Integer> tortoiseResult = executorService.submit(tortoise);
            Future<Integer> rabbitResult = executorService.submit(rabbit);
    
            //休眠10秒
            //Thread.sleep(10000);//2秒
    
            //设置flag为false停止线程
            tortoise.setFlag(false);
            rabbit.setFlag(false);
    
            //获取线程返回结果
            int tortoiseNum = tortoiseResult.get();
            int rabbitNum = rabbitResult.get();
    
            System.out.println("乌龟跑了-->"+tortoiseNum+"步");
            System.out.println("兔子跑了-->"+rabbitNum+"步");
    
            //停止服务
            executorService.shutdownNow();
        }
    }
    
    /**
     * 使用lombok来在编译期间自动生成get/set方法
     */
    @Data
    class Race implements Callable<Integer> {
        private String name;//名称
        private Long time;//延时时间
        private Boolean flag = true;
        private Integer step = 0;//步
    
        public Race(String name) {
            this.name = name;
        }
    
        public Race(String name, Long time) {
            this.name = name;
            this.time = time;
        }
    
        @Override
        public Integer call() throws Exception {
            while (flag) {
                Thread.sleep(time);
                step++;
                System.out.println(this.getName()+(step));
            }
            return step;
        }
    }
    

5.Lambda方式创建线程

/**
 * Lambda 简化(使用一次)线程方式
 *
 * Lambda 避免内部类创建过多,属于函数式编程概念
 */
public class LambdaThread {
    // 1.静态内部类
    static class Test implements  Runnable{
        @Override
        public void run() {
            System.out.println("听歌");
        }
    }

    public static void main(String[] args) {
        new  Thread(new Test()).start();

        // 2.局部内部类
        class Test2 implements  Runnable{
            @Override
            public void run() {
                System.out.println("看电视");
            }
        }
        new  Thread(new Test2()).start();

        // 3.匿名内部类(连名字都没有,必须结束接口或者父类)
        new  Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("打篮球");
            }
        }).start();

        // 4.JDK8 Lambda表示简化线程
        new  Thread(()-> {
            System.out.println("吃饭");
        }).start();
    }
}

三.线程调度

1.理解线程的优先权(priority)

  1. 当线程的优先级没有指定时,默认普通优先级(Thread.NORM_PRIORITY 默认为普通优先级 5)。
  2. 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
  3. 优先级高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。优先级不能保证线程执行的顺序,由操作系统线程调度算法决定
  4. 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程

Thread类的常量描述
static int MAX_PRIORITY线程可以具有的最高优先级,取值为10
static int MIN_PRIORITY线程可以具有的最低优先级,取值为1
static int NORM_PRIORITY分配给线程的默认优先级,取值为5

注意: 设置优先级并不能保证线程一定先执行。我们可以通过一下代码来验证。

public class PriorityTest {
    public static void main(String[] args) {
        Priority t1 = new Priority("张三");
        Priority t2 = new Priority("李四");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

class Priority extends Thread {
    public Priority(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.getName() + " 线程运行开始!");
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程" + this.getName() + "运行 : " + i);
            try {
                sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.getName() + " 线程运行结束!");
    }
}

2.如何改变线程的状态以及停止线程

2.1.停止线程的两种方法

  1. 自然停止:线程体自然执行完毕

  2. 外部干涉:通过线程体标识

    • 线程类中定义线程体能够使用的标识
    • 线程体使用该标识
    • 提供对外的方法改变该标识
    • 外部根据条件调用该方法即可

具体见代码:

/**
 * 线程的状态:
 * 如何干涉线程的状态以及停止方法:
 */
public class ThreadStop {
    public static void main(String[] args) {
        Study study = new Study();

        new Thread(study, "张三").start();

        // 外部干涉
        for (int i = 0; i < 100; i++) {
            if (50 == i) {//外部干涉
                study.stop();
            }
            System.out.println("main-->" + i);
        }
    }
}

class Study implements Runnable {
    // 1.线程体中定义线程体使用的标识
    private boolean flag = true;

    @Override
    public void run() {
        //2.线程体中使用标识
        while (flag) {
            System.out.println(Thread.currentThread().getName() + "-->study thread......");
        }
    }

    // 3.对外提供方法改变标识
    public void stop() {
        this.flag = false;
    }
}

可见,当i=50时,stop方法并不是立刻被执行的,这与CPU的调度有关

  • 但还是可以看出通过外部干涉的方法使线程终止
  • Thread类也提供了stop方法,但是不建议使用,API中也明确提出该方法不安全

2.1.线程调度方法

2.2.1.内置方法

  • join ()
    • 阻塞指定线程等到另一个线程完成以后再继续执行(简单说,在哪个线程的线程体中调用join,哪个线程就会进入等待状态,并且释放锁)
  • sleep () 使线程停止运行一段时间,将处于阻塞状态
    • 如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
    • sleep()模拟网络延时,放大了发生问题的可能性
  • yield ()
    • 让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态
    • 如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
  • setDaemon()
    • 可以将指定的线程设置成后台线程
    • 创建后台线程的线程结束时,后台线程也随之消亡
    • 只能在线程启动之前把它设为后台线程
  • interrupt()
    • 并没有直接中断线程,而是需要被中断线程自己处理
  • stop()
    • 结束线程,不推荐

2.2.2.yield()与join()区别

  • yield()会让当前运行线程回到可运行状态,让具有相同优先级的其他线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

    public class YieldTest {
        public static void main(String[] args) {
            Test1 t1 = new Test1("张三");
            Test1 t2 = new Test1("李四");
            new Thread(t1).start();
            new Thread(t2).start();
        }
    }
    
    class Test1 implements Runnable {
        private String name;
        public Test1(String name) {
            this.name=name;
        }
        @Override
        public void run() {
            System.out.println(this.name + " 线程运行开始!");
            for (int i = 1; i <= 5; i++) {
                System.out.println(""+this.name + "-----" + i);
                // 当为3的时候,让出资源
                if (i == 3) {
                    Thread.yield();
                }
            }
            System.out.println(this.name + " 线程运行结束!");
        }
    }
    

上述中的例子我们可以看到,启动两个线程之后

  • 执行yield()的线程有可能在进入到可执行状态后马上又被执行
  • main方法也是个线程。如果直接执行的话main方法执行完毕了,可子线程还没执行完毕,这里我们就让子线程使用join方法使main方法等待子线程终止

在这里顺便说下,yield和sleep的区别。

  • yield: yield只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行
  • sleep:sleep使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行

代码2

public class JoinTest {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+ "主线程开始运行!");
        Join t1=new Join("A");
        Join t2=new Join("B");
        t1.start();
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
    }

}

class Join extends Thread{
    public Join(String name) {
        super(name);
    }
    public void run() {
        System.out.println(this.getName() + " 线程运行开始!");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程"+this.getName() + "运行 : " + i);
            try {
                sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.getName() + " 线程运行结束!");
    }

注释掉所有join()<左>效果 与 使用join()<右>如下

  • 结论:如果t1、t2调用join,主线程会进入等待状态,必须等待t1、t2线程终止才能继续执行(如效果图右)

3.线程的常用方法

方法描述
sleep()在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);不会释放对象锁
join()父线程进入等待状态,等待t线程终止,继续执行
yield()暂停当前线程,允许具有相同优先级的其他线程获得运行机会, 但执行yield()的线程有可能在进入到可执行状态后马上又被执行
wait()强迫一个线程等待。它是Object的方法,也常常和sleep作为比较。需要注意的是wait会释放对象锁,让其它的线程可以访问;使用wait必须要进行异常捕获,并且要对当前锁实例所调用,即必须采用synchronized中的对象。
isAlive()判断线程是否还“活”着,即线程是未终止
activeCount()程序中活跃的线程数
enumerate()枚举程序中的线程
static Thread currentThread()得到当前线程
setDaemon()设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
getName()得到线程的名字
setName(String name)设置线程的名字
int getPriority()获取线程的优先级
int setPriority()设置一个线程的优先级
void() start()调用run( )方法启动线程,开启一个线程,但不保证立即运行,交给cpu调度运行
void run()调用线程的线程体
interrupt()一个线程是否为守护线程
notify()通知一个线程继续运行。它也是Object的一个方法,经常和wait方法一起使用
status()获取线程的状态 NEW RUNNABLE

4.线程的生命周期

在这里插入图片描述

在这里插入图片描述
新建/新生状态(New)

  • 使用new关键字创建一个线程的时候,线程还没有开始执行,该线程对象就处于新生状态。
  • 处于新生状态的线程有自己的内存空间,通过调用start()进入就绪状态 (而不是运行状态)

就绪状态(Runnable)

  • 当线程调用了start方法之后,线程就进入就绪状态;处于就绪状态的线程不一定立即运行run方法,只有获取到cpu时间才可以执行run方法
  • 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU ,当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。
  • 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
    产生就绪状态的四种情况
    • 调用start()
    • 阻塞解除,重新进入就绪状态
    • yield(), 高峰量前,中断线程,避免一个线程占用资源过多
    • jvm本身将cpu从本地线程切换到其他线程,此线程进入就绪状态,根据自己的算法进行切换

运行状态(running)

  • 当线程获取到了cpu运行时间之后,就进入到运行状态了,调用run方法;
  • 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。
  • 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
  • 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态(blocked)

  • 正在运行的线程还没有结束,暂时让出cpu,这时其他就绪线程就有机会获得cpu时间;

  • 处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。

  • 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
    以下原因会导致线程进入阻塞状态

    1. 线程调用sleep方法进入睡眠状态;
    2. 线程在调用一个在i/o上被阻塞的操作
    3. 线程试图去获得一个锁,但是这个锁被其他线程持有;
  • 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态(Dead)

  • 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。
    • 一个是正常运行的线程完成了它的全部工作;
    • 另一个是线程被强制性地终止,如通过执行stop/destory方法来终止一个线程[不推荐使用】,
    • 是线程抛出未捕获的异常
      注意: 可以用isAlive方法来判断线程是否还活着,只要是线程处于运行或者阻塞状态,就返回true;如果线程状态是New且不是可运行的状态或者线程死亡了,则返回false;

四.线程重要概念

在多线程编程时,你需要了解以下几个概念:

  1. 线程同步
  2. 线程间通信
  3. 线程死锁
  4. 线程控制:挂起、停止和恢复

1.线程同步

描述

  1. synchronized (obj){ }中的obj称为同步监视器
  2. 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
  3. 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,也就是该对象本身

Lock锁

  1. JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活
  2. java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
  3. ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
  4. 注意:如果同步代码有异常,要将unlock()写入finally语句块

Lock和synchronized的区别

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)

线程同步的好处

  • 解决了线程安全问题

线程同步的缺点

  1. 性能下降
  2. 会带来死锁
/**
 * 线程不安全,以及synchronized使用不当实例
 */
public class ThreadSynchronizedTest {
    public static void main(String[] args) {
        Web12306 web = new Web12306();

        //新建状态
        Thread thread1 = new Thread(web, "路人甲");
        Thread thread2 = new Thread(web, "黄牛乙");
        Thread thread3 = new Thread(web, "攻城狮");

        //就绪状态(不保证立即运行,由cpu调度)
        //运行实际:运行代码前两条路,之后可能还是一条路
        thread1.start();
        thread2.start();
        thread3.start();

    }
}

class Web12306 implements Runnable {
    private boolean flag = true;
    private int num = 10;

    @Override
    public void run() {
        while (flag) {
            testSync1();
        }
    }

    /**
     *线程不安全
     */
    public void testSync6() {
        if (num <= 0) {
            flag = false;//跳出循环
            return;
        }
        //a  b  c
        synchronized (this) {
            try {
                Thread.sleep(500);//模拟 延时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
        }

    }

    /**
     * 线程不安全  锁定资源不正确
     */
    public void testSync5() {
        //a  b  c
        synchronized ((Integer) num) {
            if (num <= 0) {
                flag = false;
                return;
            }

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
        }
    }

    /**
     * 锁定范围不正确 线程不安全
     */
    public void testSync4() {
        //   c  1
        synchronized (this) {
            //b
            if (num <= 0) {
                flag = false;
                return;
            }
        }
        // b
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
    }//a -->1

    /**
     * 线程安全  锁定正确
     */
    public void testSync3() {
        synchronized (this) {
            if (num <= 0) {
                flag = false;
                return;
            }

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
        }
    }

    public synchronized void testSync2() {
        if(num <= 0){
            flag = false;
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName()+"抢到了"+ (num--));
    }

    /**
     * 线程不安全
     */
    public void testSync1() {
        if(num <= 0){
            flag = false;
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
    }
}

下图分别对应 testSync6——testSync1 方法调用结果

同步监视器的执行过程

  1. 第一个线程访问,锁定同步监视器,执行其中代码
  2. 第二个线程访问,发现同步监视器被锁定,无法访问
  3. 第一个线程访问完毕,解锁同步监视器
  4. 第二个线程访问,发现同步监视器未锁,锁定并访问

2.线程死锁

  1. 过多的同步容易造成死锁
  2. 当两个线程相互等待对方释放“锁”时就会发生死锁
  3. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  4. 多线程编程时应该注意避免死锁的发生
public class ThreadDeadLock {
    public static void main(String[] args) {
        Object goods = new Object();
        Object money = new Object();

        // 生产者
        Producers producers = new Producers(goods, money);
        // 消费者
        Consumers consumers = new Consumers(goods, money);

        // 生产者线程
        Thread producersThread = new Thread(producers, "生产者");
        // 消费者线程
        Thread consumersThread = new Thread(consumers, "消费者");

        producersThread.start();
        consumersThread .start();
    }
}

//生产者
class Producers implements Runnable {
    Object goods;
    Object money;

    public Producers(Object goods, Object money) {
        this.goods = goods;
        this.money = money;
    }

    @Override
    public void run() {
        while (true) {
            toGoods();
        }
    }

    public void toGoods() {
        synchronized (goods) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (money) {
            }
        }
        System.out.println(Thread.currentThread().getName() + "一手给货");
    }
}

//消费者
class Consumers implements Runnable {
    Object goods;
    Object money;

    public Consumers(Object goods, Object money) {
        this.goods = goods;
        this.money = money;
    }

    @Override
    public void run() {
        while (true) {
            toMoney();
        }
    }

    public void toMoney() {
        synchronized (money) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (goods) {
            }
        }
        System.out.println(Thread.currentThread().getName() + "一手给钱");
    }
}
//结果导致程序阻塞

在这里插入图片描述

生产者与消费者模式解决线程死锁

多个线程共享一份资源的时候,会发生死锁的现象,我们一般是通过生产者与消费者模式进行解决。也有称为信号灯法

  1. wait() :等待,释放锁
  2. sleep 休眠,不释放锁
  3. notify()/notifyAll(): 唤醒
  4. synchronized
public class ThreadDeadProducersaAndConsumersMode {
    public static void main(String[] args) {
        Moive moive = new Moive();

        Player player = new Player(moive);
        Watcher watcher = new Watcher(moive);

        Thread playerThread = new Thread(player, "生产者");
        Thread watcherThread = new Thread(watcher, "消费者");

        playerThread.start();
        watcherThread.start();
    }
}

/**
 * 共享电影资源类
 */
class Moive {
    private String pic;

    // 信号灯
    // flag->T 生产者生产,消费者等待, 生产完成后通知消费
    // flag->F 消费者消费 生产者等待, 消费者完成后通知生产
    private boolean flag = true;

    /**
     * 播放
     */
    public synchronized void play(String pic) {
        if (!flag) {// 生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 开始生产
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("生产了:" + pic);
        // 生产完毕
        this.pic = pic;
        // 通知消费
        this.notify();
        // 生产者停下
        this.flag = false;
    }

    public synchronized void watch() {
        if (flag) {// 消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 开始消费
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("消费了:" + pic);
        // 消费完毕
        // 通知生产
        this.notifyAll();
        // 消费者停止
        this.flag = true;
    }
}


/**
 * 生产者
 */
class Player implements Runnable {
    private Moive moive;

    public Player(Moive moive) {
        this.moive = moive;
    }

    @Override
    public void run() {
        for (int i = 0; i <= 20; i++) {
            if (0 == i % 2) {
                moive.play("左青龙");
            } else {
                moive.play("右白虎");
            }
        }
    }
}

/**
 * 消费者
 */
class Watcher implements Runnable {
    private Moive moive;

    public Watcher(Moive moive) {
        this.moive = moive;
    }

    @Override
    public void run() {
        for (int i = 0; i <= 20; i++) {
            moive.watch();
        }
    }
}

在这里插入图片描述
一下省略, 一共会执行20轮,

五.线程过期方法suspend/resume/stop

1.如何使用

这三个方法已经是jdk是过期的方法,为什么仍然要单独拿出来说呢?

  • 主要目的是理解jdk多线程API设计的初衷,理解并且更好使用线程API。那么就来说说这三个方法吧:stop方法用于终止一个线程的执行,resume方法用于恢复线程的执行,suspend方法用于暂停线程的执行。
    • 要注意的resume方法需要和suspend方法配对使用,因为被暂停的线程需要执行恢复方法才能继续执行。
方法说明
public void suspend()该方法使线程处于挂起状态,可以使用resume()方法恢复
public void stop()该方法使线程完全停止
public void resume()该方法恢复使用suspend()方法挂起的线程
public void wait()导致当前线程等到另一个线程调用notify()
public void notify()唤醒在此对象监视器上等待的单个线程
请注意,最新版本的Java已经不再使用suspend(),resume()和stop()方法,因此您需要使用可用的替代方法。
class RunnableDemo implements Runnable {
    public Thread thread;
    private String threadName;
    boolean suspended = false;

    RunnableDemo(String name) {
        threadName = name;
        System.out.println("创建 " + threadName);
    }

    public void run() {
        System.out.println("运行 " + threadName);
        try {
            for (int i = 10; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", " + i);
                // Let the thread sleep for a while.
                Thread.sleep(300);
                synchronized (this) {
                    while (suspended) {
                        wait();
                    }
                }
            }
        } catch (InterruptedException e) {
            System.out.println(threadName + " interrupted.");
        }
        System.out.println(threadName + " exiting.");
    }

    public void start() {
        System.out.println(threadName + "处于就绪状态");
        if (thread == null) {
            thread = new Thread(this, threadName);
            thread.start();
        }
    }

    void suspend() {
        suspended = true;
    }

    synchronized void resume() {
        suspended = false;
        notify();
    }
}

public class TestThread {

    public static void main(String args[]) {
        RunnableDemo r1 = new RunnableDemo("线程1");
        r1.start();

        RunnableDemo r2 = new RunnableDemo("线程2");
        r2.start();

        try {
            Thread.sleep(1000);
            r1.suspend();
            System.out.println("线程1:suspend ");
            Thread.sleep(1000);
            r1.resume();
            System.out.println("线程1:resume");

            r2.suspend();
            System.out.println("线程2:suspend");
            Thread.sleep(1000);
            r2.resume();
            System.out.println("线程2:resume");
        } catch (InterruptedException e) {
            System.out.println("主线程 Interrupted");
        }
        try {
            System.out.println("Waiting for threads to finish.");
            r1.thread.join();
            r2.thread.join();
        } catch (InterruptedException e) {
            System.out.println("主线程 Interrupted");
        }
        System.out.println("主线程 exiting.");
    }
}

在这里插入图片描述

2.使用stop()的风险

  • 当调用stop()方法时会发生两件事:
    1. 即刻停止run()方法中剩余的全部工作,包括在catch或finally语句中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭
    2. 立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题
public class Main {
    public static void main(String[] args) throws Exception {
        TestObject testObject = new TestObject();
        Thread thread = new Thread() {
            public void run() {
                try {
                    testObject.print("1", "2");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };

        thread.start();
        Thread.sleep(1000);
        thread.stop();
        System.out.println("first : " + testObject.getFirst() + " " + "second : " + testObject.getSecond());

    }
}

class TestObject {
    private String first = "ja";
    private String second = "va";

    public synchronized void print(String first, String second) throws Exception {
        this.first = first;
        Thread.sleep(10000);
        this.second = second;
    }

    public String getFirst() {
        return first;
    }

    public void setFirst(String first) {
        this.first = first;
    }

    public String getSecond() {
        return second;
    }

    public void setSecond(String second) {
        this.second = second;
    }
}
//first : 1 second : va
  • stop()的不安全主要是针对于第二点:释放该线程所持有的所有的锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误

3.使用suspend()和resume()的风险

suspend()和resume()必须要成对出现,否则非常容易发生死锁,这两个操作就好比播放器的暂停和恢复。

  • 不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。
    • 如果一个线程在resume目标线程之前尝试持有这个重要的系统资源锁再去resume目标线程,这两条线程就相互死锁了
public class Test {
    public static void main(String[] args) throws Exception {
        TestObject testObject = new TestObject();
        Thread t1 = new Thread(() -> testObject.print());
        t1.setName("A");
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            System.out.println("B已启动,但进入不到print方法中");
            testObject.print();
        });
        t2.setName("B");
        t2.start();
    }
}

class TestObject {
    public synchronized void print() {
        if (Thread.currentThread().getName().equals("A")) {
            System.out.println("A 线程 独占该资源了");
            Thread.currentThread().suspend();
        }
    }
}
//A 线程 独占该资源了
//已启动,但进入不到print方法中
  • 但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。此时,通过 jps 和 jstack 命令,来观察线程状态,对于被挂起的线程,它的线程状态居然还是Runnable。

六.线程通信

如果你知道进程间通信,那么就很容易理解线程间通信。 当您开发两个或多个线程交换一些信息的应用程序时,线程间通信很重要。
有三个简单的方法和一个小技巧,使线程通信成为可能。 所有三种方法都列在下面

方法描述
public void wait()使当前线程等到另一个线程调用notify()方法。
public void notify()唤醒在此对象监视器上等待的单个线程。
public void notifyAll()唤醒所有在同一个对象上调用wait()的线程。
这些方法已被实现为Object中的最终(final)方法,因此它们在所有类中都可用。 所有这三种方法只能从同步上下文中调用。

这个例子显示了两个线程如何使用wait()和notify()方法进行通信。 您可以使用相同的概念来创建一个复杂的系统。

class Chat {
    boolean flag = false;

    public synchronized void Question(String msg) {
        if (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(msg);
        flag = true;
        notify();
    }

    public synchronized void Answer(String msg) {
        if (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(msg);
        flag = false;
        notify();
    }
}

class Thread1 implements Runnable {
    Chat chat;
    String[] str = {"Hi", "How are you ?", "I am also doing fine!"};

    public Thread1(Chat chat) {
        this.chat = chat;
        new Thread(this, "Question").start();
    }

    public void run() {
        for (int i = 0; i < str.length; i++) {
            chat.Question(str[i]);
        }
    }
}

class Thread2 implements Runnable {
    Chat chat;
    String[] str = {"Hi", "I am good, what about you?", "Great!"};

    public Thread2(Chat chat) {
        this.chat = chat;
        new Thread(this, "Answer").start();
    }

    public void run() {
        for (int i = 0; i < str.length; i++) {
            chat.Answer(str[i]);
        }
    }
}

public class ThreadCommunication{
    public static void main(String[] args) {
        Chat m = new Chat();
        new Thread1(m);
        new Thread2(m);
    }
}

实例

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墩墩分墩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值