二、多线程基础
使用java代码的Thread就可以实现并行。前提是多个cpu的情况下。
这个主要是来学习查看源码。以及核心内容。
2.1、创建并启动线程
案例:假设你想在浏览网页看新闻的同时还想听听音乐。
2.2.1、尝试并行运行
package com.bjsxt;
/**
* @author whl
* @date 2020/10/14
*/
public class TryConcurrency01 {
public static void main(String[] args) {
browseNews();
enjoyMusic();
}
// 浏览新闻
private static void browseNews() {
while (true) {
System.out.println("The good news.");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 欣赏音乐
private static void enjoyMusic() {
while(true) {
System.out.println("The nice music.");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果
只有一个任务在运行
package com.bjsxt;
/**
* @author whl
* @date 2020/10/14
*/
public class TryConcurrency02 {
public static void main(String[] args) {
// jdk1.8之前
// 使用匿名内部类方式使得线程处于就绪状态,等待cpu调度 并行
/*new Thread(){
@Override
public void run() {
browseNews();
}
}.start();*/
// jdk1.8 @FunctionalInterface
/*new Thread(() -> {
browseNews();
}).start();*/
// jdk1.8 类的方法引用
new Thread(TryConcurrency02::browseNews).start();
enjoyMusic();
}
// 浏览新闻
private static void browseNews() {
while (true) {
System.out.println("The good news.");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 欣赏音乐
private static void enjoyMusic() {
while(true) {
System.out.println("The nice music.");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果
现象是控制台显示包含听音乐和浏览新闻的语句,
本质是系统在使用多个cpu同时执行看新闻的代码,听音乐的代码,使用Thread线程完成“并行”。
2.2.2、并行运行
2.2、使用Jconsole观察线程
2.1.2中的代码中创建了一个Thread并启动,那么此时JVM中有多少个线程呢?我们可以使用Jconsole或者Jstack命令来查看,这两个JVM工具都是JDK自身提供,如下图所示:
windows下查看jvm中进程的命令
cmd窗口输入 jps,控制台输出进程id和进程名
查看进程中的线程 jconsole
操作后查看进程下面的线程情况
查看非守护线程
修改线程默认的名字
2.3线程生命周期
进程包含堆和方法区,内存中保存指令,成员变量,常量。
Thread t = new Thread(()->{线程体});(新建状态)
这一步是创建一个Thread对象,与创建一个Student对象没区别。本质是在内存中分配一块空间保存对象信息。
t.start();(可运行状态)
创建一个线程,在内存中分配一个空间保存线程信息,线程名称Thread-0,线程状态runnable,程序计数器保存当前执行的的指令地址,线程优先级,局部变量这些;
上一步只是创建一个对象,这一步才是创建一个线程。这一步线程还在内存中,没有在cpu上执行。
cpu上执行(运行装态)
cpu根据程序计数器取指令,译码,执行。线程状态running。
System.out.println(“The good news.”);(阻塞状态)
程序计数器保存下一条指令地址。没有在cpu上执行。线程状态blocked。
输出结束(可运行状态)
线程状态runnble。
cpu上执行(运行装态)
cpu根据程序计数器取指令,译码,执行。线程状态running。
Thread.sleep(1000L);(阻塞状态)
程序计数器保存下一条指令地址。没有在cpu上执行。线程状态blocked。
1秒后(可运行状态)
线程状态runnble。
点击红色按钮终止进程(终止状态)
线程异常终止
2.3线程start方法源码剖析
如图两次调用start方法,会报错。
线程结束后,再次启动线程会报错terminate running
发现用idea调试jdk源码,Thread的start方法进不去,看不见threadStatus的值,好烦。https://blog.csdn.net/qq_32097903/article/details/109160217这一波操作解决了。终于可以debug源码了。
typora源码笔记截图:
这个线程源码使用了模板方法设计模式。
**模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中.模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤.**见 https://blog.csdn.net/qq_32097903/article/details/109177105
2.6、Thread模拟营业厅叫号
代码
package com.bjsxt.chapter04;
/**
* 柜台窗口
* @author whl
* @date 2020/10/20
*/
public class CounterWindow extends Thread {
// 窗口名称
private final String windowName;
// 最多受理50个业务 被所有对象使用
private static final int MAX = 50;
// 起始号码 一个对象一个
// 不推荐使用static
// static修饰的变量不会跟随类的销毁而消失,跟随jvm销毁销毁,生命周期很长
private static int index = 1;
public CounterWindow(String windowName) {
this.windowName = windowName;
}
@Override
public void run() {
while(index <= MAX) {
System.out.format("请【%d】号到【%s】办理业务\n",index++,windowName);
try{
Thread.sleep(1000L);
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new CounterWindow("一号窗口").start();
new CounterWindow("二号窗口").start();
new CounterWindow("三号窗口").start();
new CounterWindow("四号窗口").start();
}
}
运行结果:
不推荐使用static修饰变量index,因为有可能是一串业务运算计算复杂…没明白?(策略模式)
灵魂拷问:
创建线程的方式有几种?
回答:
一种!创建线程的方式只有一种。实现线程的执行单元的方式有两种。因为java中能代表线程的只有Thread。Runnable接口是将业务相关的代码抽离成一个接口。最终实现类对象也会被塞进Thread对象中。
2.7、Runnable 模拟营业大厅叫号机程序
2.7.1、Runnable 的职责
Runnable 接口非常简单,只定义了一个无参数无返回值的 run 方法,在 JDK8 的概念 中属于一个函数式接口,源码如下:
package java.lang;
@FunctionalInterface
public interface Runnable
{
public abstract void run();
}
@Override public void run()
{ // 如果构造 Thread 时传递了 Runnable,则会执行 Runnable 的 run 方法
if (target != null) { target.run(); }
// 否则需要重写 Thread 类的 run 方法 }
通过源码发现,创建线程只有一种方式那就是构造 Thread 类,而实现线程的执行单元 则有两种方式,第一种是重写 Thread 类的 run 方法,第二种是实现 Runnable 接口的 run 方法,并且将 Runnable 实例用作构造 Thread 的参数。
官方为什么这么设计?Runnable 接口存在的必要是什么?接下来我们通过案例带大 家详细了解一下
2.7.2、策略模式
无论是 Runnable 的 run 方法,还是 Thread 类本身的 run 方法(事实上 Thread 类也 是实现了 Runnable 接口)都是想将线程的控制和业务逻辑的运行分离开来,达到职责分明、 功能单一的原则,这一点与 GoF 设计模式中的策略模式很相似,在本节中,我们一起学习 一下什么是策略模式,然后再来对比 Thread 和 Runnable 两者之间的区别。
2.7.2.1、概念
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改,我们 创建表示各种策略的对象和运算规则随着策略对象的改变而改变。策略模式把对象本身和运 算规则进行了分离。 这里所指的对象本身就是 Thread 线程对象,而运算规则可以理解为 run 方法中的业务 逻辑
2.7.2.2、案例
为了更好的理解这个模式,我们举例说明一下,我们出去旅游的时候可能有很多种出行 方式,比如说我们可以坐汽车、坐火车、坐飞机等等。不管我们使用哪一种出行方式,最终 的目的地都是一样的。也就是选择不同的方式产生的结果都是一样的。
有了这个例子,我相信你应该对其思想有了一个基本的认识,下面看一下其正式的概念 介绍:定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换
2.7.2.2.1、分析
策略模式把对象本身和运算规则进行了分离,因此我们整个模式被分为三个部分:
- 抽象策略类(Strategy):策略的抽象,出行方式的抽象;
- 具体策略类(ConcreteStrategy):具体的策略实现,每一种出行方式的具体实 现;
- 环境类(Context):用来操作策略的上下文环境,也就是我们游客。 下面我们通过代码去实现一遍就能很清楚的理解了。
2.7.2.2.2、实现
a. 定义抽象策略接口
package com.bjsxt.web.controller;
public interface TravelStrategy {
void tralvelAlgorithm();
}
b. 具体策略类
package com.bjsxt.web.controller;
public class TrainStrategy implements TravelStrategy {
public void tralvelAlgorithm() {
System.out.println("坐火车...");
}
}
package com.bjsxt.web.controller;
public class PlaneStrategy implements TravelStrategy {
public void tralvelAlgorithm() {
System.out.println("坐飞机...");
}
}
package com.bjsxt.web.controller;
public class CarStrategy implements TravelStrategy {
public void tralvelAlgorithm() {
System.out.println("坐汽车...");
}
}
package com.bjsxt.web.controller;
public class BikeStrategy implements TravelStrategy {
public void tralvelAlgorithm() {
System.out.println("坐自行车...");
}
}
c. 环境类实现
package com.bjsxt.web.controller;
public class Traveler {
// 维护策略接口对象的一个引用
private TravelStrategy travelStrategy;
// 通过 set 方法设置旅行策略
public void setTravelStrategy(TravelStrategy t){
this.travelStrategy = t;
}
// 通过 构造方法设置旅行策略
Traveler(TravelStrategy t){
this.travelStrategy = t;
}
// 调用对应策略的实现
public void travleStyle(){
this.travelStrategy.tralvelAlgorithm();
}
public static void main(String[] args) {
// 出行策略
new Traveler(new CarStrategy()).travleStyle();
new Traveler(new BikeStrategy()).travleStyle();
new Traveler(new PlaneStrategy()).travleStyle();
new Traveler(new TrainStrategy()).travleStyle();
}
}
通过以上案例我们可以清晰感受到策略模式带来的好处,下面我们来总结一下:
优点:
我们之前在选择出行方式的时候,往往会使用 if-else 语句,也就是用户不选择 A 那么 就选择 B 这样的一种情况。这种情况耦合性太高了,而且代码臃肿,有了策略模式我 们就可以避免这种现象;
策略模式遵循开闭原则,实现代码的解耦合。扩展新的方法时也比较方便,只需要继承 策略接口就好了。
缺点:
客户端必须知道所有的策略类,并自行决定使用哪一个策略类;
策略模式会出现很多的策略类;
客户端在使用这些策略类的时候,这些策略类由于继承了策略接口,所以有些数据可能 用不到,但是依然初始化了。
2.7.2.3、拓展
重写 Thread 类的 run 方法和实现 Runnable 接口的 run 方法还有一个很重要的不同 那就是 Thread 类的 run 方法是不能和共享的,也就是说 A 线程不能把 B 线程的 run 方法 当作自己的执行单元,而使用 Runnable 接口则很容易就能实现这一点,使用同一个Runnable 的实例构造不同的 Thread 实例。
2.7.3、模拟营业大厅叫号机程序
代码:
package com.bjsxt;
/**
* @author whl
* @date 2021/2/3
*/
public class CounterDemo {
public static void main(String[] args) {
class MyCounter implements Runnable {
private final int MAX_NUM = 50;
// 不用static修饰了
private int index = 0;
@Override
public void run() {
while(index < MAX_NUM) {
System.out.format("请【%d】号顾客到【%s】号窗口办理业务!\n",++index,Thread.currentThread().getName());
}
}
}
MyCounter myCounter = new MyCounter();
Thread a = new Thread(myCounter,"A");
Thread b = new Thread(myCounter,"B");
Thread c = new Thread(myCounter,"C");
Thread d = new Thread(myCounter,"D");
a.start();
b.start();
c.start();
d.start();
}
}
运行结果
可以看到上面的代码中并没有对 index 进行 static 的修饰,并且我们也将 Thread 中 run 的代码逻辑抽取到了 Runnable 的一个实现中。 程序的输出结果虽然和之前是一样的,但是这次是使用了同一个 Runnable 接口,这样 他们的资源就是共享的,不会再出现每一个叫号机都从 1 打印到 50 这样的情况。
注意:不管是之前 static 修饰 index 的方式,还是用实现 Runnable 接口的方式,这 两个程序多运行几次或者将线程阻塞时间缩短或者 MAX 的值从 50 增加到 500、1000 等 稍微大一些的数字就会出现一个号码出现两次的情况,也会出现某个号码根本不会出现的情 况,更会出现超过最大值的情况,这是因为共享资源 index 存在线程安全的问题,