珍惜当下 展望未来!

多线程基础

多线程基础

参考视频:https://www.bilibili.com/video/BV16J411h7Rd?p=17

1.进程和线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

2.并行和并发

并发:

单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是 同时运行的 。

总结为一句话就是: 微观串行,宏观并行一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent

image-20211216224855988

image-20211216224732605

并行

多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的

image-20211216224904836

image-20211216224916602

例子:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)

3.应用

异步调用

以调用方角度来讲,

  • 如果 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

设计

多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停...

如果能使用异步的话,尽量使用异步,避免主线程阻塞,提高效率

结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
  • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

提高效率(案例)

充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。

计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总需要 1 ms
  • 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
  • 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个 线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms

注意: 需要在多核 cpu 才能提高效率,单核仍然时是轮流执行

结论

  • 1.单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

  • 2.多核 cpu 可以并行跑多个线程但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】) 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

    1. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

4.创建线程的方法

1.直接使用Thread类

调用start方法执行启动线程(start方法使得线程进行就绪状态,执行需要通过底层的cpu调度)

public class Demo1 {
    public static void main(String[] args) {
        //创建线程对象(匿名内部类的方式)
      Thread thread1 = new Thread("thread1"){
          //线程执行逻辑
          @Override
          public void run() {
              System.out.println("thread1-----");
          }
      };
      //开启线程
      thread1.start();
    }
}

2.Ruanble配合thread

线程和任务分开, runnable放执行任务,thread执行的就是runnable的run方法

public class Demo2 {
    public static void main(String[] args) {
//        Runnable runnable = new Runnable() {
//            public void run() {
//                System.out.println(Thread.currentThread().getName());
//                System.out.println("aaaaaaaa");
//            }
//        };

        //lambda表达式写法
        Runnable runnable = ()->{
            System.out.println("aaa");
        };
        Thread thread = new Thread(runnable,"t2");
        thread.start();
    }
}

thread和runnable对比:

  • runnable将任务和线程分开,更加灵活,同一个runnable任务可以被多个线程使用
  • 最后都是通过thread来开启线程的

3.FutureTask结合Thread

FutureTask实际上也是间接实现了Runable接口

image-20211222225722876

可以获得线程执行的返回结果(内部callable的执行结果)

public class Demo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("running...");
                Thread.sleep(1000);
                return 100;
            }
        });

        Thread thread = new Thread(futureTask,"t3");
        thread.start();

        //获取执行结果
        Integer res = futureTask.get();
        System.out.println(res);
    }
}

观察多个线程同时执行

public class Test {
    public static void main(String[] args) {
        new Thread(()->{
            while (true){
                System.out.println(100);
            }

        }).start();
        new Thread(()->{
            while (true){
                System.out.println(200);
            }
        }).start();
    }
}

结论:

多个线程同时开启,不能保证执行顺序,是交替执行的,由底层cpu决定

程序员没法控制

5.查看进程,线程命令

windows

#任务管理器可以查看进程和线程数,也可以用来杀死进程

#tasklist 查看进程
tasklist | findstr java  #管道符筛选

#taskkill 杀死进程
taskkill /F /PID 13332  #杀死进程id为13332的进程

Linux

ps -ef #查看所有进程
ps -ef | grep java #管道符筛选

ps -fT -p <PID> #查看某个进程(PID)的所有线程

kill 杀死进程
kill -9 进程id #强制杀死

top #动态查看进程信息,按大写 H 切换是否显示线程
top -H -p <PID>  #查看某个进程(PID)的所有线程

Java(jdk带的)

jps #命令查看所有 Java 进程

jstack <PID> #查看某个 Java 进程(PID)的所有线程状态

jconsole #来查看某个 Java 进程中线程的运行情况(图形界面)

使用jconsole图形化界面:

image-20211224112345389

jconsole 远程监控配置

远程加上以上配置运行Java类,

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类

例如:

java -Djava.rmi.server.hostname=47.107.93.172 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=3344 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false Test

jcosole连接远程:

image-20211224113919146

image-20211224120030201

6.原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 每个线程有自己的私有栈内存,与其他线程互不干扰

一个线程一个栈,一个方法一个栈帧

image-20211224142120593

线程运行-栈帧图解

image-20211224143439360

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

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

