第一篇:深入并发,线程相关知识全解析

一、前言

本文介绍Java线程相关知识(不包括线程同步+线程通信,这个内容在笔者的另一篇博客中介绍过了),包括:线程生命周期、线程优先级、线程礼让、后台线程、联合线程。

二、线程生命周期

2.1 引子:线程生命周期

本节阐述线程生命周期相关知识,Java支持多线程技术,除了Main函数主导一个main线程以外,可以用代码创建一系列的前台线程、后台线程(本文后面会讲),每一个线程都有自己的生命周期,线程生命周期的不同状态有不同的说法:

(1)有的说Java线程5种状态,这是因为将“等待状态Waiting+限时等待状态Timed_Waiting”作为一种状态,5种状态为:
新建状态New、可运行状态Runnable(Running+Ready)、等待状态Waiting+Timed_Waiting、阻塞状态Blocked、结束状态Terminated

(2)有的说Java线程6种状态,这是因为将将Running和Ready两种状态拆分开了,6种状态为:
新建状态New、准备状态Ready、运行状态Running、等待状态Waiting+Timed_Waiting、阻塞状态Blocked、结束状态Terminated

或者将等待状态Waiting和限时等待状态Timed_Waiting两种状态拆开,6种状态为:
新建状态New、可运行状态Runnable(Running+Ready)、等待状态Waiting、计时等待状态Timed_Waiting、阻塞状态Blocked、结束状态Terminated

(3)有的说Java线程7种状态,这是因为将将Running和Ready两种状态拆分开、等待状态Waiting和限时等待状态Timed_Waiting两种状态拆开,7种状态为:
新建状态New、准备状态Ready、运行状态Running、 等待状态Waiting、计时等待状态Timed_Waiting、阻塞状态Blocked、结束状态Terminated

不管采用哪种说法,Java线程状态以下几种,新建状态New、可运行状态Runnable(Running+Ready)、等待状态(等待状态Waiting+限时等待状态Timed_Waiting)、阻塞状态Blocked、结束状态Terminated

本文采用6种状态的说法,下面2.3 会具体阐述。

2.2 用一段代码来演示一个完整的生命周期

代码1:

