Java锁应用及源码原理分析

一 前言

  上一篇《Java锁手册》中以特性为角度对Java中的各类锁进行了初步的介绍,并且以平衡性能损耗和解决并发安全问题为核心思想,把锁的演进过程简单的梳理了一遍。所以对本文中出现的一些概念性问题如果不甚了解,可以回头再去看看上文。

  作为一名开发者,光说不练假把式,如果说《Java锁手册》是文,那么这篇就是武,程序员玩的就是个文武双全咯。

二 CAS源码分析及应用

  CAS作为一个非阻塞式的无锁算法,其应用场景相当广泛,在java.util.concurrent.atomic包下面,所有原子类的实现都涉及CAS操作。各原子类的实现上,均依赖Java中的一个本地方法类——Unsafe,它的作用就是通过不同操作系统的系统指令完成CAS内存操作,下面看以下AtomicBoolean的实现:

package java.util.concurrent.atomic;
import sun.misc.Unsafe;

/**
 * A {@code boolean} value that may be updated atomically. See the
 * {@link java.util.concurrent.atomic} package specification for
 * description of the properties of atomic variables. An
 * {@code AtomicBoolean} is used in applications such as atomically
 * updated flags, and cannot be used as a replacement for a
 * {@link java.lang.Boolean}.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class AtomicBoolean implements java.io.Serializable {
    private static final long serialVersionUID = 4654671469794556979L;
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
	……
}

  这只是AtomicBoolean源码的声明部分,其信息量已经非常大了。首先注释中已经告诉我们CAS操作是通过Unsafe的compareAndSwapInt实现的:

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

  其次,为什么说AtomicBoolean能够保证原子性,并发访问时能够实现对其他线程的可见性,就在于其value的声明为volatile:

private volatile int value;

  接下来看看CAS方法的实现:

/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return {@code true} if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(boolean expect, boolean update) {
	int e = expect ? 1 : 0;
	int u = update ? 1 : 0;
	return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

  CAS算法在《Java锁手册》介绍过了,不细说。我们只需要知道,CAS过程的确是通过Unsafe的compareAndSwapInt实现的,因为这个方法是native的,懂C/C++的盆友自己深入挖掘,反正我不会,点到为止。

  那么再说说如AtomicBoolean这类的原子类型有什么样的应用场景。就拿AtomicBoolean举例,我们常常在并发编程模式下,需要控制线程的执行逻辑,通过一个全局的标识位做为线程执行开关,此时就要求所有线程必须能够获取标识位的状态,那么AtomicBoolean就是一个非常好用的类型,简单示意代码如下:

public class Test {
	/**
	 * 全局的开关标识位,控制所有线程仅一个能获得执行机会
	 */
	private static AtomicBoolean opened = new AtomicBoolean(false);

	Thread thread = new Thread(new Runnable() {

		@Override
		public void run() {
			if (opened.compareAndSet(false, true)) {
				// TODO 如果尚未开始则将标识位置为true,进入执行逻辑,否则不执行
			}
		}
	});
}

  因为标识位被任何一个线程修改状态之后,所有其他线程都会从主存获取到最新的状态值(可见性),所以能非常有效的控制线程执行权限。

  由此也可以引申出,某些初始化过程仅允许执行一次的逻辑,也可以通过如上方式实现(包括各类资源的池的初始化,应用处理线程池、数据库连接池,还有各类资源、属性的配置过程等等)。

三 重入锁ReentrantLock

  除了Java中大名鼎鼎的synchronized关键字,还有一个非常重要的锁实现——ReentrantLock,这个锁称之为重入锁,即允许已获取锁的线程直接进入嵌套的其他临界区。

  ReentrantLock和synchronized一样,都是独占锁,但是它的应用场景更加广泛(但我相信没有多少开发者用过),原因在于它的实现方式,它不仅提供了独占特性,还支持公平和非公平两种模式可选(公平锁和非公平锁)。

  看一下ReentrantLock的定义:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
	……
	}
	……
}

  ReentrantLock实现了Lock接口,Lock接口的介绍大家自行百度下,我重点说明下ReentrantLock的其他特性。可以看到ReentrantLock内部持有一个Sync类型的内部类成员,并且该成员派生自AbstractQueuedSynchronizer(后文称AQS),这个成员就是实现加锁和释放锁的灵魂,并且Sync还有两个派生类,同样定义在ReentrantLock中:

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {...}

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {...}

  其中NonfairSync实现了非公平锁特性,而FairSync则实现了公平锁特性,那么ReentrantLock如何控制采用那种特性模式呢?在构造函数里:

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
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();
}

  显而易见的,ReentrantLock默认采用非公平锁模式,只有显示的通过fair == true参数来构造,才可以启动公平锁模式。

  非公平锁模式没有排队问题,所以实现相对简单些,我们再看一下NonfairSynch的主要实现逻辑:

