java多线程系列 ---- 第一篇认识线程

目录

  • 1 线程介绍
  • 2 创建并启动一个线程
    • 尝试一下
    • 使用线程
    • 使用Jconsole监测
  • 3 线程生命周期
  • 4 深入了解系统源码
    • start源码及注意事项
    • 模板设计模式
  • 5 Runable
    • 策略模式
    • 例子

本文章所用demo地址:https://download.csdn.net/download/u013513053/11566762

线程的意思就是同时运行。
我记得有个思维练习题是这样说的:

烧水沏茶时,洗水壶要用 1 分钟,烧开水要用 10 分钟,洗茶壶 要用 2 分钟,洗茶杯2分钟,拿茶叶需要1分钟,如何安排能够尽快的喝到茶?

现在这道题对于大多数人来说非常简单,那就是先洗水壶,烧开水,烧水的同时洗茶壶,洗茶杯,拿茶叶。这样安排效率高,因为这里没有等待水烧开才去做下一步,在等待水烧开的时候把其他的准备工作也做好了。

计算机也可以这样提高效率,这就是多线程

1 线程介绍

现在99.99%的操作系统都支持多任务。对计算机来讲,每个 任务就是一个进程(Process),每个进程内至少有一个线程(Thread)。
其实线程很简单。
自打学习程序以来,就有什么顺序、条件、循环。但他们其实是一种,那就是顺着逻辑一条路走。不管怎么嵌套怎么复杂,依然还是一条线缠来缠去。一团毛线球也只是一根毛线在那缠。那一根毛线也是线程,就是说的主线程,就是上面说的至少有一个的线程。
多线程那就是好几根毛线在那缠,最显著的就是效率的提高。以前上学的时候老师罚抄,有聪明者会一手拿多根笔写,写一遍抵两三遍。

线程是程序执行的一个路径,在每个线程内,都有当前程序执行所需的局部变量表、程序计数器(记录程序运行到哪了)、生命周期等内容

2 创建并启动一个线程

2.1 尝试一下