package mypackage;
//  线程的生命周期,演示线程的六种状态: NEW 新建状态   RUNNABLE 可运行状态(ready就绪状态+running运行状态) TIMED_WAITING 计时等待状态
//   WAITING 等待状态  BLOCKED 阻塞状态  TERMINATED  终止状态(进入终止状态后只能结束,无法再返回回来)
class Block {   //共享资源类
	public boolean waitStatus = true;
	public void waitOp() throws InterruptedException {
		synchronized (this) {
			System.out.println(Thread.currentThread().getName() + "  进入wait");
			wait();
			// waitThread被notifyThread唤醒
			System.out.println(Thread.currentThread().getName() + "  next to Block.wait(), in the loop");
		}
	}
	public void notifyOp() {
		synchronized (this) {
			this.waitStatus = false; // 修改标志位 等待状态waitStatus=false
			notifyAll(); // 唤醒WaitRunnable
			System.out.println(Thread.currentThread().getName() + "  向正在wait当前对象的线程发出notify");
		}
	}
}
class WaitThread implements Runnable {
	private Block block;
	public WaitThread(Block block) {
		super();
		this.block = block;
	}
	@Override
	public void run() {  
		//该方法中测试了 sleep(毫秒数)进入计时等待状态和wait()进入等待状态
		// 前者只能等待2秒后自动唤醒   后者由notifyThread使用notify()/notifyAll()唤醒
		try {
			System.out.println(Thread.currentThread().getName() + "  被启动,开始执行...");
			// Thread.sleep(毫秒数) 进入计时等待状态 线程计时等待状态中,线程依然存活,线程isAlive() 仍然为true
			System.out.println(Thread.currentThread().getName() + "  开始sleep 2s");
			Thread.sleep(2000);
			System.out.println(Thread.currentThread().getName() + "  从sleep中醒来");
			// 从sleep中醒来后,进入wait状态
			while (block.waitStatus) {
				block.waitOp(); // 进入等待状态,线程在等待状态中,线程依然存活,线程isAlive()
								// 仍然为true,等待状态只有一个出口,notify()/notifyAll()
				// waitThread被notifyThread唤醒
				System.out.println(Thread.currentThread().getName() + "  next to WaitRunnable.waitOp(), in the loop");
			}
			System.out.println(Thread.currentThread().getName() + "  out of wait loop");
			System.out.println(Thread.currentThread().getName() + "  任务执行结束,即将终止...");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
class NotifyThread implements Runnable {
	private Block block;
	public NotifyThread(Block block) {
		super();
		this.block = block;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "  被启动,开始执行...");
		block.notifyOp();
		synchronized (block) {
			try {
				System.out.println(Thread.currentThread().getName() + "  开始sleep 6s,并且不会释放当前对象的锁");
				// 进入计时等待状态有两种方式 wait(毫秒数) sleep(毫秒数) 两者不同在于
				// wait(毫秒数)释放锁 ,其他线程(本程序中另一个线程)获得锁并运行
				// sleep(毫秒数)不释放锁,其他线程(本程序中另一个线程)不可以获得锁并运行
				// 现在notifyThread sleep(6000) 不释放锁,所以waitThread也不能运行
				// ,只能等到6秒后notifyThread醒来
				Thread.sleep(6000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		// notifyThread醒来了,打印一下
		System.out.println(Thread.currentThread().getName() + "  从sleep中醒来");
	}
}
public class Test {
	private static final String IN_THE_STATE = "当前的状态为:";
	private static final String ISALIVE_RETRUN = "isAlive()返回为:";
	public static void printStatus(Thread thread) {
		System.out.println(thread.getName() + "  " + IN_THE_STATE + thread.getState().toString() + "  " + ISALIVE_RETRUN
				+ thread.isAlive());
	}
	public static void main(String[] args) throws InterruptedException {
		// 该对象将作为后面加锁的对象
		Block block = new Block();
		// new Thread() 进入新建状态
		Thread waitThread = new Thread(new WaitThread(block));
		Thread notifyThread = new Thread(new NotifyThread(block));
		printStatus(waitThread); // 打印当前状态,当前状态为新建状态 线程尚未启动,isAlive()为false
		printStatus(notifyThread);// 打印当前状态,当前状态为新建状态 线程尚未启动,isAlive()为false
		// .start() 进入可运行状态 (包括就绪状态和运行状态)
		// 就绪状态进入运行状态是CPU调度,程序员无法控制,所本程序中对这两种状态不做区分,统一为可运行状态
		waitThread.start();
		printStatus(waitThread); // 打印当前状态,当前状态为可运行状态 线程启动,isAlive()为true
		Thread.sleep(1000);
		// TIMED_WAITING
		printStatus(waitThread); // 打印当前状态,当前状态为计时等待状态
		Thread.sleep(2000);
		// WAITING
		printStatus(waitThread); // 打印当前状态,当前状态为wait()等待状态,等待被notifyThread()唤醒才能继续下去
		Thread.sleep(1000); // 主线程main线程等待1s
		notifyThread.start(); // 启动notify线程,去唤醒处于等待状态的waitThread
		printStatus(notifyThread); // 打印notifyThread当前状态,当前状态为可运行状态
									// 线程启动,isAlive()为true
		Thread.sleep(1000); // 主线程main线程等待1s
		// BLOCKED or TERMINATED
		// TERMINATED:运行到此处时,如果notifyThread执行完notify操作后,调度器立马切换至thread的情况下,thread会先行终止,调度器再调度notifyThread
		printStatus(waitThread); // 现在处于notifyThread sleep(6000) 睡眠6秒过程中  我们打印一下被notifyThread唤醒的waitThread处于什么状态
									// waitThread为Block状态,islive为true
		//这是因为waitThread虽然被notifyThread唤醒,从等待状态中出来,但是因为notifyThread现在一直占用同步锁,waitThread无法获得同步锁,所以处于阻塞状态
		//等待状态和阻塞状态区别:Java中,每个对象都有两个池,锁池和等待池,
		//阻塞状态两种:I/O阻塞(本程序不涉及,略)和获取同步锁失败而阻塞(本程序当前情况),线程对象处于锁池,锁池的中线程对象获取到同步锁之后就可以进入可运行状态运行,即阻塞状态的线程获取同步锁成功后就可以运行
		//等待和计时等待状态:sleep(毫秒数)只能等待自动唤醒,wait(毫秒数)可以等待自动唤醒或notify()/notifyAll()唤醒,wait()只能notify()/notifyAll()唤醒,三种等待都是处于等待池中
		//wait()和wait(毫秒数)释放同步锁,其间不占用同步锁,sleep(毫秒数)占用同步锁,其间不释放同步锁		
		printStatus(notifyThread); // 现在处于notifyThread sleep(6000) 睡眠6秒过程中
									// notifyThread 为计时等待状态,islive为true
		Thread.sleep(6000); // 主线程睡眠6s 让两个子程序有足够的时间进入终止状态
		// 终止状态 主线程给了6s 足够进入终止状态了
		printStatus(waitThread); // waitThread 进入终止状态,isAlive()为false
		printStatus(notifyThread); // notifyThread 进入终止状态,isAlive()为false
		// 注意:线程一旦进入终止状态,只能结束,无法再返回来
	}
}

输出1:

Thread-0  当前的状态为:NEW  isAlive()返回为:false
Thread-1  当前的状态为:NEW  isAlive()返回为:false
Thread-0  当前的状态为:RUNNABLE  isAlive()返回为:true
Thread-0  被启动,开始执行...
Thread-0  开始sleep 2s
Thread-0  当前的状态为:TIMED_WAITING  isAlive()返回为:true
Thread-0  从sleep中醒来
Thread-0  进入wait
Thread-0  当前的状态为:WAITING  isAlive()返回为:true
Thread-1  当前的状态为:RUNNABLE  isAlive()返回为:true
Thread-1  被启动,开始执行...
Thread-1  向正在wait当前对象的线程发出notify
Thread-1  开始sleep 6s,并且不会释放当前对象的锁
Thread-0  当前的状态为:BLOCKED  isAlive()返回为:true
Thread-1  当前的状态为:TIMED_WAITING  isAlive()返回为:true
Thread-1  从sleep中醒来
Thread-0  next to Block.wait(), in the loop
Thread-0  next to WaitRunnable.waitOp(), in the loop
Thread-0  out of wait loop
Thread-0  任务执行结束,即将终止...
Thread-0  当前的状态为:TERMINATED  isAlive()返回为:false
Thread-1  当前的状态为:TERMINATED  isAlive()返回为:false

小结:本程序(已经注释的比较清楚了,这里不再赘余)使用synchronized+标志位waitStatus+wait()+notify()/notifyAll(),程序中的两个线程waitThread和notifyThread,既使用到了线程同步+线程通信,又监听了六个状态,帮助读者很好的理解Java线程生命周期六个状态。

2.3 线程生命周期的相关问题

问题(1):线程有几种状态?状态之间如何转换?

回答(1):没有绝对的答案,本文采用Java官方的解释,6种状态,分别是 :NEW 新建状态 RUNNABLE 可运行状态(包括ready就绪状态+running运行状态) TIMED_WAITING 计时等待状态
WAITING 等待状态 BLOCKED 阻塞状态 TERMINATED 终止状态(进入终止状态后只能结束,无法再返回回来)

关于状态之间的转换图,我认为这张图最能符合标准:

补充:因为可运行状态Runnable包括Ready就绪状态和Running运行状态,但是就绪状态进入运行状态是CPU调度,程序员无法控制,所本文中对这两种状态不做区分,统一为可运行状态,两者中转换如图:

问题(2):六种状态isAlive()返回值?

回答(2):新建状态和终止状态isAlive()为false,表示线程此时不存活;其他四种状态isAlive()为true,表示线程此时为存活。

问题(3):进入终止状态terminate还可以再回到可运行状态吗?
回答(3):不可以,进入终止状态线程只能死亡,不可以再回到可运行状态。

问题(4):等待状态(包括计时等待)和阻塞状态的区别?wait()、wait(毫秒数)和sleep(毫秒数)区别?
回答(4):等待状态和阻塞状态区别:Java中,每个对象都有两个池,锁池和等待池。

区分好锁池和等待池
1、举例子(可运行状态 - 阻塞状态 - 等待状态):互斥锁是女神,只有一个,锁池里有若干备胎,锁池中的任何一个通过synchronized或lock抢占和女神在一起的一段时间;等待池里的更卑微,是一大堆还没资格成为备胎的舔狗,被wait()/wait(毫秒数)从锁池打入到等待池,只能卑微的等待,必须使用 notify()/notifyAll() 或时间到达时候,才能重新从等待池到锁池中,再在锁池中synchronized/lock争夺到同步锁才有资格和女神共处。
2、从源码角度上看:阻塞状态处于双链表同步队列中,等待队列的处于单链表等待队列中。
lock()和unlock()就是操作同步队列:lock()将线程放到同步队列(即AQS队列)中,开始执行同步代码块内容,unlock()将存放线程的节点从同步队列中拿出来,表示这个线程工作完成。
await()和signal()就是操作等待队列:await()将线程放到等待队列里面,signal()从等待队列中拿出存放线程的节点。
3、从进入状态角度看:wait()方法让线程释放同步锁,从可运行状态变为等待状态,进入等待池,等待notify()唤醒进入锁池;执行完同步代码块让线程释放同步锁,从可运行状态变为阻塞状态,进入锁池,开始下一次竞争锁。即锁池和等待池是独立的两个东西,整个过程是 wait() 阻塞进入等待池并不是同步代码块的结束,只是当前线程释放锁,但是同步代码块并没有结束,一旦被唤醒,还是要从阻塞的地方开始执行,执行未完成的同步代码块,知道同步代码块结束,才释放同步锁,进入锁池。

阻塞状态两种:
I/O阻塞(本程序不涉及,略)和获取同步锁失败而阻塞(本程序当前情况),线程对象处于锁池,锁池的中线程对象获取到同步锁之后就可以进入可运行状态运行,即阻塞状态的线程获取同步锁成功后就可以运行;

等待和计时等待状态:

进入等待状态(包括计时等待)方式有三种(sleep(毫秒数)、wait()、wait(毫秒数))
sleep(毫秒数)只能等待自动唤醒,wait(毫秒数)可以等待自动唤醒或notify()/notifyAll()唤醒,wait()只能notify()/notifyAll()唤醒,三种等待都是处于等待池中。

另外,wait()和wait(毫秒数)释放同步锁,其间不占用同步锁,sleep(毫秒数)占用同步锁,其间不释放同步锁

方法名线程状态唤醒状态期间
wait()进入等待状态只能notify()/notifyAll()唤醒释放同步锁,其间不占用同步锁
wait(毫秒数)进入计时等待状态可以等待自动唤醒或notify()/notifyAll()唤醒释放同步锁,其间不占用同步锁
sleep(毫秒数)进入计时等待状态只能等待自动唤醒占用同步锁,其间不释放同步锁

三、线程优先级

Java中提供了操作线程优先级的方法setter-getter, int getPriority() :返回线程的优先级。 void setPriority(int newPriority) : 更改线程的优先级。 另外,提供了三个表现线程优先级的常量,

MAX_PRIORITY=10,最高优先级

MIN_PRIORITY=1,最低优先级

NORM_PRIORITY=5,默认优先级,

线程优先级有1~ 10,10种,程序员也可以直接使用数字1~10设置线程优先级。线程优先级代码实现且见代码1:

代码——线程优先级:

package mypackage;

public class SimplePriorities implements Runnable {
	private int countDown = 5;

	public SimplePriorities(int priority) {
		Thread.currentThread().setPriority(priority);
	}

	public String toString() {
		return Thread.currentThread() + ": " + countDown;
	}

	@Override
	public void run() {

		while (true) {
			System.out.println(this);
			if (--countDown == 0) {
				return;
			}
		}
	}

	public static void main(String[] args) {
		new Thread(new SimplePriorities(Thread.MIN_PRIORITY)).start();
		new Thread(new SimplePriorities(Thread.MAX_PRIORITY)).start();

	}

}

输出:

Thread[Thread-1,10,main]: 5
Thread[Thread-1,10,main]: 4
Thread[Thread-1,10,main]: 3
Thread[Thread-0,1,main]: 5
Thread[Thread-1,10,main]: 2
Thread[Thread-0,1,main]: 4
Thread[Thread-1,10,main]: 1
Thread[Thread-0,1,main]: 3
Thread[Thread-0,1,main]: 2
Thread[Thread-0,1,main]: 1

小结:我们看到,程序开始的时候,即使在客户端低优先级的线程Thread-0先创建和执行,但是输出结果中,高优先级的线程Thread-1可以获得更多执行机会(前面5个,Thread-1打印了4个,Thread-0打印1个,后面5个没有意义,因为Thread-1已经快打印完了,每个线程countDown总数只有5)(注意:打印结果不同且当前线程数和countDown数字较小,特例没有意义)

注意1:值得注意的是,线程优先级并不是指线程执行的先后顺序,而是线程被执行的概率权重。事实上,除非程序员使用标志位做线程通信,否则Java并没有提供任何线程执行先后顺序的机制,哪个线程先执行只取决于CPU调度。
注意2:此外,不同的操作系统支持的线程优先级不同的,建议使用上述三个优先级MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY,不要自定义。

四、线程礼让

线程礼让是以线程优先级为基础的(本文先介绍线程优先级,再介绍线程礼让),线程礼让yield方法只会给相同优先级或者更高优先级的线程运行的机会. 且看如下代码:

代码——线程礼让(在线程优先级代码的基础上添加):

package mypackage_线程礼让;
public class SimplePriorities implements Runnable {
	private int countDown = 5;
	public SimplePriorities(int priority) {
		Thread.currentThread().setPriority(priority);
	}
	public String toString() {
		return Thread.currentThread() + ": " + countDown;
	}
	@Override
	public void run() {
		while (true) {
			Thread.yield();
			System.out.println(this);
			if (--countDown == 0) {
				return;
			}
		}
	}
	public static void main(String[] args) {
		new Thread(new SimplePriorities(Thread.MIN_PRIORITY)).start();
		new Thread(new SimplePriorities(Thread.MAX_PRIORITY)).start();
	}
}

输出:

Thread[Thread-1,10,main]: 5
Thread[Thread-0,1,main]: 5
Thread[Thread-1,10,main]: 4
Thread[Thread-0,1,main]: 4
Thread[Thread-1,10,main]: 3
Thread[Thread-1,10,main]: 2
Thread[Thread-1,10,main]: 1
Thread[Thread-0,1,main]: 3
Thread[Thread-0,1,main]: 2
Thread[Thread-0,1,main]: 1

小结:在run()方法中加上Thread.yield()之后,可以更大程度上保证高优先级的线程打败低优先级的线程,因为yield()只能将线程礼让给同等优先级和更高优先级的线程,本程序中Thread-0会礼让给Thread-1,但是Thread-1不会礼让给Thread-0,所以Thread.yield()本身更大程度上保证高优先级的线程打败低优先级的线程(注意:不绝对,况且的这里的countDown计数器和客户端线程数都很少)。

附:线程礼让yield()和线程休眠sleep()的相同点和不同点
sleep方法和yield方法的区别:
1):相同点:都能使当前处于运行状态的线程放弃CPU,把运行的机会给其他线程.
2):不同点——转让运行机会的条件不同:sleep方法会给其他线程运行机会,但是不考虑其他线程的优先级,yield方法只会给相同优先级或者更高优先级的线程运行的机会.
3):不同点——转让后进入的状态不同:调用sleep方法后,线程进入计时等待状态,调用yield方法后,线程进入就绪状态.
4):不同点——两方法开发中用途不同:sleep()方法开发中更多的用于模拟延迟,让多线程并发访问同一个资源的错误效果更明显.;yield()方法开发中很少会使用到该方法,该方法主要用于调试或测试,它可能有助于因多线程竞争条件下的错误重现现象。

五、后台线程

5.1 引子:后台线程

Java中,前台线程创建的线程默认是前台线程(可以通过setDaenon(true)方法设置为后台线程),后台线程创建的线程默认是后台线程。由于main线程是一个前台线程,所以我们新建的线程默认都是前台线程,本节阐述Java线程另一种线程——后台线程。

注意1:设置后台线程:thread.setDaemon(true),该方法必须在start方法调用前,否则出现IllegalThreadStateException异常。
注意2:前台线程停止,后台线程也停止,即使是finally块的内容也不执行了,且见本节代码演示。
注意3:main线程是前台线程

5.2 前台线程与后台线程

代码1——前台线程停止,后台线程也停止:

package mypackage;
public class Test {
	public static void main(String[] args) {
		Thread daemonThread = new Thread(new DaemonThread());
		daemonThread.setDaemon(true); // thread.setDaemon(true),该方法必须在start方法调用前,否则出现IllegalThreadStateException异常。
		daemonThread.start();
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(500);
				// mainThread中每次休眠比后台线程少,这个mainThread就比后台线程先结束,从而证明前台线程结束后后台线程也会结束
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("This is mainThread");
		}
	}
}
class DaemonThread implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("This is DaemonThread");
		}
	}
}

