欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java中的锁机制,你真的了解吗?

程序员文章站 2022-05-04 21:00:24
...

学到锁说明你已经学过多线程了,只有在多线程并发的情况下才会涉及到锁,相信大家用的最多的要数synchronized了,因为这个也是最简单的,直接加在方法上就可以使一个方法同步。那么除了synchronized之外,还有没有其他的锁呢,这个还真有。我们来看看:

Java中的锁机制,你真的了解吗?

这个是Java里边锁相关的一些类,*接口有三个,

  • Lock
  • Condition
  • ReadWriteLock

我们来看Lock接口的一些方法:

public interface Lock {

    void lock();
    
    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();
  
    Condition newCondition();
}

可以看到,他提供了6中方法。我们接下来看Condition接口提供的一些方法:

public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

他里边提供了7中方法,下面我们来看ReadWriteLock的方法:

public interface ReadWriteLock {
   
    Lock readLock();
  
    Lock writeLock();
}

他里边只提供了2中方法,分别是ReadLock和WriteLock。

首选我们来看Lock的实现类一共有三种,分别是:

ReentrantLock

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:

  • 重入性的实现原理
  • 公平锁和非公平锁

Java中的锁机制,你真的了解吗?

除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。

说了这么多可能大家对可重入这个词还不是很理解,我们往下看:

void m1() {
    lock.lock();
    try {
        // 调用 m2,因为可重入,所以并不会被阻塞
        m2();
    } finally {
        lock.unlock()
    }
}

void m2() {
    lock.lock();
    try {
        // do something
    } finally {
        lock.unlock()
    }
}

假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生。这里请大家思考一个问题,ReentrantLock 的可重入特性是怎样实现的呢?简单说一下,ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state--。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。这就是可重复特性的大致实现流程。

那什么公平什么是非公平呢?

公平与非公平指的是线程获取锁的方式。公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。

既然既然有两种不同的加锁模式,那么他们有什么优缺点呢?答案如下:

公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。

在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。

公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync

+--- ReentrantLock.FairSync.java
final void lock() {
    // 调用 AQS acquire 获取锁
    acquire(1);
}

+--- AbstractQueuedSynchronizer.java
/**
 * 该方法主要做了三件事情:
 * 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
 * 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
 *    并将节点放入同步队列尾部
 * 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
 */
public final void acquire(int arg) {
    // acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

+--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    // 如果同步状态 c 为0,表示锁暂时没被其他线程获取
    if (c == 0) {
        /*
         * 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。
         * 如果没有,调用 compareAndSetState 尝试设置同步状态。
         */ 
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 将当前线程设置为持有锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程为持有锁的线程,则执行重入逻辑
    else if (current == getExclusiveOwnerThread()) {
        // 计算重入后的同步状态,acquires 一般为1
        int nextc = c + acquires;
        // 如果重入次数超过限制,这里会抛出异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置重入后的同步状态
        setState(nextc);
        return true;
    }
    return false;
}

+--- AbstractQueuedSynchronizer.java
/** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    /*
     * 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。
     * 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false
     */
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

ReentrantLock 中获取锁的流程并不是很复杂,上面的代码执行流程如下:

  1. 调用 acquire 方法,将线程放入同步队列中进行等待
  2. 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
  3. 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑

分析完公平锁相关代码,下面再来看看非公平锁的源码分析,如下:

+--- ReentrantLock.NonfairSync
final void lock() {
    /*
     * 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁
     * 那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。抢占失败
     * 再调用 acquire 方法将线程置于队列尾部排队。
     */
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

+--- AbstractQueuedSynchronizer
/** 参考上一节的分析 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

+--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

+--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    
    // 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。
    if (c == 0) {
        // 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
        if (compareAndSetState(0, acquires)) {
            // 设置当前线程为锁的持有线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入
    else if (current == getExclusiveOwnerThread()) {
        // 计算重入后的同步状态值,acquires 一般为1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 设置新的同步状态值
        setState(nextc);
        return true;
    }
    return false;
}

非公平锁的实现也不是很复杂,其加锁的步骤大致如下:

  1. 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回
  2. 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待
  3. 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
  4. 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑

ReentrantLock类常用API:

  • getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
  • getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
  • getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
  • hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
  • hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁
  • hasQueuedThreads()是否有线程等待此锁
  • isFair()该锁是否公平锁
  • isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
  • isLock()此锁是否有任意线程占用
  • lockInterruptibly()如果当前线程未被中断,获取锁
  • tryLock()尝试获得锁,仅在调用时锁未被线程占用,获得锁
  • tryLock(long timeout TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

tryLock和lock和lockInterruptibly的区别:

  • tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false
  • lock能获得锁就返回true,不能的话一直等待获得锁
  • lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常

下面我们来看一个简单的案例:

package com.xz.day03;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock1 {

	static int a = 0;
	public static void main(String[] args) {

		ReentrantLock rlock = new ReentrantLock();
		new Thread(new Runnable() {

			@Override
			public void run() {
				while (true) {

					rlock.lock();
					try {
						System.out.println(Thread.currentThread().getName() + "---" + ++a);

						try {
							Thread.sleep(500);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					} finally {
						rlock.unlock();
					}
				}
			}
		}).start();

		new Thread(new Runnable() {

			@Override
			public void run() {
				while (true) {

					rlock.lock();
					try {
						System.out.println(Thread.currentThread().getName() + "---" + ++a);

						try {
							Thread.sleep(500);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					} finally {
						rlock.unlock();
					}
				}
			}
		}).start();
	}

}

第二种实现Runnable的写法:

package com.xz.day03;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock2 {

	static  int a = 0;
	public static void main(String[] args) {
		ReentrantLock rlock = new ReentrantLock();
		//这里的rlock可以传进去,也可以在线程里边定义成局部变量
		F f = new F(rlock,a);
		new Thread(f).start();
		new Thread(f).start();
	}
}

class F implements Runnable{

	private ReentrantLock rlock = new ReentrantLock();
	private Integer a;
	
	public F(ReentrantLock rlock,Integer a) {
//		this.rlock = rlock;
		this.a = a;
	}
	
	@Override
	public void run() {
		
		while(true) {
			try {
				rlock.lock();
				++a;
				System.out.println(Thread.currentThread().getName()+"--"+a);
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}finally {
				rlock.unlock();
			}
		}
	}
	
}

有问题可以在下面评论,技术问题可以私聊。