锁类型 | 描述 | 优点 | 缺点 | 举例 |
---|---|---|---|---|
公平锁 | 线程按申请锁的顺序获取锁 | 有序 | 吞吐量较低 | new ReentrantLock(true) |
非公平锁 | 线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁 | 吞吐量相对大 | 高并发时可能导致优先级反转或饥饿 | new ReentrantLock() |
可重入锁(递归锁) | 线程可以任意进入它已经持有的锁所包围的代码块中 | 避免重复调用时的死锁 | ReentrantLock / synchronized | |
自旋锁 | 加锁失败时,线程不进入阻塞而是通过循环等待锁 | 减少线程上下文切换的消耗 | 长时间循环消耗 CPU | CAS 原子类 |
独占锁 / 排他锁 / 互斥锁 | 锁被一个线程独享,加了独占锁的资源不能加其他锁 | synchronized | ||
共享锁 | 锁可以又多个线程共享,加了共享锁的资源可以继续加共享锁 | 并发程度高 | ReentrantReadWriteLock | |
读写锁 | 一种共享锁,管理一个共享读锁和一个独占写锁 | ReentrantReadWriteLock | ||
乐观锁 | 认为读多写少,通常不会并发抢锁 | 快 | CAS、数据版本 | |
悲观锁 | 认为写多读少,通常并发抢锁 | 安全 | 会锁住资源,被锁的资源会阻塞其他需要此资源的线程 | synchronized |
无锁 | 对象上的监视器处于无并发状态 | synchronized 阶段 0 | ||
偏向锁 | 同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价 | 低并发时减少 CAS 开销 | synchronized 阶段 1 | |
轻量级锁 | 同步代码被多个线程访问,优先依赖自旋尝试获取锁 | 低并发时效率高 | 高并发时除线程切换还加上 CAS 开销 | synchronized 阶段 2 |
重量级锁 | 自旋到一定程度锁膨胀时切换 | 效率低 | synchronized 阶段 3 | |
统一锁 | 被锁的资源是资源全体 | synchronized | ||
分段锁 | 被锁的资源是资源的一部分 | 效率高,同一组资源分几段就能容纳最大几段并发 | 不能无限分段 | ConcurrentHashMap |
// 默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁
线程按申请锁的顺序获取锁
公平锁会维护一个等待队列,
若当前线程是队列中第一个,则获取锁
否则,加入等待队列队尾,按先进先出规则排队
非公平锁
线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁
线程申请非公平锁时会直接跳到申请队列开头,如果没有申请成功,再按照类似公平锁排队
synchronized 因为有抢锁,所以也是非公平锁
优点
吞吐量比公平锁大
节省了频繁切换线程上下文的浪费
缺点
高并发时可能导致优先级反转或饥饿
线程的外层函数获取锁后,内层[递归]函数可自动获取锁
线程可以任意进入它已经持有的锁所包围的代码块中
示例
public class LOC {
Lock lock = new ReentrantLock();
public synchronized void m1() throws Exception{
System.out.println(Thread.currentThread().getName()+" m1");
// 同一个线程,已持有锁,访问另一个需要锁的同步方法,自动获取线程上的锁
m2();
}
public synchronized void m2() throws Exception{
System.out.println(Thread.currentThread().getName()+" m2");
}
public void m3() throws Exception{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" m3");
// 同一个线程,已持有锁,访问另一个需要锁的方法,自动获取线程上的锁
m4();
}finally {
lock.unlock();
}
}
public void m4() throws Exception{
lock.lock();
// 可重入锁,锁两层约等于递归一次,还是可以获取持有的锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" m4");
}finally {
//只要解锁与加锁匹配就没问题
lock.unlock();
lock.unlock();//若注释此行,不报错,但解锁时阻塞
}
}
public static void main(String[] args) {
LOC loc = new LOC();
new Thread(()->{
try {
new LOC().m1();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
new LOC().m3();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态
public class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread t = Thread.currentThread();
while(!atomicReference.compareAndSet(null,t)){
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+" waiting");
}
System.out.println(t.getName()+" locked");
}
public void unlock(){
Thread t = Thread.currentThread();
// 解锁时不用自旋,如果不是说明已经解了
// 但必须使用 compareAndSet 防止解了其他线程的锁
atomicReference.compareAndSet(t,null);
System.out.println(t.getName()+" unlocked");
}
public void run (){
lock();
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+ " run");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
unlock();
}
}
public static void main(String[] args) {
SpinLock demo = new SpinLock();
new Thread(demo::run,"A").start();
new Thread(demo::run,"B").start();
}
}
自旋锁 与 非自旋锁
非自旋锁 在加锁失败时会进入 阻塞(Block)状态,再次被唤醒时需要从 阻塞(Block)状态 切换为 运行(Runnable)状态,状态切换时涉及线程上下文的切换,性能较差。
自旋锁 在加锁失败时会进入 自旋状态,自旋其实就是就是一个不停尝试获取锁的循环,此时线程始终是 运行(Runnable)状态 的,当真的获取到锁时,也不会涉及到线程状态或上下文的切换,性能相对 非自旋锁 高很多
缺点
适用场景
因为 自旋锁 会一直占有 CPU,因此 自旋锁 适用于很快可能获取锁的场景,即持有锁的线程可以快速处理完成并释放锁的场景,比如 CAS 操作
独占锁
锁被一个线程独享
共享锁
锁可以又多个线程共享
读写锁
读写锁中管理多个锁,一个共享读锁和一个独占写锁
public class ReadWriteLockDemo {
private static Map<String,String> map = new HashMap<>();
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void put(String key,String value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" put "+key);
TimeUnit.MILLISECONDS.sleep(100);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" put done "+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
public static void get(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" get "+key);
map.get(key);
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(Thread.currentThread().getName()+" get done "+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public static void main(String[] args) {
for(int i=0;i<3;i++){
int finalI = i;
new Thread(()->{
put(String.valueOf(finalI),UUID.randomUUID().toString().substring(0,8));
},String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<3;i++){
int finalI = i;
new Thread(()->{
get(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
读时无所谓,写时必须每个线程独占整个锁
synchronized
、ReenterLock
都是悲观锁版本号机制 Version
和 CAS