import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static void main(String[] args) {
        playMusic();
        readArticle();
    }

    /**
     * 听音乐,模拟多个任务
     */
    private static void playMusic(){
        while (true){
            System.out.println("听音乐");
            sleep(1);//为了让程序不那么快的运行,睡眠一会
        }
    }

    /**
     * 读文章,模拟多个任务
     */
    private static void readArticle(){
        while (true){
            System.out.println("读文章");
            sleep(1);
        }
    }

    private static void sleep(int seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们使用这段代码,希望可以同时听音乐和读文章。但是从结果看来,只执行了听音乐,读文章不会运行。
在这里插入图片描述

2.2 使用线程并发运行交替输出

改造一下代码,使用多线程。java中的多线程使用的是Thread 类,以后会再次详细说明。

import java.util.concurrent.TimeUnit;

public class Demo2 {
    public static void main(String[] args) {
        
        new Thread(Demo2::playMusic).start();//java8 lambda语法,比较简洁
       
        /**  之前的写法
			new Thread(){
            	@Override
            	public void run() {
                	playMusic();
            	}
        	}.start();
		*/
        readArticle();
    }

    /**
     * 听音乐,模拟多个任务
     */
    private static void playMusic(){
        while (true){
            System.out.println("听音乐");
            sleep(1);//为了让程序不那么快的运行,睡眠一会
        }
    }

    /**
     * 读文章,模拟多个任务
     */
    private static void readArticle(){
        while (true){
            System.out.println("读文章");
            sleep(1);
        }
    }

    private static void sleep(int seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:
在这里插入图片描述
可以看到听音乐和读文章都有执行,交替输出。

2.3 使用Jconsole

JVM为我们提供了很好的工具帮助我们查看线程的状况,我们可以使用Jconsole或者Jstack来进行查看。
如果配置好了环境变量,可以运行java命令,则可以在cmd中输入jconsole 命令启动。
在这里插入图片描述
或者找到jdk安装路径下面 jconsole.exe点击运行
在这里插入图片描述

然后点击选择本地进程,这里会自动自动搜索本机运行的所有虚拟机进程,选择自己的程序点击链接
在这里插入图片描述
如果提示安全链接失败,没有关系,使用不安全的连接继续
在这里插入图片描述
我们可以看到顶部切换选项卡,点击切换到线程
在这里插入图片描述
这里我们可以看到有很多线程,但实际上我们开启的线程只有main和Thread-0。main是有jvm启动时创建的,Thread-0 是上面显示创建的。然后还有一些其他的守护线程,比如垃圾回收线程等。建议在创建线程的时候给线程起一个名字,这样找到线程会比较容易。
在这里插入图片描述

3 线程生命周期

线程的生命周期主要是五个阶段
在这里插入图片描述

3.1 new 创建

当我们new一个Thread的时候,此时并不处于运行状态。只有调用了start方法,才会进入线程运行。单纯new的跟普通的对象没什么两样

3.2 runnable 等待运行/可运行

new出来的Thread对象调用start方法进入runnable 状态,等待运行。为什么不是调用start就运行呢?因为计算机的cpu是总管,有很多事情需要运算处理,新的 任务过来先排号,等待cpu的资源。这个状态是说明当前任务已经具备执行的资格,就差让cpu调度运行了。这就好比去一家饭店,饭店里面本身就有吃饭的,你后来的就得排队等待前面的人腾地。
当获得CPU资源后(scheduler调度),进入running运行中状态

3.3 running 运行中

cpu通过调度选中了可执行队列中的线程,那么此时才会真正执行当前线程的逻辑代码。一个running状态的线程也是runnable状态,runnable准确的说是可以运行,正在运行的状态也属于可运行状态。但是反过来就不成立。
在当前状态下,变换的状态比较多

  • 直接进入terminated 状态:当线程执行完毕或者调用了jdk已经不推荐的stop,进入停止
  • 进入blocked状态:当调用了sleep或者调用了wait方法,或者进行到阻塞的IO操作,或者需要获取的资源正在被其他使用者加了锁使用,需要等待资源释放。
  • 进入runnable状态:CPU调度当前线程失去cpu资源,调用了yield方法放弃了CPU的执行权

3.4 blocked 阻塞

阻塞状态处于一个等待非CPU资源的其他资源的状态。如果说runnable状态已经具备执行的资格,那么blocked状态是执行资格还不具备。再说饭馆的例子,排队排着发现自己钱不够吃饭的,没办法,只能退出来去取到钱后再回来重新排队。
进入阻塞状态上面已经介绍了,再说一下阻塞状态切换至的几种状态

  • 进入terminated 状态:调用了jdk不推荐使用的stop或者意外死亡
  • 进入runnable状态
    1、线程阻塞状态结束,比如读取到了想要的数据
    2、线程完成了指定的休眠
    3、被其他线程用notify/notifyall唤醒
    4、线程获取到某个锁资源
    5、线程在阻塞过程被打断,如其他线程调用了interpret

3.5 terminated 结束

此状态是线程的最终状态,这个状态不会切换至其他任何状态。除了上面提到的进入terminated状态外,还有一些情况也会进入。如

  • 线程正常运行结束
  • 线程运行出错意外结束
  • jvm crash 所有线程都结束

4 深入了解

当我们调用start方法,启动了一个线程,但最终执行的是run方法。底层是如何运行的?

4.1 Thread start方法源码及注意事项

看一下start 源码

public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

start 的源码比较简单,最核心的部分就是 start0 这个方法,也就是JNI方法

  private native void start0();

也就是说,start方法会调用 native 修饰的start0这个方法。native修饰表示是底层方法,不是由java实现。那么run方法何时调用的?从start方法的注释上我们可以看到这段描述。意思就是,当线程开始执行时,JVM将会执行线程的run方法。
在这里插入图片描述
通过start方法的源码我们可以看到

  • Thread被构造后,threadStatus这个内部属性为 0
  • 不能两次启动Thread,否则会出现 IllegalThreadStateException 异常
  • 线程启动后会被加入ThreadGroup中,之后会详细说明ThreadGroup
  • 一个线程生命周期结束,再次调用start方法也是不允许的
public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        thread.start();
        thread.start();
    }

执行这段代码将会抛出IllegalThreadStateException 异常
在这里插入图片描述
然后我们再看一下线程生命周期结束之后再次调用

   public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        thread.start();
        TimeUnit.SECONDS.sleep(3);
        thread.start();
    }

这里执行也会报IllegalThreadStateException 异常
在这里插入图片描述
虽然抛出去的异常是一样的,但它们是有本质区别。一个是重复启动不被允许,第二个是线程已经结束,没有线程可以使用

我们调用start方法,最终执行了run方法。我们把run方法称为线程的执行单元。我们可以看到Thread中run方法。

  @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

如果没有使用到Runnable接口,则Thread的run方法就是一个空的实现。所以直接new一个Thread并调用start也是没有问题,只是没有逻辑处理,没有意义。

new Thread().start();

模板设计模式

其实Thread的run和start方法就是一个比较典型的模板设计模式,父类编写算法结构代码 ,子类实现逻辑细节。
下面用一个例子来实现模板设计模式

public abstract class Demo5 {

    public final void print(String msg){
        System.out.println("调用了print方法");
        customPrint(msg);
        System.out.println("==================");
    }

    public abstract void customPrint(String msg);

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

        Demo5 t1 = new Demo5() {
            @Override
            public void customPrint(String msg) {
                System.out.println("hello "+msg);
            }
        };
        t1.print("张三");
    }
}

