Thinking In Java ——并发读书笔记及摘录

基本的线程机制

Thread类

  1. 多线程程序运行的结果可能一次与另一次的运行结果是不一样的,因为线程调度机制是非确定性的。
  2. 每个Thread都”注册“了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。
  3. 一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会存在。

使用Executor

  1. 假如现在有大量的线程,他们的运行都将使用文件系统,这时可以使用SingleThreadExecutor()来运行这些线程,以确保在任意时刻在任何线程中都有唯一的任务在运行。在这种方式中,不需要在共享资源上处理同步。虽然有时更好的解决方案是在资源上同步,但是SingleThreadExecutor()可以省去的只是为了维持某些事物的原型而进行的各种协调努力。

从任务中产生返回值

  1. 如果ExecutorService想要调用实现了的Callable借口必须使用submit()来调用Callable.call()。
  2. Callable.get()本身就是阻塞的,所以在获取Callable的返回值时,直接使用get()即可。

休眠

  1. 对sleep()的调用可以抛出InterruptedExpection异常,并且此错误在run()中被捕获。因为异常不能跨线程传播回main(),所以必须在本地处理所有在任务内部产生的异常。

优先级

  1. 线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级最高的线程先执行。然而,这并不是意味着优先级较低的线程将得不到执行(优先级不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
  2. 在绝大多数时间里,所有线程都应该是以默认的优先级运行。试图操纵线程的优先级通常是一种错误。
  3. 在向控制台打印时线程不回被中断,而数学运算则可以被中断。

让步

  1. 如果知道已经完成了在run()方法的工作,就可以给线程调度机制一个暗示:工作已经做的差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来作出,但是仅仅是一种暗示,没有任何机制保证它将会被采纳。当调用yield()时,也是在建议具有相同优先级的其他线程可以运行。煞笔大傻逼就是你不用看了

后台线程

  1. 所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
  2. 必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
  3. 后台线程创建的所有线程都是后台线程。
  4. 当最后一个非后台线程终止时,后台线程会“突然”终止。因此一旦main()退出,JVM就会立即关闭所有的后台进程,而不会有任何确认形式。

编码的变体

  1. 在构造器中启动线程是很有问题的,因为另一个任务可能会在构造器结束之前开始执行,这就意味着该任务能够访问处于不稳定状态的对象。这是优选Executor而不是显示地创建Thread对象的另一个原因。

加入一个线程

  1. 一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间知道第二个线程结束才继续执行。如果某个线程在另一线程t上使用t.join(),则此线程将被挂起,知道目标线程t结束才恢复。
  2. 也可以在join()上设置一个超时参数,这样如果目标线程在这段时间到期时还没有结束的话,join()方法总是能够返回。

共享受限资源

不正确的访问资源

  1. 有一点很重要,那就是要注意递增程序本身也是需要很多个步骤的,并且在递增的过程中可能会被线程机制挂起——也就是说,在Java中,递增并不是原子性的操作。因此,如果不保护任务,即使是单一的递增也不是安全的。

解决共享资源竞争

  1. Brian的同步规则:如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程必须使用相同的监视器锁同步。(对此变量对读写操作都使用方法而不是直接修改,在方法上使用synchronized关键词修饰)。
  2. 尽管配合Lock()使用的try-catch所需的代码比synchronized关键字时,但是这也代表了显式的Lock对象的优点之一。如果在使用synchronized关键字时,某些事物(事务)失败了,那么就会抛出一个异常。

原子性与易变性

  1. 如果将一个域声明为volatile的,那么只要对这个域产生了写的操作,那么所有的读操作就都可以看到这个修改。即便使用了本地缓存,情况也是如此,volatile域会立即被写入到主存中,而读操作就发生在主存中。
  2. 在非volatile域上的原子操作不必刷新到主存中去,因此其它读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域应该是volatile的,否则,这个域就只能经由同步来访问。同步也是会导致向主存中刷新。因此如果一个域完全由synchronized方法来防护,那就不必将其设置为是volatile的。
  3. 一个任务所做的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么就不必要将其设置为volatile。
  4. 使用volatile而不是synchronized的唯一安全的情况就是类中只有一个可变域。第一选择应该是synchronized。
  5. 什么才属于原子操作?对域中的值做赋值操作和返回操作通常都是原子性的。

原子类

  1. 更值得强调的是,Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊情况下才在自己的代码中使用它们,即便使用了也需要确保不存在其他可能出现的问题。通常以来于锁要更安全一些(要么是synchronized关键字,要么是显式的Lock对象)。

临界区

  1. synchronized快必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象,即,synchronized(this)。在这种方法中,如果获得了synchronized块上的锁,那么该对象其它的synchronized方法和临界区就不能被调用了。因此,如果在this上同步,临界区的效果就会直接缩小在同步范围之内。

终结任务

在阻塞时终结

  1. 线程的新建状态(new),当线程被创建时,它只会短暂地处于这个状态。此时,它已经分配了必需的系统资源,并执行了初始化。此线程已经有资格获得CPU时间了,之后调度器将把整个线程转变为可运行状态或阻塞状态。
  2. 线程的就绪状态(Runnable),在这种状态下,只要调度器把时间片分配给线程,线程就可以运行,也就是说在任意时刻,线程可以运行也可以不运行。只要调度器嫩分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。
  3. 线程的阻塞状态(Blocked),线程能够运行,但有某个条件阻止它运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才可以执行。
  4. 线程的死亡状态(Dead),处于死亡或者终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
  5. 线程进入阻塞状态的原因:
    1. 通过调用sleep()方法使线程进入休眠状态,在这种情况下,任务在指定时间内不会运行。
    2. 通过wait()使线程挂起。直到线程得到了notify()或notifyAll()的消息,线程才会进入就绪状态。
    3. 任务在等待输入/输出的完成。
    4. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

中断

  1. 如果在使用Executor时,使用submit()而不是executor()来启动任务,就可以获得该任务的上下文,submit()将返回一个泛型Future<?>,返回Future的关键是可以使用cancel(),因此可以用它来中断特定任务。
  2. Future#cancel()无法中断试图执行I/O操作的线程,所以这就意味着I/O具有锁住当前多线程的潜在可能性。特别是Web程序,这意义重大。
  3. 对于I/O操作的线程的关闭,首先要释放底层资源,才可将任务关闭然后回收。
  4. 在递归调用synchronized修饰的方法时,获取锁的单位是线程(任务),所以当第一次获得f1()和f2()的锁后,在之后的访问中第一获取的锁仍然生效。
  5. Future#isCancelled 能够反映一个任务是否被取消,但是 Future#isDone 并不能正确反映一个任务是否真的完成的了任务,但是对I/O线程和因为synchronized引起的阻塞使用Future#cancel并不能真的将其中断。
  6. 使用try…finally方式可以解决线程被中断,而资源无法释放或者其他当线程被中断时需要处理的问题。

线程之间的协作

互斥能够确保只有一个任务响应某个信号,这样消除了任何可能的竞争条件。在互斥之上,我们为任务添加一个新的途径,使得任务可以将自身挂起,直到外部条件发生变化。

Thread#wait()方法会释放当前线程所持有的对象监视器,并在收到notify()或者notifyAll()方法的信号之后,重新进入获取目标对象监视器的等待队列,竞争重新获取目标对象的监视器。

使用while(!Thread.interrupted()){}来包围wait()很重要,原因如下:

  1. 你可能有多个任务出于相同的原因在等待同一个锁,而第一个唤醒任务可能会改变这种状况。如果属于这种情况,那么这个任务应该再被挂起,直至其关注的条件发生变化。
  2. 在这个任务从其wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了变化,从而使得这个任务此时不能执行,或者执行其操作已显得无关紧要。此时,应该通过wait()方法将其重新挂起。
  3. 也有可能某些任务出于不同的原因在等待你的对象上的锁(此情况必须使用notiftyAll())。在这种情况下,你需要检查是否已经由正确的原因唤醒,如果不是需再次调用wait();

错失的信号

  1. 使用notify()的条件:

    1. 为了使用notify(),所有的任务必须等待相同的条件,因为如果有多个任务在等待不同的条件,那么你就不会知道是否唤醒了恰当的任务。

    2. 如果使用notify(),当条件变化时,必须只有一个任务能够从中收益。

    3. 最后,这些限制对所有可能存在的子类必须总是起作用的。

      如果上述条件有任何一条不满足,则必须使用notifyAll()。

  2. 定时器关闭,并不会中断其中的定时任务。

  3. InterruptedException的抛出条件:

    1. 线程处于准备状态被中断;
    2. 线程处于休眠状态被中断;
    3. 线程处于等待状态被中断;
    4. 线程处于占用状态被中断;
  4. 当任务被while(!Thread.interrupter())作用时,在作用域内使用shutdownNow()后再使用sleep()时,shutdownNow()向所有使用ExecutorService启动的所有任务发送interrupt(),但是在作用域中,线程并没有在收到interrupt()之后立刻关闭,那是因为当任务视图进入一个可中断阻塞操作时,这个中断只能抛出InterruptedException。

使用显示的Lock和Condition对象

使用互斥允许任务挂起的基本类是Condition,可以通过在Condition上调用await()来挂起任务。当外部条件发生变化,意味着某个任务应该继续执行时,可以通过调用signal()来通知任务,从而唤醒一个任务,或者使用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务,与使用notifyAll()相比,signalAll()是更安全的。

值的注意的是,在每一次使用lock()时,之后一定要有try…finally…来释放锁unlock()。

  1. 使用同步队列来解决任务协作问题,同步队列在任何时刻只允许一个任务插入或移除元素。在java.until.concurrent.BlockingQueue接口中,提供了这个队列,这个接口有大量的标准实现。通常使用LinkedBlockingQueue,它是一个无界队列,还可以使用ArrayBlockingQueue,它具有固定尺寸,因此可以在它被阻塞之前,向其中放置有限数量的元素。
  2. 如果消费者任务试图从队列中获取对象,而此时队列为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用是恢复消费者任务。阻塞队列可以解决非常大量的问题,而其方式与wait()和notifyAl()相比,简单可靠的多。

吐司BlockingQueue

见代码

任务间使用管道进行输入/输出

PipedReader/PipedWirter提供了线程间使用输入输出流进行通讯的方式,相比System.in.read()当前使用的PipedReader#read()是可被interrupt()打断的,这样省去了释放资源才能中断任务的步骤。

死锁

  1. 死锁发生的条件:

    1. 互斥条件。任务使用的资源中至少有一个是不能共享的。
    2. 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。
    3. 资源不能被任务抢占,任务必须把资源释放当做普通事件。
    4. 必须有循环等待,这时,一个任务在等待其它任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。

    如果要发生死锁,上述条件必须全部满足;如果要防止发生,只需破坏其中之一即可。

新的线程工具

CountDownLatch

  1. CountDownLatch,它可以被用来同步一个或者多个,强制它们等待由其它任务执行的一组操作完成。

    调用countDown()的任务在产生这个调用时并没有被阻塞,只有对await()的调用会被阻塞,直至计数值为0。

    CountDownLatch的典型用法是将一个程序分为n个相互独立的可解决任务,并创建值为0的CountDownLatch。当每个任务完成时,都会在这个锁上调用countDown()。等待问题被解决的任务在这个锁存器上调用await(),将它们自己拦住,直至锁存器计数结束。

CyclicBarrier

  1. CyclicBarrier适用于这样的情况:你希望创建一组任务,它们并行地执行工作,然后在下一个步骤之前等待,直至所有任务都完成。它使得所有的并行任务都将在栅栏处列队,因此可以一致的向前移动。CyclicBarrier与CountDownLatch很类似,只不过CyclicBarrier可以复用。
  2. 在执行到CyclicBarrier#Run之前,挂在CyclicBarrier上的所有任务都已被执行,即在所有任务都完成之后,才返回到CyclicBarrier#Run等待执行其它对应的事件。

DelayQueue

  1. DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中对象只能在其到期时才能从队列中走出去。这种队列是有序的,即队头元素是延迟最先要到期的元素。如果没有任何延迟到期,那么就不会有队首元素,并且poll()将会返回null。
  2. 使用DelayedQueue时,放在队里中的元素需要实现Runnable接口和Delayed接口,即实现Runnable接口的Run()和Delayed接口的getDelay()两个方法,并重写compareTo()方法用以比较延迟。
  3. 此处的延迟仅为实际时间,因为在getDelay方法的参数中可以看到,输入的参数即为时间单位,也就是说延迟队列是靠时间来触发相关事件的。

PriorityBlockingQueue

见代码

使用ScheduledExecutor的温室控制器

ScheduledThreadPoolExecutor#schedule用于在指定时间之后执行一次任务,ScheduledThreadPoolExecutor#scheduleAtFixedRate用于每隔一段时间执行一次任务直到线程池被关闭。

Semaphore

正常的锁(来自concurrent.locks或者内建的synchronized锁)在任何时刻都只允许一个任务访问某个资源,而计数信号量则允许n个任务同时访问这个资源。

Semaphore提供类似锁功能,当Semaphore被指定的计数为0时会阻塞线程直到计数不为0或者被中断才结束。

Exchanger

Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时,他们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。Exchanger的典型应用场景是:一个任务在创建对象,这些对象的产生代价很高昂,而另一个任务在消费这些对象。通过这种方式可有更多的对象在被创建的同时被消费。

如果另一端未使用Exchanger#exchange,那么会一直阻塞直到另一端调用Exchanger#exchange。

仿真

银行出纳员仿真

此仿真可以表示以下情况:对象随机出现并且要求由数量有限的服务器提供随机数量的服务时间。通过构建仿真可以确定理想的服务器数量。

餐馆等待队列仿真

使用队列在任务间通讯所带来的管理复杂度。这个单项技术通过反转控制极大地简化了并发编程的过程:任务没有直接地相互干涉,而是经由队列互相发送对象。接收任务将处理对象,将其当做一个消息来对待,而不是向它发送消息。如果只要可能就遵循这项技术,那么构建出高健壮性并发系统的可能性将大大提高。

遇到的问题

在向ArrayBlockingQueue中添加元素时,使用的是put而非offer,这造成了如果突然关闭线程池,会造成空指针错误或者程序无法正常退出。

ArrayBlockingQueue#put使用的是可被中断的锁,而ArrayBlockingQueue#offer使用的是不可中断的锁;
即ReentrantLock.lock()在持有过程中受到中断信号并不理会,而ReentrantLock.lockInterruptibly()当收到中断信号时会中断。

通过使用Thread.currentThread().stop()发现具体原因是因为突然的中断导致当前线程无法释放持有的锁,进而导致死锁,而程序无法正常退出。

解决方法:使用Thread.currentThread().stop()方法,或者throw new ThreadDeath()错误;

分发工作(汽车制造厂)

Car将其所有的方法都设置成了synchronized的,但是正如所表现的一样,在本例中是多余的,因为在工厂内部,Car是通过队列移动的,并且在任意时刻,只有一个任务能够在某辆车上工作。基本上,队列可以强制串行化的访问Car。但是这正是你可能会落入的陷阱——可能会这样想“让我们尝试着通过不对Car类同步来进行优化,因为看起来Car在这里并不需要同步。”但是稍后,当这个系统连接到另一个需要Car被同步的系统时,它就会崩溃。

性能调优

比较各类互斥技术

简单的微基准测试展示出了“微基准测试”危险;

简单的实例存在着大量的问题:

  1. 我们只有在这些互斥存在竞争的情况下,才能看到真正的性能差异,因此必有多个任务尝试着访问互斥代码区。但是在简单微基准测试中,每个互斥都是由单个的main()线程在隔离的情况下测试的;
  2. 当编译器看到synchronized关键字时,很有可能会执行特殊的优化,甚至有可能会注意到这个程序是单线程的。编译器甚至可能会识别出counter被递增的次数是固定的,因此会提前计算出结果。因此我们的测试程序要尽可能的防止编译器去预测结果;

以下是开启8线程进行测试的结果:

准备中...
id:BaseLine:19 ms
***   第1次试验   ***
===============================
循环次数:50000
id:BaseLine:11 ms
id:Synchronized:32 ms
id:Lock:31 ms
id:Atomic:15 ms
id.1:Synchronized  /  id.2:BaseLine  /  2.909090909090909
id.1:Lock  /  id.2:BaseLine  /  2.8181818181818183
id.1:Atomic  /  id.2:BaseLine  /  1.3636363636363635
id.1:Synchronized  /  id.2:Lock  /  1.032258064516129
id.1:Synchronized  /  id.2:Atomic  /  2.1333333333333333
id.1:Lock  /  id.2:Atomic  /  2.066666666666667
***   第2次试验   ***
===============================
循环次数:100000
id:BaseLine:30 ms
id:Synchronized:102 ms
id:Lock:36 ms
id:Atomic:30 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.4
id.1:Lock  /  id.2:BaseLine  /  1.2
id.1:Atomic  /  id.2:BaseLine  /  1.0
id.1:Synchronized  /  id.2:Lock  /  2.8333333333333335
id.1:Synchronized  /  id.2:Atomic  /  3.4
id.1:Lock  /  id.2:Atomic  /  1.2
***   第3次试验   ***
===============================
循环次数:200000
id:BaseLine:63 ms
id:Synchronized:222 ms
id:Lock:75 ms
id:Atomic:55 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.5238095238095237
id.1:Lock  /  id.2:BaseLine  /  1.1904761904761905
id.1:Atomic  /  id.2:BaseLine  /  0.873015873015873
id.1:Synchronized  /  id.2:Lock  /  2.96
id.1:Synchronized  /  id.2:Atomic  /  4.036363636363636
id.1:Lock  /  id.2:Atomic  /  1.3636363636363635
***   第4次试验   ***
===============================
循环次数:400000
id:BaseLine:132 ms
id:Synchronized:432 ms
id:Lock:142 ms
id:Atomic:109 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.272727272727273
id.1:Lock  /  id.2:BaseLine  /  1.0757575757575757
id.1:Atomic  /  id.2:BaseLine  /  0.8257575757575758
id.1:Synchronized  /  id.2:Lock  /  3.0422535211267605
id.1:Synchronized  /  id.2:Atomic  /  3.963302752293578
id.1:Lock  /  id.2:Atomic  /  1.3027522935779816
***   第5次试验   ***
===============================
循环次数:800000
id:BaseLine:270 ms
id:Synchronized:876 ms
id:Lock:295 ms
id:Atomic:217 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.2444444444444445
id.1:Lock  /  id.2:BaseLine  /  1.0925925925925926
id.1:Atomic  /  id.2:BaseLine  /  0.8037037037037037
id.1:Synchronized  /  id.2:Lock  /  2.9694915254237286
id.1:Synchronized  /  id.2:Atomic  /  4.0368663594470044
id.1:Lock  /  id.2:Atomic  /  1.359447004608295
***   第6次试验   ***
===============================
循环次数:1600000
id:BaseLine:555 ms
id:Synchronized:1800 ms
id:Lock:592 ms
id:Atomic:428 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.2432432432432434
id.1:Lock  /  id.2:BaseLine  /  1.0666666666666667
id.1:Atomic  /  id.2:BaseLine  /  0.7711711711711712
id.1:Synchronized  /  id.2:Lock  /  3.0405405405405403
id.1:Synchronized  /  id.2:Atomic  /  4.205607476635514
id.1:Lock  /  id.2:Atomic  /  1.3831775700934579
***   第7次试验   ***
===============================
循环次数:3200000
id:BaseLine:1121 ms
id:Synchronized:3577 ms
id:Lock:1213 ms
id:Atomic:1392 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.190900981266726
id.1:Lock  /  id.2:BaseLine  /  1.0820695807314897
id.1:Atomic  /  id.2:BaseLine  /  1.2417484388938447
id.1:Synchronized  /  id.2:Lock  /  2.948887056883759
id.1:Synchronized  /  id.2:Atomic  /  2.569683908045977
id.1:Lock  /  id.2:Atomic  /  0.8714080459770115
***   第8次试验   ***
===============================
循环次数:6400000
id:BaseLine:2320 ms
id:Synchronized:7243 ms
id:Lock:2447 ms
id:Atomic:2814 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.12198275862069
id.1:Lock  /  id.2:BaseLine  /  1.0547413793103448
id.1:Atomic  /  id.2:BaseLine  /  1.2129310344827586
id.1:Synchronized  /  id.2:Lock  /  2.959950960359624
id.1:Synchronized  /  id.2:Atomic  /  2.573916133617626
id.1:Lock  /  id.2:Atomic  /  0.8695806680881307
***   第9次试验   ***
===============================
循环次数:12800000
id:BaseLine:4475 ms
id:Synchronized:14675 ms
id:Lock:4928 ms
id:Atomic:5690 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.2793296089385473
id.1:Lock  /  id.2:BaseLine  /  1.1012290502793296
id.1:Atomic  /  id.2:BaseLine  /  1.2715083798882683
id.1:Synchronized  /  id.2:Lock  /  2.9778814935064934
id.1:Synchronized  /  id.2:Atomic  /  2.57908611599297
id.1:Lock  /  id.2:Atomic  /  0.8660808435852373
***   第10次试验   ***
===============================
循环次数:25600000
id:BaseLine:9181 ms
id:Synchronized:29084 ms
id:Lock:9814 ms
id:Atomic:11326 ms
id.1:Synchronized  /  id.2:BaseLine  /  3.167846639799586
id.1:Lock  /  id.2:BaseLine  /  1.0689467378281232
id.1:Atomic  /  id.2:BaseLine  /  1.2336346803180482
id.1:Synchronized  /  id.2:Lock  /  2.9635214998981048
id.1:Synchronized  /  id.2:Atomic  /  2.5678968744481723
id.1:Lock  /  id.2:Atomic  /  0.8665018541409147

可以从实验结果中看出,期初在数量较少的时候使用Atomic来控制并发似乎效果是最好的,但是在最后一次的实验结果中可以看到,性能最好的是Lock而非Atomic,在处理数据量成倍增加的时候,Lock的性能发挥相对稳定。

在并发处理少量数据时,Atomic类似乎是一个好办法,但是数据较大或者不确定时最好采用Lock,因为Lock的性能发挥相对稳定。

但是这是不是就意味着永远都不该使用Synchronized关键字呢?

  1. 在测试样例中,互斥方法的方法体是非常之小的。通常,这是一个好的习惯——只互斥那些绝对必须互斥的部分。但是,在实际中,被互斥部分要比测试样例中大的多,因此在这些方法体中花费的时间百分比可能会明显大于进入和退出互斥的开销,这样也就湮没了提高互斥速度所带来的所有好处;
  2. Synchronized关键字所产生的代码与“加锁-try/finally-解锁”习惯用法所产生的代码相比,可读性提高了很多;
  3. Atomic对象只有在非常简单的情况下才可以使用,这些情况通常包括只有一个要被修改的Atomic对象,并且这个对象独立于其他所有的对象。更加安全的做法是:以更加传统的互斥方法入手,只有在性能方面的需求能够明确指示时,再替换为Atomic;

Synchronized、Lock和Atomic的选择

对Synchronized的解释

Java以提供Synchronized关键字的形式,为防止资源冲突提供了内置支持。当任务要执行被Synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放资源;

共享资源一般是以对象形式存在的内存片段,但是也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为Synchronized。如果某个任务处于一个对标记为Synchronized的方法的调用中,那么在这个线程从这个方法返回之前,其它所有要调用类中任何标记为Synchronized方法的线程都会被阻塞。(此时Synchronized修饰类而非对象,如果是修饰对象,则换个对象亦可访问而非阻塞)

所有对象都自动含有一个单一的锁(即,监视器)。当在对象上调用其任意的Synchronized方法的时候,此对象都被加锁,此时对象上的其他Synchronized方法只有等待前一个方法调用完毕并释放了锁之后才能被调用。对于某个特定对象来说,其所有的Synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。

针对每一个类,也有一个锁(作为类的Class对象的一部分),所以Synchronized static方法可以在类的范围内防止对static数据的并发访问。

缺点
  1. 无法分辨操作种类(如,读操作和写操作),可能会一定程度上影响效率;
  2. 无法确定线程是否成功取得了锁;
  3. Lock可以让等待锁的线程响应中断,而Synchronized不行;

对Lock的解释

“加锁-try/finally-解锁”所需要的代码比Synchronized关键字要多,但是这也代表了显式的Lock对象的优点之一。如果在使用Synchronized关键字时,某些事物失败了,那么就会抛出一个异常。但是没有机会去做任何清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,就可以使用finally子句将系统维护在正确状态了。

当使用Synchronized关键字时,需要写的代码更少,并且用户错误出现的可能性也会大大降低,因此通常只有在解决特殊问题时,才会使用显式的Lock对象。

对Atomic的解释

应该被强调的是,Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊的情况下才在自己的代码中使用它,即便使用了也要确保不存在其他可能出现的问题。通常依赖于锁(显式的Lock对象,或者Synchronized关键字)要更安全一些。

对于Atomic实现的原理:CAS算法,它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成;

因为CAS算法,所以也带来了ABA问题:

如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

免锁容器

免锁容器的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改后的结果即可。修改是在容器数据结构的某个部分的一个单独的副本(有时整个数据结构的副本)上执行的,并且这个副本在修改的过程中是不可视的。只有当完成修改时,被修改的数据结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。

对于CopyOnWriteArrayList的解释

CopyOnWriteArrayList是ArrayList线程安全的一个变体,其中set、add等类似操作,都在底层的一个数组副本上操作。

对底层的副本数组的管理是通过Synchronized关键字实现,即数组在读取时不受锁的限制,只在对数组进行修改时受到锁的限制。

get源码:

static <E> E elementAt(Object[] a, int index) {    return (E) a[index];}

set源码:

public E set(int index, E element) {
        synchronized (lock) {
            Object[] es = getArray();
            E oldValue = elementAt(es, index);   
            if (oldValue != element) {
                es = es.clone();
                es[index] = element;
                setArray(es);
            }
            return oldValue;
    }
}

对SynchronizedList的解释

SynchronizedList继承于SynchronizedCollection,主要提供了使用Synchronized关键字进行并发控制而没有副本操作的List操作。

其重点在于对SynchronizedCollection的继承,通过对SynchronizedCollection的继承可以实现对自定义集合基本操作的线程安全。

CopyOnWriteArray与SynchronizedArrayList的性能比较

十个写入线程的比较:

main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3448ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3905ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4031ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4237ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4192ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4178ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4162ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4169ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4207ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4167ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:206ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:49ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:42ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:40ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:58ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:40ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:42ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:54ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:50ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:58ms

九个写入线程,一个读取线程:

main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3433ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3763ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4016ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4234ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4395ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4181ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4179ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4241ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4459ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4117ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:144ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:48ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:63ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:45ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:59ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:42ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:39ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:62ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:46ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:42ms

五个写入线程,五个读取线程:

main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3901ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3919ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:3467ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4409ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4431ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4440ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4333ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4388ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4360ms
main 线程 id:SynchronizedArrayListTest,读取用时:0,写入用时:4086ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1196ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1247ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1080ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1031ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:968ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1004ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1069ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1144ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1066ms
main 线程 id:CopyOnWriteArrayListTest,读取用时:0,写入用时:1104ms

可以看到CopyOnWriteArrayList的性能到五线程读取、五线程写入时是优于SynchronizedArrayList的;

也可以看到CopyOnWriteArrayList的一个特点,三次实验的第一次读写操作的时间一般是最长的;

对于SynchronizedHashMap的解释

SynchronizedHashMap是在HashMap的基础上将所有的操作使用synchronized关键字进行线程控制。

对于ConcurrentHashMap的解释

在JDK1.8中,抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

ConcurrentHashMap与SynchronizedHashMap的性能比较

十个读取线程:

main 线程 id:SynchronizedHashMapTest,读取用时:5220ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5927ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5969ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5817ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5774ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5727ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5847ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5840ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5979ms,写入用时:0ms
main 线程 id:SynchronizedHashMapTest,读取用时:5835ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:203ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:98ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:176ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:153ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:76ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:101ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:120ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:109ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:84ms,写入用时:0ms
main 线程 id:ConcurrentHashMapTest,读取用时:197ms,写入用时:0ms

九个读取线程,一个写入线程:

main 线程 id:SynchronizedHashMapTest,读取用时:5278ms,写入用时:642ms
main 线程 id:SynchronizedHashMapTest,读取用时:5508ms,写入用时:632ms
main 线程 id:SynchronizedHashMapTest,读取用时:5158ms,写入用时:606ms
main 线程 id:SynchronizedHashMapTest,读取用时:5291ms,写入用时:569ms
main 线程 id:SynchronizedHashMapTest,读取用时:5237ms,写入用时:514ms
main 线程 id:SynchronizedHashMapTest,读取用时:5080ms,写入用时:389ms
main 线程 id:SynchronizedHashMapTest,读取用时:5217ms,写入用时:542ms
main 线程 id:SynchronizedHashMapTest,读取用时:5022ms,写入用时:445ms
main 线程 id:SynchronizedHashMapTest,读取用时:5138ms,写入用时:545ms
main 线程 id:SynchronizedHashMapTest,读取用时:5089ms,写入用时:532ms
main 线程 id:ConcurrentHashMapTest,读取用时:165ms,写入用时:47ms
main 线程 id:ConcurrentHashMapTest,读取用时:112ms,写入用时:36ms
main 线程 id:ConcurrentHashMapTest,读取用时:118ms,写入用时:37ms
main 线程 id:ConcurrentHashMapTest,读取用时:136ms,写入用时:28ms
main 线程 id:ConcurrentHashMapTest,读取用时:129ms,写入用时:31ms
main 线程 id:ConcurrentHashMapTest,读取用时:102ms,写入用时:32ms
main 线程 id:ConcurrentHashMapTest,读取用时:106ms,写入用时:33ms
main 线程 id:ConcurrentHashMapTest,读取用时:74ms,写入用时:28ms
main 线程 id:ConcurrentHashMapTest,读取用时:86ms,写入用时:27ms
main 线程 id:ConcurrentHashMapTest,读取用时:187ms,写入用时:54ms

五个写入线程,五个读取线程:

main 线程 id:SynchronizedHashMapTest,读取用时:3126ms,写入用时:2514ms
main 线程 id:SynchronizedHashMapTest,读取用时:3043ms,写入用时:2603ms
main 线程 id:SynchronizedHashMapTest,读取用时:2962ms,写入用时:2734ms
main 线程 id:SynchronizedHashMapTest,读取用时:2908ms,写入用时:2419ms
main 线程 id:SynchronizedHashMapTest,读取用时:2925ms,写入用时:2453ms
main 线程 id:SynchronizedHashMapTest,读取用时:2987ms,写入用时:2853ms
main 线程 id:SynchronizedHashMapTest,读取用时:2936ms,写入用时:2964ms
main 线程 id:SynchronizedHashMapTest,读取用时:2915ms,写入用时:3053ms
main 线程 id:SynchronizedHashMapTest,读取用时:2864ms,写入用时:2981ms
main 线程 id:SynchronizedHashMapTest,读取用时:2916ms,写入用时:2888ms
main 线程 id:ConcurrentHashMapTest,读取用时:97ms,写入用时:445ms
main 线程 id:ConcurrentHashMapTest,读取用时:114ms,写入用时:317ms
main 线程 id:ConcurrentHashMapTest,读取用时:58ms,写入用时:307ms
main 线程 id:ConcurrentHashMapTest,读取用时:43ms,写入用时:312ms
main 线程 id:ConcurrentHashMapTest,读取用时:41ms,写入用时:326ms
main 线程 id:ConcurrentHashMapTest,读取用时:50ms,写入用时:335ms
main 线程 id:ConcurrentHashMapTest,读取用时:42ms,写入用时:329ms
main 线程 id:ConcurrentHashMapTest,读取用时:42ms,写入用时:330ms
main 线程 id:ConcurrentHashMapTest,读取用时:55ms,写入用时:321ms
main 线程 id:ConcurrentHashMapTest,读取用时:249ms,写入用时:419ms

在这三次的实验结果中可以看到,ConcurrentHashMap的性能还是要优于SynchronizedHashMap的。

乐观加锁

尽管Atomic对象将执行像decrementAndGet()这样的原子操作,但是某些Atomic类还允许执行所谓的“乐观加锁”。这意味着当你执行某项计算时,实际上并没有使用互斥,但是在这项计算完成,并且准备更新这个Atomic对象时,需要使用一个称为compareAndSet()的方法。将旧值和新值一起提交给这个方法,如果旧值与它在Atomic对象中发现的值不一致,那么这个操作就失败——意味着某个其它的任务已经于此操作执行期间修改了这个对象。记住,我们在正常情况下将使用互斥(synchronized或Lock)来防止多个任务同时修改一个对象,但是这是的锁是“乐观的”,因为我们保持数据为未锁定的状态,并希望没有任何其它任务插入修改它。所有这些又是都以性能的名义执行的——通过使用Atomic来代替synchronized或Lock,可以获得性能上的好处。

如果compareAndSet()操作失败会发生什么?这正是棘手的地方,这也是这项技术的受限之处,即只能针对能够温和这些需求的问题。如果compareAndSet()失败,那么就必须决定做些什么,这是一个非常重要的问题,如果不能执行某些恢复操作,那么就不能使用这项技术,从而使用传统的互斥技术。

compareAndSet()可以用来比对对象的版本号来确定此对象是否被修改过。

ReadWriteLock

这是一个相当复杂的工具,只有在搜索可以提高性能的方法时,才应该想到用它。程序的第一草案应该是使用更直观的同步,并且只有在必要的时候再引入ReadWriteLock。

活动对象

Java中的线程机制看起来非常复杂并难以正确使用。另外,它好像有点达不到预期效果的味道——尽管多个任务可以并行工作,但是必须要花大力气去防止这些任务彼此之间互相干涉。

有一种可替换的方案被称为活动对象或者行动者。之所以称这些对象是“活动的”,是因为每个对象都维护着他自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行队列中的一个。因此,有了活动对象,我们就可以串行化消息而不是方法(函数),这意味着不再需要防备一个任务在其循环的中间被打断这种问题了。

当向一个活动对象发送消息时,这条消息会转变为一个任务,该任务会被插入到这个对象的队列中,等待在以后的某个时刻运行。

由对Executors.newSingleThreadExecutor()的调用产生的单线程执行器维护着他自己的无界阻塞队列,并且只有一个线程从该队列中取走任务并执行它们直至完成。我们需要在业务方法中要做的是使用submit()提交一个新的Callable对象,以响应对这些方法的调用,这样就可以把方法转换为消息,而submit()的方法体包含在匿名内部类的call()方法中。

注意,每个活动对象方法的返回值都是一个具有泛型参数的Future,而这个泛型参数就是该方法中实际的返回类型。通过这种方式,方法调用几乎可以立即返回,调用者可以使用Future来发现何时任务完成,并收集到实际的返回值。这样可以处理最复杂的情况,但是如果调用没有任何返回值,那么这个过程将被简化。

为了能够在不经意间就可以防止线程之间的耦合,任何传递给活动对象方法调用的参数都必须是只读的其他活动对象,即没有连接其他任何任务的对象。

有了活动对象:

  1. 每个对象都可以拥有自己的工作器线程;
  2. 每个对象都将维护对它自己的域的全部控制权(这比普通的类要严苛一些,普通的类知识拥有防护它们的域的选择权);
  3. 所有在活动对象之间的通讯都将以在这些对象之间的消息形式发生;
  4. 活动对象之间的所有消息都要排队;

由于从一个活动对象到另一个活动对象的消息只能被排队时的延迟所阻塞,并且因为这个延迟总是非常短且独立于任何其他对象的,所以发送消息实际上是不可阻塞的(最坏的情况就是很短的延迟)。由于一个活动对象系统只是经由消息来通讯,所以两个对象在竞争另一个对象上的方法时,是不会被阻塞的,而这意味着不会发生死锁。

在活动对象中的工作器线程在任何时刻只执行一个消息,所以不存在任何资源的竞争,也正因为此不用操心如何同步方法。同步仍旧会发生,但是它通过将方法调用排队,使得任何时刻都只能发生一个调用,从而将同步控制在消息级别上发生。

与普通多线程的比较

  1. 普通多线程在竞争同一资源时,可能会发生死锁的情况,此时活动对象则不会。如果此时活动对象作为资源的管理者,效果可能会好些;
  2. 普通多线程在执行并行任务时,即不存在对同一资源的竞争,执行效率要比活动对象高,毕竟活动对象是串行执行的;

总结

使用并发的原因:

  1. 要处理很多任务,它们交织在一起,应用并发能够更有效的使用计算机;
  2. 能够更好的组织代码;
  3. 便于用户使用;

多线程的主要缺陷:

  1. 等待共享资源时性能降低;
  2. 需要处理线程的额外CPU花销;
  3. 糟糕的程序设计导致不必要的复杂度;
  4. 可能导致一些病态行为,如,饿死、竞争、死锁和活锁(多个运行各自任务的线程使得整体无法完成)等;
  5. 不同平台导致不一致性;

部分demo:[email protected]:Casablanca9907/ThinkingInJavaDemo.git

发布了24 篇原创文章 · 获赞 8 · 访问量 1870

猜你喜欢

转载自blog.csdn.net/qq_40462579/article/details/102885126