并发是用于多处理器编程的基本工具。Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他的任务。
并发程序使我们可以将程序划分为多个分离的、独立运行的任务。通过多线程机制,这些独立任务中的每一个都将由执行线程来驱动。在使用线程时,CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己一直占用CPU,但事实上CPU时间是划分成片段分配给了所有任务。
1、基本的线程机制
1.1 Thread类
public class LiftOffThread extends Thread {
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOffThread () { }
@Override
public void run() {
while (countDown-- > 0) {
System.out.print("#" + id + " (" + (countDown > 0 ? countDown : "LiftOff!") + "), ");
Thread.yield();
}
}
public static void main(String[] args) {
LiftOffThread t = new LiftOffThread ();
t.start();
System.out.println("waiting for liftoff!");
}
}
1.2 Runnable接口
想要定义任务,只需要实现Runnable接口并实现编写run()方法,使得该任务可以执行命令,并将Runnbale对象提交给Thread构造器。
public class LiftOffRunnable implements Runnable {
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOffRunnable () { }
@Override
public void run() {
while (countDown-- > 0) {
System.out.print("#" + id + " (" + (countDown > 0 ? countDown : "LiftOff!") + "), ");
Thread.yield();
}
}
public static void main(String[] args) {
Thread t = new Thread(new LiftOffRunnable ());
t.start();
System.out.println("waiting for liftoff!");
// LiftOffRunnable liftOff = new LiftOffRunnable();
// liftOff.run();
}
}
运行结果:
waiting for liftoff!
#0 (9), #0 (8), #0 (7), #0 (6), #0 (5), #0 (4), #0 (3), #0 (2), #0 (1), #0 (LiftOff!),
标识符id可以区分任务的多个实例,因为是final的,一旦初始化之后不能修改。Thread.yiled()可以将CPU从一个线程转移给另一个线程。
1.3 Executor
java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。使用线程池的优点有:重用存在的线程,减少对象创建、消亡的开销;可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞;提供定时执行、定期执行、单线程、并发数控制等功能。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0 ; i<5 ; i++){
executorService.execute(new LiftOffRunnable());
}
executorService.shutdown();
}
运行结果:
#0 (9), #1 (9), #1 (8), #0 (8), #1 (7), #1 (6), #0 (7), #1 (5), #1 (4),
#1 (3), #1 (2), #1 (1), #1 (LiftOff!), #0 (6), #0 (5), #0 (4), #0 (3),
#0 (2), #0 (1), #0 (LiftOff!), #4 (9), #4 (8), #4 (7), #4 (6), #3 (9),
#2 (9), #3 (8), #2 (8), #3 (7), #2 (7), #3 (6), #2 (6), #3 (5), #2 (5),
#3 (4), #2 (4), #3 (3), #2 (3), #3 (2), #2 (2), #3 (1), #2 (1), #3 (LiftOff!),
#2 (LiftOff!), #4 (5), #4 (4), #4 (3), #4 (2), #4 (1), #4 (LiftOff!),
shutdown()方法的调用可以防止新任务被提交给这个Executor。
CachedThreadPool()在程序执行过程中通常会创建与所需数量相同的线程,然后再它回收旧线程时停止创建新线程,因此是合理的Executor首选。只有当这种方式会引发问题时,才需要切换至FixedThreadPool。
Executors提供四种线程池:
1)newFixedThreadPool:创建固定数目线程的线程池。
2)newCachedThreadPool:创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
3)newSingleThreadExecutor:创建一个单线程化的Executor。
4)newScheduledThreadPool:创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
1.4 Callable接口–从任务中产生返回值
如果希望任务完成时能够返回一个值,那么可以实现Callable接口的call()方法,且必须使用ExecutorService.submit()方法调用它。submit()方法会产生Future对象,使用get()方法可以获取结果,如果Future没有执行完成,get()方法将阻塞,直至结果准备就绪。
public class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
return "result of TaskWithResult " + id;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<>();
for (int i = 0; i < 5; i++) {
results.add(executorService.submit(new TaskWithResult(i)));
}
for (Future<String> future : results) {
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
}
1.5 休眠
影响任务行为的一种简单方式是调用sleep(),这将使任务中止执行给定的时间。
TimeUnit.MILLISECONDS.sleep(100);
Thread.sleep(100);
1.6 优先级
线程的优先级将该线程的重要性传递给了调度器,调度器将倾向于让优先级最高的线程先执行。
Thread.currentThread().setPriority(Thread.MAX_PROORITY);
一般只使用MAX_PROORITY、NORM_PROORITY、MIN_PROORITY三种级别。
注意,优先级是在run()方法开头部分设定的,在构造器中设置它们不会有好处,因为executor在此刻还没有开始执行任务。
1.7 让步
Thread.yiled()给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用CPU了(但是仅仅是一个暗示,不能保证一定会被采纳)。
1.8 后台线程
后台线程指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的一部分。因此,当所有非后台程序结束时,程序也就终止了,同时还会杀死进程中的所有后台线程。反过来,只要有任何非后台线程还在运行,程序就不会终止。使用setDaemon(true)方法,将线程设置为后台线程。一个后台线程创建的所有线程将被自动设置为后台线程。后台线程有可能在不执行finally子句的情况下就会终止其run()方法。
1.9 加入一个线程
如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
2、 共享受限资源
2.1 解决资源共享竞争
你永远都不知道一个线程在何时运行。假如你坐在桌子边正要去吃盘子中的最后一片肉,就在你的叉子快要够着它时,这片肉消失了,因此你的线程被挂起,而另一个餐者进入并吃掉了最后一片肉。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。
1)synchronized
Java提供关键字synchronized的形式,为防止冲突而提供的内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。声明方式如下所示:
synchronized void f() {/* … */}
synchronized void g() {/ *… */}
如果某个任务对对象调用了f(),对于同一个对象而言,就只能等待f()调用结束并释放了锁之后,其它任务才能调用f()和g()。因此,对于某个特定对象而言,其所有的synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。
在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数器变为0。在任务第一次给对象加锁的时候,计数器变为1。当这个相同的任务在这个对象上获得锁时,计数器都会递增。显然,只有先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为0时,锁被完全释放,此时别的任务就可以使用此资源。
2)显式锁
Java.util.concurrent.locks中有显式的互斥机制,Lock对象必须被显式地创建、锁定和释放。与内置锁相比,代码缺少优雅性,但是对于解决某些类型的问题来说,它更加灵活。使用方式如下所示:
public class test{
private Lock lock = new ReentrantLock();
public int next(){
lock.lock();
try{
doSomethig();
return value;
}finally{
lock.uncock();
}
}
}
注意:return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。
如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常,但是你没有机会做任何清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,就可以使用finally子句将系统维护在正确的状态了。
通常我们使用synchronized关键字,该方式代码量少,且出错的可能性降低,只有在解决特殊问题时才会使用显式的Lock对象。例如显式的Lock对象可以尝试着获取锁且最终获取锁失败,或者尝试着获取锁一段时间,然后放弃它,语法如下所示:
public class test{
private ReentrantLock lock = new ReentrantLock();
public void untimed(){
boolean captured = lock.tryLock();
try{
System.out.println("tryLock():" + captured);
}finally{
if (captured)
lock.uncock();
}
}
public void timed(){
boolean captured = false;
try{
captured = lock.tryLock(2, TimeUnit.SECONDS);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
try{
System.out.println("tryLock(2, TimeUnit.SECONDS):" + captured);
}finally{
if (captured)
lock.uncock();
}
}
}
显式的Lock赋予了更细粒度的控制力。
2.2 原子性与易变性
原子操作是不能被线程调度机制中断的操作。原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。由于JVM可以将64位(long和double)的读取和写入当做两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务看到不正确结果的可能性,有时被称为字撕裂,当定义long和double变量时,如果使用volatile关键字,就会获得原子性。
volatile关键字还确保了应用中的可视性。如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。如果一个域完全由synchronized方法或者语句块来防护,那就不必将其设置为volatile的。一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么就不需要将其设置为volatile的。
1)主内存与工作内存
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。它们之间的最大区别就是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
2)实例
public class Atomicity{
int i;
void f1() { i++; }
void f2() { i+=3; }
}
在Java中这种操作不是原子性的,从JVM指令中可以到出,如下:
void f1();
Code:
0: aload_0
1: dup
2: getField
5: iconst_1
6: iadd
7: putfield
10: return
void f2();
Code:
0: aload_0
1: dup
2: getField
5: iconst_3
6: iadd
7: putfield
10: return
每条指令都会产生一个get和put,它们之间还有一些其他的指令。因此在获取和放置之间,另一个任务可能会修改这个域,所以,这些操作不是原子性的。
2.3 原子类
Java SE5引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供下面形式的原子性条件更新操作:
boolean compareAndSet(expectedValue, updateValue);
2.4 临界区
有时候,只希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为“临界区”,它使用synchronized关键字建立。如下所示:
synchronized(syncObject){
doSomething();
}
这也被称为同步控制块;在进入此代码之前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就需要等到锁被释放之后,才能进入临界区。使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。
2.5 线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程创建不同的存储。因此,如果有5个线程都要使用变量x所表示的对象,那么线程本地存储就会生成5个用于x的不同的存储块。创建个管理线程本地存储可以由Java.lang.ThreadLocal类来实现。
ThreadLocal对象通常当做静态域存储,在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容,其中get()方法将返回与其线程相关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。
3、终结任务
3.1 在阻塞时终结
线程的状态:
1)新建(new):当线程被创建时,它只是短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。
2)就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。
3)阻塞(Blocked):线程能够运行,但是某个条件阻止它的运行,当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入就绪状态,它才有可能执行操作。
a)等待阻塞:通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll(),线程才会进入就绪状态。
b)同步阻塞:任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
c)其他阻塞:调用sleep()使任务进入休眠状态,在指定的时间内不会运行;或者调用join()方法;任务在等待某个输入/输出完成。
4)死亡(Dead):处于死亡或者终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
3.2 中断
Thread类包含interrupt()方法,因此可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断操作状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断将被复位。Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式。
为了调用interrupt(),必须持有Thread对象。新的concurrent类库似乎在避免对Thread对象的直接操作,转而尽量通过Executor来执行所有操作。如果在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。如果使用Executor,那么通过submit()而不是executor()来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型Future<?>,其中有一个未修饰的参数,可以在其上调用cancel(),并因此可以用它来中断某个特定任务,f.cancel(true)。
I/O和在synchronized块上的等待是不可中断的。可以通过关闭底层资源以释放锁。
3.3 检查中断
中断状态可以通过interrupt()来设置,可以通过调用interrupted()来检查中断状态,这不仅可以表明interrupt()是否被调用过,而且还可以清除中断标志。清除中断标志可以确保并发结构不会就某个任务被中断这个问题通知你两次。
try{
while(!Thread.interrupted()){
doSomething():
}
}catch(InterruptedException e){
print("Exiting via InterruptedException");
}
正确清理资源是非常重要的,所有需要清理的对象创建操作的后面,都必须紧跟try-catch子句,从而使得无论run()循环如何退出,清理都会发生。
4、线程之间的协作
4.1 wait()与notifyAll()
wait()会在等待外部世界产生变化的时候将任务挂起,并且只有notify()或者notifyAll()发生时,即表示发生了感兴趣的事务,这个任务才会被唤醒并去检查所产生的变化。调用sleep()的时候并没有释放锁,调用yield()也属于这种情况。当一个任务在方法里面遇到对wait()的调用时,线程的执行将被挂起,对象的锁将被释放。
wait()的调用有两种方式:一种是接受毫秒数作为参数,与sleep里面的参数意义相同,不同的是:1)在wait()期间锁是释放的;2)可以通过notify()、notifyAll()或时间到期,从wait中恢复。第二种是常用wait()的形式不接受任何参数,无限等待下去,直至线程收到notify()或者notifyAll()消息。
wait()、notify()和notifyAll()都是基类Object的一部分,而不属于Thread的一部分。
错失的信号:
T1:
synchronized (sharedMonitor){
sharedMonitor.notify();
}
T2:
while (someCondition){
// point 1
synchronized (sharedMonitor){
sharedMonitor.wait();
}
}
假设T2对someCondition求值并发现为true,在 point 1线程可能切换至T1,而T1将执行其设置,并调用notify()。当T2得以继续执行时,时机已经太晚了,以至于不能意识到这个条件已经发生了变化,因此会盲目进入wait()。此时notify()将错失,而T2也将无限等待这个已经发送过的信号,从而产生死锁。正确写法如下所示:
synchronized (sharedMonitor){
while (someCondition){
sharedMonitor.wait();
}
}
使用notify()而不是notifyAll()是一种优化。使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果希望使用notify(),就必须保证被唤醒的是恰当的业务。当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。
4.2 生产者与消费者
两个任务在被生产和消费时进行握手,通过wait()、notify()或者notifyAll()实现,系统必须以有序的方式关闭。
wait()和notifyAll()以一种非常低端的方式解决了任务互操作问题,即每次交互时都握手。在很多情况下,可以使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在Java.util.concurrent.BlockingQueue接口中提供这个队列,可以使用LinkedBlockingQueue,这是一个无界队列;还可以使用ArrayBlockingQueue,具有固定尺寸。如果消费者任务试图从队列中获取对象,而该队列此时为空,那么这些队列可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。
4.3 任务间使用管道进行输入/输出
提供线程功能的类库以“管道”的形式对线程间的输入/输出提供了支持。在Java类库中对应的是PipedWriter类(允许任务向管道写)和PipedReader类(允许不同任务从同一个管道中读取)。这个模型可以看成是“生产者-消费者”问题的变体。管道基本上是一个阻塞队列。
5、死锁
某个任务在等待另一个任务,而后者又在等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁,被称为死锁。
哲学家就餐是一个经典的死锁例证。指定五个哲学家,这些哲学家将花部分时间思考,花部分时间就餐,他们思考的时候不需要任何共享资源,但是他们就餐时,将使用有限数量的餐具,作为哲学家,他们很穷,所以他们只能买五根筷子,他们围在桌子周围,每人之间放一根筷子。当一个哲学家要进餐时必须同时获得左边和右边的筷子。如果他左边或者右边的筷子已经有人在使用了,那么他必须等待,直至可得到筷子。如果哲学家花在思考上的时间非常少,那么筷子的竞争将非常激烈,很容易发生死锁。
产生死锁的条件有四条:
1)互斥条件:任务使用的资源中至少有一项是不能共享的。
2)请求和保持条件:至少有一个任务必须持有一个资源且在等待获取一个当前被别的任务持有的资源。
3)不剥夺条件:任务已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4)环路等待条件:必须循环等待。
要产生死锁,以上四个条件缺一不可,所以要防止死锁,只需要破坏其中一个即可。
6、新类库中的构件
Java SE5的Java.util.concurrent中引入了大量设计用来解决并发问题的新类。
6.1 CountDownLatch
CountDownLatch被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。可以向CountDownLatch设置一个初始计数值,任何在这个对象上调用await()的方法都将阻塞,直至这个计数值达到0。其他任务在结束其工作时,可以在该对象上调用countDown()来减小这个计数值。CountDownLatch被设计为只触发一次,计数值不能被重置。下面演示用法:
class TaskPortion implements Runnable {
private static int counter = 0;
private final int id = counter++;
private static Random rand = new Random(47);
private final CountDownLatch latch;
public TaskPortion(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
doWork();
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void doWork() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
System.out.println(this + "completed");
}
public String toString() {
return String.format("%1$-3d", id);
}
}
class WaitingTask implements Runnable {
private static int counter = 0;
private final int id = counter++;
private static Random rand = new Random(47);
private final CountDownLatch latch;
public WaitingTask(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
latch.await();
System.out.println("Latch barrier passed for " + this);
} catch (InterruptedException e) {
System.out.println(this + " interrupted");
}
}
public String toString() {
return String.format("WaitingTask %1$-3d", id);
}
}
public class testPart1 {
static final int SIZE = 100;
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(SIZE);
for (int i = 0; i < 10; i++) {
exec.execute(new WaitingTask(latch));
}
for (int i = 0; i < SIZE; i++) {
exec.execute(new TaskPortion(latch));
}
System.out.println("Launched all tasks");
exec.shutdown();
}
}
运行结果如下所示:
Launched all tasks
36 completed
23 completed
......
21 completed
74 completed
85 completed
93 completed
Latch barrier passed for WaitingTask 3
Latch barrier passed for WaitingTask 7
Latch barrier passed for WaitingTask 0
......
Latch barrier passed for WaitingTask 4
Latch barrier passed for WaitingTask 8
Latch barrier passed for WaitingTask 9
6.2 CyclicBarrier
CyclicBarrier适用于这种情况:当希望创建一组任务,它们并行的执行工作,然后在进入下一个步骤之前等待,直至所有任务都完成。它使得所有的并行任务都将在栅栏处队列,因此可以一致地向前移动。和CountDownLatch非常像,只是CountDownLatch只触发一次,而CyclicBarrier可以多次重用。
CyclicBarrier barrier = new CyclicBarrier(number, new Runnable(){…});
6.3 DelayQueue
DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中对象只能在其到期时才能从队列中取走。这个队列是有序的,即队头对象的延迟到期的时间最长。注意:不能将null元素放置到这种队列中。为了排序,Delayed接口还继承了Comparable接口,因此必须实现compareTo(),使其可以产生合理的比较。
6.4 PriorityBlockingQueue
PriorityBlockingQueue是一个很基础的优先级队列,具有可阻塞的读取操作。
6.5 Semaphore
Semaphore(计数信号量)允许n个任务同时访问同一个资源。可以将信号量看作是向外分发使用资源的“许可证”,实际上没有使用任何许可证对象。
Semaphore semaphore = new Semaphore(size,true);
semaphore.acquire();
semaphore.release();
6.6 Exchanger
Exchanger是在两个任务之间交换对象的栅栏。当这个任务进入栅栏时, 它们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。当调用Exchanger.exchanger()方法时,它将阻塞直至对方任务调用它自己的exchanger()方法时,这两个exchanger()方法将全部完成。
7、 性能调优
7.1 比较各类互斥技术
synchronized: 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。
ReentrantLock: ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
Atomic: 和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。
所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
7.2 免锁容器
免锁容器的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某一部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。
在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成时,一个原子性操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException,因此你不必编写特殊的代码去防范这种异常,就像你以前必须作的那样。
CopyOnWriteArraySet将使用CopyOnWriteArrayList来实现其免锁行为。
ConcurrentHashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,任何修改在完成之前,读取者仍旧不能看到它们。ConcurrentHashMap不会抛出ConcurrentModificationException异常。
CopyOnWriteArrayList适合用在“读多,写少”的“并发”应用中,换句话说,它适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在“扩容”的概念,每次写操作(add or remove)都要copy一个副本,在副本的基础上修改后改变array引用,所以称为“CopyOnWrite”,因此在写操作是加锁,并且对整个list的copy操作时相当耗时的,过多的写操作不推荐使用该存储结构。
一个ConcurrentHashMap由多个segment组成,每一个segment都包含了一个HashEntry数组的hashtable, 每一个segment包含了对自己的hashtable的操作,比如get,put,replace等操作,这些操作发生的时候,对自己的hashtable进行锁定。由于每一个segment写操作只锁定自己的hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个hashtable锁定的情况。
1)CopyOnWriteArrayList
构造器:
// 底层数据结构
private transient volatile Object[] array;
// 设置Object[]
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 创建了一个0个元素的数组
final void setArray(Object[] a) {
array = a;
}
添加元素:
// 在数组末尾添加元素
public boolean add(E e) {
// 1.上锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 2.获取当前的数组
Object[] elements = getArray();
// 3.获取当前数组长度
int len = elements.length;
// 4.复制新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 5.新数组的末尾元素设成e
newElements[len] = e;
// 6.设置全局array为新数组
setArray(newElements);
return true;
} finally {
// 7.解锁
lock.unlock();
}
}
// 获取数组
final Object[] getArray() {
return array;
}
获取元素:
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
删除元素:
public boolean remove(Object o) {
Object[] snapshot = getArray();
// 获取元素下标
int index = indexOf(o, snapshot, 0, snapshot.length);
// 移除元素
return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements, int index, int fence) {
// 元素为null
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
// 元素不为null
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
private boolean remove(Object o, Object[] snapshot, int index) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取原数组
Object[] current = getArray();
// 获取原数组长度
int len = current.length;
// 如果此时已经进行过写操作
// findIndex校准索引
if (snapshot != current) findIndex: {
//在当前代码获取锁时,有可能len已经小于index,故需要取二者中较小的;
int prefix = Math.min(index, len);
//情形1:[0,index)区间的元素有删除操作,导致index所指元素前移
for (int i = 0; i < prefix; i++) {
//如果元素引用已经替换
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//情形2:当前数组删除元素较多,导致数组长度小于原先index
if (index >= len)
return false;
//情形3:[0,index)区间的元素没有删除操作,index所指元素未移动
if (current[index] == o)
break findIndex;
//情形4:[0,index)区间有添加元素操作,导致index所指元素后移,重新定位元素索引
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1, newElements, index, len - index - 1);
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
ArrayList的remove使用了System.arraycopy(这是一个native方法),而这里没使用,所以理论上这里的remove的性能要比ArrayList的remove要低。
遍历元素:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
// 数组快照
private final Object[] snapshot;
// 数组索引
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() { return cursor < snapshot.length; }
public boolean hasPrevious() { return cursor > 0; }
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() { return cursor; }
public int previousIndex() { return cursor-1; }
public void remove() { throw new UnsupportedOperationException(); }
}
总结:
a)线程安全,读操作时无锁的ArrayList
b)底层数据结构是一个Object[],初始容量为0,之后每增加一个元素,容量+1,数组复制一遍
c)增删改上锁、读不上锁
d)遍历过程由于遍历的只是全局数组的一个副本,即使全局数组发生了增删改变化,副本也不会变化,所以不会发生并发异常
e)读多写少且脏数据影响不大的并发情况下,选择CopyOnWriteArrayList
2) Collections.synchronizedList()
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
// 构造函数
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
// 构造函数
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
// 重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
synchronized (mutex) {return list.equals(o);}
}
// 重写hashCode方法
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
// get方法
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
// set方法
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
// add方法
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
// remove方法
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
// 查找下标
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
// 查找下标
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
// addAll
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
// listIterator,必须由用户手动同步
public ListIterator<E> listIterator() {
return list.listIterator();
}
// listIterator,必须由用户手动同步
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
// subList
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex), mutex);
}
}
// replaceAll
@Override
public void replaceAll(UnaryOperator<E> operator) {
synchronized (mutex) {list.replaceAll(operator);}
}
// sort
@Override
public void sort(Comparator<? super E> c) {
synchronized (mutex) {list.sort(c);}
}
}
SynchronizedCollection:
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c;
final Object mutex; // 用于同步的对象
// 构造函数
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
// 构造函数
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
// size方法
public int size() {
synchronized (mutex) {return c.size();}
}
// isEmpty方法
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
// contains方法
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
// iterator,必须由用户手动同步
public Iterator<E> iterator() {
return c.iterator();
}
.......
}
这里面对元素操作的方法全都用了 synchronized 关键字修饰,而本身也只是调用了对应类的对应方法,所以说 Collections 类中为我们准备的线程安全的集合类就是把相关的方法都用 synchronized 关键字修饰了一下以保证线程安全。对于iterator方法需要由用户手动同步。
3) Collections.synchronizedMap()
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m;
final Object mutex; //用于同步的对象
// 构造函数
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
// 构造函数
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
// size方法
public int size() {
synchronized (mutex) {return m.size();}
}
// isEmpty方法
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
// containsKey方法
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
// containsValue方法
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
// get方法
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
// put方法
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
// remove方法
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
......
private transient Set<K> keySet;
private transient Set<Map.Entry<K,V>> entrySet;
private transient Collection<V> values;
public Set<K> keySet() {
synchronized (mutex) {
if (keySet==null) keySet = new SynchronizedSet<>(m.keySet(), mutex);
return keySet;
}
}
public Set<Map.Entry<K,V>> entrySet() {
synchronized (mutex) {
if (entrySet==null) entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
return entrySet;
}
}
public Collection<V> values() {
synchronized (mutex) {
if (values==null) values = new SynchronizedCollection<>(m.values(), mutex);
return values;
}
}
......
}
synchronizedMap同样把相关的方法都用 synchronized 关键字修饰了一下以保证线程安全。
4) ConcurrentHashMap
参考另一篇博客:https://blog.csdn.net/weixin_44331516/article/details/89227692
7.3 乐观
加锁
某些Atomic类还允许执行所谓的“乐观加锁”,当你计算某项计算时,实际上是没有互斥的,但是在这项计算完成,并且准备更新这个Atomic对象时,需要使用一个称为compareAndSet()的方法。将旧值和新值一起提交给这个方法,如果旧值与在Atomic对象中发现的值不一致,那么这个操作就失败–这意味着某个其他的任务已经于此操作执行期间修改了这个对象。
7.4 ReadWriteLock
ReadWriteLock对向数据结构相对不频繁写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。可允许同时有多个读取者,只要它们都不试图写入即可。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
WriteLock writeLock = lock.writeLock();
writeLock.lock();
writeLock.unlock();
ReadLock readLock = lock.readLock();
readLock.lock();
readLock.unlock();
ReadWriteLock是一个相当复杂的工具,只有当你在搜索可以提高性能的方法时,才应该想到它。