Java中锁的分类有:
- 可中断锁
- 可重入锁(递归锁)
- 公平锁/非公平锁
- 独占锁(互斥锁)/共享锁
- 乐观锁/悲观锁
- 自旋锁
- 偏向锁/轻量级锁/重量级锁(锁的状态)
- 分段锁(锁的设计)
上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。
synchronized与Lock对比
|
synchronized |
Lock |
别名 |
隐式锁 |
显式锁 |
实现方式 |
Java中的关键字,语言内置实现 |
是一个接口 |
锁的释放方式 |
同步代码执行结束或抛出异常时自动释放 |
finally 块中手动释放 |
多个锁嵌套的释放顺序 |
依次获取(释放)锁, 最先获取的最后释放 |
自由获取(释放)锁 |
发生异常时 |
自动释放线程占有的锁,不会导致死锁 |
需在 finally 块中手动释放锁,否则会导致死锁 |
定时阻塞获取锁 |
不支持 |
支持 |
判断是否已成功获取锁 |
无法办到 |
tryLock() 返回值是 boolean |
性能 |
|
竞争激烈时(大量线程同时获取锁),性能更优 |
|
|
|
可中断锁 (获取锁时能否响应线程中断指令) |
非 |
是
tryLock(long time, TimeUnit unit)
lockInterruptibly() |
可重入锁 |
是 |
是 |
公平锁 |
非公平锁,无法判断锁是否公平 |
默认非公平锁。 实现类的构造方法可指定锁的公平性。isFair() 判断锁是否公平。 |
独占锁(互斥锁)/共享锁 |
独占锁(互斥锁) |
独占锁:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock 共享锁:ReentrantReadWriteLock.ReadLock 通过 AQS(AbstractQueuedSynchronizer)实现独占和共享。 |
读写锁 |
无 |
ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.WriteLock |
可中断锁
中断锁指的是可中断获取锁的等待状态,也就是在获取锁时可以响应线程中断指令,从而中断获取锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁。
可重入锁(递归锁)(特性)
可重入互斥锁 - 维基百科
reentrant :可重入; 可重入的; 重入; 可再入的; 重进入;
可重入锁又名递归锁。特点:
- 可重复进入持有相同锁的同步作用域(同步代码块、同步方法、Lock锁同步代码)。
- 在一个线程中可以多次获取同一把锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导致锁不被释放。
如果不设计成可重入锁,反复给自己加锁就会产生死锁。
锁类型 |
锁对象 |
可重入锁 |
synchronized |
可重入锁 |
ReentrantLock(Re entrant Lock 重新进入锁) |
可重入锁 |
ReentrantReadWriteLock.ReadLock |
可重入锁 |
ReentrantReadWriteLock.WriteLock |
不可重入锁 |
没有内置对象,需自定义 |
可重入锁演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class Test { public static void main(String[] args) { new Test().同步代码块(); new Test().同步方法(); Test.静态同步方法(); Test.Lock锁(); }
public void 同步代码块() { synchronized (this) { System.out.println("同步代码块,第一次获取锁"); synchronized (this) { System.out.println("同步代码块,第二次获取锁"); } } }
public synchronized void 同步方法() { System.out.println("同步方法,第一次获取锁"); 同步方法2(); }
public synchronized void 同步方法2() { System.out.println("同步方法,第二次获取锁"); }
public static synchronized void 静态同步方法() { System.out.println("静态同步方法,第一次获取锁"); 静态同步方法2(); }
public static synchronized void 静态同步方法2() { System.out.println("静态同步方法,第二次获取锁"); }
public static void Lock锁() { Lock lock = new ReentrantLock(); lock.lock(); System.out.println("ReentrantLock,第一次获取锁"); lock.lock(); System.out.println("ReentrantLock,第二次获取锁"); try { } finally { lock.unlock(); lock.unlock(); } } }
|
不可重入锁演示
实际开发中几乎不会用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| public class Test { public static void main(String[] args) { Lock lock = new UnReentrantLock(); lock.lock(); System.out.println("UnReentrantLock,第一次获取锁"); lock.lock(); System.out.println("UnReentrantLock,第二次获取锁"); try { } finally { lock.unlock(); lock.unlock(); } } }
class UnReentrantLock implements Lock { private Thread bindThread;
@Override public void lock() { synchronized (this) { while (bindThread != null) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.bindThread = Thread.currentThread(); } }
@Override public void unlock() { synchronized (this) { if (bindThread != Thread.currentThread()) { return; } bindThread = null; notifyAll(); } }
@Override public void lockInterruptibly() throws InterruptedException {
}
@Override public boolean tryLock() { return false; }
@Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; }
@Override public Condition newCondition() { return null; } }
|
公平锁/非公平锁
是否公平:指的是每个线程获取锁的机会是否平等。
公平锁:多个线程按照申请锁的顺序来获取锁。比如同时有多个线程在等待同一个锁,当这个锁被释放时,等待时间最久的线程(最先申请的线程)会获得该锁。
非公平锁:多个线程争抢同一个锁时,有可能后申请的线程比先申请的线程优先获取锁。可能造成优先级反转或者饥饿现象(导致某些线程永远获取不到锁)。优点是比公平锁吞吐量大。
对于 synchronized
而言,也是一种非公平锁。由于其并不像 ReentrantLock
是通过 AQS(AbstractQueuedSynchronizer) 的来实现线程调度,所以并没有任何办法使其变成公平锁。
对于 ReentrantLock
和 ReentrantReadWriteLock
实现而言,默认是非公平锁,构造方法可指定锁的公平性。
在 ReentrantLock 和 ReentrantReadWriteLock 中都定义了两个静态内部类,分别用来实现非公平锁和公平锁。可以在创建对象时,通过构造方法指定锁的公平性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { }
public class ReentrantLock implements Lock, java.io.Serializable { abstract static class Sync extends AbstractQueuedSynchronizer { } static final class FairSync extends Sync { } static final class NonfairSync extends Sync { } }
|
非公平锁演示
下面示例中,每次都是 Thread-0 拿到锁,验证了 synchronized 是非公平锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class Test { public static void main(String[] args) { Task task = new Task(); Thread thread0 = new Thread(task); Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread0.start(); thread1.start(); thread2.start(); } }
class Task implements Runnable { @Override public void run() { while (true) { synchronized (this) { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
|
修改上面示例 Task 类如下,验证 Lock 的非公平锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Task implements Runnable {
private Lock lock = new ReentrantLock();
@Override public void run() { while (true) { lock.lock(); try { System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
|
公平锁演示
修改上面示例 private Lock lock = new ReentrantLock(true);
即可。
判断锁是否公平
1 2 3
| public final boolean isFair() { return sync instanceof FairSync; }
|
该方法只能判断 ReentrantLock、ReentrantReadWriteLock:
1 2 3 4 5 6
| public class Test { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); System.out.println(lock.isFair()); } }
|
独占锁(互斥锁)/共享锁
排他锁 - 维基百科 、 互斥锁 - 维基百科
共享锁 - 百度百科
读写锁 - 维基百科
独占锁(互斥):该锁一次只能被一个线程所持有。例如:synchronized、ReentrantLock、读写锁中的写锁 ReentrantReadWriteLock.WriteLock
共享锁(非互斥):该锁可被多个线程所持有。例如:读写锁中的读锁 ReentrantReadWriteLock.ReadLock,但读写、写读 、写写的过程是互斥的。
Lock 锁的 独占 与 共享 是通过 AQS(AbstractQueuedSynchronizer)来实现的。
共享锁演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class Test { public static void main(String[] args) { Task task = new Task(); Thread thread0 = new Thread(task); Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread0.start(); thread1.start(); thread2.start(); } }
class Task implements Runnable {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = this.readWriteLock.readLock();
@Override public void run() { this.readLock.lock(); try { System.out.println(Thread.currentThread().getName()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); } } }
|
独占锁演示
修改上述代码:
1 2 3 4 5 6
| class Task implements Runnable {
private Lock writeLock = this.readWriteLock.writeLock(); }
|
乐观锁/悲观锁
乐观锁与悲观锁指的不是锁类型,而是指 并发策略 是乐观的还是悲观的。
乐观并发控制 - 维基百科
悲观并发控制 - 维基百科
悲观锁:只要编码中 利用到锁,并发策略就是悲观的。
乐观锁:无锁编程,通常采用 CAS 等原子操作来实现。典型的例子就是原子类,通过 CAS 自旋实现原子操作。
自旋锁
自旋锁 - 维基百科
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,避免线程上下文切换带来的开销。但由于获取锁的线程一直处在运行状态,所以不适合单核单线程的CPU。
通常采用 CAS 等原子操作来实现,可以参考 自旋锁的实现 。
偏向锁/轻量级锁/重量级锁(锁的状态)
这三种锁是指锁的状态,并且是针对 synchronized
。在 Java 5 通过引入锁升级的机制来实现高效 synchronized
。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
分段锁(锁的设计)
分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以 ConcurrentHashMap
来说一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为 Segment,它即类似于 HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。
当需要 put 元素的时候,并不是对整个 HashMap 进行加锁,而是先通过 hashcode 来知道他要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计 size 的时候,就是获取 HashMap 全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。