7.线程常见方法

run和start

调用run

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("t1");
        }, "t1");
        thread.run();
        System.out.println("main");

    }
}

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

调用start

 thread.start();

程序在 t1 线程运行, System.out.println("t1")调用是异步的

结论

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,run 中的代码在新的线程t1中执行

sleep和yield

都是Thread中的静态native方法

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 ,底层还是Thread.sleep

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器
public class Test1 {
    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            int count = 1;
            while (true) {
                Thread.yield();//让出去执行权
                System.out.println("         >>>t1。。。。:" + count++);
            }
        });
        Thread t2 = new Thread(() -> {
            int count = 1;
            while (true) {
                System.out.println(">>>t2。。。。:" + count++);
            }
        });
        t1.start();
        t2.start();

    }
}

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
  • java中线程优先级为1到10,数字越大越高,默认为5
public class Test1 {
    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            int count = 1;
            while (true) {
                System.out.println("         >>>t1。。。。:" + count++);
            }
        });
        Thread t2 = new Thread(() -> {
            int count = 1;
            while (true) {
                System.out.println(">>>t2。。。。:" + count++);
            }
        });
        //设置优先级
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();

    }
}

案例:sleep限制cpu的消耗

sleep 实现

在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序

在1个核心的机器上测试效果更明显

public class Test3 {
    public static void main(String[] args) {
        while (true){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

不加sleep几乎占满整个cpu:

image-20211224170656394

加了sleep后占用少量cpu

image-20211224171007719

join

等待线程(调用join方法的线程)运行结束

public class Test4 {
    private static  int a = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            a = 100;
        }, "t1");
        t1.start();
        t1.join();//主线程等待t1运行完  再打印
        System.out.println(a);
    }
}
  • join可以使线程按照顺序执行(异步变成同步)
  • join底层原理就是wait方法

interrupt

打断sleep,wait ,join的线程

sleep,wait ,join方法被interrupt方法打断后,会清空打断状态,打断标记(thread.isInterrupted())会重新置为false

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        //让thread先睡眠再打断
        Thread.sleep(1000);
        thread.interrupt();
        System.out.println("打断标记:"+thread.isInterrupted());//false
    }
}

打断正常运行的线程

打断正常运行的线程, 不会清空打断状态 为true

通过打断标志来判断是否停止运行线程

public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted){
                    System.out.println("被打断了 退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
    }
}

打断park线程

LockSupport.park()使用 使当前线程执行停止到这个地方,不会停止程序。。

public class Test8 {
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            System.out.println("park");
            LockSupport.park();//当前线程执行停止到这个地方。。
            System.out.println("unpark");
        },"t1");
        t1.start();
    }
}

打断 park 线程, 不会清空打断状态 为true

public class Test8 {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            System.out.println("park");
            LockSupport.park();//当前线程执行停止到这个地方。。
            System.out.println(Thread.currentThread().isInterrupted());//true
            System.out.println("unpark");
        },"t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();

    }
}

如果打断标记已经是 true, 则 park 会失效

public class Test8 {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            System.out.println("park");
            LockSupport.park();
            System.out.println(Thread.currentThread().isInterrupted());//true
            System.out.println("unpark");

            //如果打断标记已经是 true, 则 park 会失效
            LockSupport.park();
            System.out.println(111);
            System.out.println(111);
            System.out.println(111);
            System.out.println(111);
        },"t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();

    }
}

提示:可以使用 Thread.interrupted() 清除打断状态(设置为false),使lock重新生效

public class Test8 {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            System.out.println("park");
            LockSupport.park();
            System.out.println(Thread.currentThread().isInterrupted());//true
            System.out.println("unpark");

            //如果打断标记已经是 true, 则 park 会失效
            Thread.interrupted();//清除打断状态  设置为false
            System.out.println(Thread.currentThread().isInterrupted());//false
            LockSupport.park();
            System.out.println(111);
            System.out.println(111);
            System.out.println(111);
            System.out.println(111);
        },"t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();

    }
}

终止模式之两阶段终止模式

Two Phase Termination

在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程
    • 目的仅是停止一个线程,但这种做法会让整个程序都停止(有点小题大做)

两阶段终止模式

前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。

顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令。使得t2能够优雅的停止,不至于影响整体程序

