浅析Java并发编程(三)线程的状态&协作

2017-06-19 by 简单的土豆




前言

搞清楚Java中线程都有哪些状态,线程间如何进行协作,这是使用Java进行并发编程的基础。本文是作者自己对Java中线程的状态、线程间协作、相关API使用的理解与总结,不对之处,望指出,共勉。


线程的状态

NEW
新建状态,表示线程已创建,但未启动。
RUNNABLE
就绪状态,表示线程已被JVM执行,但是可能还没获得操作系统CPU资源的调度需要等待一会儿(转瞬之间)。
BLOCKED
阻塞状态,表示线程被阻塞,可能正在尝试进入synchronized关键字修饰的同步代码块、方法,等待获取监视器锁(monitor ),或者是进入synchronized关键字修饰的同步代码块、方法后被调用Object.wait()

WAITING
等待状态,通常调用Object.wait()会进入该状态,直到其他线程调用Object.notify() 或Object.notifyAll()才会唤醒。
TIMED_WAITING
有限时间的等待状态,通常调用Object.wait(long timeout) 或Object.wait(long timeout, int nanos)会进入该状态,与等待状态类似,区别就是时间到了之后如果没有被Object.notify()或Object.notifyAll()唤醒的话会自动唤醒。
TERMINATED
终止状态,线程已经执行完毕。
线程的协作

线程间的协作主要通过java.lang.Object的成员方法wait()、notify()、notifyAll()和java.lang.Thread的静态方法sleep(long millis)、yield()、join()等进行。



wait()
使一个线程处于等待状态,并且释放所持有的对象的监视器锁,进入等待池直到notify或notifyAll方法来唤醒。调用此方法要处理java.lang.InterruptedException异常。
wait(long timeout)
如上,区别在于可以自行设置最大等待时间(毫秒),到时间没有被唤醒则自动唤醒。

wait(long timeout, int nanos)
如上,提供更精确的等待时间(纳秒)。


public class WaitTest {
public synchronized void work() {
System.out.println("Begin Work");
try { //等待1000毫秒后,自动唤醒继续执行 wait(1000);
} catch (InterruptedException e) { e.printStackTrace();
}
System.out.println("Work End");
}
public static void main(String[] args) {
WaitTest test = new WaitTest();
new Thread(() -> test.work()).start();
}
}

notify()
唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关。



notifyAll()
唤醒所有处于等待状态的线程,该方法并不是将对象的监视器锁给所有线程,而是让它们去竞争锁,只有获得锁的线程才能进入就绪状态。


public class NotifyTest {
public synchronized void work() {
System.out.println("Begin Work");
try { //等待唤醒 wait();
} catch (InterruptedException e) { e.printStackTrace();
}
System.out.println("Work End");
}
public synchronized void continueWork() {
notifyAll();
}
public static void main(String[] args) throws InterruptedException {
NotifyTest test = new NotifyTest();
new Thread(() -> test.work()).start();
//等待3000毫秒后唤醒,继续工作。
Thread.sleep(3000);
test.continueWork();
}
}

注意:wait()、notify()、notifyAll() 的实现依赖于monitor,所以这些方法必须运行在被synchronized关键字修饰的方法或代码块中(因为使用synchronized关键字可以获得monitor,详见上一篇文章),否则在运行时会抛出java.lang.IllegalMonitorStateException异常,你可以尝试去掉上面代码中的synchronized关键字,再执行代码试试。



生产者-消费者并发模型


有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区投放产品。


生产者-消费者并发模型是并发编程中一个经典的模型,最早由Dijkstra提出,用以演示它提出的信号量机制。在并发编程中使用该模型能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。可以通过wait()、notifyAll()来实现该模型。


产品


