java进阶1 -「线程」
一 进程和线程
什么是进程
进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建, 运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(.exe
文件的运行)。
什么是线程
线程与进程相似,但线程是一个比进程更小的执行单位,线程是操作系统能够进行运算调度的最小单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈.
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
二 线程状态机
阻塞和等待的区别
阻塞: 当一个线程试图获得对象锁(非juc库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由jvm调度器来决定唤醒自己,而不是需要由另一个线程来显式唤醒自己,不响应中断
等待: 当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语意更丰富,可响应中断。例如: Object.wait(), Thread.join() 以及LockSupport.park()
需要强调的是虽然synchronized和JUC里的Lock都实现锁的功能,但线程进入的状态是不一样的。synchronized会让线程进入阻塞态,而JUC里的lock是用LockSupport.park()/unpark()来实现阻塞/唤醒的,会让线程进入等待态。但事实上,虽然等待锁时进入的状态不一样,但被唤醒后又都进入runnable态,从行为效果来看又是一样的。
三 面试问题
1 谈谈对 sleep, yield, join, interrupt, suspend的理解
(1) sleep
让线程睡眠,交出cpu,让cpu去执行其他的任务。不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
(2) yield
让线程交出cpu,让cpu去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出cpu的时间。
注意调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获得cpu执行时间。
(3) join
join()实际是利用了wait(),只不过它不用等待notify()/notifyAll(),且不受其影响。它结束的条件是:1)等待时间到;2)目标线程已经run完(通过isAlive()来判断)。
join和synchronized的区别是: join在内部使用wait()方法进行等待,而synchronized关键字使用的是"对象监视器"作为同步。
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } //0则需要一直等到目标线程run完 if (millis == 0) { while (isAlive()) { wait(0); } } else { //如果目标线程未run完且阻塞时间未到,那么调用线程会一直等待。 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
(4) interrupt
此操作会中断等待中的线程,并将线程的中断标志位置位。如果线程在运行态则不会受此影响。
可以通过以下三种方式来判断中断:
1)isInterrupted()
此方法只会读取线程的中断标志位,并不会重置。
2)interrupted()
此方法读取线程的中断标志位,并会重置。
3)throw InterruptException
抛出该异常的同时,会重置中断标志位。
(5) suspend/resume
挂起线程,直到被resume,才会苏醒
但调用suspend()的线程和调用resume()的线程,可能会因为争锁的问题而发生死锁,所以JDK 7开始已经不推荐使用了。
2 java如何停止一个线程
Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法。但是由于潜在的死锁威胁,在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。
private class Runner extends Thread{ volatile boolean bExit = false; public void exit(boolean bExit){ this.bExit = bExit; } @Override public void run(){ while(!bExit){ System.out.println("Thread is running"); try { Thread.sleep(500); } catch (InterruptedException ex) { Logger.getLogger(ThreadTester.class.getName()).log(Level.SEVERE, null, ex); } } } }
3 在java中wait和sleep方法的不同
共同点:两者都可以暂停线程的执行。
区别:
sleep()
方法没有释放锁,而wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒。sleep()
是Thread
类的静态本地方法,wait()
则是Object
类的本地方法。为什么这样设计呢?下一个问题就会聊到。
4 为什么 wait() 方法不定义在 Thread 中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object
)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object
)而非当前的线程(Thread
)。
类似的问题:为什么 sleep()
方法定义在 Thread
中?
因为 sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
5 为什么Thread类的sleep()和yield()方法是静态的
Thread类的sleep和yield都是作用在当前正在执行的线程上运行,所以其他处于等待状态的线程上调用这些方法是没有意义的。设置为静态表明在当前执行的线程上工作,避免开发错误地认为可以在其他非运行线程调用这些方法。
6 线程的优先级
Java中线程的优先级分为1-10这10个等级,如果小于1或大于10则JDK抛出IllegalArgumentException()的异常,默认优先级是5。在Java中线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。注意程序正确性不能依赖线程的优先级高低,因为操作系统可以完全不理会Java线程优先级
7 java线程池中submit()和execute()方法有什么区别
两者都可以向线程池提交任务,execute()方法的返回类型是void, 它定义在Executor接口中,而submit()方法可以返回有计算结果得到Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。
public interface ExecutorService extends Executor { ... }
8 java中Runnable和Callable有什么不同
两者都代表那些要在不同的线程中执行的任务。Runnable从jdk1.0就开始有了,Callable是在jdk1.5增加的。它们的主要区别是Callable的call()方法可以返回值和抛出异常,而Runnable的run()没有这些功能。Callable可以装载有计算结果的Future对象。
9 守护进程及其典型应用
Java中有两种线程,一种是用户线程,另一种是守护线程。当进程中不存在非守护线程了,则守护线程自动销毁。通过setDaemon(true)设置线程为后台线程。注意thread.setDaemon(true)必须在thread.start()之前设置,否则会报IllegalThreadStateException异常, 你不能把正在运行的常规线程设置为守护线程;在Daemon线程中产生的新线程也是Daemon的.
守护线程最典型的应用就是 GC (垃圾回收器)
User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
10 线程间的同步的方式有哪些?
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
下面是几种常见的线程同步的方式:
- 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。 - 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。 - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步。
11 wait, notify, notifyAll用法
首先要明确,只能在synchronized同步方法或者同步代码块中使用这些。在执行wait方法后,当前线程释放锁(这点与sleep, yield不同)。调用了wait函数的线程会一直等待,直到有其他线程调用了同一个对象的notify或notifyAll方法。需要注意的是,被唤醒并不代表立刻获得对象的锁,要等待执行notify方法的线程执行完,也即退出synchronized代码块后,当前线程才会释放锁,进而wait状态的线程才可以获得该对象锁
不在同步代码块会有IllegalMonitorStateException异常(RuntimeException)
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
notify方法只会(随机)唤醒一个正在等待的线程,而notifyAll方法会唤醒所有正在等待的线程。如果一个对象之前没有调用wait方法,那么调用notify方法是没有任何影响的
12 interrupted和isInterrupted的区别
interrupted 判断当时线程是否已经是中断状态,执行后清除状态标志
isInterrupted 判断当时线程是否已经是中断状态,执行后不清除状态标志
public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); } private native boolean isInterrupted(boolean ClearInterrupted);
13 单核 CPU 上运行多个线程效率一定会高吗?
单核 CPU 同时运行多个线程的效率是否会高,取决于任务的性质。一般来说有两种类型的任务:CPU密集型和IO密集型。CPU密集型的线程主要进行计算和逻辑处理,需要占用大量的CPU资源。IO密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待IO设备的响应,而不占用太多的CPU资源。
在单核CPU上,同一时刻只能有一个线程在运行,其他线程需要等待CPU的时间片分配。如果线程是CPU密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。如果线程是IO 密集型的,那么多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。
14 什么是线程死锁?如何避免死锁?(衍生问题众多)
认识线程死锁
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } }
输出
Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1)
获得 resource1
的监视器锁,然后通过Thread.sleep(1000);
让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
上面的例子符合产生死锁的四个必要条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。
如何预防和避免线程死锁?
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
<P1、P2、P3.....Pn>
序列为安全序列。
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 2").start();
输出
Thread[线程 1,5,main]get resource1 Thread[线程 1,5,main]waiting get resource2 Thread[线程 1,5,main]get resource2 Thread[线程 2,5,main]get resource1 Thread[线程 2,5,main]waiting get resource2 Thread[线程 2,5,main]get resource2 Process finished with exit code 0
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
15 可以直接调用 Thread 类的 run 方法吗?
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread
,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
16 进程间通信的方式
1. 管道( pipe ):管道是一种半双工(什么是半双工 https://tech.hqew.com/news_3834739)的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3. 信号量( semaphore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
4. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
6. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
7. 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
17 高频设计题 - 有线程T1/T2/T3,如何确保T1执行之后执行T2,T2执行之后执行T3
a join方法
final Thread T1 = new Thread(new Runnable() { @Override public void run() { System.out.println("T1..."); } }); final Thread T2 = new Thread(new Runnable() { @Override public void run() { try { T1.join(); }catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("T2..."); } }); final Thread T3 = new Thread(new Runnable() { @Override public void run() { try { T2.join(); }catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("T3..."); } }); T3.start(); T2.start(); T1.start();
其他参看 https://www.cnblogs.com/yy3b2007com/p/8784383.html
18 高频设计题 - 生产者消费者模型
- 生产者持续生产,直到缓冲区满,阻塞;缓冲区不满后,继续生产
- 消费者持续消费,直到缓冲区空,阻塞;缓冲区不空后,继续消费
- 生产者可以有多个,消费者也可以有多个
可通过如下条件验证模型实现的正确性:
- 同一产品的消费行为一定发生在生产行为之后
- 任意时刻,缓冲区大小不小于0,不大于限制容量
前置接口定义
消费者:
public abstract class AbstractConsumer implements Runnable { protected abstract void consume() throws InterruptedException; @Override public void run() { while (true) { try { consume(); } catch (InterruptedException e) { e.printStackTrace(); break; } } } }
生产者:
public abstract class AbstractProducer implements Runnable { protected abstract void produce() throws InterruptedException; @Override public void run() { while (true) { try { produce(); } catch (InterruptedException e) { e.printStackTrace(); break; } } } }
bean:
public interface Model { Runnable newRunnableConsumer(); Runnable newRunnableProducer(); } public class Task { private int no; public Task(int no) { this.no = no; } public int getNo() { return no; } }
实现1 - blockingQueue
BlockingQueue的写法最简单。核心思想是,把并发和容量控制封装在缓冲区中。
public class BlockingQueueModel implements Model { private final BlockingQueue<Task> blockingQueue; BlockingQueueModel(int capacity) { this.blockingQueue = new LinkedBlockingQueue<>(capacity); } private final AtomicInteger taskNo = new AtomicInteger(0); @Override public Runnable newRunnableConsumer() { return new AbstractConsumer() { @Override public void consume() throws InterruptedException { Task task = blockingQueue.take(); // 固定时间范围的消费,模拟相对稳定的服务器处理过程 TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500)); System.out.println("consume: " + task.getNo()); } }; } @Override public Runnable newRunnableProducer() { return new AbstractProducer() { @Override public void produce() throws InterruptedException { // 不定期生产,模拟随机的用户请求 TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000)); Task task = new Task(taskNo.getAndIncrement()); blockingQueue.put(task); System.out.println("produce: " + task.getNo()); } }; } public static void main(String[] args) { Model model = new BlockingQueueModel(3); Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start()); Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start()); } }
运行结果:
由于数据操作和日志输出是两个事务,所以上述日志的绝对顺序未必是真实的数据操作顺序,但对于同一个任务号task.getNo
,其consume日志一定出现在其produce日志之后,即:同一任务的消费行为一定发生在生产行为之后。
实现2 - wait和notify
Object类提供的wait()方法与notifyAll()方法。 朴素的wait&¬ify
机制不那么灵活,但足够简单
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class WaitNotifyModel implements Model { private final Object BUFFER_LOCK = new Object(); private final Queue<Task> queue = new LinkedList<>(); private final int capacity; public WaitNotifyModel(int capacity) { this.capacity = capacity; } private final AtomicInteger taskNo = new AtomicInteger(0); @Override public Runnable newRunnableConsumer() { return new AbstractConsumer() { @Override public void consume() throws InterruptedException { synchronized (BUFFER_LOCK) { while (queue.isEmpty()) { BUFFER_LOCK.wait(); } Task task = queue.poll(); assert task != null; TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500)); System.out.println("consume: " + task.getNo()); BUFFER_LOCK.notifyAll(); } } }; } @Override public Runnable newRunnableProducer() { return new AbstractProducer() { @Override public void produce() throws InterruptedException { TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000)); synchronized (BUFFER_LOCK) { while (queue.size() == capacity) { BUFFER_LOCK.wait(); } Task task = new Task(taskNo.getAndIncrement()); queue.offer(task); System.out.println("produce: " + task.getNo()); BUFFER_LOCK.notifyAll(); } } }; } public static void main(String[] args) { Model model = new BlockingQueueModel(3); Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start()); Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start()); } }
实现3 - lock和condition
java.util.concurrent包提供的Lock && Condition,对于实现二的简单变形
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class LockConditionModel implements Model { private final Lock BUFFER_LOCK = new ReentrantLock(); private final Condition CONDITION = BUFFER_LOCK.newCondition(); private final Queue<Task> queue = new LinkedList<>(); private final int capacity; public LockConditionModel(int capacity) { this.capacity = capacity; } private final AtomicInteger taskNo = new AtomicInteger(0); @Override public Runnable newRunnableConsumer() { return new AbstractConsumer() { @Override public void consume() throws InterruptedException { BUFFER_LOCK.lockInterruptibly(); try { while (queue.isEmpty()) { CONDITION.await(); } Task task = queue.poll(); assert task != null; TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500)); System.out.println("consume: " + task.getNo()); CONDITION.signalAll(); } finally { BUFFER_LOCK.unlock(); } } }; } @Override public Runnable newRunnableProducer() { return new AbstractProducer() { @Override public void produce() throws InterruptedException { TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000)); BUFFER_LOCK.lockInterruptibly(); try { while (queue.size() == capacity) { CONDITION.await(); } Task task = new Task(taskNo.getAndIncrement()); queue.offer(task); System.out.println("produce: " + task.getNo()); CONDITION.signalAll(); } finally { BUFFER_LOCK.unlock(); } } }; } public static void main(String[] args) { Model model = new BlockingQueueModel(3); Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start()); Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start()); } }
实现4 - 更高并发性能的lock和condition
实现三有一个问题,通过实践可以发现,实现二,三的效率明显低于实现一,并发瓶颈很明显,因为在锁 BUFFER_LOCK
看来,任何消费者线程与生产者线程都是一样的。换句话说,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)操作缓冲区 buffer。
而实际上,如果缓冲区是一个队列的话,“生产者将产品入队”与“消费者将产品出队”两个操作之间没有同步关系,可以在队首出队的同时,在队尾入队。理想性能可提升至两倍。
去掉这个瓶颈
那么思路就简单了:需要两个锁 CONSUME_LOCK
与PRODUCE_LOCK
,CONSUME_LOCK
控制消费者线程并发出队,PRODUCE_LOCK
控制生产者线程并发入队;相应需要两个条件变量NOT_EMPTY
与NOT_FULL
,NOT_EMPTY
负责控制消费者线程的状态(阻塞、运行),NOT_FULL
负责控制生产者线程的状态(阻塞、运行)。以此让优化消费者与消费者(或生产者与生产者)之间是串行的;消费者与生产者之间是并行的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class LockConditionPreferModel implements Model { private final Lock CONSUMER_LOCK = new ReentrantLock(); private final Condition NOT_EMPTY_CONDITION = CONSUMER_LOCK.newCondition(); private final Lock PRODUCER_LOCK = new ReentrantLock(); private final Condition NOT_FULL_CONDITION = PRODUCER_LOCK.newCondition(); private AtomicInteger bufLen = new AtomicInteger(0); private final Buffer<Task> buffer = new Buffer<>(); private final int capacity; public LockConditionPreferModel(int capacity) { this.capacity = capacity; } private final AtomicInteger taskNo = new AtomicInteger(0); @Override public Runnable newRunnableConsumer() { return new AbstractConsumer() { @Override public void consume() throws InterruptedException { int newBufSize; CONSUMER_LOCK.lockInterruptibly(); try { while (bufLen.get() == 0) { System.out.println("buffer is empty..."); NOT_EMPTY_CONDITION.await(); } Task task = buffer.poll(); newBufSize = bufLen.decrementAndGet(); assert task != null; TimeUnit.MILLISECONDS.sleep(500 + (long) (Math.random() * 500)); System.out.println("consume: " + task.getNo()); if (newBufSize > 0) { NOT_EMPTY_CONDITION.signalAll(); } } finally { CONSUMER_LOCK.unlock(); } if (newBufSize < capacity) { PRODUCER_LOCK.lockInterruptibly(); try { NOT_FULL_CONDITION.signalAll(); } finally { PRODUCER_LOCK.unlock(); } } } }; } @Override public Runnable newRunnableProducer() { return new AbstractProducer() { @Override public void produce() throws InterruptedException { TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000)); int newBufSize; PRODUCER_LOCK.lockInterruptibly(); try { while (bufLen.get() == capacity) { System.out.println("buffer is full..."); NOT_FULL_CONDITION.await(); } Task task = new Task(taskNo.getAndIncrement()); buffer.offer(task); newBufSize = bufLen.incrementAndGet(); System.out.println("produce: " + task.getNo()); NOT_FULL_CONDITION.signalAll(); } finally { PRODUCER_LOCK.unlock(); } if (newBufSize > 0) { CONSUMER_LOCK.unlock(); try { NOT_EMPTY_CONDITION.signalAll(); } finally { CONSUMER_LOCK.unlock(); } } } }; } private static class Buffer<E> { private Node head; private Node tail; Buffer() { head = tail = new Node(null); } private void offer(E e) { tail.next = new Node(e); tail = tail.next; } private E poll() { head = head.next; E e = head.item; head.item = null; return e; } private class Node { E item; Node next; Node(E item) { this.item = item; } } } public static void main(String[] args) { Model model = new BlockingQueueModel(3); Arrays.asList(1, 2).forEach(x -> new Thread(model.newRunnableConsumer()).start()); Arrays.asList(1, 2, 3, 4, 5).forEach(x -> new Thread(model.newRunnableProducer()).start()); } }
需要注意的是,由于需要同时在UnThreadSafe的缓冲区 buffer 上进行消费与生产,我们不能使用实现二、三中使用的队列了,需要自己实现一个简单的缓冲区 Buffer。Buffer要满足以下条件:
- 在头部出队,尾部入队
- 在poll()方法中只操作head
- 在offer()方法中只操作tail
实现要点:
1. 持有两种锁
2. 每次生产(/消费)结束会检验数据状态更新另一种锁,当然更新的过程要用相应的锁同步。
还能进一步优化吗
我们已经优化掉了消费者与生产者之间的瓶颈,还能进一步优化吗?
如果可以,必然是继续优化消费者与消费者(或生产者与生产者)之间的并发性能。然而,消费者与消费者之间必须是串行的,因此,并发模型上已经没有地方可以继续优化了。
不过在具体的业务场景中,一般还能够继续优化。如:
- 并发规模中等,可考虑使用CAS代替重入锁
- 模型上不能优化,但一个消费行为或许可以进一步拆解、优化,从而降低消费的延迟
- 一个队列的并发性能达到了极限,可采用“多个队列”(如分布式消息队列等)
补充一下LinkedBlockingQueue在新增和删除时候的各个方法的区别:
一般情况建议用offer和poll,是即时操作,如果带时间的offer和poll相当于限时的同步等待
永久的同步等待使用put和take(@see 上面的实现一)
19 高频设计题 - 交替打印A,B
public class Test { public static void main(String[] args) throws Exception { ReentrantLock lock = new ReentrantLock(true); Condition condition = lock.newCondition(); AtomicBoolean isA = new AtomicBoolean(true); new Thread(() -> { for (int i = 0; i < 10; i++) { lock.lock(); try { while (!isA.get()) { //不是自己打印的标志就释放锁 try { condition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print("A"); isA.set(false); condition.signalAll(); } finally { lock.unlock(); } } }).start(); new Thread(() -> { for (int i = 0; i < 10; i++) { lock.lock(); try { while (isA.get()) { //不是自己打印的标志就释放锁 try { condition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.print("B"); isA.set(true); condition.signalAll(); } finally { lock.unlock(); } } }).start(); } }
public class Test { public static void main(String[] args) { BlockingQueue<Integer> queueA = new LinkedBlockingQueue<>(1); BlockingQueue<Integer> queueB = new LinkedBlockingQueue<>(1); new Thread(()->{ for (int i = 0; i < 10; i++) { try { queueA.put(i); System.out.print("A"); queueB.put(i); }catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); new Thread(()->{ for (int i = 0; i < 10; i++) { try { queueB.take(); System.out.print("B"); queueA.take(); }catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); } }