小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
1.悲观锁
1.悲观锁的代表是 synchronized 和 Lock 锁
1.其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
2.线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
3.实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
2. lock 与 synchronized 的区别
1.语法层面
1.synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
2.Lock 是接口,源码由 jdk 提供,用 java 语言实现
3.使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
2.功能层面
1.二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
2.Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
3.Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock (针对对多写少的场景)
3.性能层面
1.在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不错
2.在竞争激烈时,Lock 的实现通常会提供更好的性能
3. lock
1.公平锁的公平体现
1.已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
2.公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
3.非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
4.公平锁会降低吞吐量,一般不用
2.条件变量
1.ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
2.与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制
2.乐观锁
1.乐观锁的代表是 AtomicInteger,使用 CAS 来保证原子性
1.其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
2.由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
3.它需要多核 cpu 支持,且线程数不应超过 cpu 核数
2. CAS
-
CAS 的意思是 compare and swap,比较并交换。
-
CAS 有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则获取新的内存值,重试直到成功为止。
模拟AtomicInteger的源码测试一下CAS的实现,代码如下:
/**
* 模拟CAS实现
*/
public class SyncVsCas {
private static final Unsafe U;
private static final long BALANCE;
static {
try {
// 由于JDK1.8中 Unsafe 可以直接操作内存有保护机制不能直接使用 使用反射获取对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
U = (Unsafe)field.get(null);
// 通过反射获取内部类 Account 的 balance 属性值
BALANCE = U.objectFieldOffset(Account.class.getDeclaredField("balance"));
} catch (Exception ex) {
throw new Error(ex);
}
}
static class Account {
volatile int balance = 10;
}
//模拟CAS操作
private static void basicCas(Account account) {
while (true) {
//获取内存当中的值
int o = account.balance;
//准备更新的值
int n = o + 5;
// 进行cas操作如果更新成功结束循环,不成功进入下一轮比较
if(U.compareAndSwapInt(account, BALANCE, o, n)){
break;
}
}
System.out.println(account.balance);
}
public static void main(String[] args) {
Account account = new Account();
basicCas(account);
}
}
我们在代码中更新的值,就是获取原初始值10加5,正常执行没有其他线程并发修改的情况下,得到了我们想要的结果15.
模拟并发情况,在debug模式下,代码执行过程中修改了原始值为100
此时比较由于原始值已变,所以更新值失败,进入下一轮比较,在获取到的原始值就是100
此时再比较由于没有修改原始值,所以更新成功结束循环
3. CAS 的缺点
1. ABA 问题
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号 +1,那么A-B-A 就会变成1A-2B-3A
2. 循环时间长开销大
一般情况下使用CAS要配合自旋(死循环)一起,如果高并发的时候,会出现有很多请求多次循环也成功不了的情况,给cpu带来非常大的消耗。ConcurrentHashMap有类似的解决方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
3.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量a=1,b=x,合并一下ab=1x,然后用CAS来操作ab。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。