java并发实战笔记

每一个想学习Java多线程的人,手里至少有这本书或者至少要看这本书,2012年在看这本书的时候,当时正开发支付平台的后台应用,正好给了我大量的实践机会。强烈建议大家多看几遍。

代码中比较容易出现 bug 的场景:

不一致的同步,直接调用 Thread.run ,未被释放的锁,空的同步块,双重检查加锁,在构造函数中启动一个线程, notify 或 notifyAll 通知错误, Object.wait 和 Condition.await 未在同步方法或块中调用,把 Lock 当锁用,调用 Condition.wait 方法,在休眠或等待时持有锁,自旋循环 .

1. 多线程可以提高资源的利用率,可以充分利用现代多核处理器的特性,让每个线程负责处理同类型的任务,更加容易维护,同时通过异步处理提高响应性。

2. 多线程之间为更方便的实现数据共享采用了共享相同内存地址空间的形式,并且是并发运行,导致多个线程可能会同时访问或修改其他线程正在使用的变量值,导致安全性,同时如果线程之间相互等待对方拥有的锁,会出现活跃性即死锁问题。如果线程计算部分不多,更多的线程只会导致频繁的切换上下文,让 CPU 的时间更多的花在线程调度而不是任务执行上。

3.java 同步的几种方式: synchronized,volatile, 显示锁 , 原子变量 , 线程及对象的基础同步方法。

4. 所谓线程安全就是当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为。

5. 将复合操作放在一个原子操作中执行 , 或用 相同的锁 来保护每个共享的和可变的变量。

6. 增加同步必然会导致代码的复杂性,为性能牺牲代码简单性时不要太盲目,因为越复杂的代码,其不安全性越大。

7. 当执行时间较长的计算或可能无法快速完成的操作时 , 如网络 I/O ,一定不要持有锁。

8. 线程之间变量的读取,在没有同步的情况下,编译器,处理器,以及运行时都有可能对部分指令进行重排导致并发问题。日常开发中常见的 set/get, 如果没有都加上 synchronized, 在多线程环境下也存在同样的问题。

9. 对于非 volatile 的 64 位 long 或 double, 由于 JVM 允许对他们的读取分解为高低 32 位来读取 ,多线程下会发生只读取部分 32 位的问题,因此对这些变量 , 要用 volatile 或锁保护读取。

10. 对 volatile 变量 , 编译器和运行时不会将该变量上的操作与其他内存操作一起重排序 , 也不会被缓存在寄存器或者对其他处理器不可见的地方 , 而是直接同步到内存 , 保证其他线程读取的时候返回最新写入的值。确切的说, volatile 变量只保证可见性 , 对于自增或自减的操作并不能保证其原子性,因此不是线程安全的。因此不要过多的依赖此对象,最好在满足以下全部三个条件的情况下才考虑使用:

          (a) 对变量的写入不依赖当前变量的值 , 即所谓的不是自增或自减情况 , 或者可以保证只有单个线程对其更新

          (b) 该变量不参与到不变性条件的判断

          (c) 访问该变量时不用加锁

另一方面: 当且仅当一个变量参与到包含其他状态变量的不变性条件时,才可以声明为 volatile类型。

11. 发布对象的几种方式: 1. 将对象的引用保存到其他代码中或 public 域中。 2. 非私有方法返回该对象引用或该对象引用作为参数传递。 3. 发布 Collections 组合 . 4. 通过已发布对象的非私有变量引用或方法获取到的对象    5. 类的内部类实例隐含的包含了对该类实例的引用。

12. 正确的或安全的发布一个对象,即是保证对象的引用以及对象的状态必须同时对其他线程可见。 不正确的发布可变对象会导致线程安全问题,以下是一些保证变量或对象线程安全的方法:

        1. 不要在构造过程中使 this 逸出,即不要在构造函数中创建并启动线程 , 不要调用可改写的方法 , 或注册事件监听或对内部类实例化。    2. 多使用线程封闭 , 即尽量把对象放在单线程中不参与共享 .    3. 使用栈封闭,即在方法内部用局部变量访问对象。    4. 用 ThreadLocal 封装变量,为每个线程提供一个只属于该线程的变量副本。可以视 ThreadLocal<T> 为 Map<Thread,T>, 另外还有一个好处就是当线程终止后,该值也会被回收。 ( 缺点 :ThreadLocal 变量类似全局变量,会降低代码的可重用性,并在类之间引入隐含的耦合性 ) 。   5. 多用不可变对象 ( 对象正确创建未 this逸出,且创建后其状态不能修改 , 且所有域都是 final) ,其一定是线程安全的。 6. 在 静态初始化函数 中初始化一个对象引用 (JVM 内部的同步机制保证了这种发布方式的安全性 )  7. 将对象的引用保存到 volatile 类型的域, AtomicReferance 对象,某个正确构造对象的 final 域,或一个由锁保护的域中。   8. 将对象放入线程安全的容器中可以由容器内部的同步机制保障对象安全发布。