输出
在这里插入图片描述
这里的print 方法就相当于Thread 的start方法,customPrint 类似于 run方法。这样做的好处是程序由父类控制,并且是final修饰,不允许被重写子类只需要去实现想要的逻辑任务就可以了。

5 Runable

Thread类本身是实现的Runnable接口,我们也可以从外部传入一个Runnable的实现。在一些文章中,经常会说创建线程的两种方式 ,第一种Thread,第二种Runnable。这种说法是不严谨的。在JDK中线程只有Thread这个类。上面也可以看到Thread中run方法的源码

public
class Thread implements Runnable {
	//省略其他代码···
	@Override
	    public void run() {
	        if (target != null) {
	            target.run();
	        }
	    }
	    //省略其他代码···
}

这里的target就是传入的Runnable对象,在这里有个判断,如果传入的Runnable对象不为空,则执行传入的Runnable对象的run方法。如果为空,就执行自身的run方法。如果是继承了Thread类并重写了run方法,就要注意这一点。否则没有这个条件判断,使用Runnable是不成功的。

5.1 Runnable 策略模式

Thread的run或者使用Runnable都讲线程的控制和本身的业务分离开来,做到了职责分明,功能单一的原则。这种设计模式跟策略模式 很相似。我们来看一下策略模式的例子

public class Demo6 {

    public interface Operation {
        int doOperation(int num1, int num2);
    }

    public static class Calculator {

        private Operation strategy;

        public Calculator(Operation strategy) {
            this.strategy = strategy;
        }

        public int executeOperation(int num1, int num2) {
            return strategy.doOperation(num1, num2);
        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator(new Operation() {
            @Override
            public int doOperation(int num1, int num2) {
                return num1 + num2;
            }
        });
        int result = calculator.executeOperation(1, 3);
        System.out.println(result);
    }
}

这里实现一个计算的例子。首先有一个接口Operation ,这里规定了返回类型,输入的内容。Operation 接口类似于Runnable接口。然后有一个Calculator 计算器类。Calculator 计算器类相当于Thread类,用来处理计算器的方法,最终调用doOperation方法。doOperation方法相当于Runnable中的run方法。
当我们创建一个Calculator 类时,需要传入一个Operation 对象,实现doOperation方法。在doOperation方法中我们可以实现自己的逻辑,让两个数字相加、相减、相乘、相除、比较大小,求最大值、求最小值等等都可以,这就是自己的逻辑。
好处就是1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。职责分明,每个类功能单一。
通过这个例子或许能清楚一些Thread与Runnable 之间的关系。

5.2 使用Runnable模拟叫号

我们使用Runnable的一个好处就是,可以共享变量。如果使用Thread的run方法,则不能实现共享的变量。我们可以实现一个Runnable的类,然后这个类交由多个Thread去使用,这样多个线程是运行的同一个对象的run方法,所使用的属性也是同一个Runnable对象的属性

public class TicketRunnable implements Runnable {
    private int index = 1;

    private static final int MAX_NUMBER = 100;

    @Override
    public void run() {
        while (index<MAX_NUMBER){
            System.out.println(Thread.currentThread()+ " 当前的号码是:"+index);
            index++;
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //模拟叫号

        final TicketRunnable ticket = new TicketRunnable();

        new Thread(ticket,"一号窗口").start();
        new Thread(ticket,"二号窗口").start();
        new Thread(ticket,"三号窗口").start();
        new Thread(ticket,"四号窗口").start();
        new Thread(ticket,"五号窗口").start();
        new Thread(ticket,"六号窗口").start();
    }
}

输出
在这里插入图片描述

当然,这样做也并不是完美的,多运行几次会发现,可能会出现有的号码被跳过,有的号码重复,最后超过最大值或者不到最大值。这是因为线程访问同一个资源,存在线程安全问题。当多个线程同时访问一个资源时,很容易就导致了状态不同。这个地方就需要考虑数据同步问题。我们可以给这个数据加上一把锁,使用synchronized关键字就可以。对于synchronized关键字之后再做介绍

发布了95 篇原创文章 · 获赞 36 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u013513053/article/details/99545925