上篇文章咱们介绍了 synchronized 这个关键字,经过它能够基本实现线程间在临界区对临界资源正确的访问与修改。可是,它依赖一个 Java 对象内置锁,某个时刻只能由一个线程占有该锁,其余试图占有的线程都得阻塞在对象的阻塞队列上。java
但实际上还有一种状况也是存在的,若是某个线程得到了锁但在执行过程当中因为某些条件的缺失,好比数据库查询的资源还未到来,磁盘读取指令的数据未返回等,这种状况下,让线程依然占有 CPU 等待是一种资源上的浪费。git
因此,每一个对象上也存在一个等待队列,这个队列上阻塞了全部得到锁并处于运行期间缺失某些条件的线程,因此整个对象的锁与队列情况是这样的。github
Entry Set 中阻塞了全部试图得到当前对象锁而失败的线程,Wait Set 中阻塞了全部在得到锁运行期间因为缺失某些条件而交出 CPU 的线程集合。数据库
而当某个现场称等待的条件知足了,就会被移除等待队列进入阻塞队列从新竞争锁资源。bash
Object 类中有几个方法咱们虽然不常使用,可是确实线程协做的核心方法,咱们经过这几个方法控制线程间协做。微信
public final native void wait(long timeout)
public final void wait()
public final native void notify();
public final native void notify();
复制代码
wait 类方法用于阻塞当前线程,将当前线程挂载进 Wait Set 队列,notify 类方法用于释放一个或多个处于等待队列中的线程。框架
因此,这两个方法主要是操做对象的等待队列,也便是将那些得到锁可是运行期间缺少继续执行的条件的线程阻塞和释放的操做。dom
可是有一个前提你们须要注意,wait 和 notify 操做的是对象内置锁的等待队列,也就是说,必须在得到对象内置锁的前提下才能阻塞和释放等待队列上的线程。简单来讲,这两个方法的只能在 synchronized 修饰的代码块内部进行调用。ide
下面咱们看一段代码:工具
public class Test {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(){
@Override
public void run(){
synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run(){
synchronized (lock){
System.out.println("hello");
}
}
};
thread1.start();
thread2.start();
Thread.sleep(2000);
System.out.println(thread1.getState());
System.out.println(thread2.getState());
}
}
复制代码
运行结果:
能够看到,程序是没有正常结束的,也就是说,有线程还未正常退出。线程一优先启动于线程二,因此它将先得到 lock 锁,接着调用 wait 方法将本身阻塞在 lock 对象的等待队列上,并释放锁交出 CPU。
线程二启动时可能因为线程一依然占有锁而阻塞,但当线程一释放锁之后,线程二将得到锁并执行打印语句,随后同步方法结束并释放锁。
此时,线程一依然阻塞在 lock 对象的等待队列上,因此整个程序没有正常退出。
演示这么一段程序的意义是什么呢?就是想告诉你们,虽然阻塞队列和等待队列上的线程都不能获得 CPU 正常执行指令,可是它们却属于两种不一样的状态,阻塞队列上的线程在得知锁已经释放后将公平竞争锁资源,而等待队列上的线程则必须有其余线程经过调用 notify 方法通知并移出等待队列进入阻塞队列,从新竞争锁资源。
一、sleep 方法
sleep 方法用于阻塞当前线程指定时长,线程状态随即变成 TIMED_WAITING,但区别于 wait 方法。二者都是让出 CPU,可是 sleep 方法不会释放当前持有的锁。
也就是说,sleep 方法不是用于线程间同步协做的方法,它只是让线程暂时交出 CPU,暂停运行一段时间,时间到了将由系统调度分配 CPU 继续执行。
二、join 方法
join 方法用于实现两个线程之间相互等待的一个操做,看段代码:
public void testJoin() throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run(){
for (int i=0; i<1000; i++)
System.out.println(i);
}
};
thread.start();
thread.join();
System.out.println("main thread finished.....");
}
复制代码
抛开 join 方法不谈,main 线程中的打印方法必定是先执行的,而实际上这段程序会在线程 thread 执行完成以后才执行主线程的打印方法。
实现机理区别于 sleep 方法,咱们一块儿看看:
方法的核心就是调用 wait(delay) 阻塞当前线程,当线程被唤醒计算从进入方法到当前时间共通过了多久。
接着比较 millis 和 这个 now,若是 millis 小于 now 说明,说明等待时间已经到了,能够退出方法返回了。不然则说明线程提早被唤醒,须要继续等待。
须要注意的是,既然是调用的 wait 方法,那么等待的线程必然是须要释放持有的当前对象内置锁的,这区别于 sleep 方法。
下面咱们写一个颇有意思的代码,实现操做系统中的生产者消费者模型,借助咱们的 wait 和 notify 方法。
生产者不停生产产品到仓库中直到仓库满,消费者不停的从仓库中取出产品直到仓库为空。若是生产者发现仓库已经满了,就不能继续生产产品,而消费者若是发现仓库为空,就不能从仓库中取出产品。
public class Repository {
private List<Integer> list = new ArrayList<>();
private int limit = 10; //设置仓库容量上限
public synchronized void addGoods(int count) throws InterruptedException {
while(list.size() == limit){
//达到仓库上限,不能继续生产
wait();
}
list.add(count);
System.out.println("生产者生产产品:" + count);
//通知全部的消费者
notifyAll();
}
public synchronized void removeGoods() throws InterruptedException {
while(list.size() <= 0){
//仓库中没有产品
wait();
}
int res = list.get(0);
list.remove(0);
System.out.println("消费者消费产品:" + res);
//通知全部的生产者
notifyAll();
}
}
复制代码
写一个仓库类,该类提供两个方法供外部调用,一个是往仓库放产品,若是仓库满了则阻塞到仓库对象的等待队列上,一个是从仓库中取出产品,若是仓库为空则阻塞在仓库的等待队列上。
public class Producer extends Thread{
Repository repository = null;
public Producer(Repository p){
this.repository = p;
}
@Override
public void run(){
int count = 1;
while(true){
try {
Thread.sleep((long) (Math.random() * 500));
repository.addGoods(count++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
定义一个生产者类,生产者随机的向仓库添加产品。若是没有能成功的添加,会被阻塞在循环里。
public class Customer extends Thread{
Repository repository = null;
public Customer(Repository p){
this.repository = p;
}
@Override
public void run(){
while(true){
try {
Thread.sleep((long) (Math.random() * 500));
repository.removeGoods();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
定义一个消费者类,消费者类随机的从仓库中取一个产品。若是没有成功的取出一个产品,一样会被阻塞在循环里。
public void testProducerAndCustomer() {
Repository repository = new Repository();
Thread producer = new Producer(repository);
Thread consumer = new Customer(repository);
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("main thread finished..");
}
复制代码
主线程启动这两个线程,程序运行的状况大体是这样的:
生产者生产产品:1
消费者消费产品:1
生产者生产产品:2
消费者消费产品:2
生产者生产产品:3
消费者消费产品:3
。。。。。
。。。。。
消费者消费产品:17
生产者生产产品:21
消费者消费产品:18
生产者生产产品:22
消费者消费产品:19
生产者生产产品:23
消费者消费产品:20
生产者生产产品:24
生产者生产产品:25
生产者生产产品:26
消费者消费产品:21
生产者生产产品:27
生产者生产产品:28
消费者消费产品:22
消费者消费产品:23
生产者生产产品:29
生产者生产产品:30
。。。。。。
。。。。。。
复制代码
仔细观察,你会发现,消费者者永远不会消费一个不存在的产品,消费的必定是生产者生产的产品。刚开始多是生产者生产一个产品,消费者消费一个产品,而一旦消费者线程执行的速度超过了生产者,必然会因为仓库容量为空而被阻塞。
生产者线程的执行速度能够超过消费者线程,而消费者线程的执行速度若是一直超过生产者就会致使仓库容量为空而导致本身被阻塞。
总结一下,synchronized 修饰的代码块是直接使用的对象内置锁的阻塞队列,线程获取不到锁天然被阻塞在该队列上,而 wait/notify 则是咱们手动的控制等待队列的入队和出队操做。但本质上都是利用的对象内置锁的两个队列。
这两篇文章介绍的是利用 Java 提供给咱们的对象中的内置锁来完成基本的线程间同步操做,这部分知识是后续介绍的各类同步工具,集合类框架等实现的底层原理。
文章中的全部代码、图片、文件都云存储在个人 GitHub 上:
欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。