文章目录
- countDownLatch
- volatile
- CAS
- jdk1.6对synchronized的优化
- 自旋锁
- 锁消除
- 锁粗化
- 轻量级锁
- 偏向锁
- java AtomicBoolean compareAndSet Demo
- threadlocal
- concurrent queue
- 原子操作是否需要同步
- copyonwrite容器
- 可重入锁
- 公平与非公平
- 并发编程步骤
countDownLatch
此类位于java.util.concurrent.countDownLatch。latch是门闩的意思。
有位博主打了个比方,在此引用:“三二一,芝麻开门”。
跟信号量的思想相似,不过countDownLatch更加简单。只有两个方法,一个是countDown,一个是await。countDown方法的效果就是倒数,await的作用是等待倒数至0。
一个背景是,即使各线程的工作量划分非常合理,它们也总会先后完成。
配合使用的效果是,确保数个协作的并发线程都完成后,才继续向下执行。
volatile
volatile的本意是易变的。一个背景是,易变的东西是不适合缓存的。
在C中,volatile的作用是,确保读写都访问变量地址(而不是缓存)。
在Java中也是同样的意思。
同步会让主存刷新。所以如果一个变量完全由 synchronized 的方法或代码
段 (或者 java.util.concurrent.atomic 库里类型之一) 所保护,则不需要让变量用volatile。
volatile关键字可以阻止重排 volatile变量周围的读写指令。这种重排规则称为 happens before 担保原则。这项原则保证在 volatile 变量读写之前发生的指令先于它们的读写之前发生。同样,任何跟随 volatile 变量之后读写的操作都保证发生在它们的读写之后。
bruce建议不要用这个关键字,用atomic系列来替代。
CAS
全称是compare and swap。已经得到汇编指令支持。
回想一下那道经典题,两个线程各执行100次自增,有哪些可能的结果。
1.线程a和b一开始都读到0,存在线程内存中;
2.线程a执行99次,将99写入主存;
3.线程b执行1次,将1写入主存;
4.线程a读到1;
5.线程b执行99次,将99写入主存;
6.线程a执行1次,将2写入主存。
其实,线程b在第三步的时候如果compare一下就会发现问题了。线程b在第一步读是0,在第三步读到的是99,这说明线程b在“读取-计算-准备写入”期间,有其它线程也在工作。
compare and swap就在于此,在写入之前,compare一下读到的值(99)和预期值(0),如果不同,就不写入,而是用读到的值重新计算。也就是重新进行“读取-计算-准备写入”的过程。
乐观,就是假设冲突较少,或者说“相信”多线程不会出问题。悲观,就是假设冲突较多。
因此,乐观锁适合并发量小,冲突确实比较少的场景;悲观锁则反之。
CAS和synchronized相比,好处在于,避免了线程状态的切换。
java中,Atomic开头的包装类,如AtomicInteger,底层实现就是CAS。相比synchronized,更加轻量。
jdk1.6对synchronized的优化
自旋锁
上文已经介绍。
锁消除
消除不可能存在数据竞争的锁
锁粗化
粗化的好处是不用加锁多次。
轻量级锁
无竞争条件时,通过CAS消除同步互斥
偏向锁
无竞争条件时,消除同步互斥,也不进行CAS操作。
java AtomicBoolean compareAndSet Demo
实现了一个计时器。
public class TimeController implements Runnable {
private waitSeconds;
public static AtomicBoolean isStop = new AtomicBoolean(false);//初始值
public TimeController(long waitSeconds) {
if(waitSeconds == 0) {
waitSeconds = Long.MAX_VALUE;
}
}
@Override
public void run() {
while(waitSeconds > 0) {
if(isStop.get() == true) {
//提示信息
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//提示信息
}
waitSeconds--;
}
isStop.compareAndSet(false, true);//第一个参数是expect,第二个参数是update
}
}
threadlocal
相当于线程的私有存储空间,有get和set方法。
concurrent queue
据说java concurrent queue的size方法是O(n)的。还可深究。
原子操作是否需要同步
能通过Goetz测试的专家可以尝试用原子操作来代替同步。否则不要尝试。
可见性问题比原子性问题多得多。
原子操作并不解决可见性的问题。相反,同步机制强制多核处理器系统上的一个任务做出的修改必须在应用程序中是可见的。
对属性赋值,返回,通常是原子性的。
在Java中,自增不是原子性的。
copyonwrite容器
比如,CopyOnWriteArrayList。
多线程环境下保证线程安全(以免修改到一半被读取。只有在完成修改之后才可见), 并通过读写分离提高性能。
CopyOnWriteArraySet 使用 CopyOnWriteArrayList 来实现其无锁行为。
ConcurrentHashMap 和 ConcurrentLinkedQueue 使用类似的技术来允许并发读写,但是只复制和修改集合的一部分,而不是整个集合。
可重入锁
意思是,在一个线程中可以多次获取同一把锁。
比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
公平与非公平
公平,先来的先获得锁。
非公平,谁抢到算谁的。
非公平的效率较高。synchronized是非公平的。
这两种情况一般一种是默认,另一种可以通过参数设置。
并发编程步骤
1.尽量别用。
2.尽量用stream.parallel和completableFutures。
3.不要共享变量,任务间的信息传递应该通过concurrent库中的并发数据结构。
4.共享变量时用atomic类,或者将所有对共享变量的访问加synchronized。
5.volatile和synchronized一般都可以避免,尽量用concurrent库组件来实现。