JAVA多线程的创建和API

1. 多线程的创建

创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元则有两种方式,第一种是重写Threadrun方法,第二种是实现Runnable接口的run方法,并且将Runnable实例用作构造Thread的参数。

重写Thread类的run方法和实现Runnable接口的run方法还有一个很重要的不同,那就是Thread类的run方法是不能共享的,也就是 A 线程不能把 B 线程的run方法当作自己的执行单元,而使用Runnable接口则很容易就能实现这一点,使用同一个Runnable的实例构造不同的Thread实例。


2. 线程的生命周期

  • NEW
  • RUNNABLE
  • RUNNING
  • BLOCKED
  • TERMINATED

NEW: 创建状态

  • 使用new关键字创建的对象

RUNNABLE:可执行状态

  • 具备执行的资格,但并没有真正的执行起来而是在等待CPU的调度
  • NEW状态通过start方法进入RUNNABLE状态
  • RUNNABLE的线程只能意外终止或者进入RUNNING状态

RUNNING:运行状态

  • 可直接进入TERMINATED状态,比如调用了stop方法或者判断某个逻辑标识
  • 进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet
  • 进行某个阻塞的IO操作,比如因特网数据的读写而进入了BLOCKED状态
  • 获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态
  • 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态
  • 线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态

BLOCKED:阻塞状态

  • 直接进入TERMINATED状态,比如调用了stop方法或者意外死亡(JVM Crash)
  • 线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态
  • 线程完成了指定时间的休眠,进入到了RUNNABLE状态
  • Wait中的线程被其他线程notify/notifyAll唤醒,进入到RUNNABLE状态
  • 线程获取到了某个锁资源,进入RUNNABLE状态
  • 线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态

TERMINATED:终止状态
一个线程的最终状态

  • 线程运行正常结束,结束生命周期
  • 线程运行出错意外结束
  • JVM Crash,导致所有的线程都结束

3. Thread.run()Thread.start() 方法的区别

  1. Thread.run() 顺序执行,相当于执行 run()函数,并没有启动子线程,他只是main线程的一个方法
    Thread.start() 是启动子线程
  2. 当线程运行结束之后,再执行Thread.run()方法,会直接返回
    执行Thread.start(),会抛出java.lang.IllegalThreadStateException异常,说明线程状态是不可逆的
package com.example.demo.thread;

import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;

