Q1:Java有哪几种锁?
在java技术中,尝尝听到下列关于锁的名词,在应用开发层面,都是基于synchronized和ReentrantLock的集成或实现。
公平锁/非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
- 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
可重入锁
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,详细解读:https://blog.csdn.net/qiuyang18/article/details/77606749
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
独享锁/共享锁
- 独享锁是指该锁一次只能被一个线程所持有。ReadWriteLock
- 共享锁是指该锁可被多个线程所持有。Synchronized
互斥锁/读写锁
- 互斥锁在Java中的具体实现就是ReentrantLock
- 读写锁在Java中的具体实现就是ReadWriteLock
乐观锁/悲观锁
- 乐观锁和悲观锁不是特定的锁,而是指看待并发同步的角度。
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。悲观锁在Java中的使用,就是利用各种锁。
- 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁
- 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
偏向锁/轻量级锁/重量级锁
- 这三种锁是指锁的状态,并且是针对Synchronized。是JVM通过对象监视器在对象头中的字段来表明的。
自旋锁
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign .compareAndSet(current, null); } }
- 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
本回答参考:https://www.cnblogs.com/qifengshi/p/6831055.html
Q2:synchronized 和 ReentrantLock 有什么区别?
- synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
- ReentrantLock,通常翻译为再入锁,它的语义和 synchronized 基本相同。
- 再入锁通过代码直接调用 lock() 方法获取。
- 代码书写也更加灵活。能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。
- 必须要明确调用 unlock() 方法释放锁。
Q3:有人说 synchronized 最慢,这话靠谱吗?
早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。对于synchronized,JVM通过偏向锁/轻量级锁/重量级锁去优化实现,性能有很大改进。
Q4:讲一下线程安全:
- 线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性。
保证线程安全的两个办法:
- 封装
- 不可变
线程安全需要几个基本特性:
- 原子性
- 可见性:volatile
- 有序性:防止被指令重排序(JVM部分会总结到)
Q5:synchronized 底层如何实现?什么是锁的升级、降级?
- synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现。如果用 javap 反编译,可以看到类似片段,利用 monitorenter/monitorexit 对实现了同步的语义:
11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield #2 // Field sharedState:I
18: dup_x1
…
56: monitorexit
- 在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
- JDK6以后,JVM 对此提供了三种不同的 Monitor 实现。也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。
- 所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
- 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
- 如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
- 当 JVM 进入安全点的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
Q5 并发包里的其他锁,为什么我们需要读写锁(ReadWriteLock)等其他锁呢?:
- ReadWriteLock 是一个单独的接口,它通常是代表了一对儿锁,分别对应只读和写操作,标准类库中提供了再入版本的读写锁实现(ReentrantReadWriteLock),对应的语义和 ReentrantLock 比较相似。
- StampedLock 竟然也是个单独的类型,从类图结构可以看出它是不支持再入性的语义的,也就是它不是以持有锁的线程为单位。
- ReentrantLock 和 synchronized 简单实用,但是行为上有一定局限性,通俗点说就是“太霸道”,要么不占,要么独占。有的时候不需要大量竞争的写操作,而是以并发读取为主,需要进一步优化锁操作的粒度。
- Java 并发包提供的读写锁等扩展了锁的能力。
- 基于的原理是多个读操作是不需要互斥的,因为读操作并不会更改数据,所以不存在互相干扰。
- 而写操作则会导致并发一致性的问题,所以写线程之间、读写线程之间,需要精心设计的互斥逻辑。
public class RWSample {
private final Map<String, String> m = new TreeMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public String get(String key) {
r.lock();
System.out.println(" 读锁锁定!");
try {
return m.get(key);
} finally {
r.unlock();
}
}
public String put(String key, String entry) {
w.lock();
System.out.println(" 写锁锁定!");
try {
return m.put(key, entry);
} finally {
w.unlock();
}
}
// …
}
- 但在实际应用中,读写锁因为相对比较大的开销,所以JDK 在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。
public class StampedSample {
private final StampedLock sl = new StampedLock();
void mutate() {
long stamp = sl.writeLock();
try {
write();
} finally {
sl.unlockWrite(stamp);
}
}
Data access() {
long stamp = sl.tryOptimisticRead();
Data data = read();
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
data = read();
} finally {
sl.unlockRead(stamp);
}
}
return data;
}
// …
}
本文来源于:https://time.geekbang.org/column/article/9042 文章的总结,老师讲的很棒。推荐看原文
还没掌握的知识点:
- AQS:https://www.cnblogs.com/waterystone/p/4920797.html
- 使用过 ReentrantLock 中的哪些方法呢?分别解决什么问题?
所有的Lock都是基于AQS来实现了。AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。