目录
1.悲观锁 VS 乐观锁
互斥同步属于一种悲观的并发策略:
其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁,这会导致用户态到核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销。
基于冲突检测的乐观并发策略,:
不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;
如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,
最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。
锁类型 | 描述 | 实现举例 | 优劣 |
---|---|---|---|
悲观锁 | 悲观地认为别人也在修改,修改数据前加锁 | Java中
扫描二维码关注公众号,回复:
10908802 查看本文章
|
性能开销 |
乐观锁 | 乐观地认为别人不会修改,不会上锁 | CAS、atomic类 版本号 update table set... where version=#{version} 类似于CAS |
这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free)编程。
乐观锁适用于锁冲突少(读多写少)的场景,提高吞吐量,悲观锁适用于写多的场景。其实synchorinzed和ReentrantLock在实现上也借助CAS进行了优化,其实并不是直接阻塞线程的。
2.公平锁 VS 非公平锁
锁类型 | 描述 | 实现举例 |
---|---|---|
公平锁 | 先到先得 |
ReentrantLock fairLock = new ReentrantLock(true); FairSync |
非公平锁 | 存在插队 |
ReentrantLock unfairLock = new ReentrantLock(false); NonfairSync |
为什么ReentrantLock的NonfairSync明明会将阻塞的线程排队啊?为什么是不公平的呢?
因为虽然在锁被占用的时候把其余竞争的线程插入了队列,且锁释放时会唤醒队头线程,但是如果在锁释放的时刻有新的线程竞争的话,不能确保队头线程能够获取锁,也有可能是新线程获取到锁。
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
3.独占锁 VS 共享锁
锁类型 | 描述 | 实现举例 |
---|---|---|
独占锁 | 任何时候都只有一个线程能得到锁(互斥锁) |
ReentrantReadWriteLock.WriteLock |
共享锁 | 允许多个线程同时进行读操作 | ReentrantReadWriteLock.ReadLock |
4.可重入锁
当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,
当一个线程再次获取它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以多次地进入被该锁锁住的代码。
如synchronized、
ReentrantLock都是可以重入的,
实现
进入临界区:就是在发现锁被占用时会判断占用锁的线程id与当前线程ID,如果相同则可以进入,且计数值++;
离开(释放):计数器值-1。当计数器值为0时,锁里面的线程标示被重置为null,释放锁。
5. 自旋锁
一个线程在获取锁失败后,会被park(系统调用)挂起(需要切换到操作系统内核态);当锁释放时又需要去unpark(系统调用)唤醒之前被刮起的线程。
系统调用(从用户状态切换到内核状态)的开销很大,会影响并发性能。
自旋锁基于此优化:
当前线程在获取锁失败,不马上阻塞自己,循环多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值),
很有可能在后面几次尝试中其他线程已经释放了锁。
如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。