深入理解Java高并发
原版图书链接 Mastering_Concurrency_Programming_with_Java_8
文章目录
遗留问题(过后解决)
- 一.5.2)
- 一.5.5)
- 一.7.6)
一、并发的设计原则
1、基础的并发概念
1)基础概念
- 并发(Concurrency):
- 多任务在单核单CPU的调度下,似乎同时运行(在用户可接受的一段时间内)
- 对于不同方法和机制的程序员,必须对共享资源进行同步任务和同步访问权限
- 并行(parallelism):
- 多任务在不同的机器上,或者不同的处理器上(或核上),同一时刻同时运行
- 同一任务不同实例,在同一时刻同时运行在不同的数据集上。
- 同步(synchronization):
- 任务B依赖于另一个任务A,只有当A运行后,B才可以运行
- 引入同步可以解决并发出错问题,但同时也带来了算法的开销
- 但如果有细粒度的同步方法(小任务,高互通),由于同步带来的开销会很高,算法吞吐率会不好。
- 互斥(mutual exclusion):
- 是一机制,该机制保证在同一时刻,只有一个任务访问共享资源。
- 互斥有多种实现方法
2)同步的方法
- 信号量:
- 可以控制对一个或多个单元的资源的访问控制
- 需要有一个变量来存储可以访问资源的数目
- 需要有两个原子操作来管理这个变量
- 互斥锁mutex就是一个特殊的信号量,只有两个值(free和busy)
- 监视器:
- 可以互斥访问共享资源
- 有一个互斥锁mutex,和一个条件变量,和两个操作(等待该条件(出阻塞队列)或者唤起该条件(入阻塞队列))
3)线程安全
- 如果用户的共享数据由同步机制保护着,那么该代码片段(方法或对象)就是线程安全的
- 无阻塞的比较和交换原语(compare-and-swap CAS),以及数据的不可变性(String),也是线程安全的 (不知理解是否正确)
- 区别CAS和volatile CAS原理
- 原子变量:是指通过原子操作去获得和设置值,该原子操作可以是同步机制,也可以是用CAS的上锁-开锁行为
2、在并发应用中可能发生的问题
1)资源竞争
2)死锁
死锁的必要条件:
- 互斥: 同一时刻只能有一个任务使用资源
- 请求和保持: 一个任务在使用互斥资源的同时,请求另一个互斥资源
- **非抢占:**只有使用该资源的任务才可以去释放资源
- **环路等待:**任务A等待获取任务B中的资源,而任务B等待C中的资源,…,而任务N等待A的资源
避免死锁的方法:
- 死锁忽视(ignore): 最常用的机制,认为死锁不会发生,如果发生,则重启应用
- **死锁检测与解除(Detection):**如果分析检测到出现死锁,则可以采用完成该任务,或者强迫其资源释放
- **死锁预防(Prevention):**破坏死锁的4个必要条件
- **死锁避免(Avoidance):**通过分析比较该空闲资源与任务所需资源,判断该操作是否要启动
更详细的避免死锁的方法请看 死锁的四个必要条件和解决办法
3)资源饥饿
- 线程长时间不能得到该资源导致饿死
- 公平是解决饥饿的办法
4)优先级抢占
- 高优先级的任务可以抢占低优先级任务的资源(Priority inversion)
3、并发算法的设计
起始:串行算法
-
利用串行算法,检测该并发算法是否根据输入的数据产生正确的输出
检测一系列并发算法根据相同的输入是否产生相同的输出
-
计算两个算法的吞吐量,去比较并发算法是否改善了响应时间,或者是否在一定量的数据下及时处理完毕。
1)分析
- 我们需要分析这些算法,去寻找可以并行运行的代码片段。
- 我们要特别关注那些运行时间占很大部分的代码(逻辑部分)
- 通过对这些占很大时间的部分进行并发处理,才能获得较好的性能。
- 好的例子是:这两部分代码或步骤互相独立的循环运行(比如建立数据库连接,加载配置文件,初始化一些对象,这些步骤相互不影响)
2)设计
一旦知道哪些部分可以处理成并行之后,需要对其设计
- 代码的改变会影响应用的改变
- 代码的结构
- 数据结构的组织
- 两种方法改变代码
- 任务的分解:
- 将代码分解成2个或2个以上独立运行的任务
- 也许一些任务之间需要按一定顺序执行,或者必须等待相同时间(wait at the same point),那么必须有同步机制来处理。
- 数据的分解:
- 当你拥有同一个任务的多个实例,并且该任务在数据集的子集工作时,那么该数据是共享资源,就有必要数据分解
- 需要对共享数据的临界区进行保护
- 任务的分解:
- 解决方案中的粒度也是重点
- 使用所有可以使用的处理器和核数
- 在使用同步机制时,会引入额外的必须执行的指令。
- 如果切分的粒度过细,则额外代码同步时会使性能退化
- 如果切分的粒度过粗,则无法充分利用所有的资源。
- 在多线程处理任务时,一定要考虑粗粒度和细粒度之间的平衡
3)实现
4)测试
5)调优
最后一步是比较并行算法和顺序算法的吞吐量(throughput) ,也可以去比较不同参数(粒度或者任务数量等)
度量标准:
-
加速比(SpeedUp):
S p e e d U p = T s e q u e n t i a l T c o n c u r r e n c y SpeedUp = \frac{T_{sequential}}{T_{concurrency}} SpeedUp=TconcurrencyTsequential
优化前系统耗时/优化后系统耗时。
-
Amdahl定律: 计算并行计算的最大预期改进。
-
Gustafson-Barsis定律:
- Amdahl定律有限制:在相同的数据集下增加核心数
- 但通常情况下,多核你就想处理更多数据。
备注:
- 这两个定律从不同的角度诠释了加速比与系统串行化程度、cpu核心数之间的关系,它们是我们在做高并发程序设计时的理论依据。
详细请参考这 Amdahl’s law and Gustafson’s law
6)小总结
- 并不是所有的算法都可以并行处理。例如循环计数,该数依赖于之前的数,就不能对循环进行并行处理。
- 在设计并行算法之前,一定要有一个好的性能的串行算法为开始。
- 在设计并行算法时,要考虑以下指标:
- 效率(Efficiency): 并行算法的结束时间一定要比串行算法的时间更短
- 简单(Simplicity): 无论你是用的是并行还是串行算法,你必须尽可能保证其简单。这将会方便实现,测试,调试和维护。
- 可移植性(Portability): 需要保证在其他平台上,小改一下也可以运行(java不用考虑这些)
- 可拓展性(Scalability): 如果增加了核心数,该算法会发生什么。所以在设计并行算法时,需要去利用所有可用的资源。
4、Java并发API
1)基础并发类
Thread
类:该类代表执行并发程序的线程Runnable
接口:另一种方式创建java并发程序ThreadLocal
类:为thread存储局部变量的类ThreadFactory
接口:是一种设计模式,可以创建自定义的thread
2)同步机制
- 作用:
- 定义访问共享资源的临界区
- 在同一时间同步不同的任务
synchronized
关键字:可以定义代码块的临界区Lock
接口:- 提供比
synchronized
更加灵活的同步操作(synchronization) ReentrantLock
实现了可以关联条件的LockReentrantReadWriteLock
将操作分成读和写操作StampedLock
是Java8的新特性,对于控制读写访问,包含3种模式。
- 提供比
Semaphore
类:该类实现了经典的信号量方法来实现同步问题。java支持二进制或普通的信号量。CountDownLatch
类:该类允许任务等待多操作后的结束CyclicBarrier
类:该类允许多线程同步在同一时间上。Phaser
类:该类允许我们将执行的任务划分成多个阶段。没有任务可以提前到下一阶段直至所有任务都完成当前阶段
3)执行器(Executors)
执行器框架是一种机制,该机制可以允许我们在实现并发任务时,将线程创建和管理拆分开来。
Executor
和ExecutorService
接口:它们包含的方法和executors类似。ThreadPoolExecutor
:该类可以允许我们得到伴有线程池的executor,以及自定义并行任务的最大数目。ScheduledThreadPoolExecutor
:是特殊的executor,可以延迟或者定期执行任务。Executors
:该类简化了executor的创建Callable
接口:可以替代Runnable接口,拆分的任务可以返回值Future
接口:该接口包含Callable接口的值的方法 ,并且该方法可以控制该值的状态。
4)Fork/Join框架
Fork/Join框架定义了一种专门用来解决分而治之问题的executor
ForkJoinPool
:该类实现了跑多任务的executorForkJoinTask
:该类是一个可以在ForkJoinPool
中执行的任务。ForkJoinWorkerThread
:该类是一个可以在ForkJoinPool
中执行的线程。
5)并行流(Parallel Streams)
流和Lamda表达式是java8的新特性。流作为一种方法添加到Collection
接口和其他数据源上,并且允许处理数据结构的所有元素,生成新的结构,过滤数据和通过map和reduce来实现算法
并行流是一种特殊的流,可以实现并行操作。
Stream
接口:定义了执行流的所有操作Optional
: 是一个容器对象,可能或不可能包含非空值Collectors
:该类实现了规约(reduce)操作,该操作可用来对流序列的操作- Lamda表达式:流和lamda表达式一起工作。大部分流接受lamda表达式作为参数,进而实现更精简的操作
6)并发数据结构
Java的ArrayList, Hashtable等不能实现并发编程除非使用外部同步机制,但是会带来额外计算开销。
如果你在多线程中修改了它们,会抛出异常(eg:ConcurrentModificationException
和
ArrayIndexOutOfBoundsException
)
Java API中支持并发的数据结构分类:
- **阻塞数据结构: **如果数据结构空但你想要值的时候,它拥有阻塞任务的方法
LinkedBlockingDeque
: This is a blocking listLinkedBlockingQueue
: This is a blocking queuePriorityBlockingQueue
:This is a blocking queue that orders its elements based on its priority
- 非阻塞数据结构: 如果操作要立刻执行,则返回null或者抛异常
ConcurrentLinkedDeque
: This is a non-blocking listConcurrentLinkedQueue
: This is a non-blocking queueConcurrentSkipListMap
: This is a non-blocking navigable mapConcurrentHashMap
: This is a non-blocking hash map
- Java基本类型的原子实现
AtomicBoolean
,AtomicInteger
,AtomicLong
, 和AtomicReference
5、并发设计模式
在软件工程中,设计模式是普通问题的一种解决方案。这种方案被使用很多次,足够证明它是该问题的最优解决方案,你可以使用它,避免造轮子。
1)唤起(Signaling):
- 实现了一个任务唤起另一个任务的事件。
- 这里的section2一定是在section1之后执行
public void task1(){
section1();
commonObject.notify();
}
public void task2(){
commonObject.wait();
section2();
}
2)约会(Rendezvous):
概念:
-
唤起模式的一种泛化形式,任务A等待任务B唤起某个事件,任务B等待A唤起某个事件
public void task1(){
section1_1();
commonObject1.notify();
commonObject2.wait();
section1_2();
}
public void task2(){
section2_1();
commonObject2.notify();
commonObject1.wait();
section2_2();
}
-------------
感觉书中代码有点怪,有些时候会产生死锁,也会出现一个线程没有执行完。
个人觉得应该下面这种,可以执行完,但也有可能发生死锁;
public void task1(){
section1_1();
commonObject2.wait();
commonObject1.notify();
section1_2();
}
public void task2(){
section2_1();
commonObject2.notify();
commonObject1.wait();
section2_2();
}
小实验:(以后回来验证)
-
实现A,B交替执行
-
注意不是只有commonObject一个对象,进行唤起、等待操作
-
假设只有一个对象,如果不对其进行同步操作(synchronized),以java8为例,会抛出
illigalMonitorStateException()
诡异的java.lang.IllegalMonitorStateException
public void task1() throws InterruptedException { System.out.println("section1_1"); synchronized(commonObject){commonObject.notify();} synchronized(commonObject){commonObject.wait();} System.out.println("section1_2"); } public void task2() throws InterruptedException { System.out.println("section2_1"); synchronized(commonObject){commonObject.notify();} synchronized(commonObject){commonObject.wait();} System.out.println("section2_2"); } --- 输出结果 section1_1 section2_1 section1_2 (线程2一直等待)
-
假设有两个对象,以Java8的Object对象的wait,notify方法为例 (事实上应该用lock进行锁的释放和获取)
public void task1() throws InterruptedException { System.out.println("section1_1"); synchronized (commonObject1){ System.out.println(Thread.currentThread().getName() + "唤起1"); commonObject1.notify(); } synchronized (commonObject2){ System.out.println(Thread.currentThread().getName() + "等待2"); commonObject2.wait(); } System.out.println("section1_2"); } public void task2() throws InterruptedException { System.out.println("section2_1"); synchronized (commonObject2){ System.out.println(Thread.currentThread().getName() + "唤起2"); commonObject2.notify(); } synchronized (commonObject1){ System.out.println(Thread.currentThread().getName() + "等待1"); commonObject1.wait(); } System.out.println("section2_2"); }
-
如果A唤起对象1,而此时B没有等待对象1,则B永远得不到对象1;死锁
-
如果B唤起对象2,而此时A没有等待对象2,则A永远得不到对象2;死锁
-
如果A唤起对象1,此时B等待对象1,则B可以获得对象1;有一方结束
真就验证了约会随缘
--- 输出结果 test1: section1_1 Thread-0唤起1 Thread-0等待2 section2_1 Thread-1唤起2 Thread-1等待1 section1_2 test2: section1_1 section2_1 Thread-0唤起1 Thread-1唤起2 Thread-1等待1 Thread-0等待2 (死锁)
-
-
如果使用lock的condition的await,signal方法,也会出现
illigalMonitorStateException()
ReentrantLock(二):正确使用Condition实现等待与通知- 在使用内置监视器锁时,返回的 Condition 实例支持与 Object 的监视器方法(wait、notify 和 notifyAll)相同的用法。
- 在Condition.await()方法调用之前使用lock.lock()获得同步监视器
public void task1() throws InterruptedException { System.out.println("section1_1"); lock.tryLock(); System.out.println(Thread.currentThread().getName() + "唤起1"); commonObject1.signal(); lock.unlock(); lock.tryLock(); System.out.println(Thread.currentThread().getName() + "等待2"); commonObject2.await(); lock.unlock(); System.out.println("section1_2"); } public void task2() throws InterruptedException { System.out.println("section2_1"); lock.tryLock(); System.out.println(Thread.currentThread().getName() + "唤起2"); commonObject2.signal(); lock.unlock(); //必须在Condition.await()方法调用之前使用lock.lock()获得同步监视器 lock.tryLock(); System.out.println(Thread.currentThread().getName() + "等待1"); commonObject1.await(); lock.unlock(); System.out.println("section2_2"); } ------- 输出结果 section1_1 Thread-0唤起1 Thread-0等待2 section2_1 Thread-1唤起2 Thread-1等待1 section1_2 Thread-0等待2 (Thread-0未结束)
##### 3)互斥锁(Mutex):
- 互斥锁是一种可以**实现临界区,保证互斥**的一种机制
- java中,可以使用 `synchronized`,
`ReentrantLock`类或者`Semaphore`类来实现互斥锁
```java
public void task() {
preCriticalSection();
lockObject.lock() // The critical section begins
criticalSection();
lockObject.unlock(); // The critical section ends
postCriticalSection();
}
4)多元访问(Multiplex):
- 是互斥锁的一种泛化
- 允许一定数量的任务在临界区同时执行(eg:有相同的资源拷贝)
- java中,使用
Semaphore
来初始化任务的数量
public void task() {
preCriticalSection();
semaphoreObject.acquire();
criticalSection();
semaphoreObject.release();
postCriticalSection();
}
5)双重检查加锁(Double-checked locking):
- 当你获得锁后,需要检查状态。如果状态会false,此时就会增加获得锁的开销
- 懒加载时会用到该设计模式
- java中没有
Singleton
可以实现该模式,需要自己编写
法一:
public class Singleton{
private Object reference;
private Lock lock=new ReentrantLock();
public Object getReference() {
if (reference==null) {
lock.lock();
try {
if (reference == null) {
reference=new Object();
}
} finally {
lock.unlock();
}
}
return reference;
}
}
如果两个任务同时判断条件,将会产生两个Object(疑问:不是加锁了吗,怎么可能会有两个任务同时检查判断???)
法二:
如果要提高性能,则不要使用显性的同步机制。
class Singleton {
private static class LazySingleton {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return LazySingleton.INSTANCE;
}
}
6)读写锁(Read-write lock)
- 普通锁对于读操作来说太影响性能,因为并发读是没有问题的
- 读写锁内部有两把锁,一把处理读,一把处理写
- 主要操作如下
- A正在读,B想读,可以
- A正在读,B想写,不可以,直到所有任务读完为止
- A正在写,B想写,不可以,直到该任务写完为止
- java8中的
ReentrantReadWriteLock
可以实现该模式 - 使用时需要认真考虑读写的优先级,如果太多读任务,写任务会等很久
7)线程池(Thread pool)
- 为了减少为任务创建线程的开销
- 线程集合一般是固定数量,它会到等待队列中去获取任务并执行,不会摧毁线程
- java8中用
ExecutorService
来实现该模式
8)线程局部存储器(Thread local storage)
- 该模式为任务定义了如何局部地使用全局或静态的变量。
- 对于一个类的静态变量,所有的类对象有可能同时访问该变量,如果使用Thread local storage,每个线程都会获得该变量的不同的实例。
- java8中用
ThreadLocal
来实现该模式
6、Java内存模型
1)缓存,指令重排带来的问题
- 缓存可以有效的增加应用的性能,但是它会导致数据的不一致性。当一个任务在缓存中修改变量的值,没有及时写回主内存。那么其他任务读的值也许是更新之前的旧的值。
- 为了提高应用的性能,编译器和代码优化器对代码指令进行重排优化。在串行应用中,这不会发生问题,但是在并发应用中,会引起意料之外的结果。
为了解决这些问题,编程语言引入内存模型,
- 该模型描述独立的任务之间如何通过主存来通信,当一个任务导致主存某一数据发生变化,该变化对其他任务可见(visible to another)。
- 该模型还定义了什么代码的优化是被允许的,以及在哪些场合是被允许的。
2)内存模型的种类
有些比较严格(所有任务总可以访问同样的值),有些不那么严格(只有一些指令可以更新主存中的值)
java的原始内存模型存在一些问题,后来在java5重新定义。该模型和java8相同,定义在JSR133。基础定义如下
-
定义了关键字
volatile
,synchronized
, 和final
的行为。 -
保证同步并发程序在该架构(JMM + JVM)上运行正确,不用考虑操作系统,CPU架构核数等
-
创建了一种命名为“之前发生(happens-before)“的
volatile read
,volatile write
,lock
, 和unlock
的命令的偏序关系 -
如果一个事件在另一个之前发生,那么前一个是可见的,并且顺序在后者之前。
-
如果一个任务获得监视器(monitor),那么缓存是无效的。
-
如果一个任务释放监视器(monitor),那么缓存中的值会flush到主内存中
-
对编码者透明
7、并发设计的小技巧
1)识别正确的独立任务
不适合:
- 如果你有多个任务,这些任务有顺序依赖性,也许你没有兴趣去并发执行它们,也没有兴趣用同步机制去保证它们的顺序。让它们串行运行即可。
- 如果是循环,则不能使用并发,因为所有的步骤产生的数据依赖于前一个步骤,或者存在某些状态信息从某一步到另一步。
适合:
-
如果这些任务有一些先决条件( prerequisites
),并且这些先决条件彼此相互独立,那么可以并发运行这些先决条件,然后使用同步机制来控制在完成所有先决条件之后的任务的运行。
2)高水准机制实现并发
-
Java并发API中,我们可以使用
Thread
,Lock
类来创建和同步线程 -
同时它提供了高水平的并发对象,例如
executors
和Fork/Join
框架,允许我们执行多并发的任务。这些高水平的并发机制有如下好处:- 你不用担心线程的创建和管理(Java API做的事),你只需创建任务,并将它们发送给execution即可。
- 它比直接使用线程有更高的性能。例如线程池
- 有高级特征使API功能更强大。例如使用executors,你能获取任务的返回值,封装在
Future
对象中。 - 应用有更好的移植性和可扩展性。
- 在未来的Java版本中,应用也许会更快,因为Java 的内核,JVM的优化,会使JDK API更加合身(tailored)
-
总之,在实现并发算法之前一定要先分析Java API提供的高水平的同步机制。
3)考虑可扩展性
-
当你使用并发算法时,主要的目的是为了利用计算机的所有资源,特别是处理器和核数的数量。但是这些资源的数目也许会改变。
-
当你通过分解数据设计并发算法时,不要提前假设该应用将会执行的处理器和核数的数量。而是动态使用系统信息,例如使用Java的
Runtime.getRuntime().availableProcessors())
来获取处理器数,并利用它计算要执行的任务的个数。虽然这么做会带来开销,但提高了算法的可扩展性。 -
当你通过分解任务设计并发算法时,这种情况会更加困难,你需要依赖独立任务的数量,而且如果分解的**任务数量过多会增加同步机制带来的开销,**系统的全局性能会更加糟糕。
所以需要认真分析算法决定是否使用动态的任务数量。
4)使用线程安全(thread-safe)的API
- 如果有线程安全的API,直接用;如果没有,加上必要的同步机制,特别是在资源竞争的时候。
- 例如要使用list,而ArrayList线程不安全,所以优先使用线程安全的list,
ConcurrentLinkedDeque
,CopyOnWriteArrayList
, 或者LinkedBlockingDeque
5)不要假设执行顺序
-
任务的执行顺序依赖于处理机的调度。
-
假设的结果常用来处理资源竞争的问题。而算法的最终结果依赖于任务的执行顺序。
很难去检测资源竞争条件的原因,所以必须认真仔细,不要忘了加上所有必要的同步元素。
6)宁愿使用局部线程变量
相比静态和共享变量,宁愿使用局部线程变量
-
使用
ThreadLocal
类保证每一个线程能够不通过锁,信号量等类来访问自己的实例变量。而是通过为每个线程单独一份存储空间,牺牲空间来解决冲突,不存在竞争关系。 -
ThreadLocal中保存着Thread_id,每个Thread中有ThreadLocalMap,用来存储线程的所有局部变量,而ThreadLocal负责访问和维护ThreadLocalMap。
-
另一种使用方法是采用
ConcurrentHashMap<Thread, MyType>
,并用var.put(Thread.currentThread(), newValue)
来绑定线程和值。但存在竞争,效率比ThreadLocal
低。但它也有好处,就是可以完全清理掉map,使得Thread中的value消失。
疑问:ThreadLocal与之对比,为什么没有这个效果,用得不好会发生内存泄漏??
7)寻找更加简单的并行算法
- 对于同样的问题,往往有不同的方法。有些算法更快,有些使用更少的资源,有些适合特殊的输入数据(例如归并排序)
- 在设计并行算法之前,建议先设计串行算法
- 可以检测并行算法的正确性
- 可以衡量并行算法带来的性能改进
- 并非所有的算法都可以并行,或者说不简单!!!所以最好的开始是,在设计串行算法时,还要考虑到(该算法的)并行处理的最优效能和吞吐量,并选出合适的串行算法。
8)尽量使用不可变对象
-
在处理资源竞争的问题时,特别是对于面向对象的语言来说,每个对象都有get,set。多线程操作会出现数据不一致。而不可变对象一旦初始化,就不能修改,如果修改它,就会产生新的对象(eg:String类,+=会产生新的String)
-
在并发应用中使用不可变对象的好处
- 你无需同步机制去保护该类该方法。如果多个任务修改同一个值,会产生新的对象。所以不存在多任务同时对同一个对象进行操作
- 不存在数据不一致性的问题
-
同时,它有缺点。如果产生太多的对象,这会影响吞吐量和内存使用。
如果简单的对象没有内部数据结构,则通常无需让其不可变(immutable)。
让和其他对象不合并的集合对象变成不可变型,会带来严重性能问题
9)锁的顺序使用时避免死锁
-
最好避免死锁的机制是使所有任务按照相同的顺序访问共享资源。最简单的方法是为资源赋值。当一个任务需要多个资源时,需要按顺序获取该资源(例如先访问R1资源,再访问R2资源)。
//不会发生死锁 public void operation1() { lock1.lock(); lock2.lock(); …. } public void operation2() { lock1.lock(); lock2.lock(); ….. }
-
如果T1先访问R1,再访问R2,而T2先访问R2,再访问R1,就会死锁
10)能使用原子变量不要用同步机制
-
在一些情况下,使用volatile,而不使用同步机制,例如只有一个写,多个读的时候。
而其他场景,使用
lock
,synchronized
或其他同步方法。 -
java5中,API提供源自变量,支持变量的原子操作,它们有一个方法
compareAndSet(oldValue, newValue)
,该机制可以检测该值是否已经被赋上新的值。如果该值和原来的值不同,则将该值变新,并返回true。还有其他原子性方法:
getAndIncrement()
和getAndDecrement()
。 -
该方法是“不使用锁(lock-free)”的,所以性能优于同步机制
-
常见的原子变量
AtomicInteger
AtomicLong
AtomicReference
AtomicBoolean
LongAdder
DoubleAdder
11)竟可能短时间上锁
-
锁和同步机制一样,可以定义一个临界区,只允许同一时刻一个任务运行此处,其他任务只能阻塞在这,等待锁的释放。
-
为了不退化应用的性能,必须使临界区尽可能的小,只有和其他任务共享的资源的指令才必须上锁。
-
避免执行临界区中你无法控制的代码。例如,你想将数据写入数据库中,并等待用户自定义的Callable,但你不知道Callable中究竟是什么,也许会阻塞输入输出。
对你而言,如果该算法必须这么做的话,那么在库文档中具体化这个行为,并且限制用户提供的代码(例如它无需使用锁),
ConcurrentHashMap
类中的compute()是个好的文档例子
12)利用懒加载来预防
- 懒加载:延迟对象创建直到你第一次要使用该对象。
- 懒加载在并发应用中会出问题:多个任务同时调用该对象的初始化方法。
- 这个问题已得到解决,请见Initialization-on-demand holder idiom
13)在临界区中避免阻塞操作
- 临界区内任务在等待I/O输入而阻塞,而临界区外的任务在等待临界区资源而阻塞,会降低系统的性能。