输出1:

This is mainThread
This is DaemonThread
This is mainThread
This is mainThread
This is DaemonThread
This is mainThread
This is mainThread

小结1:该程序(即代码1)充分说明了,前台线程停止,那么它创建的后台线程也停止,但是如果是后台线程中finally块里面的内容,会任何时候都得到执行吗?且看代码2。

代码2——前台线程停止,后台线程也停止,即使是finally块的内容也不执行了:

package mypackage2;
public class Test {
	public static void main(String[] args) {
		Thread daemonThread = new Thread(new DaemonThread());
		daemonThread.setDaemon(true); // thread.setDaemon(true),该方法必须在start方法调用前,否则出现IllegalThreadStateException异常。
		daemonThread.start();
	}
}
class DaemonThread implements Runnable {
	@Override
	public void run() {
		try {
			System.out.println("This is DaemonThread");
		} finally {
			System.out.println("This is finally Blocks");
		}
	}
}

输出2:

This is DaemonThread

小结2:该程序(代码2)有三种输出情况:

第一,为空,什么都不输出,这是问题在后台线程尚未打印This is DaemonThread,主线程就结束了;

第二,输出为This is DaemonThread\n This is finally Blocks ,这是因为后台线程完全打印完后,主线程才结束;

第三,输出为This is DaemonThread,这是因为后台线程尚未执行完,finally中的代码未执行完,主线程结束。