13. 如果需要对一组数据以原子方式执行某个操作 , 为避免竞态条件 , 可以创建一个不可变类来包含这些数据 ( 如果数据是数组或其他可变对象,该类对应的变量为 clone 的副本以保证不可变性 ) ,通过把这些数据保存到该不可变类的实例上,并且用 volatile 来确保该实例的可见性,这样可以保证线程操作数据的安全。如果该类对象是可变的 , 当然可以加锁来确保原子性。

14. 使用同步和封转来保护对象状态即变量的不变性条件及后验条件,使得相关变量必须在单个原子操作中进行读取或更新。换句话说,借助原子性与封装性,满足状态变量的有效值或状态转化上的各种约束条件,使得状态变量有效转换,是确保线程安全的有效手段。

15. 实例封闭即是将一个对象的所有访问代码路径都封装到另一个对象里,可以通过类私有变量,局部变量,单个线程里等方式,保证被封闭的对象不会逸出,不会超出它们既定的作用域。常见的例子如同步包装器工作如 Collections.synchronizedList 对容器对象的唯一引用以实现将底层容器对象封闭从而达到线程安全的目的。

16. 委托现有的同步容器来保障线程安全一般对针对 " 面 " 上的存取,如果类身包含复合操作,则该类必须自己提供加锁机制来保证这些复合操作的原子性。

17. 当为现有的类添加一个原子操作时,利用组合并用同一个锁来保护同步操作可以实现。

18. 同步容器类: Vector , Hashtable ,以及由 Collections.synchronizedXxx 等工厂方法包装的同步封装器类,它们实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。因此如果基于这些共有方法衍生出了一些新操作,必须注意这些操作有可能不是原子的从而引发同步问题。

缺点: 同步容器将所有对容器状态的访问都串行化以实现它们的线程安全目的,因此当多个线程同时竞争锁时,吞吐量将严重降低,并发性能严重受到影响。 ( 正是由于上述原因 ,java5 后开始提供多种并发容器来代替同步容器,以极大地提高伸缩性并降低风险 )

19. 并发容器类: java5 提供了多种并发容器以代替同步容器的低并发性,并增加了一些常见的复合操作,如 if-not-add, 替换,以及条件删除,使得这些复合操作原子化。列举及大致说明如下:

          a:ConcurrentHashMap: 也是基于散列的 Map ,利用粒度更细的分段锁机制使得任意数量的读取线程可以并发访问 Map ,并使得一定数量的写入线程可以并发的修改 Map ,从而在并发访问下实现更高的吞吐量。

          b:CopyOnWriteArrayList(Set): 保留一个指向底层基础数组的引用,每次修改对象时,都会复制创建并重新发布一个新的容器副本,由于复制底层数组需要一定的开销,因此这些容器仅适用于迭代操作远远多于修改操作的场景。

          c:Queue: 用来保存一组等待处理的元素,如传统 FIFO 的 ConcurrentLinkedQueue , ( 非同步的 ) 优先队列 PriorityQueue, 还有其他的 , 可参见 API

          d:BlockingQueue :可阻塞的 Queue ,如 LinkedBlockingQueue,ArrayBlockingQueue,PriorityBlockingQueue, 以及无存储容量的 SynchronousQueue( 该容器的 put 和 take 方法会一致堵塞 ,直到有另一个线程已经准备好参与到交付过程,因此仅当有足够多的消费者,并且总是有一个消费者准备好获取交付工作时 , 才适合使用此同步队列 ) 。所有这些阻塞队列适用于生产者 - 消费者模式。

          e:Deque 及 BlockingDeque : java6 新增的双端及可阻塞双端队列,适用于工作密取模式。每个消费者拥有各自的双端队列,当完成自己的队列是可以从其他队列的尾部开始获取工作,从而减少竞争提供并发。

