二十一、 并发
- 顺序编程:即程序中的所有事物在任意时刻都只能执行一个步骤。
- 并发编程:程序能够并行地执行程序中的多个部分。
进程:一个在内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可有多个线程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
21.1 并发的多面性
并发需要解决的问题有多个;实现并发的方式有多种;
并且,上述两者之间没有明确的映射关系。
用并发解决的问题大体上可分为“速度”和“设计可管理性”两种。
21.1.1 更快的执行
并发通常是提高运行在单处理器上的程序的性能。
并发实现:
- 最直接的方式是在操作系统级别使用进程。(进程是运行在它自己的地址空间内的自包容的程序)
- java采用的更加传统的方式:在顺序型语言的基础上提供对线程的支持
并发和并行的区别:
- 并发:轮流处理多个任务:其实是按顺序执行的,cpu在任一时间只执行一个线程,通过给不同线程分配时间段的形式来进行调度,只是看起来好像多个任务是同时执行的;
- 并行:同时处理多个任务:就是多个任务同一时刻在同时进行;
21.1.2 改进代码设计
抢占式线程机制:调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。
非抢占线程机制:每个线程可以需要CPU多少时间就占用CPU多少时间。在这种调度方式下,可能一个执行时间很长的线程使得其他所有需要CPU的线程”饿死”。在处理机空闲,即该进程没有使用CPU时,系统可以允许其他的进程暂时使用CPU。占用CPU的线程拥有对CPU的控制权,只有它自己主动释放CPU时,其他的线程才可以使用CPU。
线程机制的选择:Java采用的是抢占式线程机制。
21.2 基本线程机制
21.2.1 定义任务
任务:线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
线程执行:当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处。它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。
21.2.2 Thread类(线程)
任务类型:
- 无结果返回的任务Runnable:将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread的构造器(作为参数传入)
public class RunnableTest {
public static void main(String[] args) {
TestRunable tr = new TestRunable();
Thread t = new Thread(tr);
t.start();
}
}
- 有结果返回的任务Callable:配合使用Future实现获取任务结果。
public class TestCollable implements Callable<String> {
@Override
public String call() throws Exception {
return "小子";
}
}
public class CallableMain {
public static void main(String[] args) throws InterruptedException, ExecutionException {
TestCollable tc = new TestCollable();//Callable出来的是Future对象
Future<String> f = new FutureTask<>(tc);
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(tc);//Callable启动线程必须用submit方法
System.out.println(f.get());//获取call的值
exec.shutdown();
}
}
new Thread(runnable) 创建线程对象
t.start() 启动线程
Thread.sleep(100) 休眠
Thread.yield() 让步(暗示cpu可转为调度其他线程) --慎用!
Thread.currentThread() 获取当前thread对象
t.setProirity(proitity) 设置线程优先级
t.setDaemon(true or false) 设置是否为后台任务
t.isDaemon() 是否为后台任务
t.join() 特定线程在另一线程上调用t.join(),特定线程被挂起,直到另一线程执行结束才恢复执行-慎用(建议改为使用CyclicBarrier)!
t.interrupt(): 中断join()方法的调用
t.isAlive() 线程是否结束
t.isInterrupted() 线程是否已中断
21.2.3 使用Executor
*Executor(执行器):Java SE5的 java.util.concurrent包中定义的执行器,用作管理 Thread对象,从而简化并发编程。并在客户端和任务执行之间提供了间接层;
*Executor:允许你管理异步任务的执行,而无须显示的管理线程的生命周期;非常常见的场景:单个Executor被用来创建&管理系统中的所有任务。
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0;i<5;i++){
exec.execute(new RunableTest());
}
exec.shutdown();
shutdown():当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
四种不同的线程池(Executor的静态工厂构造实现)
newCachedThreadPool
概念:可缓存的线程池。没有固定大小,如果线程池中的线程数量超过任务执行的数量,会回收60秒不执行的任务的空闲线程。当任务数量增加时,线程池自己会增加线程来执行任务。而能创建多少,就得看jvm能够创建多少。
应用场景:default:合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。
实现方式:ExecutorService exec -= new CashedThreadPool();
newFixedThreadPool
概念:可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量了。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。
应用场景:这个实例会复用 固定数量的线程 处理一个 共享的无边界队列 。任何时间点,最多有 nThreads 个线程会处于活动状态执行任务。如果当所有线程都是活动时,有多的任务被提交过来,那么它会一致在队列中等待直到有线程可用。如果任何线程在执行过程中因为错误而中止,新的线程会替代它的位置来执行后续的任务。所有线程都会一致存于线程池中,直到显式的执行 ExecutorService.shutdown() 关闭。
实现方式:ExecutorService exec = Exectors.newFixedThreadPool(5);
SingleThreadExecutor
概念:单例线程池。就像是线程数量为1的FixedThreadPool。(它还提供了一种重要的并发保证,其他线程不会(即没有两个线程会)被调用。
每个任务都是按照它们被提交的顺序,并且是在下一个任务开始之前完成的。因此,SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己(隐藏)的悬挂任务队列。
应用场景:独占资源:多个线程都需要使用共用的文件资源时,使用此机制可保证:任何时刻在任何线程中只有一个任务在运行。
实现方式:ExecutorService exec = Exectors.newSingleThreadPool();
通常用作网络连接。
newScheduledThreadPool
- 概念:也是固定线程数量大小的线程池,可以延迟或者定时周期执行任务
21.2.4 从任务中产生返回值
Runnable:是执行工作的独立任务,但是它不返回任务值。
Callable:如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService.submit()方法调用它。
21.2.5 休眠
- 休眠:.sleep() 将使任务中止执行(线程阻塞)指定的时间。
- 应用场景:阻塞当前线程,用于等待I/O或其他耗时线程完成操作。
Thread.sleep(100) //Java SE5 new style : TimeUnit.MILLISECOND.sleep(100);
21.2.6 优先级
- 优先级:Thread.setPriority(Thread.MAX_PRIORITY) ;new Thread(Thread.MAX_PRIORITY)将当前线程的优先级(重要性)传递给调度器。
- 说明:绝大多数场景建议:线程按照默认的优先级执行,试图操控线程优先级通常是错误的,并不一定只执行优先级高的。
21.2.7 让步
- 让步:.yield() : 暗示线程调度器:本任务已完成当前工作,可以让其他线程使用CPU了。
- 说明:yield()让步机制经常被误用,此机制不保证一定会被线程调度器采用。–慎用!
21.2.8 后台线程
后台线程:(damon线程): 指在程序运行的时候在后台提供一种通用服务的线程,并且此线程并不属于程序不可或缺的部分。
当所有非后台线程结束时,程序已结束,同时会杀死进程中所有的后台进程。(有非后台线程运行,则进程不会结束)。
后台线程的try-finally{}块部分在线程结束时不一定会被执行。
设置方法:必须在.start()前setDaemon(),才能设置进程为后台进程
21.2.9 编码的变体
在构造器中启动线程可能会变得有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能访问处于不稳定状态的对象,这是优选Executor而不显式创建Thread对象的另一个原因。
- Thread
- Thread+Runnable
- Thread+Callable+Future
21.2.10 常用术语
任务:Java中,任务(Runnable&Callable ≠ 线程Thread),
Thread(线程):Java中,Thread本身并不执行任何操作,它只是驱动赋予它的任务。Executor执行器将负责处理线程的创建和管理。
Java的线程机制:基于来自C的低级实现方式,开发者需要深入研究&并完全理解其所有实现细节。
21.2.11 加入一个线程(建议改为使用CyclicBarrier)
t.join (para): 特定线程可以在另一线程中调用t.join():特定线程被挂起,直到另一线程执行结束才恢复执行-慎用!
方法参数说明:para 超时参数,如果目标线程在这段时间到期仍未结束的话,join()方法总能返回。
t.interrupt(): 中断join()方法的调用
21.2.12 创建有响应的用户界面
把运算放在任务里单独运行,可在进行运算的同时监听控制台输入
public class ResponsiveUI extends Thread {
private static volatile double d = 1;//可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的
public ResponsiveUI() {
setDaemon(true);
start();
}
public void run() {
while(true) {
d = d+(Math.PI+Math.E);
}
}
public static void main(String[] args) throws IOException {
new ResponsiveUI();
System.in.read();
System.out.println(d);
}
}
21.2.13 线程组(跳过)
21.2.14 捕获异常
- 由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常。
- 解决方案:Java SE5新增的Executor解决此问题。
21.3 共享受限资源
21.3.1 不正确地访问资源
- boolean类型的赋值和返回值操作是原子性的:即诸如赋值和返回值这样的简单操作在发生时没有中断的可能,因此你不会看到这个域处于在执行这些简单操作的过程中的中间状态。
- 在Java中,递增不是原子性的操作。因此,如果不保护任务,即使单一的递增也不是安全的。
21.3.2 解决共享资源竞争
解决共享资源访问竞争的方案:任务访问资源时,加锁。
序列化访问共享资源:基本上所有的并发模式在解决线程冲突问题的时候,都采用此方案。即给定时间只允许一个任务访问共享资源(实现方式:比如方法前添加synchronized)。
互斥量(mutex):因为方法|代码块 添加了锁,产生的互相排斥的效果。此机制称为互斥量。
synchronized(锁):Java以此关键字,为防止资源冲突提供了内置支持。
对象锁:所有的对象都自动含有一个单一的锁(也称监视器)。当在对象上调用任意synchronized方法,此对象都被加锁,此时该对象上其他synchronized方法只有等到前一个方法执行调用完毕并释放了锁以后才能被调用。
Class类锁:针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static 方法也可以在类的范围防止对static数据的并发访问,static方法中使class类锁
同步规则:Brian的同步规则:“如果你正在写一个变量,它可能接下来将被另一个线程读取;或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程都必须使用相同的监视器锁同步。”
使用显示的Lock对象
显式Lock对象:Java SE5的concurrent类库包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式的创建,锁定和释放(与内置锁机制相比,代码缺乏优雅性)。
显式锁选择原则:优先选择内置锁,只有解决特殊问题才改用显式锁。
ReentrantLock:显式重入锁:允许尝试着获取但最终未获取锁,如其他人已经获得此锁,那你就可以离开一段时间执行其他事情,不用一直等直至此锁被释放。
21.3.3 原子性与易变性
原子性:是不能被线程调度机制中断的操作。一旦操作开始,它一定可以在可能发生的“上下文切换”前(切换到其他线程)执行完毕。
基本数据类型的原子性:原子性可以应用于除了long&double之外的所有基本类型之上的“简单操作”(简单的赋值和返回操作,java递增操作不是原子性的)。
long&double变量的原子性:可以结合volatile 保证long&double变量“简单操作”的原子性(???如何理解)。
可见性:即对变量进行写操作,那么所有读取此变量的线程都能立即读取到此修改。volatile 关键字可以保证变量的可见性。
如果多个任务在同时访问某个域,那么这个域就应该是volatile,或者同步(建议同步)
一个任务的任何写入操作对这个任务内部可视没有必要设置为volatile。
使用volatile而不是synchronized唯一安全的情况是类中只有一个可变的域。
21.3.4 原子类
AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类。
不常用
21.3.5 临界区
- 临界区(critical section):也称同步代码块,使用synchronized{}包含的代码块。
- 当只希望防止多个线程同时访问方法内部的部分代码块(而不是整个方法)时,通过这个方式分离出来的代码块称为“临界区”。
synchronized(syncObject){
//This code can be accessed
//by only one task at a time
}
21.3.6 在其他对象上同步
本对象的同步块:必须给定一个在其上进行同步的对象。最合理的方式:使用方法正在被调用的当前对象:synchronized(this){}
其他对象上的同步块:有时必须在另外一个对象上同步,此场景下,必须保证所有相关的任务都在同一个对象上同步。synchronized(otherObject){}
21.3.7 线程本地存储
它为每个线程都对这个变量附了一个id,确保你拿到的是你个人专享
- 防止共享资源冲突的方法二:根除对变量的共享
- 线程本地存储:是一种自动化机制,可以为相同变量的每个线程都创建不同的存储。
- ThreadLocal类:实现创建&管理线程的本地存储。ThreadLocal value;
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random();
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public static void increment() {
value.set(value.get()+1);//会将参数插入到为其线程存储的对象中,并返回存储中原有的对象
}
public static int get() {
return value.get();//将返回与其线程相关联的对象的副本
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++) {
exec.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
exec.shutdown();
}
}
public class Accessor implements Runnable {
private int id;
public Accessor(int idn) {
this.id = idn;
}
@Override
public void run() {
// TODO 自动生成的方法存根
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#"+id+":"+ThreadLocalVariableHolder.get();
}
}
21.4 终结任务
21.4.1 装饰性花园
21.4.2 在阻塞时终结
线程状态
1.新建(new):当线程被创建时,只短暂的处于此状态;此时已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。
2.就绪(Runnable):在此状态,只要调度器将时间片分配给线程,线程就会运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。
3.阻塞(Blocked):线程能够运行,但是某个条件阻止它的运行。当线程处于阻塞状态时,调度器会忽略此线程,不会分配线程任何 CPU时间。直到线程重新进入就绪状态,它才有可能执行操作。
4.死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是run()方法返回,但是任务的线程可以被中断。
一个任务进入阻塞状态的原因:
- 通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定时间内不会运行,仅让出cpu执行权,而不会释放锁。
- 通过调用wait()使线程挂起。直到线程得到notify()或notifyAll()消息,线程进入就绪状态,既会让出cpu执行权,也会释放锁。
- 任务在等待某个输入/输出完成
- 任务视图在某个对象上调用其同步控制方法,但对象锁不可用,因为另一任务已经获取了这个锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YCN4nt3-1606717866502)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1595035928915.png)]
21.4.3 中断
方法1:Thread.interrupt():可以终止被阻塞的任务;此方法将设置线程的中断状态。
方法2:Executor.shutdownNow(),它将发送一个interrupt()调用给它启动的所有线程。
方法3:Future.cancel():是一种中断由Executor启动的单个线程的方式。
Executor 通过调用submit()而不是excutor()来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型的Future<?>,持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定任务。
被互斥所阻塞
21.4.4 检查中断
21.5 线程之间的协作
线程协作:当使用线程来同时运行多个任务时,可以通过使用锁(同步)来同步不同任务的行为,从而使得一个任务不会干涉另一个任务的资源。即不同任务交替访问某个共享资源(通常是内存),可以使用互斥使得任何时刻只有一个任务可以访问这项资源。
任务间握手:任务协作的关键问题,可以通过Object的wait()¬ify(),或者Condition对象的await()&signal()方法来实现。
21.5.1 wait()与notifyAll()
wait():使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。–可避免采用忙等待的方式。
忙等待(空循环):不断地进行空循环,这被称为忙等待, 通常是一种不良的周期使用方式。
notify()或notifyAll() :发生时,即表示发生了某些感兴趣的事物,这个任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种在任务之间对活动同步的方式。
sleep()&wait()的区别:
sleep():调用时,锁没有被释放,调用yield()也属于此情况;
wait():将释放锁(并且线程被挂起),意味着另外一个任务可以获得这个锁;并且再通过notify()或notifyAll()或者时间到期,则会从wait()中恢复执行。
wait(), notify()以及notifyAll()有一个比较特殊的方面:那就是这些方法是基类Object的一个部分,而不是属于Thread的一部分。
错失的信号:
当两个线程使用notify()+wait() 或者notifyAll()+wait()进行协作时,可能会错过某个信号。
21.5.2 notify()与notifyAll()
当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。
21.5.3 生产者与消费者
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B1hA9mCy-1606717866506)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1595316163103.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDoOsyBe-1606717866507)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1595317325660.png)]
使用显式的Lock和Condition对象:
显式Lock:Lock lock = new ReentrantLock();
Condition对象:使用互斥并允许任务挂起的基本类。
Condition.await():挂起任务(类似于Object.wait())
Condition.signal()&signalAll():外部条件变化时,通知这个&所有任务,从而唤醒对应任务(类似于Object.notify() ¬ifyAll())
21.5.4 生产者-消费者与队列
- 方法1:使用wait()和notifyAll()
- 方法2:使用高级容器(同步队列):LinkedBlockingQueue(无界队列) 或者ArrayBlockingQueue(固定尺寸,可在被阻塞之前,向其中放置有限数量元素。
这种同步队列若消费者任务视图从队列中获取对象,而该队列为空,那这队列还可挂起消费者任务,当有更多元素可用时恢复消费者任务,相比wait()好太多。
21.6 死锁
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
由Edsger Dijkstrar提出的哲学家就餐问题是一个经典的死锁例证。要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:
- 互斥条件。任务使用的资源中至少有一个是不能共享的。这里,一根Chopstick(筷子)一次就只能被一个Philosopher(哲学家)使用。
- 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。也就是说,要发生死锁,Philosopher必须拿着一根Chopstick并且等待另一根。
- 资源不能被任务抢占,任务必须把资源释放当作普通事件。Philosopher很有礼貌,他们不会从其他Philosopher那里抢占Chopstick。
- 必须有循环等待,这时,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。在DeadlockingDiningPhilosophers.java中,因为每个Philosopher都试图先得到右边的Chopstick,然后得到左边的Chopstick,所以发徨了循环等待。
备注:所以要防止死锁的话,只需破坏其中一个即可。防止死锁最容易的方法是破坏第4个条件。
21.7 新类库中的构件
两个任务 a,b执行任务 必须由a先执行
21.7.1 CountDownLatch(减法计数门闩)
适用场景:它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。即一个或多个任务需要等待,等待到其它任务,比如一个问题的初始部分,完成为止。
你可以向CountDownLatch对象设置一个初始值,任何在这个对象上调用wait()的方法都将阻塞,直到这个计数值到达0.其他因结束其工作时,可以在访对象上调用countDown()来减小这个计数值。CountDownLatch被设计为只解发一次,计数值不能被重置。如果你需要能够重置计数值的版本,则可以使用CyclicBarrier。
调用countDown()的任务在产生这个调用时并没有被阻塞,只有对await()的调用会被阻塞,直至计数值到达0。
CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决任务,并创建值为n的CountDownLatch。当每个任务完成时,都会在这个锁存器上调用countDown()。等待问题被解决的任务在这个锁存器上调用await(),将它们自己挂起,直至锁存器计数结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0bVX7vf0-1606717866509)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1595820822342.png)]
21.7.2 CyclicBarrier(栅栏)
**适用场景:**你希望创建一组任务,它们并行地执行工作,然后在进行下一下步骤之前等待,直至所有任务都完成(看起来有些像Join())。它使得所有的并行任务都将在栅栏处列队,因此可以一致地向前移动。
栅栏动作是作为匿名内部类来使用的。
例如程序赛马程序:HorseRace.java
备注:CountDownLatch&CyclicBarrier的区别:
CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;
CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。
21.7.3 DelayQueue
DelayQueue:是一个无界的BlockingQueue(同步队列),用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。
这种队列是有序的,即队头对象是最先到期的对象。如果没有到期的对象,那么队列就没有头元素,所以poll()将返回null(也正因为此,我们不能将null放置到这种队列中)。
如上所述,DelayQueue就成为了优先级队列的一种变体。
21.7.4 PriorityBlockingQueue
PriorityBlockingQueue:这是一个很基础的优先级队列,它具有可阻塞的读取操作。
这种队列的阻塞特性提供了所有必需的同步,所以你应该注意到了,这里不需要任何显式的同步。–不必考虑当你从这种队列中读取时,其中是否有元素,因为这个队列在没有元素时,将直接阻塞读取者。
21.7.5 使用ScheduledExecutor的室温控制器
“温室控制系统”可以被看作是一种并发问题,每个期望的温室事件都是一个预定时间运行的任务。
ScheduledThreadPoolExecutor可以解决这种问题:
schedule() 用来运行一次任务
scheduleAtFixedRate() 每隔规定的时间重复执行任务。
两个方法接收delayTime参数。可以将Runnable对象设置为在将来的某个时刻执行。
21.7.6 Semaphre(计数信号量)
正常的锁:(来自concurrent.locks显式锁,或者内建的synchronized锁):在任何时刻只允许一个任务访问。
Semaphre(计数信号量):允许n个任务同时访问这个资源。可以看作向外分发使用资源的许可证,尽管实际上并没有任何许可证对象。
21.7.7 Exchanger
Exchanger:在两个任务之间交换对象的栅栏,完成线程间的数据交互。
典型场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。通过这个方式,可以有更多的对象在被创建的同时被消费。
21.8 仿真
21.8.1 银行出纳员仿真(ArrayBlockingQueue)
21.8.2 饭店仿真(SynchronousQueue)
21.8.3 分发工作(LinkedBlockingQueue)
不同BlockingQueue的区别:
1.ArrayBlockingQueue:是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
2.LinkedBlockingQueue:是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
3.SynchronousQueue:没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
21.9 性能调优
21.9.1 比较各类互斥技术
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eiAwvOwU-1606717866510)(C:\Users\srqnk\AppData\Roaming\Typora\typora-user-images\1595312994970.png)]
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
// TODO 自动生成的方法存根
for(;;) {
synchronized(this) {
if(tickets>0) {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"窗口正在出售第"+tickets+"张票");
tickets--;
}
}
}
}
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
21.9.2 免锁容器
ConcurrentHashMap、ConcurrentLinkedQueue
利用了CopyOnWriteArrayList实现了免锁行为
允许并发的读取和写入,但容器中只有部分内容而不是整个容器可被复制和修改。
21.9.3 乐观加锁
乐观锁:,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
21.9.4 ReadWriteLock
- 只允许一个线程写入(其他线程既不能写入也不能读取);
start();
}
}
### 21.9.2 免锁容器
ConcurrentHashMap、ConcurrentLinkedQueue
利用了CopyOnWriteArrayList实现了免锁行为
允许并发的读取和写入,但容器中只有部分内容而不是整个容器可被复制和修改。
### 21.9.3 乐观加锁
乐观锁:,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
### 21.9.4 ReadWriteLock
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。