我们重点谈论第三种输出,这一个输出可以充分说明,后台线程中即使是finally块中的语句,也不一定会得到执行。这与我们的认知的是矛盾的,通常来说,finally块的语句一定会得到执行,所以开发的时候一般将jdbc连接的close或io的close放在finally块里面。

原因简单阐述,这是因为main()退出时,JVM就会立即关闭所有的后台进场,并不会有任何你希望出现的确认形式。

解决方法,只能让程序员自己来解决这个问题,写程序的时候,要慎用后台线程,如代码2,若将main()中setDaemon()注释掉,就永远可以看到finally子句的执行,所有要慎用后台线程,否则finally子句将不再安全。

5.3 后台线程:小结

后台线程最重要的点就是生命周期跟随创建它的前台线程,前台线程死亡,后台线程立刻死亡,即使是finally中的子句也不一定会执行。

六、联合线程

6.1 引子:联合线程

定义:Java中,联合线程使用join()方法实现,线程的join方法表示一个线程等待另一个线程完成后才执行。join方法被调用之后,线程对象处于阻塞状态。 有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程,且见本节代码。

这里会被问到一个面试问题,如何让线程有序执行?回答为:join、线程通信(synchronized+标志位+wait()+notify()/notifyAll() 或者 lock+标志位+await()+signal()/signalAll() )。