20. 在同步容器显式迭代 (for-each,Iterator) 或隐式迭代 (toString,hashCode,equals,contailsAll,removeAll,retainAll) 过程中,修改容器会出现 ConcurrentModificationExcepiton 异常。但是在并发容器中,它们提供的迭代器不会抛出 ConcurrentModificationException 异常,因此不需要在迭代过程中对容器加锁。

21. 同步工具类: 它们提供了一些特定的结构化属性 , 封装了一些决定线程等待还是执行的状态 , 并提供了操作这些状态以及高效的等待同步工具类进入预期状态的方法。主要包括:

          a:CountDownLatch 闭锁

b:FutureTask

          c:Semaphore 信号量

          d:CyclicBarrier 栅栏

22. 并发任务的抽象,首先是要找到单个任务的边界,尽量使得各任务相互独立,任务之间不相互依赖,大多数服务器应用都采用了自然的任务边界,即以独立的客户请求为边界。一般来说每项任务还应该表示应用程序的一小部分处理能力,从而使整个应用程序表现出更好的吞吐量和响应性。

23. 如果任务的执行时间较长,创建过多的线程不仅会耗费 JVM 时间,消耗更多的内存资源,而且大量的线程将竞争 CPU 的有限资源,另外线程栈的地址空间也会限制创建过多的线程。

24. 各种线程池的创建方式及各自的一些特点:

          a)newFixedThreadPool: 固定长度的线程池,如果某个线程发生了未预期的 Exception 而结束,线程池会补偿一个新的线程。

          b)newCachedThreadPool: 动态可缓存的线程池 , 如果当前线程池规模超过处理需求,则回收空闲线程,否则添加新线程。

          c)newSingleThreadExecuto: 创建单个工作者线程来执行任务,如果这个线程异常结束,则会创建另一个线程来替代。可以确保依照任务在队列中的顺序串行执行 (FIFO,LIFO, 优先级等 )

          d)newScheduledThreadPool: 以延时或定时方式来执行任务的固定长度线程池。

25.ExecutorService 扩展了 Executor 接口以提供解决执行服务生命周期的问题, ExecutorService 生命周期有三种状态:运行,关闭和已终止。 shutdown 方法平缓关闭:不再接受新的任务,并等待已经提交的以及尚未开始执行的任务执行完成。 shutdownNow 方法粗暴关闭:尝试取消所有运行中的任务,并且丢弃队列中尚未开始执行的任务。

26.ExecutorService 关闭后提交的任务交由 Rejected Execution Handler 来处理,它会抛弃任务或使得 execute 方法抛出一个未检查的 RejectedExecutionException 。可以调用 awaitTermination(通常调用它后会立即调用 shutdown) 来等待 ExecutorService 到达终止状态,或者调用 isTerminated 来轮询等待。

27.Timer 类处理延迟任务与周期任务是有缺陷的,一是它在执行所有任务的时候只会创建一个线程,这样一旦某个任务执行时间超过间隔时间,后续任务将会连续执行或被丢弃。另外更严重的是,如果某个 TimeTask 抛出了未检查异常而终止了执行线程,那么整个 Timer 将被取消。在 Java5 后,不要再使用 Timer 。可以用 DelayQueue 与 ScheduledThreadPoolExecutor 组合构建自己的调度服务。

扩展:关于 Timer 与 ScheduleExecutorService 执行 Runnable 任务是否抛出异常对程序的影响差异比较:

         类型                                     

   任务 catch 异常

任务不 catch 异常         

     Scheduled 线程池 ( 多个线程数 )

会循环执行,但主要在 一个 线程内

如果发生异常,控制台 无异常堆栈 打出,线程池也不再循环执行,但仍活着。                             

        Timer( 多个Timer)

多个 Timer 都会按照循环策略执行

每个 Timer 抛出各自的异常堆栈 , Timer 也同时终止 , 待全部 Timer 终止后程序退出

28. 在 Executor 框架中,已提交但尚未开始的任务可以取消, 如果是已经开始执行的任务,只有当它们能响应中断时才可以取消。 Future.get 如果抛出了异常,会封装成 ExecutionException,可以通过 getCause 来获取初始异常。