public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(()->{
			try {
				System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread1");
				TimeUnit.SECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		Thread thread2 = new Thread(()->{
			try {
				System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread2");
				TimeUnit.SECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println("thread1 run method start time is " + dateFormat(System.currentTimeMillis()));
		thread1.run();
		System.out.println("thread1 run method end time is " + dateFormat(System.currentTimeMillis()));

		System.out.println("thread2 run method start time is " + dateFormat(System.currentTimeMillis()));
		thread2.run();
		System.out.println("thread2 run method end time is " + dateFormat(System.currentTimeMillis()));
		TimeUnit.SECONDS.sleep(20);

		System.out.println("thread1 start method start time is " + dateFormat(System.currentTimeMillis()));
		thread1.start();

		System.out.println("thread2 start method start time is " + dateFormat(System.currentTimeMillis()));
		thread2.start();

		thread1.join();
		System.out.println("thread1 start method end time is " + dateFormat(System.currentTimeMillis()));

		thread2.join();
		System.out.println("thread2 start method end time is " + dateFormat(System.currentTimeMillis()));

		System.out.println("after thread terminal, thread1 run method start time is " + dateFormat(System.currentTimeMillis()));
		thread1.run();
		System.out.println("after thread terminal, thread1 run method end time is " + dateFormat(System.currentTimeMillis()));

		System.out.println("after thread terminal, thread2 run method start time is " + dateFormat(System.currentTimeMillis()));
		thread2.run();
		System.out.println("after thread terminal, thread2 run method end time is " + dateFormat(System.currentTimeMillis()));

		System.out.println("after thread terminal, thread1 start method start time is " + dateFormat(System.currentTimeMillis()));
		thread1.start();
		System.out.println("after thread terminal, thread1 end method start time is " + dateFormat(System.currentTimeMillis()));


	}

	private static String dateFormat(long time) {
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:mss");
		return simpleDateFormat.format(time);
	}
thread1 run method start time is 2019-12-10 18:16:49:1649
Thread name: main-thread1
thread1 run method end time is 2019-12-10 18:16:59:1659
thread2 run method start time is 2019-12-10 18:16:59:1659
Thread name: main-thread2
thread2 run method end time is 2019-12-10 18:17:09:1709
thread1 start method start time is 2019-12-10 18:17:29:1729
thread2 start method start time is 2019-12-10 18:17:29:1729
Thread name: Thread-0-thread1
Thread name: Thread-1-thread2
thread1 start method end time is 2019-12-10 18:17:39:1739
thread2 start method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 run method start time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 run method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread2 run method start time is 2019-12-10 18:17:39:1739
after thread terminal, thread2 run method end time is 2019-12-10 18:17:39:1739
after thread terminal, thread1 start method start time is 2019-12-10 18:17:39:1739
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.example.demo.thread.ThreadDemo.main(ThreadDemo.java:56)

Process finished with exit code 1

4. Thread API

4.1 线程 sleep

public static native void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException

sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃monitor锁的所有权

推荐使用TimeUnit.SECONDS.sleep(10);代替Thread.sleep(10000)

4.2 线程 yield

yield方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的CPU资源,如果CPU的资源不紧张,则会忽略这种提醒。

调用yield方法会使当前线程从 RUNNING 状态切换到 RUNNABLE 状态

yield方法只是一个提示(hint),CPU 调度器并不会担保每次都能满足 yield 提示

区别:

  • sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗
  • yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
    -sleep会使线程短暂 block,会在给定的时间内释放CPU资源
  • yield会使RUNNING状态的线程进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话)
  • sleep几乎百分之百地完成了给定时间的休眠,而yield的提示并不能一定担保。
  • 一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会

4.3 设置线程优先级

public final void setPriority(int newPriority) //设定优先级
public final int getPriority() //获取优先级

设置线程的优先级是一个 hint 操作

  • 对于 root 用户,它会 hint 操作系统你想要设置的优先级别,否则它会被忽略
  • 如果 CPU比较忙,设置优先级可能会获得更多的CPU时间片,但是闲时优先级的高低几乎不会有任何作用

线程的优先级为 1 - 10

线程默认的优先级和它的父类保持一致,一般情况下都是5,因为main线程的优先级就是5

4.4 获取线程ID

public long getId() //获取线程的唯一ID

线程的ID在整个JVM进程中都是唯一的,并且是从0开始递增

4.5 获取当前线程

public static Thread currentThread()

用于返回当前执行线程的引用

Thread.currentThread().getName();

4.6 设置线程上下文类加载器

public ClassLoader getContextClassLoader() //获取线程上下文的类加载器
public void setContextClassLoader(ClassLoader cl) // 设置该线程的类加载器

破坏了双亲委派原则

4.7 线程 interrupt

  • public void interrupt()
  • public static boolean interrupted()
  • public boolean isInterrupted()
4.7.1 interrupt

以下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的 interrupt 方法,就可以打断阻塞

  • Objectwait 方法
  • Objectwait(long) 方法
  • Objectwait(long, int) 方法
  • Threadsleep(long) 方法
  • Threadsleep(long, int) 方法
  • Threadjoin() 方法
  • Threadjoin(long) 方法
  • Threadjoin(long, int) 方法
  • InterruptibleChannelio 操作
  • Selectorwakeup 方法
  • 其他方法

上述方法会使当前线程进入阻塞状态,若另外的一个线程调用被阻塞线程的 interrupt 方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。

一旦线程在阻塞情况下被打断,都会抛出一个称为InterruptedException的异常,这个异常就像一个 signal(信号)一样通知当前线程被打断了

4.7.2 isInterrupted

isInterruptedThread的一个成员方法,它主要判断当前线程是否被中断,该方法仅仅是对 interrupt 标识的一个判断,并不会影响标识发生任何改变

4.7.3 interrupted

interrupted是一个静态方法,它也用于判断当前线程是否被中断,调用该方法会直接擦除掉线程的interrupt 标识,需要注意的是,如果当前线程被打断了,那么第一次调用 interrupted方法会返回true,并且立即擦除了 interrupt 标识,第二次包括以后的调用永远都会返回false,除非在此期间线程又一次的被打断。

4.7.4 interrupt 注意事项

如果一个线程设置了 interrupt 标识,那么接下来的可中断方法会立刻中断

4.8 线程 join

sleep一样也是一个可中断的方法,也就是说,如果有其它线程执行了对当前线程的interrupt操作,它也会捕获到中断信号,并且擦除线程的 interrupt 标识。

  • public final void join() throws InterruptedException
  • public final synchronized void join(long millis) throws InterruptedException
  • public final synchronized void join(long millis, int nanos) throws InterruptedException

join 某个线程 A,会使当前线程 B进入等待,直到线程 A结束生命周期,或者到达给定的时间,那么在此期间B线程是处于 BLOCKED 的,而不是A线程。

join 会使当前线程永远的等待下去,直到期间被另外的线程中断,或者join 的线程执行结束,当然也可以使用join的另外两个重载方法,指定毫秒数,在指定的时间到达之后,当前线程也会退出阻塞。

如果一个线程已经结束了生命周期,那么调用它的join方法的当前线程不会被阻塞,直接继续运行

package com.example.demo.thread;

import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;

public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(()->{
			try {
				System.out.println("Thread name: " + Thread.currentThread().getName() + "-thread1 start");
				TimeUnit.SECONDS.sleep(10);
				System.out.println("thread end");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println("start: " + dateFormat(System.currentTimeMillis()));
		thread1.start();
		TimeUnit.SECONDS.sleep(20);
		System.out.println("join start: " + dateFormat(System.currentTimeMillis()));
		thread1.join();
		System.out.println("join end: " + dateFormat(System.currentTimeMillis()));
	}

	private static String dateFormat(long time) {
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:mss");
		return simpleDateFormat.format(time);
	}
}
//result
start: 2019-12-11 11:50:07:5007
Thread name: Thread-0-thread1 start
thread end
join start: 2019-12-11 11:50:27:5027
join end: 2019-12-11 11:50:27:5027

4.9 关闭一个线程

4.9.1 正常关闭
  1. 线程结束生命周期正常结束
  2. 捕获中断信号关闭线程:检查线程interrupt标识,或者线程中有可中断方法时捕获中断信号
  3. 使用volatile开关控制:因为interrupt标识很有可能被擦除,或者逻辑单元中不会调用任何可中断方法,所以使用volatile修饰的开关 flag 关闭线程也是一种常用的方法
4.9.2 异常退出

在一个线程的执行单元中,是不允许抛出checked异常的,不论Thread中的run方法还是Runnable中的run方法,如果线程在运行过程中需要捕获checked异常并且判断是否还有运行下去的必要,那么此时可以将checked异常封装成unchecked异常(RuntimeException)抛出而结束线程的生命周期。

4.9.3 进程假死

假死就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来就像死了一样,但事实上他是没有死的,程序之所以出现这样的情况,绝大部分的原因就是某个线程阻塞了,或者线程出现了死锁的情况。这时可以借助jstack、jconsole、jvisualvm等工具来分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值