6.2 联合线程代码演示

代码1:

package mypackage;
public class Test {
	public static void main(String[] args) throws InterruptedException {
		Thread joinThread = new Thread(new JoinThread());
		for (int i = 0; i < 5; i++) {
			System.out.println("main :" + i);
			if (i == 1) {
				joinThread.start();
			}
			if (i == 3) {
				joinThread.join(); // 强制运行joinThread,main线程被设置为阻塞状态
			}
		}
	}
}
class JoinThread implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println("JoinThread: " + i);
		}
	}
}

输出1:

main :0
main :1
main :2
main :3
JoinThread: 0
JoinThread: 1
JoinThread: 2
JoinThread: 3
JoinThread: 4
main :4

小结:join()方法就用阻塞当前正在运行的线程(代码1中为main线程),运行新建加入的线程(代码1中的JoinThread),等到JoinThread运行完之后再运行mian线程。可以更加简单的理解,就是在main线程中嵌入一个JoinThread线程。

6.3 联合线程:小结

联合线程就是在某线程(代码1中main线程)中嵌入一个新线程(代码1中JoinThread),然后运行的时候当然是按照嵌入的运行。

七、面试金手指

7.1 线程生命周期和六种状态

在这里插入图片描述

补充:因为可运行状态Runnable包括Ready就绪状态和Running运行状态,但是就绪状态进入运行状态是CPU调度,程序员无法控制,所本文中对这两种状态不做区分,统一为可运行状态,两者中转换如图:
在这里插入图片描述