public class Product {
private String name;
public Product(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

生产者


public class Producer implements Runnable {
private final int maxSize = 10;//产品最大库存量
private List<Product> buffer;
public Producer(List<Product> buffer) {
this.buffer = buffer;
}
public void run() {
while (true) { synchronized (buffer) { while (buffer.size() >= maxSize) { try { buffer.wait();//将当前线程放入等锁(buffer对象的锁)池,并释放锁。 } catch (InterruptedException e) { e.printStackTrace(); } } //模拟生产需要500毫秒 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Product product = new Product("iPhone 手机"); buffer.add(product); System.out.println("生产者[" + Thread.currentThread().getName() + "]生产了一个产品:" + product); buffer.notifyAll();//生产完毕通知等待池内的其他线程(生产者或消费者都有可能) }
}
}
}

消费者


public class Consumer implements Runnable {
private List<Product> buffer;
public Consumer(List<Product> buffer) {
this.buffer = buffer;
}
public void run() {
while (true) { synchronized (buffer) { while (buffer.isEmpty()) { try { buffer.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("消费者[" + Thread.currentThread().getName() + "]消费了一个产品:" + buffer.remove(0)); buffer.notifyAll();//消费完毕通知等待池内的其他线程(生产者或消费者都有可能) }
}
}
}

测试


public class Tester {
public static void main(String[] args) {
test1();
}
//测试wait、notify实现
public static void test1(){
List<Product> buffer = new LinkedList<Product>();
ExecutorService es = Executors.newFixedThreadPool(5);
//两个生产者
es.execute(new Producer(buffer));
es.execute(new Producer(buffer));
//三个消费者
Consumer consumer = new Consumer(buffer);
es.execute(new Consumer(buffer));
es.execute(new Consumer(buffer));
es.execute(new Consumer(buffer));
es.shutdown();
/**
输出:
...
生产者[pool-1-thread-2]生产了一个产品:iPhone 手机
生产者[pool-1-thread-2]生产了一个产品:iPhone 手机
消费者[pool-1-thread-5]消费了一个产品:iPhone 手机
消费者[pool-1-thread-5]消费了一个产品:iPhone 手机
消费者[pool-1-thread-5]消费了一个产品:iPhone 手机
消费者[pool-1-thread-5]消费了一个产品:iPhone 手机
消费者[pool-1-thread-5]消费了一个产品:iPhone 手机
生产者[pool-1-thread-1]生产了一个产品:iPhone 手机
消费者[pool-1-thread-4]消费了一个产品:iPhone 手机
生产者[pool-1-thread-2]生产了一个产品:iPhone 手机
...
*/
}
}

查看生产者-消费者模型源码



sleep(long millis)
使一个正在运行的线程处于“睡眠状态”,sleep()方法只是暂时让出CPU的执行权,并不会释放监视器锁,调用此方法要处理java.lang.InterruptedException异常。关于该方法还有个段子代码,如下。


public class Interceptor {
void after(){
//让所有接口都慢一点,产品经理下次让你优化性能的时候你减一点睡眠时间就OK了。。。
Thread.sleep(100);
}
}

sleep(long millis, int nanos)
如上,时间单位精确到纳秒
yield()
使一个正在运行的线程暂停执行(让出CPU)让其他线程执行,即回到就绪状态。public class YieldTest {
public void work(){
for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + " Working"); Thread.yield();
}
}
public static void main(String[] args) {
YieldTest test = new YieldTest();
new Thread(() -> test.work()).start();
new Thread(() -> test.work()).start();
/**
输出: Thread-0 Working Thread-1 Working Thread-0 Working Thread-1 Working Thread-0 Working Thread-1 Working
**/
}
}

join()
使主线程等待子线程执行完成后再执行,换句话说就是将线程的并行执行变为串行执行。


public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + " Work End"));
thread1.start();
thread1.join();//合并到主线程,主线程将等待该子线程执行完毕才会执行
Thread thread2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + " Work End"));
thread2.start();
thread1.join();//合并到主线程,主线程将等待该子线程执行完毕才会执行
System.out.println("Main Thread Work End");
/**
输出: Thread-0 Work End Thread-1 Work End Main Thread Work End
不使用join(): Main Thread Work End Thread-0 Work End Thread-1 Work End
*/
}
}

join(long millis)
如上,但是主线程只会等待一定的时间(毫秒),如果时间到了该子线程仍未执行完,则放弃等待,执行主线程自己的代码。


public class TimedJoinTest{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> { try { //模拟子线程需要执行500毫秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " Work End");
});
thread.start();
thread.join(100);//合并到主线程,主线程将等待该子线程执行完毕才会执行,只等待100毫秒,过时不在等。
System.out.println("Main Thread Work End");
/**
输出: Main Thread Work End Thread-0 Work End
删除Thread.sleep(500);或者降到100以内: Thread-0 Work End Main Thread Work End
*/
}
}

join(long millis, int nanos)
如上,时间单位精确到纳秒

查看本文相关源码

参考
Java Platform, Standard Edition 8 API Specification

第七城市

栏目导航(关闭)