2、Java线程

1、创建和运行线程

方法一、直接使用Thread

	// 直接使用Thread(继承/匿名内部类)
    Thread t1 = new Thread("t1") {
        @Override
        public void run() {
            log.debug("hello1");
        }
    };
    t1.start();

方法二、使用Runnable配合Thread

	// 使用Runnable配合Thread(函数式接口)
        Thread t2 = new Thread(() -> log.debug("hello2"), "t2");
        t2.start();

原理

  • 方法一是把线程和任务合并到了一起,方法二把线程任务分开了
  • 用Runnable更容易与线程池等高级API融合
  • 用Runnable让任务类脱离了Thread继承体系,更加灵活

方法三、FutureTask配合Thread

        // FutrueTask配合Thread(实现Runnable)
        FutureTask<Integer> task = new FutureTask<Integer>(() -> {
            log.debug("hello3");
            return 100;
        });
        Thread t3 = new Thread(task, "t3");
        t3.start();
        log.debug("结果是:{}", task.get());

2、观察多个线程同时运行

主要是理解

  • 交替执行
  • 谁先谁后,不用我们控制

3、查看进程线程的方法

windows

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

linux

  • ps -ef查看所有进程
  • ps fT -p <PID>查看某个进程(PID)的所有线程状态
  • kill杀死进程
  • top按大写H切换是否显示线程
  • top -H -p <PID>查看某个进程(PID)的所有线程

Java

  • jps命令查看所有java进程
  • jstack <PID>查看某个Java进程(PID)的所有线程状态
  • jconsole来查看某个Java中线程的运行情况(图形界面)

4、原理之线程运行

栈与栈帧

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

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

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

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

5、常见方法

方法名static功能说明注意
start()启动一个新的线程,在新的线程运行run方法中的代码start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没有分给他)。每个线程对象的start方法只能调用一次,如果调用多次会出现IllegaThreadStateException
run()新线程启动后会调用的方法如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待n毫秒
getId()获取线程长整型idid唯一
getName()获取线程名称
setName()修改线程名称
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的几率
getState()获取线程状态Java中线程状态用6个enum表示,分别为NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()判断线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断的线程正在sleep,wait,join会导致被打盹啊的线程抛出InterruptedException,并清除打断标记,如果打断正在运行的线程,则会设置打断标记,park的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时会让出cpu的时间片给其他线程
yield()static提示线程调度器让出当前线程对cpu的使用主要是为了测试和调试

6、start与run

  • 直接调用run方法是在当前线程中执行run方法,没有启动新线程
  • 使用start是启动新的线程,通过新的线程执行run中的代码

7、sleep与yield

sleep

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

yield

  1. 调用yield会让点前线程从RUNNING进入RUNNABLE就绪状态然后调度器执行其他线程
  2. 具体的实现依赖于操作系统的任务调度器
		Thread t1 = new Thread(() -> {
            int count = 0;
            for (; ; )
                log.debug("" + count++);
        }, "t1");

        Thread t2 = new Thread(() -> {
            int count = 0;
            for (; ; ) {
                Thread.yield();
                log.debug("" + count++);
            }
        }, "t2");

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

线程优先级

  • 线程优先级会提示(hint)调度器有线调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没用
        Thread t1 = new Thread(() -> {
            int count = 0;
            for (; ; )
                log.debug("" + count++);
        }, "t1");

        Thread t2 = new Thread(() -> {
            int count = 0;
            for (; ; )
                log.debug("" + count++);
        }, "t2");

        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);

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

8、join方法详解

为什么需要join
下面代码执行,打印r是什么

		static int r = 0;
		public static void main(String[] args) throws InterruptedException {
		 	test1();
		}
		private static void test1() throws InterruptedException {
		 	log.debug("开始");
		 	Thread t1 = new Thread(() -> {
		 	log.debug("开始");
		 	sleep(1);
		 	log.debug("结束");
		 	r = 10;
		 	});
		 	t1.start();
		 	log.debug("结果为:{}", r);
		 	log.debug("结束");
		}

分析

  • 因为主线程和线程1是并行的,t1线程需要一秒之后才能算出r=10
  • 而主线程一开始就要打印r的结果,所以只能打印出r=10

从调用的角度来讲,如果

  • 需要等待结果返回,才能继续运行的就是同步
  • 不需要等待结果返回,就能继续运行的就是异步
1s 后
终止
main
t1.jion
t1.start
r=10

等待多个结果
问,下面代码cost大约多少秒?

	static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test2();
    }
    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
            try {
                sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

分析如下

  • 第一个join:等待t1时,t2并没有停止,而在运行
  • 第二个join:1s后,执行到此,t2也运行了1s,因此只需要再等待1s

如果颠倒两个join呢
最终结果还是一样的

1s 后
t1 终止
2s 后
t2 终止
1s 后
t1 终止
2s 后
t2 终止
main
t1.join
t2.join - 仅需等待1s
t1.start
r = 10
t2.start
r = 20
main
t1.start
r = 10
t1.join -无需等待
t2.join
t2.start
r = 20

有实效的join
等够时间

	static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test3();
    }
    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

输出