(1)有的说Java线程5种状态,这是因为将“等待状态Waiting+限时等待状态Timed_Waiting”作为一种状态,5种状态为:
新建状态New、可运行状态Runnable(Running+Ready)、等待状态Waiting+Timed_Waiting、阻塞状态Blocked、结束状态Terminated

(2)有的说Java线程6种状态,这是因为将将Running和Ready两种状态拆分开了,6种状态为:
新建状态New、准备状态Ready、运行状态Running、等待状态Waiting+Timed_Waiting、阻塞状态Blocked、结束状态Terminated

或者将等待状态Waiting和限时等待状态Timed_Waiting两种状态拆开,6种状态为:
新建状态New、可运行状态Runnable(Running+Ready)、等待状态Waiting、计时等待状态Timed_Waiting、阻塞状态Blocked、结束状态Terminated

(3)有的说Java线程7种状态,这是因为将将Running和Ready两种状态拆分开、等待状态Waiting和限时等待状态Timed_Waiting两种状态拆开,7种状态为:
新建状态New、准备状态Ready、运行状态Running、 等待状态Waiting、计时等待状态Timed_Waiting、阻塞状态Blocked、结束状态Terminated

不管采用哪种说法,Java线程状态以下几种,新建状态New、可运行状态Runnable(Running+Ready)、等待状态(等待状态Waiting+限时等待状态Timed_Waiting)、阻塞状态Blocked、结束状态Terminated