29.CompletionService 将已经完成的任务按照完成顺序放置到其内置的 BlockingQueue 队列上,每次 get 取到的都是最新完成的任务结果。 可以用 Callable<Void> 来表示无返回的任务。

30.invokeAll 按照任务集合中迭代器的顺序将所有的 Future 添加到返回的集合中。 invokeAll 会等待所有任务完成或超时 才返回结果,不像 submit 立即异步返回,因为 invokeAll 内部对 FutureList 做了循环 get 等待。

31. 可以使用线程的中断以及类库中提供的中断支持来实现任务的可取消特性。每个线程都有一个 boolean 类型的中断状态,当中断线程时,此状态将设置为 true 。关于线程中断有三个方法:

          1)interrupt: 线程实例方法 , 调用后中断实例线程 , 设置该实例线程的中断状态为 true

          2)isInterrupted: 线程实例方法 , 返回实例的中断状态 ture/false

          3)interrupted: 静态方法,将清除当前线程的中断状态,并返回线程之前的状态。 注意:如果调用此方法清除当前线程的中断状态并返回了 true ,说明当前线程在中断之前就已经是 " 已中断 " 的状态了,如果你不做任何处理,那么之前的中断就被屏蔽掉了,可以通过抛出 InterruptedException 来响应中断或再次调用 interrupt 来恢复中断。

一旦一个线程被终止或正常结束,都不能再次调用 start 方法启动了,否则会抛出 InvalidThreadStateException.

当一个方法由于等待某个条件变成真而阻塞时,需要提供一种取消机制。

32. 常见的阻塞方法如 Thread.sleep,Object.wait 在阻塞时都会检查线程是否已中断,如果发现已中断, 则会先清除中断状态 ,然后抛出 InterruptException. ( 通常,可中断的方法会在阻塞或进行重要的工作前首先检查中断,以便能尽快的响应中断 ) 因此,在线程里调用可抛出 InterruptedException 异常的阻塞方法时将使线程处于某种阻塞状态,如果这个方法被中断,那么它将努力提前结束阻塞状态。当我们调用这些阻塞方法时,最好遵循下面的两种方法之一: 1) 抛出此 InterruptedException 异常。          2) 如果无法抛出异常,比如在 Runnable 中运行,那么也一定要捕获此异常,并调用当前线程上的 interrupt 方法恢复中断状态 , 使调用栈中的更高层代码看到此线程引发了中断。   最好不要捕获异常又不做任何响应,这样调用栈上的高层代码无法对中断采取处理措施,因为线程被中断的证据已经丢失了。

33. 在非阻塞的状态下, 如果调用线程实例的 interrupt 方法,只是设置了该实例的中断状态,并不会抛出 InterruptException ,因此如果你的代码没有明确触发 InterruptException 的地方,也就意味着该线程实例没有很好的响应中断,只是此中断状态将一直保持,直到调用 interrupted 明确清除之。

34. 对于一些不支持取消但仍会调用可阻塞的方法操作,必须在循环中调用这些阻塞方法,并在发现中断后重新尝试调用,当然当这些方法检测到已中断会抛出 InterruptException ,应该记录这个状态,并在返回前调用 interrupt 恢复中断。永远不要在方法中调用 " 调用线程 ( 宿主线程 ) " 的interrupt, 因为你无法知道当前线程的中断策略,最好的方式是在方法内创建一个线程,并对它进行中断,因为你可以控制它的中断策略。

35. 当 Future.get 抛出 InterruptException 或 TimeoutException 时,如果你知道不再需要结果了,就可以调用 Future.cancel 来取消任务。

36. 对于不可取消中断的阻塞,如 Socket IO, File IO, 等待内置锁,可以通过封装 Thread 或用 newTaskFor ,将阻塞方法的不可中断性转移到其能响应的异常上,如通过提供封装后的 cancel 方法,将不可中断的 Socket IO 读写方法在 cancel 中变为关闭 Socket, 这样 read 或 write 将抛出 IOException ,这样可以将原本应该抛出 InterruptException 转变成了 IOException.

37. 对于非正常终止的线程,比如抛出了 RuntimeException ,如果想做一些清理工作,可以有两种方式,一是设置线程的 setUncaughtExceptionHandler ,通过一个实现 Thread.UncaughtExceptionHandler 接口的类做一些清理,另一个是在线程启动时注册一个关闭钩子 Runtime.getRuntime().addShutdownHook ,这样虚拟机在关闭的时候就会执行这些钩子方法。

