并发编程核心——二多线程基础

二、多线程基础
使用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、分析
在这里插入图片描述

策略模式把对象本身和运算规则进行了分离,因此我们整个模式被分为三个部分:

  1. 抽象策略类(Strategy):策略的抽象,出行方式的抽象;
  2. 具体策略类(ConcreteStrategy):具体的策略实现,每一种出行方式的具体实 现;
  3. 环境类(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 存在线程安全的问题,

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值