问题(2):六种状态isAlive()返回值?

回答(2):新建状态和终止状态isAlive()为false,表示线程此时不存活;其他四种状态isAlive()为true,表示线程此时为存活。

问题(3):进入终止状态terminate还可以再回到可运行状态吗?

回答(3):不可以,进入终止状态线程只能死亡,不可以再回到可运行状态。

问题(4):等待状态(包括计时等待)和阻塞状态的区别?wait()、wait(毫秒数)和sleep(毫秒数)区别?

回答(4):等待状态和阻塞状态区别:Java中,每个对象都有两个池,锁池和等待池,
阻塞状态两种(锁池 synchronized和lock 线程同步,同步锁):
I/O阻塞(本程序不涉及,略)和获取同步锁失败而阻塞(本程序当前情况),线程对象处于锁池,锁池的中线程对象获取到同步锁之后就可以进入可运行状态运行,即阻塞状态的线程获取同步锁成功后就可以运行;
等待和计时等待状态( 等待池 线程休眠和线程通信):
进入等待状态(包括计时等待)方式有三种(sleep(毫秒数)、wait()、wait(毫秒数))
sleep(毫秒数)只能等待自动唤醒,wait(毫秒数)可以等待自动唤醒或notify()/notifyAll()唤醒,wait()只能notify()/notifyAll()唤醒,三种等待都是处于等待池中。
另外,wait()和wait(毫秒数)释放同步锁,其间不占用同步锁,sleep(毫秒数)占用同步锁,其间不释放同步锁

