• 线程启动
  • 线程暂停
  • 线程停止
  • 线程优先级
  • 线程安全

进程和多线程概述

进程

  • 进程是受操作系统管理的基本单元
    image.png
    又如WIN的任务管理器的exe文件都可以理解为一个进程。

*.java程序经编译之后形成*.class文件,在WIN中启动一个JVM虚拟机相当于创建了一个进程,在虚拟机中加载class文件并运行,在class文件中通过执行创建新线程的代码来执行具体的任务
举例:
1.打开一个IntelliJ程序后显示一个进程。但其下面有2个线程。一个是Java一个是tsnotifier(用于和系统文件交互)
image.png
2.在Idea中执行

public class Test1 {
    public static void main(String[] args) {
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

最后多出来2个Java相关线程(因为其在Idea中执行,所以其父进程属于Idea)
image.png

线程

在进程中独立运行的子任务。
首先线程对应的是lang包下的Thread类:
image.png
一个程序进程的子任务可能在同时运行不同的任务。也如上图的Idea所示
进程负责向操作系统申请资源,在一个进程中,多个线程可以共享进程中相同的内存或文件资源

多线程

多线程技术可以在同一时间内执行更多不同的任务,异步。和消息队列中间件类似异步解耦。

使用多线程的场景

  • 阻塞
    出现阻塞现象可以根据实际情况来使用多线程技术提高运行效率
  • 依赖
    A、B2个逻辑处理不相互依赖,可以使用多线程提高效率

不要为了使用多线程还使用多线程,要根据实际场景决定

使用多线程

一个进程正在运行时至少会有一个线程在运行。

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

currentThread()回对当前正在执行的线程对象的引用
image
得到的是main,则Java首先创建的线程是main线程。这个main线程和main方法没有任何关系,只是名字相同
http://jtao.work/archives/di-er-shi-liu-zhang--xian-cheng-ji-chu
实现多线程原始的主要方式有2类:
1.继承Thread类
2.实现Runnable接口

继承Thread类

Thread类实现了Runnable接口,其之间有多态的关系即Runnable a= new Thread()

  • 缺点
    Java不支持多继承,不利于面向对面的变成拓展
  • 实现方式
    1.继承Thread
    2.重现run方法
    3.调用start方法
    image.png
    通过上图可以看出,虽然start在print前面被调用,但run方法反而在后面执行这是因为创建线程比较耗时:
    1.通过JVM与操作系统沟通创建Thread
    2.操作系统开辟内存并且使用操作系统原生SDK函数进行创建Thread对象
    3.操作系统对Thread进行调度,以确定执行时机
    4.Thread在操作系统中被成功执行
    如果将当前线程休眠200ms,则因为thread已经被start,且不受影响,所以其会先执行
    image.png

在使用多线程技术时,**代码的运行结果与代码的执行顺序或调用顺序是无关的。**线程是一个子任务,CPU以不确定的方式,随机的时间来调用线程中的run()方法,具有不确定性

使用常见命令分析线程的信息

常用工具在WIN环境下使用见我的JVM博客,这里不细说。只说MacOS
jps、jstack,注意一些软件是无法用于商业的,需要授权
http://jtao.work/archives/xu-ni-ji-xing-neng-jian-kong--gu-zhang-chu-li-gong-ju
使用方法不具体细说了,参看我另一篇博客

public class Run3 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(500000);
                        //因为是匿名内部类,异常无法抛出只能try-catch
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

运行上述代码后
image.png

线程随机性的展现

线程的调用是随机的

/**
 * @author jiangtao
 * @date 2021/9/26 22:52
 */
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println("run="+Thread.currentThread().getName()+i);
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("MyThread");
        myThread.start();
        for (int i = 0; i < 10000; i++) {
            System.out.println("main="+Thread.currentThread().getName()+i);
        }
    }
}

image.png
由结果可以看出其输出是异步,同时进行的。

  • start方法通知“线程规划起”,此线程已经准备就绪,系统自己安排时间调用线程对象的run方法。
    start方法会调用start0方法,而start0方法是一个native方法
    image.png
  • 如果直接调用run方法,则不会创建线程,而是继续由main主线程执行,只能串行执行

执行start()的顺序不代表执行run()的顺序

/**
 * @author jiangtao
 * @date 2021/9/26 23:06
 */
public class MyThread extends Thread {
    private int i;

    public MyThread(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(i);
    }

    public static void main(String[] args) {
        IntStream.iterate(1,vo->vo+=1).limit(10).forEach(vo -> new MyThread(vo).start());
    }
}

image.png
其执行run方法的顺序和start方法顺序无关

实现Rnnable接口

因为是Functional接口,可以解决Java不能多重继承的问题。
image.png
Thread的构造函数有5种方法可以接受Runnable对象,同时又因为多态的缘故,Thread也可以接受Thread对象作为构造函数的入参

/**
 * @author jiangtao
 * @date 2021/9/26 23:20
 */
public class Run implements Runnable {
    @Override
    public void run() {

    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Run());
        new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
        new Thread(() -> {
        });
        new Thread(new Thread(){
            @Override
            public void run() {
                super.run();
            }
        });
    }
}

image.png
在大多数情况下,使用Runnable和Thread没有区别,但是涉及到多继承情况,则应该实现Runnable接口

实现Runnable接口与继承Thread类的内部流程