38.Executor 框架可以将任务的提交与任务的执行策略解耦开来,但是以下情形却需要明确指定执行策略以保障安全性及避免活跃性问题:

          a) 依赖性任务:提交给线程池的任务需要依赖其他任务 , 则会隐含的约束执行策略

          b) 单线程环境任务:单线程的 Executor 下执行任务隐含的使用线程封闭机制保障了线程安全,如果切换到多线程环境下,会可能导致并发

          c) 对时间响应敏感的任务:如果这些任务与其他时间较长的任务同时提交给线程池,在单线程及包含少量线程的 Executor 下会影响敏感任务的执行

          d) 使用 ThreadLocal 的任务:由于线程池会动态的回收或增加线程,因此 “ 只有当线程本地址的生命周期受限于任务的生命周期时,在线程池中的线程使用 ThreadLocal 才有意义 ” ,而且不应该使用 ThreadLocal 在任务之间传递值。

39. 只要线程池中的任务需要无限期的等待一些必须由池中其他任务才能提供的资源或条件,除非线程池足够大,否则将发生饥饿死锁 . 因此线程池中最好运行那些同类型并且相互独立的任务,以使线程池达到最大性能。 如果确实需要执行不同类型的任务,应该考虑使用多个线程池。

40. 线程池设置大小公式: thread = N cpu * U cpu (cpu 目标利用率 ) * (1+W/C( 等待时间与计算时间比 )) 。对于计算密集型任务,在拥有 N 个处理器的系统上,当线程池的大小为 N+1 时,通常能实现最优利用率。如果是其他资源限制,那么用该资源的总量除以每个任务对该资源的需求量,所得结果就是线程池大小的上限。

Amdahl 定律:

并发后的加速比 <=1/(F+(1-F)/N)

其中 F 为串行计算部分的百分比, N 为 CPU 数

41. 线程池的基本大小即没有任务执行时线程池的大小,只有在工作队列已满才会创建超出这个数量的线程。如果某个线程超过了存活时间,该线程被标记为可回收,如果同时当前线程池大小超过基本大小,该线程将被终止。

42. 对于没有使用 SynchronousQueue 作为工作队列的线程池 (newCacheThreadPool 默认使用该队列 ) ,如果线程池中的线程数量等于基本大小,仅当队列已满时才会创建新的线程,因此如果设置基本大小为 0 且队列未满,任务达到后先进入队列,由于此时线程数为 0 因此不会执行任务,只有待队列满时才会真正执行任务。

43. 基本任务排队方法及被何种线程池采用:

          a: 无界队列:如无界 LinkedBlockingQueue(FIFO),newFixedThreadPool 和 newSingleThreadExecutor 默认使用此队列 , 此队列的好处是能让所有线程池中的线程保持忙碌状态,缺点是一旦生产大于消费 , 队列无限制增大耗尽内存。

          b: 有界队列:如 ArrayBlockingQueue(FIFO), 有界的 LinkedBlockingQueue(FIFO) , PriorityBlockingQueue( 任务按自然顺序或实现 Comparable 排序 ) 。当有界队列满后会根据饱和策略处理 .

          c: 同步移交 (Synchronous Handoff) : SynchronousQueue ,必须有空闲线程 ( 或还能创建新线程 ) 等待接受时才可以,否则拒绝。 一般只用在线程池无界或可以拒绝任务时,如在 newCachedThreadPool 中 ( 由于运用了此队列,因此它能比固定大小的线程池提供更好的排队性能 , 特别是 Java6 优化了非阻塞算法 , 因此只要不是受特殊资源限制,都建议用 newCachedThreadPool作为默认的选择 ).

44. 四种饱和策略 (ThreadPoolExecutor 可以调用 setRejectedExecutionHandler 来设置 ) :

          a) 中止策略 : 默认策略 , 抛出未检查的 RejectedExecutionException.

          b) 抛弃策略 : 放弃该新任务

          c) 抛弃最旧的策略 : 根据 FIFO, 最旧的就是下一个将被执行的任务被抛弃以尝试提交新的任务。因此一般该策略不和 FIFO 一起用。

          d) 调用者运行策略 : 即在调用了 execute 的主线程中执行,这样在一段时间内无暇再接受新的任务,新到达的请求停留在 TCP 层,如果持续过载 ,TCP 层也会抛弃请求的 , 从而实现一种平缓的性能降低。其过程为 : 线程池 ---> 工作队列 ----> 应用程序 --->TCP 层 ---> 客户端。