00:42:42 [main] c.Sync - r1: 10 r2: 0 cost: 1018

没等够时间

static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test3();
    }
    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        t1.join(1500);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

输出

00:43:55 [main] c.Sync - r1: 0 r2: 0 cost: 1516

9、interrupt方法详解

打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态
打断sleep的线程,会清空打断状态,以sleep为例

	public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();
        sleep(500);
        t1.interrupt();
        log.debug("打断状态:{}", t1.isInterrupted());

    }

输出

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.base.Test7.lambda$test1$0(Test7.java:18)
	at java.lang.Thread.run(Thread.java:748)
00:48:01 [main] c.Sync - 打断状态:false

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

	public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if (interrupted) {
                    log.debug("打断状态:{}", interrupted);
                    break;
                }
            }
        });

        t1.start();
        sleep(500);
        t1.interrupt();
    }

输出

00:52:16 [Thread-0] c.Sync - 打断状态:true

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

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

1.错误思路

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

2.两阶段终止模式

无异常
有异常
while(ture)
有没有被打断
料理后事
结束循环
睡眠1s
执行监控记录
设置打断标记
2.1 利用isInterrupted

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

package termination;

import lombok.extern.slf4j.Slf4j;

/**
 * @author: 言叶长琴
 */
@Slf4j(topic = "c.TPTInturrept")
public class TPTInturrept {

    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    current.interrupt();
                }
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }

}

调用

TPTInturrept tptInturrept = new TPTInturrept();
        tptInturrept.start();
        Thread.sleep(3500);
        log.debug("stop");
        tptInturrept.stop();

输出

2022/03/01-01:26:53.291 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:54.309 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:55.329 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:55.789 [main] c.TPTTest - stop
2022/03/01-01:26:55.789 [监控线程] c.TPTInturrept - 料理后事
2.2利用停止标记

volatile关键字会在后面讲到

package termination;

import lombok.extern.slf4j.Slf4j;

/**
 * @author: 言叶长琴
 * 停止标记用 volatile是为了保证该变量在多个线程之间的可见性
 * 我们的例子中,即主线程把它修改为true对t1线程可见
 */
@Slf4j(topic = "c.TPTVolatile")
public class TPTVolatile {

    private Thread thread;
    private volatile boolean stop = false;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                }
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;
        thread.interrupt();
    }

}

调用

TPTVolatile tptVolatile = new TPTVolatile();
        tptVolatile.start();
        Thread.sleep(3500);
        log.debug("stop");
        tptVolatile.stop();

输出

2022/03/01-01:33:27.293 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:28.297 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:29.307 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:29.798 [main] c.TPTTest - stop
2022/03/01-01:33:29.799 [监控线程] c.TPTVolatile - 料理后事

打断park线程

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

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

    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                LockSupport.park();
                log.debug("unpark...");
                log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
            }
        }, "t1");

        t1.start();
        sleep(1000);
        t1.interrupt();

输出,注意第一次打断的时间

2022/03/01-01:40:54.285 [t1] c.Sync - park...
2022/03/01-01:40:55.295 [t1] c.Sync - unpark...
2022/03/01-01:40:55.296 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.298 [t1] c.Sync - 打断状态:true

如果要是打断标记恢复,则可以使用Thread.interrupted()

		Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                LockSupport.park();
                log.debug("unpark...");
//                log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
                log.debug("打断状态:{}", Thread.interrupted());
            }
        }, "t1");

        t1.start();
        sleep(1000);
        t1.interrupt();

输出

2022/03/01-01:44:24.388 [t1] c.Sync - park...
2022/03/01-01:44:25.403 [t1] c.Sync - unpark...
2022/03/01-01:44:25.403 [t1] c.Sync - 打断状态:true
2022/03/01-01:44:25.405 [t1] c.Sync - park...

10、不推荐的方法

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

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂时)线程运行
resume()恢复线程运行

11、主线程和守护线程

默认情况下,Java进程需要等待所有守护线程运行结束,才会结束。有一种特殊的线程叫守护线程,只要其他非守护线程没有执行完,也会强制结束。
例:

log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("运行结束...");
        }, "t1");

        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        sleep(1000);
        log.debug("运行结束...");

输出

2022/03/01-01:52:57.671 [main] c.Sync - 开始运行...
2022/03/01-01:52:57.715 [t1] c.Sync - 开始运行...
2022/03/01-01:52:58.729 [main] c.Sync - 运行结束...

注意

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

12、五种状态

这是从操作系统层面来描述的
在这里插入图片描述

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

13、六种状态

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

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

本章小结

本章的重点在于掌握

  • 线程创建
  • 线程重要 api,如 start,run,sleep,join,interrupt 等
  • 线程状态
  • 应用方面
    • 异步调用:主线程执行期间,其它线程异步执行耗时操作
    • 提高效率:并行计算,缩短运算时间
    • 同步等待:join
    • 统筹规划:合理使用线程,得到最优效果
  • 原理方面
    • 线程运行流程:栈、栈帧、上下文切换、程序计数器
    • Thread 两种创建方式 的源码
  • 模式方面
    • 终止模式之两阶段终止
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值