实现Runnable接口方法在JDK代码执行上相对较复杂
image.png
image.png

  • Thread
    可以使用无参构造target为null
  • Runnable
    有参构造,target不为null

实例变量共享造成的非线程安全问题与解决方案

多个线程交互时变量对其他线程有共享与不共享之分

不共享数据的情况

/**
 * @author jiangtao
 * @date 2021/9/26 23:37
 */
public class MyThread extends Thread{
    private int count=5;
    public MyThread(String name){
        setName(name);
    }
    @Override
    public void run() {
        while (count-->0){
            System.out.println("由"+ currentThread().getName()+"计算,count"+count);
        }
    }

    public static void main(String[] args) {
        new MyThread("A").start();
        new MyThread("B").start();
        new MyThread("C").start();
    }
}

因为count是实例变量,每个变量独享的,所以不会共享
image.png

共享数据的情况

将上述的count属性改为static,变成类共享属性,则
image.png
同时又因为多个线程访问,使得变量的值不正确,出现线程安全问题

  • 原因
    JVM中对count--的操作分解为3步骤
    1.取得原有的count值
    2.计算count-1
    3.对count重新赋值

  • 解决
    1.将属性count变为原子类AtomicInteger
    2.将被使用的方法加锁

Thread类常用API

  • currentThread()返回对当前正在执行的线程对象的引用
    即返回该代码块正在被哪个线程所调用
  • run()和srart()
    1.run立即执行,用当前线程执行,不启用新线程
    2.start执行时间不确定,启用新的线程执行
  • isAlive() 当前线程是否存活
  • sleep(long millis) 让线程休眠
  • sleep(long millis)
  • StackTraceElement[] getStackTrace()
    返回表示此线程的堆栈转储的堆栈跟踪元素数组。 如果此线程尚未启动、已启动但尚未被系统安排运行或已终止,则此方法将返回一个零长度数组。 如果返回的数组长度不为零,则数组的第一个元素表示堆栈的顶部,这是序列中最近的方法调用。 数组的最后一个元素代表堆栈的底部,它是序列中最近的方法调用。
    image.png
    image.png
  • dumpStack()
    将当前线程的堆栈跟踪打印到标准错误流。 此方法仅用于调试
    image.png
  • Map<Thread, StackTraceElement[]> getAllStackTraces()
  • getId()

中断线程

  • stop
    这种方法本质上是不安全的。 使用 Thread.stop 停止线程会使其解锁所有已锁定的监视器(这是未经检查的ThreadDeath异常向上传播堆栈的自然结果)
  • 使用推出标志使线程正常退出
  • 使用interrupt()中断线程
    image.png

检测线程是否被中断

  • static interrupted() 用于检测当前代码所在的线程是否被中断
  • isInterrupted() 用于检测被调用的Thread对象线程是否被中断
    image.png

采用异常停止

  • 方法一:调用interrupt()
    使得interrupted()处于true状态。如果出true则抛出异常后再catch住InterruptedException后做其他相应处理
    image.png
    image.png
  • 方法二:调用sleep()
    调用sleep和interrupt有类似的效果,sleep和interrupt状态同时出现就会有InterruptedException异常被抛出
    image.png
    image.png
  • 方法三:直接return
    如果判断线程被终止了则直接return退出方法即可

stop暴力停止(不推荐

直接调用线程对象stop()方法,会抛出ERROR,不需要显式捕捉处理
image.png

  • 暴力停止,容易造成多线程数据处理的不确定性
  • ThreadDeath会被抛出
  • 对锁定的对象进行解锁会导致数据得不到同步的处理,出现数据不一致问题

暂停线程

suspend()暂停线程、resume()重启线程。

  • 注意这2个方法都是废弃的,因为容易造成死锁
    suspend()方法被调用后在resume之前并不会释放对象锁,造成其他线程无法访问该对象(独占锁),因此容易死锁,被废弃
    image.png
  • 因为线程被暂停,可能会数据不完整
  • 可使用wait()、notify()、notifyAll()方法

yield()放弃CPU使用权

放弃当前CPU资源,让其他任务去占用CPU执行时间,放弃的时间大小不确定,有可能放弃之后立马又被CPU调用
image.png

线程的优先级

线程可以划分优先级,优先级高的线程得到CPU资源较多,会获得更多的CPU时间片。
使用setPriority(int)设置线程优先级
image.png
image.png

  • 优先级分为1~10个等级,如果在其他范围则会报错
  • 优先级等级高约容易获得更多的CPU时间片
  • 线程的优先级具有继承性
    A线程启动B线程,则B线程与A线程的优先级是一样的
  • 高优先级的线程总是大部分先执行完
  • 当线程优先级等级差距很大时,谁先执行完和代码的先后顺序无关
  • 优先级高的线程不总是先执行完

守护线程

Java有2种线程分类:用户线程(非守护线程)、守护线程

  • 定义
    特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。例如垃圾回收GC。其存在是为其他线程提供服务的
  • 设置守护线程
    main线程为非守护线程是胡永线程。调用setDaemon(true)代码并且传入值的线程为守护线程
    image.png
    需要在isAlive为false的时候设置,即调用start之前
  • 设置守护线程时效果
    image.png
    如上图所示,设置了守护线程后main结束,守护线程也结束了,而不是一直打印数字
  • 未设置守护线程效果
    image.png
    线程一直在运行,并未结束

这个家伙很懒,啥也没有留下😋