目录

  • 一、创建和运行线程
  • 1.使用Thread创建
  • 使用Runnable配合Thread
  • 3.FutureTask 配合 Thread
  • 二、线程运行原理
  • 1.栈与栈帧
  • 2.上下文切换
  • 三、Thread常用方法
  • 1.start() & run()
  • 2.sleep() & yield()
  • 3.join()
  • 4.interrupt()
  • 四、wait / notify
  • 1.基本使用
  • 2.wait / notify原理
  • 五、park / unpark
  • 1.基本使用
  • 2.park / unpark原理
  • 六、主线程和守护线程
  • 六、线程的状态
  • 1.五种状态
  • 2.六种状态
  • 3.Java线程状态转换


一、创建和运行线程

1.使用Thread创建

// 创建线程对象
Thread t = new Thread() {
	public void run() {
		// 要执行的任务
	}
};
// 启动线程
t.start();

使用Runnable配合Thread

  1. Thread 代表线程
  2. Runnable 实现可运行的任务
Runnable runnable = new Runnable() {
	public void run(){
		// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

Thread 与 Runnable 的关系

//Thread继承了Runnable接口,持有Runnable属性,并实现了run方法
//当调用Thread的构造方法时,如果传入Runnable,那么会赋值给targer
//线程执行时,会调用run方法,
	//1.直接继承Thread时,执行重写的run方法
	//2.传入Runnable时,执行target.run()方法

//用 Runnable 更容易与线程池等高级 API 配合
//用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
class Thread implements Runnable {
	...
	
	/* What will be run. */
    private Runnable target;

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

3.FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。

// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
	return 100;
});
// 参数1 是任务对象; 参数2 是线程名字
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

FutureTask简单分析

//FutureTask继承了Runnable,所以FutureTask对象可以作为参数传入Thread构造器
//FutureTask同时继承了Future,Future提供了获取返回值、判断任务是否完成等抽象方法
//通过FutureTask构造方法可以传入Callable实现,它的call方法不象Runnable的run方法,它可以返回值。
//在线程执行run时,就是调用了call方法(前提是有callable对象)
public class FutureTask<V> implements RunnableFuture<V> {
	//内部属性
	private Callable<V> callable;
}

public interface RunnableFuture<V> extends Runnable, Future<V>

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

二、线程运行原理

1.栈与栈帧

每个线程启动后,虚拟机就会为其分配一块栈内存。

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

2.上下文切换

以下一些原因会导致 cpu 不再执行当前的线程,转而执行另一个线程的代码。

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

三、Thread常用方法

1.start() & run()

  • start():启动一个新线程。start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException。
  • run():新线程运行时调用的方法。
public static void main(String[] args) {
	Thread t1 = new Thread("t1") {
	@Override
	public void run() {
		log.debug(Thread.currentThread().getName());
		log.debug("running ...");
		}
	};
	
	t1.run();
	//直接调用 run 是在主线程中执行了 run,没有启动新的线程
	//t1.start();
	//使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
	log.debug("do other things ...");
}

2.sleep() & yield()

sleep():让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程。

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行(需要竞争CPU)

yield():提示线程调度器让出当前线程对CPU的使用。

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器(即这个方法只是一个hint,调度器可以忽略)

3.join()

等待线程运行结束,即在当前线程调用另一个线程的join方法,表示当前线程等待另一个线程运行结束才运行。

join也可以传入等待时间作为参数,如果在这期间调用join的线程提前结束,join方法也就提前结束了;否则当前线程会等够设置的时间。

4.interrupt()

打断线程。如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置 打断标记

可以通过isInterrupted()[会清除 打断标记]和interrupted()[不会清除 打断标记]获得打断状态。

四、wait / notify

1.基本使用

下面会涉及Monitor,详见:Java并发(二)—Monitor 与上面的API不同,wait / notify它们都是线程之间进行协作的手段,都属于 Object 对象的方法必须获得此对象的锁,才能调用这几个方法

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

2.wait / notify原理

java并发任务执行完才执行下面程序_多线程

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需重新竞争

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  4. 它们状态 TIMED_WAITING

五、park / unpark

1.基本使用

// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

特点

  • park/unpark不必在加锁情况下使用
  • park/unpark 是以线程为单位来【阻塞】和【唤醒】线程(唤醒精确到某个线程),而 notify 只能随机唤醒某个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park / unpark 可以先 unpark,而 wait / notify 不能先 notify
  • 调用unpark后再次调用park,线程不会阻塞

2.park / unpark原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex

java并发任务执行完才执行下面程序_java_02

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0

java并发任务执行完才执行下面程序_thread_03

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

java并发任务执行完才执行下面程序_java_04

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0
  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    如果备用干粮耗尽,那么钻进帐篷歇息
    如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    如果这时线程还在帐篷,就唤醒让他继续前进
    如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进。因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

六、主线程和守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。

有一种特殊的线程叫做守护线程(例如:垃圾回收线程),只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

在线程开始之前,调用该线程setDaemon,设置该线程为守护线程
t1.setDaemon(true);
t1.start();

六、线程的状态

1.五种状态

操作系统 层面来描述

java并发任务执行完才执行下面程序_多线程_05

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

2.六种状态

是从 Java API 层面来描述的

//Thread.java中提供了六种状态的枚举类
public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

java并发任务执行完才执行下面程序_java_06

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

3.Java线程状态转换

1. NEW --> RUNNABLE
当调用 t.start() 方法时,由 NEW --> RUNNABLE

2. RUNNABLE <--> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
	调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
	调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
		竞争锁成功,t 线程从 WAITING --> RUNNABLE
		竞争锁失败,t 线程从 WAITING --> BLOCKED

3.RUNNABLE <--> WAITING
	当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
		注意是当前线程在t 线程对象的Monitor上等待
	t线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

4.RUNNABLE <--> WAITING
	当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
	调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
RUNNABLE

5.RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
	调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
	t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
		竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
		竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

6.RUNNABLE <--> TIMED_WAITING
	当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
		注意是当前线程在t 线程对象的监视器上等待
	当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
TIMED_WAITING --> RUNNABLE	

7.RUNNABLE <--> TIMED_WAITING
	当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
	当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

8.RUNNABLE <--> TIMED_WAITING
	当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
	调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从
TIMED_WAITING--> RUNNABLE

9.RUNNABLE <--> BLOCKED
	t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
	持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
	成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

10.RUNNABLE --> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED