系统掌握并发编程系列(二)详解Thread类的主要属性和方法
前面系列文章,我们详细分析了线程创建、运行、停止、返回的过程,点击上面链接快速查看。本文正式进入多线程与并发协同的相关内容的学习。
多线程
从计算机操作系统的发展历程来看,从早期的从头到尾执行一个能直接访问机器的所有资源单一的程序,发展到允许多个由操作系统分配资源,在进程中运独立运行的程序,程序充分利用资源、各个程序公平访问系统资源、程序开发便利性一直是三大的关注点。三大关注点同样适用在线程的发展上,线程有时被称为轻量级进程,大多数现代操作系统将线程(而不是进程)视为系统调度的基本单位。
通过前文我们已经知道,线程主要功能是完成一个特定的任务,其过程可能因为某些因素进入等待状态(如等待IO)而让出CPU,此时允许其他线程执行其他任务有助于提高CPU等系统资源利用率。允许其他线程同时运行的时候,就要考虑多个线程之间公平访问系统资源了,防止单个线程从头到尾独占资源执行,所以这又涉及多个线程之间的相互协作了。
并发协同
关于并发协同的概念,不管是线程互斥,还是线程同步,或者是线程通信,其本质还是描述多个线程同时运行,协作完成一个或者多个任务。就好比团队协作,不同的人做不同的事,但目标是一致的,就是完成老板下达的任务。如果用团队互斥,或者团队同步,或者团队通信去理解团队协作,视乎有些难理解,“协同”本身已经涵盖了“互斥”、“同步”、“通信”等动作,所以我认为用“并发协同”更容易且全面理解多线程之间的并发协作。理解了多线程的目的和并发协同的概念之后,那可能就会问多线程之间是如何协同工作的?jdk中有哪些并发协同的工具了?我们接着往下看。
传统并发协同方式
我们先来看一个经典的题目:如何控制2个线程依次输出1 2 3 4 5 6…?
题目分析:
步骤1:创建2个线程t1,t2。
步骤2:t1运行输出1,t1停止输出(等待t2输出完,进入等待状态)。
步骤3:t2运行输出2,t2停止输出(等待t1输出完,进入等待状态)。
步骤4:循环执行步骤2和步骤3,即t1和t2依次执行”运行<->等待“动作。
想想通过前面的几篇文章,用已掌握的哪些知识可以解决这个问题。在系统掌握并发编程系列(二)详解Thread类的主要属性和方法这篇文章中,详细分析了Thread类的常用方法,其中包含线程启动与执行控制、控制线程运行状态的两类方法。
并发协同方法选用分析:
1、运行线程用run()、start()这个没什么问题。
2、让线程进入等待状态,sleep(long millis)方法、继承Object类的wait()方法可以选择,但是sleep()方法需要指定睡眠时间,我们没办法知道t1(t2)执行完成后需要等t2(t1)多少时间,所以只有wait()方法可以选择。
3、刚好同样继承Object类的的notify()/notifyAll()方法,能将处于等待状态的线程唤醒。那问题又来了,t1(t2)执行完进入等待状态后谁来通知t2(t1)执行?
4、我们是否可以设置一个公共的变量用来标识是否轮到自己执行,线程t1(t2)输出前先判断是否到自己执行,没到的时候先进入等待状态,到了就输出并设置该变量的值为t2(t1)执行,然后唤醒t2(t1)执行。该变量可以初始一个值,让t1或t2先执行。
基于上面分析,我们用代码来实现:
public class ConcurrentCollaboration {
//输出数字
private static int num = 1;
//公共变量,线程执行标识
private static boolean thread1Run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//判断是否轮到自己执行
while(!thread1Run) {
//没到就进入等待
t1.wait();
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = false;
//执行完输出,通知t2执行
t1.notify();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//判断是否轮到自己执行
while(thread1Run) {
//没到就进入等待
t2.wait();
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = true;
//执行完输出,通知t2执行
t2.notify();
}
}
},"t2");
t1.start();
t2.start();
}
}
上面的代码很明显有一个编译错误问题,执行公共变量判断和输出数字是线程的任务逻辑,前面我们已经知道任务逻辑的实现与线程的创建执行时机是分开的,在任务逻辑里面线程对象可能还没有创建,也就是不能用t1.wait(),t1.notify()的方式。那么谁来调用wait()和notify()方法?我们接着往下看。
synchronized与对象监视器锁
回过头来看看wait()和notify()方法的官方注释,分析下相关使用说明。
/**
* Causes the current thread to wait until it is awakened, typically
* by being <em>notified</em> or <em>interrupted</em>.
* <p>
* In all respects, this method behaves as if {@code wait(0L, 0)}
* had been called. See the specification of the {@link #wait(long, int)} method
* for details.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor
* @throws InterruptedException if any thread interrupted the current thread before or
* while the current thread was waiting. The <em>interrupted status</em> of the
* current thread is cleared when this exception is thrown.
* @see #notify()
* @see #notifyAll()
* @see #wait(long)
* @see #wait(long, int)
*/
public final void wait() throws InterruptedException {
wait(0L);
}
调用wait()方法迫使当前线程进入等待状态直到被唤醒或中断,如果当前线程不是对象监视器(什么是象监视器?可以理解成一把锁)的拥有者将会抛出非法的锁状态异常。如果当前正处于等待状态的线程被中断,将会抛出中断异常。内部调用wait(long, int)方法实现,顺着这个看看wait(long, int)方法的说明。
/**
* Causes the current thread to wait until it is awakened, typically
* by being <em>notified</em> or <em>interrupted</em>, or until a
* certain amount of real time has elapsed.
* <p>
* The current thread must own this object's monitor lock. See the
* {@link #notify notify} method for a description of the ways in which
* a thread can become the owner of a monitor lock.
* <p>
...
* @apiNote
* The recommended approach to waiting is to check the condition being awaited in
* a {@code while} loop around the call to {@code wait}, as shown in the example
* below. Among other things, this approach avoids problems that can be caused
* by spurious wakeups.
*
* <pre>{@code
* synchronized (obj) {
* while (<condition does not hold> and <timeout not exceeded>) {
* long timeoutMillis = ... ; // recompute timeout values
* int nanos = ... ;
* obj.wait(timeoutMillis, nanos);
* }
* ... // Perform action appropriate to condition or timeout
* }
* }</pre>
...
*/
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
timeoutMillis++;
}
wait(timeoutMillis);
}
注释中提到,调用wait(long, int)方法迫使当前线程进入等待状态直到被唤醒或中断或者睡眠时间用完,当前线程必须拥有对象监视器锁,并给出了正确调用wait()方法的示例。如何让一个线程成为对象监视器锁的拥有者,请查看notify()方法。
/**
* Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods.
* <p>
* The awakened thread will not be able to proceed until the current
* thread relinquishes the lock on this object. The awakened thread will
* compete in the usual manner with any other threads that might be
* actively competing to synchronize on this object; for example, the
* awakened thread enjoys no reliable privilege or disadvantage in being
* the next thread to lock this object.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. A thread becomes the owner of the
* object's monitor in one of three ways:
* <ul>
* <li>By executing a synchronized instance method of that object.
* <li>By executing the body of a {@code synchronized} statement
* that synchronizes on the object.
* <li>For objects of type {@code Class,} by executing a
* static synchronized method of that class.
* </ul>
* <p>
* Only one thread at a time can own an object's monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of this object's monitor.
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
*/
@IntrinsicCandidate
public final native void notify();
调用该方法可以唤醒一个正在等待对象监视器锁的线程,如果有多个线程在等待对象监视器锁,其中任意一个将被选中唤醒。这个方法只能被拥有对象监视器锁的线程调用,要想拥有对象监视器锁,有3种方式可以获取:
(1)执行对象实例的synchronized修饰的方法。例如:
public synchronized void output(String name) {...}
(2)执行synchronized修饰的代码块。例如:
public void output(String name) {
synchronized(this){
...
}
}
(3)对于类对象,执行synchronized修饰的静态方法。例如:
public synchronized static void output(String name) {...}
synchronized是java内置的关键字,其字面意思是”同步,同步的“意思,是实现线程同步的关键机制,其底层基于对象监视器锁实现,所以用该关键字修饰方法或者代码块可以获得对象监视器锁,被synchronized修饰的方法或者代码块,就像一把锁一样,把方法或者代码块锁住,只能由持有该锁的线程访问。synchronized修饰对象方法时,锁对象就是调用该方法的对象;修饰类方法时,锁对象就是当前类的字节码对象;修饰代码块时,需要在synchronized后面用括号指定锁对象,可以是任意类型的对(如this,Object);
综合上述分析,线程要想通过调用wait()方法进入等待前,需要通过synchronized获得对象监视器锁;要想调用notify()方法唤醒其他线程前,它也是要先成为对象监视器锁的拥有者。结合官方给出的调用示例,可以推断wait()方法和notify()由锁对象进行调用,我们改造下上面的例子来验证这一结论。
public class ConcurrentCollaboration {
//输出数字
private static int num = 1;
//公共变量,线程执行标识
private static boolean thread1Run = true;
//锁对象
private static final Object lockObject = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//获取锁对象
synchronized (lockObject) {
//判断是否轮到自己执行
while (!thread1Run) {
//没到就进入等待
try {
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = false;
//执行完输出,通知t2执行
lockObject.notify();
}
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(num < 10) {
//获取锁对象
synchronized (lockObject) {
//判断是否轮到自己执行
while (thread1Run) {
//没到就进入等待
try {
lockObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " output " + num++);
thread1Run = true;
//执行完输出,通知t2执行
lockObject.notify();
}
}
}
},"t2");
t1.start();
t2.start();
}
}
运行结果:
t1 output 1
t2 output 2
t1 output 3
t2 output 4
t1 output 5
t2 output 6
t1 output 7
t2 output 8
t1 output 9
t2 output 10
例子还可以进一步完善,但我们的主要目的是验证如何使用wait()和notify()来实现基础的线程并发协同,以及谁来调用wait()和notify()的问题,运行结果已经符合我们的预期了。
总结
本文由多线程的概念引出并发协同以及对象监视器锁(synchronized)的概念,在调用wait()和notify()方法前必须先获得对象监视器锁,并讲解了synchronized的使用方法。通过两个线程交替顺序输出数字的经典题目,结合官方说明一步步分析线程如何获得锁,以及wait()和notify()的使用方法。在此我们总结出并发协同的核心思路:弄清楚并发的是什么,什么时候需要协同,谁来等待,谁来唤醒,通过这样的一个分析过程,希望能让大家深刻理解基础的并发协同的方式。
如果文章对您有帮助,不妨“帧栈”一下,关注“帧栈”公众号,第一时间获取推送文章,您的肯定将是我写作的动力!