1、什么是锁
锁我们可以理解为一个通行证,一个线程想要对某个共享资源进行访问,需要先获取该资源的通行证(获取锁),并且通行证只能被一个线程持有。通行证持有线程在结束对这些共享数据的访问后必须让出通行证(释放锁),以便其他线程能获取通行证,对该共享资源进行访问。在线程持有锁到释放锁这段时间所执行的代码被称为临界区。临界区一次只能被一个线程执行。
2、锁的作用
锁可以保证共享资源的原子性,可见性,以及有序性。
- 原子性:锁是通过互斥来保证原子性的。当锁被一个线程持有的时候,其他线程无法访问该锁的临界区,因此临界区的代码一次只能被一个线程执行,这使得临界区里的代码操作具有不可分割的特性,即原子性。
- 可见性:可见性保障是通过写线程冲刷处理器缓存,读线程刷新处理器缓存**实现的。锁的获得隐含者刷新处理器缓存这个动作,这使得读线程在执行临界区代码前可以将写线程对共享变量的更新同步到该线程执行处理器的告诉缓存中;而锁的释放隐含着冲刷处理器缓存这个动作,这使得写线程对共享变量所做的更新能够被推送到该线程处理器的高速缓存中,从而对读线程同步。
- 有序性:假设写线程在临界区更新三个变量,如下:
b = a + 1;
c = 2;
flag = true;
那么,由于锁保证了原子性和可见性,在读线程读取临界区的变量c = 2时,那么b一定比a大1,flag = true。
虽然锁可以保证有序性,但是并不代表不能进行重排序。
2.1、可重入锁
可重入锁是指一个线程拥有某个锁时,可以再次获得该锁。syncronized就是可重入锁。
可重入锁对象包含一个计数器,当一个线程获得一个可重入锁时,count++,当一个线程释放一个可重入锁时,count–所以,一个线程第一次获取一个可重入锁时开销比较大,后面开销比较小(只需要coun++就可以)
2.2、锁泄露
锁泄露是指由于程序错误,一个线程持有某个锁以后一直没有释放,导致了其他线程无法得到这个锁的现象。
锁的问题:锁泄露
3、内部锁
3.1内部锁的实现方法
内部锁是通过syncronized来实现的,使用方法如下:
public void test() {
syncronized(句柄锁) {
//do something
}
}
句柄锁一般用private final修饰,这是因为锁的对象一旦被改变,会导致执行同一个代码块的不同线程使用的不是同一个锁。(特别需要注意Integer和String)
public void test1() {
syncronized(this) {
//do something
}
}
public void syncronized test2() {
//do something
}
test1和test2是等价的,this表示当前对象。
3.2、内部锁的调度
多个线程同时竞争一个内部锁,只有一个线程能获得该锁,其他线程会被加入到这个内部锁的entry set中。在锁被释放后,entry set中只有一个线程能被唤醒,然后与其它Runnable状态的线程竞争该锁(还未加入entry set,第一次竞争该锁的线程。)
4、显式锁Lock
使用Lock.lock()加锁,try{}中存放临界区代码块,finally中释放锁,防止锁泄露。
5、公平锁与非公平锁
公平锁开销比较大,非公平锁直接使用cas操作
公平锁tryAcquire()实现:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//state为0说明锁可以获取
/*
公平锁与非公平锁的差异主要在这个if语句,公平锁首先会先判断队列中是否有其他线程在等待,如果有,
返回false。如果没有,使用CAS尝试抢占线程。
*/
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //可重入逻辑
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
6、读写锁:
默认实现类:ReentrantReadWriteLock
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 相应的写锁未被任何线程占有 | 对读线程共享,对写线程排他 | |
写锁 | 相应的读锁和写锁未被任何线程占有 | 对读线程和写线程都是排他的 |
public ReadWriteLockUsage {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void reader() {
readLock.lock();
try {
//do something
} finally {
readLock.unlock();
}
}
public void writer() {
writeLock.lock();
try {
//do something
} finally {
writeLock.unlock();
}
}
}
注意:
- ReadWriteLock接口实例只对应一个锁,readLock()和writeLock()返回的是同一个锁,但是充当不同作用。
- ReentrantReadWriteLock支持锁的降级,即:一个线程持有一个读写锁的写锁的时候,可以申请该读写锁的读锁。
读写锁的应用场景(缺一不可):
- 读线程比写线程多很多
- 读线程持有锁的时间很长
7、内存屏障:线程同步机制的底层助手
锁是如何保证可见性的:执行临界区代码前(获取锁后)刷新处理器缓存,保证当前线程能够读到上一个线程更新的数据;释放锁是冲刷缓冲区,保证后面持有锁的线程能够读到该线程更新的数据。JVM底层是通过内存屏障来实现上述两个动作的。