image-20211226135503588

实现方式1:利用 isInterrupted

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

需要考虑到 sleep,wait被interrupt后打断标志被清空(为false)

/**
 * @author
 * @date 2021/12/26 14:07
 */
public class Test7 {
    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt tptInterrupt = new TPTInterrupt();
        tptInterrupt.start();
        Thread.sleep(3000);
        tptInterrupt.stop();
    }

}

class TPTInterrupt {
    private Thread thread;

    //开启监控线程
    public void start() {
        thread = new Thread(() -> {
            while (true) {
                //获取打断标记
                Thread currentThread = Thread.currentThread();
                if (currentThread.isInterrupted()){
                    System.out.println("料理后事");
                    break;
                }

                try {
                    Thread.sleep(1000);
                    System.out.println("执行监控记录");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //重新打断thread  是打断标志为true
                    currentThread.interrupt();
                }

            }
        });
        thread.start();
    }


    //停止监控线程
    public void stop() {
        thread.interrupt();
    }
}

不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

image-20211226150358497

8.主线程和守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

我们自己创建的线程默认为非守护线程

public class Test9 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (true){
                System.out.println("啦啦啦啦");
            }
        },"t1");
        t1.setDaemon(true);//设置t1为守护线程
        t1.start();
        System.out.println("main");
    }
}

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

9.线程状态

5种状态

这是从 操作系统 层面来描述的

image-20211226194456706

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 ,与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
  • 【终止状态】表示线程内容已经执行完毕,生命周期已经结束,不会再转换为其它状态

6种状态

这是从 Java API 层面来描述的 根据 Thread.State 枚举,分为六种状态

image-20211226194840175

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束

案例:六种状态演示

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("t1.....");
        });

        Thread t2 = new Thread(()->{
            while (true){
            }
        });
        t2.start();

        Thread t3 = new Thread(()->{
            System.out.println("t3.....");
        });
        t3.start();

        Thread t4 = new Thread(()->{
            synchronized (Test1.class){
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t4.....");
            }
        });
        t4.start();

        Thread t5 = new Thread(()->{
            try {
                t4.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t5.....");
        });
        t5.start();


        Thread t6 = new Thread(()->{
           synchronized (Test1.class){
               System.out.println("t6....");//拿不到锁就会阻塞
           }
        });
        t6.start();


        System.out.println(t1.getState());//NEW
        System.out.println(t2.getState());//RUNNABLE
        System.out.println(t3.getState());//TERMINATED
        Thread.sleep(1000);
        System.out.println(t4.getState());//TIMED_WAITING
        System.out.println(t5.getState());//WAITING
        System.out.println(t6.getState());//BLOCKED
    }
}

10.案例:统筹(烧水泡茶)

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么 办?

  • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝。
  • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡 茶喝。
  • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡 茶喝。

哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。

水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而 这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:

image-20211227100412832

洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:

image-20211227100457483

看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。

解法1实现:

public class Test1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(1000);
                System.out.println(name+":洗水壶");
                Thread.sleep(5000);
                System.out.println(name+":烧开水");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"老王");

        Thread t2 = new Thread(()->{
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(1000);
                System.out.println(name+":洗茶壶");
                Thread.sleep(2000);
                System.out.println(name+":洗茶杯");
                Thread.sleep(1000);
                System.out.println(name+":拿茶叶");
                //等老王烧完开水才能泡茶
                t1.join();
                System.out.println(name+":泡茶");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小王");

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

解法1缺点:

  • 上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶 呢?代码最好能适应两种情况
  • 上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶 呢

11.总结:

本章的重点在于掌握

  • 线程创建

  • 线程重要 api,如 start,run,sleep,join,interrupt 等

  • 线程状态

  • 应用方面

  • 异步调用:主线程执行期间,其它线程异步执行耗时操作

  • 提高效率:并行计算,缩短运算时间

  • 同步等待:join

  • 统筹规划:合理使用线程,得到最优效果

  • 原理方面

    • 线程运行流程:栈、栈帧、上下文切换、程序计数器
    • Thread 两种创建方式 的源码
  • 模式方面

    • 终止模式之两阶段终止
posted @ 2021-12-16 23:08  嘿嘿-  阅读(126)  评论(0)    收藏  举报