Java中的锁
可重入锁
顾名思义,支持重新进入的一把锁,也就是一个线程能够对资源进行重复加锁。
Java中的ReentrantLock和Synchronized都是可重入锁
例子:
package classs.juc;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
Lock lock = new ReentrantLock();
public void say(){
lock.lock();
try {
System.out.println("say");
jump();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("释放锁");
lock.unlock();
}
}
public void jump(){
lock.lock();
try {
System.out.println("jump");
running();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void running(){
lock.lock();
try {
System.out.println("running");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(()->{
reentrantLockDemo.say();
},String.valueOf(i)).start();
}
}
}
不可重入锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
通过分析可重入锁的源码可以看到:
当再次请求获取锁的时候回进行判断,判断当前线程是不是获取到该锁的线程,则当前线程能够直接获取到该锁。而不可重入锁则直接判断getState()获取到的值,如果不为0则获取锁失败,线程阻塞。
公平锁、非公平锁:
通过对ReentrantLock进行分析来了解公平锁和非公平锁
首先看ReentrantLock的构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从构造方法可以看出默认是实现的非公平锁。
通过传入布尔值可以确定实现的是公平锁还是非公平锁。
Sync继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer
公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order //尾节点
Node h = head; // 头结点
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
相比较非公平锁,公平锁在判断时多加了一步hasQueuedPredecessors()判断。即加入到同步队列之后判断该节点是否有前驱节点,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。不会出现饥饿状态,也就是说按照线程所处的队列位置按照顺序获取锁。
非公平锁
非公平锁中没有hasQueuedPredecessors()判断。即当某一个线程先尝试获取锁,如果状态更新成功则直接获取到锁。可能会导致某一线程‘饿死’。也就是一直获取不到锁。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
共享锁、排他锁
共享锁、排他锁都是通过AQS来实现的。
共享锁是一种概念,是指可以被多个线程所持有。如果线程A对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
排它锁也是一种概念,也叫独享锁,是指这个锁同时只能被一个线程所持有。如果线程A对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
ReentrantReadWriteLock分析
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
//writelock
public void lock() {
sync.acquire(1);
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
//取出写锁的个数
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//readlock
public void lock() {
sync.acquireShared(1);
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
通过分析得到ReentrantReadWriteLock中有两把锁,分别是读锁和写锁。
其中读锁是共享锁,写锁是排他锁。
以下来自美团技术文章:理解下面的过程即可
写锁:
- 这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount©; ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
- 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
- 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
- 如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。
- 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!
tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。
因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。
读锁:
可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。
死锁
线程A持有锁C并想获得锁D的同时,线程B持有锁D想要获取锁L。两个线程永远的等待下去,这种情况就叫做死锁。
悲观锁、乐观锁与读写锁
悲观锁和乐观锁也是一种广义上的概念。
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候再去判断之前有没有别的线程更新了这个数据。如果数据被更新则采取报错或者自动尝试等操作。JAVA中的CAS算法就是采用了乐观锁的机制,通过无锁编程来实现。
悲观锁则认为自己在使用数据的时候一定有别的线程来修改,因此在获取数据的时候会先加锁,确保数据不会被其他的线程修改。JAVA中的synchronized关键字和Lock接口的实现类都是悲观锁。
通过对上面的分析,则可以得出悲观锁适合写操作多的场景。乐观锁适合读操作多的场景。
例子:
package classs.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BLLockDemo {
private Lock lock = new ReentrantLock();
private AtomicInteger atomicInteger = new AtomicInteger(1);
private Integer num1 = 1;
private Integer num2 = 1;
public synchronized void num1Add(){
num1++;
}
public void num2Add(){
lock.lock();
try {
num2++;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void atomicNum(){
atomicInteger.incrementAndGet();
}
public void printNum(){
System.out.println("num1:"+num1);
System.out.println("num2:"+num2);
System.out.println("num3:"+atomicInteger.get());
}
public static void main(String[] args) {
BLLockDemo blLockDemo = new BLLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(()->{
blLockDemo.num1Add();
},String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
new Thread(()->{
blLockDemo.num2Add();
},String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
new Thread(()->{
blLockDemo.atomicNum();
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
blLockDemo.printNum();
}
}
上面代码的输出结果都为11,说明CAS实现了多线程之间的变量同步。
自旋锁
自旋锁的实现原理同样也是CAS,例如AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自选操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
无锁、偏向锁、轻量级锁、重量级锁
这四种锁是指锁的状态,专门针对synchronized的。
Synchronized是怎样实现线程同步的?
Synchronized是悲观锁,在操作同步资源之前都需要给同步资源先加锁,这把锁的信息就是报错在Java对象头里的。
以Hotspot虚拟机为例子,对象头主要分为两部分:Mark Word(标记字段)、Klass Pointer(类型指针)
参考于并发编程艺术:
Monitor
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:
锁状态 | 存储内容 | 存储内容 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
无锁:就是没有对资源进行锁定,所有线程都能够访问并且修改同一资源,但同时只有一个线程修改成功。
偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 00 |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
无锁:就是没有对资源进行锁定,所有线程都能够访问并且修改同一资源,但同时只有一个线程修改成功。
偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。