方法名线程状态唤醒状态期间
wait()进入等待状态只能notify()/notifyAll()唤醒释放同步锁,其间不占用同步锁
wait(毫秒数)进入计时等待状态可以等待自动唤醒或notify()/notifyAll()唤醒释放同步锁,其间不占用同步锁
sleep(毫秒数)进入计时等待状态只能等待自动唤醒占用同步锁,其间不释放同步锁

7.2 线程优先级两个方法和三个常量

Java中提供了操作线程优先级的方法setter-getter,

int getPriority() :返回线程的优先级;
void setPriority(int newPriority) : 更改线程的优先级。

另外,提供了三个表现线程优先级的常量:
MAX_PRIORITY=10,最高优先级
MIN_PRIORITY=1,最低优先级
NORM_PRIORITY=5,默认优先级,

线程优先级有1~ 10,10种,程序员也可以直接使用数字1~10设置线程优先级。

注意1:线程优先级并不是指线程执行的先后顺序,而是线程被执行的概率权重。事实上,除非程序员使用标志位做线程通信,否则Java并没有提供任何线程执行先后顺序的机制,哪个线程先执行只取决于CPU调度。
注意2:不同的操作系统支持的线程优先级不同的,建议使用上述三个优先级MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY,不要自定义。

7.3 线程礼让

线程礼让yield()和线程休眠sleep()的相同点和不同点

  1. 相同点:都能使当前处于运行状态的线程放弃CPU,把运行的机会给其他线程.
  2. 不同点——转让运行机会的条件不同:sleep方法会给其他线程运行机会,但是不考虑其他线程的优先级,yield方法只会给相同优先级或者更高优先级的线程运行的机会.
  3. 不同点——转让后进入的状态不同:调用sleep方法后,线程进入计时等待状态,调用yield方法后,线程进入就绪状态.
  4. 不同点——两方法开发中用途不同:sleep()方法开发中更多的用于模拟延迟,让多线程并发访问同一个资源的错误效果更明显.;yield()方法开发中很少会使用到该方法,该方法主要用于调试或测试,它可能有助于因多线程竞争条件下的错误重现现象。

7.4 后台进程

注意1:main线程是前台线程默认新建的的线程是前台线程,要想新建后台线程,需要设置后台线程:thread.setDaemon(true),该方法必须在start方法调用前,否则出现IllegalThreadStateException异常。

注意2:后台线程的最重要的特点:前台线程停止,后台线程也停止,即使是finally块的内容也不执行了。

问题:如何处理后台线程的中finally子句必须执行?
原因:这是因为main()退出时,JVM就会立即关闭所有的后台进场,
解决:只能让程序员自己来解决这个问题,写程序的时候,要慎用后台线程,如代码2,若将main()中setDaemon()注释掉,就永远可以看到finally子句的执行,所有要慎用后台线程,否则finally子句将不再安全。

问题:为什么我们调用start()方法时会执行run()方法,而不是直接调用run()方法?
回答:新建一个线程,调用start()方法,会让一个线程进入就绪状态,当分配到时间片后,start()会进行相应的准备工作,然后在启动run()方法内的内容;而直接调用run()方法,意味着会把run方法当做一个main方法进行执行,并不会在某个线程中执行它,而是在主线程中进行执行。

7.5 联合线程

join()方法就用阻塞当前正在运行的线程(代码1中为main线程),运行新建加入的线程(代码1中的JoinThread),等到JoinThread运行完之后再运行mian线程。可以更加简单的理解,就是在main线程中嵌入一个JoinThread线程。

main :0
main :1
main :2
main :3
JoinThread: 0
JoinThread: 1
JoinThread: 2
JoinThread: 3
JoinThread: 4
main :4

小结:线程联合两点:
第一个,暂停当前线程,上面是main线程,插入新线程,上面是joinThread线程;
第二个,插入的线程执行完毕后,再执行被插入的线程(这里是joinThread线程执行完毕后,再执行main线程)。

八、尾声

本文介绍Java线程相关知识,包括:线程生命周期、线程优先级、线程礼让、后台线程、联合线程,基本涵盖Java多线程部分的各个常用知识点,希望对初学者学习者提供帮助。
天天打码,天天进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

祖母绿宝石

打赏一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值