45. 可以通过实现 ThreadFactory 接口以及继承 Thread 类来自己定义如何产生新线程 . 比如可以加入日志 , 调用 setUncaughExceptionHandler 来设置该线程由于未捕获到异常而突然终止时调用的处理程序。

46. 当然也可以调用 Executors 中的 unconfigurableExecutorService 封装一个具体的 ExecutorService 以隐藏对 ThreadPoolExecutor 的配置。另外如果继承 ThreadPoolExecutor ,可以更灵活的扩张线程池,主要有以下几个方法 :

          a)beforeExecute :任务执行前调用 , 如果抛出异常 , 则任务不被执行 , 此任务结束

          b)afterExecute :只要任务在完成后不是带有一个 Error, 不管是正常返回还是抛出一个异常都会被执行。

          c)terminated: 所有任务已经完成且所有工作者线程关闭后调用 , 可以用来释放期生命期分配的资源及发送通知,记录日志,收集统计信息等。

47. 多线程很重要的一个应用是将 “ 多个迭代之间彼此独立,而且每个迭代操作执行的工作量比管理一个新任务开销大的 ” 串行计算转换为并行,多个线程同时独立计算各部分结果,当某一个线程得到结果时,通过设置一个公共同步变量或 synchronized 方法来通知不需再产生新的任务并可以通知其他任务线程适当结束自己。当然,也要考虑实在没有结果的情况,为避免永远等待结果,可以设置一个同步计数器,每个处理任务结束时在 finally 块先将计数器减一并查看当前剩余工作线程是否为 0 ,为 0 则表示无结果,可以设置一个空结果。

并发的不良后果 - 活跃性故障

48. 活跃性故障最常见的是锁顺序死锁,即两个线程试图以 不同的顺序 来获得相同的锁,或者多个线程相互持有彼此正在等待的锁而又不释放自己已持有的锁时会发生死锁情况。包括以下几种情况; 1 :不同方法中锁的顺序不一样, A 方法先锁 Key1 ,再锁 Key2 ,而 B 方法先锁 Key2,再锁 Key1. 2 :方法中对传入的参数加锁,如果两个参数类型相同,当不同地方的调用这个方法传入的参数顺序相反,如 fromAccount , toAccount 3: 协作对象间加锁方法相互调用出现隐秘的死锁。解决死锁有以下几种方法; 1 :通过对象的 hash 值判断锁顺序从而保证锁顺序一致 2 :增加加时锁 3 : 减小同步加锁的代码块,将方法级加锁改为代码块级加锁,尽量通过良好的线程封装开放调用。 4. 使用支持定时的锁。可通过死锁时转储的信息来分析死锁发生的原因。

49. 活跃性故障另外两个不良后果,一个是线程饥饿,即由于线程优先级的调整,导致有的线程始终无法得到 cpu 执行,另一个是活锁,即多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一方都无法继续。比如过度的错误恢复代码。一般在并发应用中,通过随机等待长度的时间和回退可以避免之。

并发的不良后果 - 线程调度开销

50. 线程引入主要有以下开销:

          a: 线程之间的协调 ( 加锁 , 触发信号 , 内存同步 )

          b: 上下文切换 (JVM 和操作系统间 , 新开线程的数据结构加入缓存 , 阻塞线程被调度 )

          c: 内存同步 ( 如 synchronized 和 volatile 提供的可见性保证中会使用内存栅栏刷新缓存 , 使缓存无效,刷新硬件的写缓冲以及停止执行管道 )

          d: 阻塞 ( 竞争失败的锁无论是自旋等待还是被操作系统挂起 )

51. 对并发程序的测试包含安全性 ( 正确性 ,) 及活跃性 ( 吞吐量,响应性,可伸缩性 ) 。测试中尽量让每个计算结果都被使用到,并确保其值不可预测,哪怕是与 nanoTime 任意的比较,因为一个智能编译器将用预先计算的结果来代替计算过程。

52. 对阻塞性的测试类似异常测试,如果代码执行到了阻塞方法的下面,说明测试失败。(当然也要注意测试时有方法解除阻塞)。对并发的测试,线程数应该大于 CPU 数,如果要测试并发时存入取出元素的顺序,可以利用元素 hash 和 nanoTime 加一些 " 移位 " 的方式求和比较。利用 CyclicBarrier 和 CountDownLatch 控制线程同步执行。