/**
 * Sync object for non-fair locks
 */
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);
	}
}

  需要注意咯,不同版本的JDK对以上的实现过程可能有所区别,至少我发现了两种不同的实现,我的JDK是1.8,所以只以我本机的源码解读,如果有朋友发现不一致,请查阅版本更新说明。

  这段实现可以说非常简单了,加锁时首先通过compareAndSetState方法以CAS算法获取锁,一旦获取到了,则通过setExclusiveOwnerThread方法进行标记;否则通过acquire方法将当前线程加入一个阻塞队列,等待下次竞争,acquire方法实现如下:

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

  不再继续跟进了,回过头来再说说公平锁的实现有什么不同,只要提到公平,必然要排队,那么就一定会出现队列,所以看FairSync的实现是不是这样:

/**
 * Sync object for fair locks
 */
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;
	}
}

  可以说毫无意外的,hasQueuePredecessors函数判断队列中是否有其他线程先于当前线程,如果有再看看是不是自己(重入哟,getExclusiveOwnerThread方法);否则的话,还是通过一次CAS来获取锁,然后进行标记,和非公平锁的执行过程一致。

  那么ReentrantLock有什么应用场景呢?这里必须要和sycnhronized关键字做一个对比了。

  首先sycnhronized关键字不支持中断,一旦死锁没有任何办法解决,而ReentrantLock是支持的,为什么能支持?原因就在于acquire方法:

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

  通过源码可以很清楚的看到,一旦锁竞争失败线程进入阻塞队列,并且这个阻塞队列支持中断,所以ReentrantLock就厉害了,这个特性synchronized做不到。

  其次,因为ReentrantLock可选公平模式,这也是synchronized做不到的。

  第三点,ReentrantLock还支持竞争锁的等待时间限制,通过tryLock方式实现:

public boolean tryLock() {
	return sync.nonfairTryAcquire(1);
}

	public boolean tryLock(long timeout, TimeUnit unit)
		throws InterruptedException {
	return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

  两个重载函数,无参方法表示尝试加锁理解返回结果,而有参函数支持等待一段时间,并且支持中断。

  那么诸位在项目实施的时候,就可以根据这些差异点,酌情选择使用哪种锁类型了。这里推荐大家两篇博文,一个是专门介绍ReentrantLock源码的,一个是介绍应用的:

  1. 解读可重入锁——ReentrantLock&AQS,java8
  2. ReentrantLock(重入锁)功能详解和应用演示

四 共享锁ReentrantReadWriteLock

  不论是synchronized还是ReentrantLock都是独占锁,Java中提供了一种共享锁实现ReentrantReadWriteLock,而且实现也非常简单,看一下源码定义:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;
	......
}

  当我看到Sync的时候无比亲切,这说明它也是支持公平和非公平两种模式的,不细说了。其他的不同在于两个私有成员ReadLock和WriteLock。

  《Java锁手册》中已经说过,共享锁是支持其他线程获取锁的,只不过仅限于读操作,这个特性就是靠读写锁分离实现的。

  这里源码就不再继续详细说明了,对源码的阅读方法可以参考上文,一法通万法通是我提倡的,反反复复的贴代码啥的意义不大,如果读者实在是懒得自己看,那么我推荐一篇博文专门写ReentrantReadWriteLock的Java并发编程–ReentrantReadWriteLock

  这里我更想说的是,共享锁其存在的意义。共享锁因为允许其他线程同时读资源,那么相对于独占锁来说就可以极大的提高并发性。如果存在大量读资源的应用场景,比如说频繁的缓存访问,那么共享锁的人格魅力就实实在在的体现出来了。

  综上,其实各类锁实现都不复杂,复杂在于作为一名开发者,我们必须很清醒的知道当前应用场景的并发复杂性,并且根据并发特性来决定采用何种锁类型。

  还是那句话,优秀的并发程序一定是能够在解决并发安全问题的同时,平衡系统资源消耗来提升性能的,千篇一律的synchronized不能给你的技能水平带来任何提高,开阔的视野可以,但它要求你了解更多的技能知识,我愿意付出这样的时间成本,因为它值。

五 结语

  如果想关注更多硬技能的分享,可以参考积少成多系列传送门,未来每一篇关于硬技能的分享都会在传送门中更新链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬睡客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值