53. 垃圾回收, java 动态编译被多次执行的代码为机器码,对代码路径不真实采样 , 不真实的竞争程度 ( 测试时计算过多或过少 ) 都会影响到测试代码的效率。

54. 内置锁无法在等待时中断,且必须在获取该锁的代码块中释放。 Lock 提供了一种 无条件的,可轮询的,定时的,可中断 的锁获取操作。 必须记住在 finally 块中调用释放锁的操作 。 lockInterruptibly 方法能够在获得锁的同时保持对中断的响应。 java6 开始内置锁的性能已经与 ReentranLock 相当了,但是 ReentrantLock 是非块结构的,获取锁的操作不能与特定的栈帧关联。

55. 当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,可以使用公平锁,即当锁不可用时,新请求的线程将被放在队列里而不是一有可用锁立马插队。

56. 读写锁的分离可以提高并发,有一些选项需要注意:释放优先,读线程插队,重入性,写降读,读升写。

57. 可以用内置的条件队列,显式的 Condition 对象,以及 AbstractQueueSynchronizer 框架来构造自己的同步器。

58.Object.wait 会自动释放锁,并请求操作系统挂起当前线程。在唤醒线程后, wait 在返回前还要重新获取锁。由于多个线程可能基于不同的条件 ( 非空 , 已满 ) 在同一条件对象上等待,因此如果调用 notify 可能出现通知失误,如已经非空,但是 notify 通知却唤醒了正在等待已满状态的 wait。一般来说我们应该用 notifyAll ,除非同时满足以下两个条件 1. 所有等待线程的 等待类型 相同,2. 单进单出 ( 在条件变量上的每次通知,最多只能唤醒一个线程 )

59. 内置的条件队列相关者 Object 中的 wait,notify,notifyAll 对于 synchronized 内置锁的关系,正如 Condition 中的 await,signal,signalAll 对于显式的 Condition ,它们是一一配对使用的, 因为管理状态依赖性的机制必须与确保状态一致性的机制关联。 只不过后者提供了更多的控制,包括每个锁对应于多个等待线程集,可中断或不可中断的条件等待,公平或非公平的队列操作以及基于时限的等待。 所有的阻塞等待都是在获取到锁的前提下进行的 ,调用 wait 或 await 时释放已经得到的锁并开始等待,当 notify(notifyAll) 或 signal(signalAll) 后被选择执行的线程开始重新获取锁,并从等待处往下执行。

60.AQS 是一个构建锁和同步器的框架,其最基本的操作包括各种形式的获取操作和释放操作,其内用一个整数来表示状态信息,不同的同步实现无非用此整数外加一些辅助额外状态变量来表示,如 ReentrantLock 用它来表示所有者线程已经重复获取该锁的次数, Semaphore 用它来表示剩余的许可数量, FutureTask 用它来表示任务的状态,所有的具体同步实现都没有直接扩展 AQS,而是将它们相应的功能委托给私有的 AQS 子类。

AQS 还在内部维护一个等待线程队列,记录某个线程请求的是独占访问还是共享访问。

61.ReentrantReadWriteLock 使用一个 16 位的状态来表示写入锁计数,并使用 独占的 获取与释放方法,并使用另一个 16 位的状态来表示读取的计数,并用 共享的 获取与释放方法。因此当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会获得这个锁,如果位于队列的头部执行读取线程,那么队列中的第一个写入线程之前所有的读线程都将获得此读取锁。 ( 读取优先策略 )

62.CAS 的典型使用模式:首先从 V 中读取值 A, 并根据 A 计算新值 B ,然后再通过 CAS 以原子方式将 V 中的值由 A 变成 B ,其主要缺点是使调用者处理竞争问题 ( 通过重试,回退,放弃 ),而在锁中能自动处理竞争问题 ( 线程在获得锁之前将一直阻塞 ) 。

63.java 的原子变量类 AtomicXXX 使用底层的 CAS 平台指令为数字类型及引用类型提供一种高效的操作, java.util.concurrent 包中的大多数类在实现时则直接或间接地使用了这些原子类。

64. 创建非阻塞的算法关键在于找出将原子修改的范围缩小到单个变量上,同时还要维护数据的一致性。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页