背景
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。
自旋锁
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被当前线程改变时其他前程才能进入临界区。
自旋锁流程:获取自旋锁时,如果没有任何线程保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
简单实现原理的代码如下:
/**
* 自旋锁原理简单示例
*
* @author zacard
* @since 2016-01-13 21:40
*/
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);
}
}
要理解以上代码,我们要先弄清楚AtomicReference的作用。
AtomicReference:位于java.util.concurrent.atomic包下。从包名就可知道它的大致作用:在并发环境中保证引用对象的原子操作。
查看AtomicReference源码:
package java.util.concurrent.atomic;
import java.util.function.UnaryOperator;
import java.util.function.BinaryOperator;
import sun.misc.Unsafe;
/**
* An object reference that may be updated atomically. See the {@link
* java.util.concurrent.atomic} package specification for description
* of the properties of atomic variables.
* @since 1.5
* @author Doug Lea
* @param <V> The type of object referred to by this reference
*/
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile V value;
...(省略)
/**
* 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(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
...(省略)
发现AtomicReference实现的基本原理是使用volatile关键字和Unsafe类来保证其可见性和原子性。(PS:在此暂不作扩展阅读Unsafe类)
我们重点关注AtomicReference.compareAndSet()这个自旋锁用到的方法。从方法注释和方式实现,可以理解:这个方法的意思就是当当前的值==(注意是双等号)期望的值(即传入的第一个参数)时,把当前值更新为新值(即传入的第二个参数),并且返回true,否则返回false。
再回过头,看之前自旋锁的代码,就很好理解了。一开始AtomicReference中的值为null,当有线程获得锁后,将值更新为该线程。当其他线程进入被锁的方法时,由于sign.compareAndSet(null, current)始终返回的是false,导致while循环体一直在运行,知道获得锁的线程调用unlock方法,将当前持有线程重新设置为null:sign.compareAndSet(current, null)其他线程才可获得锁。
阻塞锁
阻塞锁,与自旋锁不同,改变了线程的运行状态。阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
阻塞锁和自旋锁最大的区别就在于,当获取锁是,如果锁有持有者,当前线程是进入阻塞状态,等待当前线程结束而被唤醒的。
简单实现原理的代码如下:
/**
* 阻塞锁原理简单示例
*
* @author zacard
* @since 2016-01-13 22:02
*/
public class BlockLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
// 获取锁
public void lock() {
Thread current = Thread.currentThread();
if (!sign.compareAndSet(null, current)) {
LockSupport.park();
}
}
// 释放锁
public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(null, current);
LockSupport.unpark(current);
}
}
要理解以上代码,我们要先弄清楚LockSupport的作用。
LockSupport:位于java.util.concurrent.locks包下(又是j.u.c)。同样,从包名和类名即可知道其作用:提供并发编程中的锁支持。
还是先查看下LockSupport的源码:
public class LockSupport {
private LockSupport() {} // Cannot be instantiated.
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
...(省略)
又是sun.misc.Unsafe这个类,在此我们不得不先扩展研究下这个Unsafe类的作用和原理了。
sun.misc.Unsafe:有个称号叫做魔术类。因为他能直接操作内存等一些复杂操作。包括直接修改内存值,绕过构造器,直接调用类方法等。当然,他主要提供了CAS(compareAndSwap)原子操作而被我们熟知。
查看Unsafe类源码:
public final class Unsafe {
private static final Unsafe theUnsafe;
...(省略)
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
...(省略)
根据代码可知:Unsafe是final类,意味着我们不能通过继承来使用或改变这个类的方法。然后构造器是私有的,也不能实例化。但是他自己保存了一个静态私有不可改变的实例“theUnsafe”,并且只提供了一个静态方法getUnsafe()来获取这个类的实例。
但是这个getUnsafe方法确有个限制:注意if语句里的判断,他表示如果不是受信任的类调用,会直接抛出异常。显然,我们平常编写的类都是不受信任的!
但是,我们有反射!既然他已经持有了一个实例,就能通过反射强行窃取这个私有的实例。
代码如下:
public void getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
Unsafe类的方法基本都是native关键字修饰的,也就是说这些方法都是原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。这也就是为什么Unsafe能够直接操作内存等一些特权功能的原因。
回过头看下LockSupport中park()和uppark()这2个方法的作用。
LockSupport.unpark():
/**
* Makes available the permit for the given thread, if it
* was not already available. If the thread was blocked on
* {@code park} then it will unblock. Otherwise, its next call
* to {@code park} is guaranteed not to block. This operation
* is not guaranteed to have any effect at all if the given
* thread has not been started.
*
* @param thread the thread to unpark, or {@code null}, in which case
* this operation has no effect
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
根据方法注释:对于给定线程,将许可证设置为可用状态。如果这个线程是因为调用park()而处于阻塞状态,则清除阻塞状态。反之,这个线程在下次调用park()时,将保证不被阻塞。
LockSupport.park():
/**
* Disables the current thread for thread scheduling purposes unless the
* permit is available.
*
* <p>If the permit is available then it is consumed and the call returns
* immediately; otherwise
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until one of three things happens:
*
* <ul>
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
* <li>The call spuriously (that is, for no reason) returns.
* </ul>
*
* <p>This method does <em>not</em> report which of these caused the
* method to return. Callers should re-check the conditions which caused
* the thread to park in the first place. Callers may also determine,
* for example, the interrupt status of the thread upon return.
*
* @param blocker the synchronization object responsible for this
* thread parking
* @since 1.6
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
根据注释:除非许可证是可用的,不然将当前线程的调度设置为不可用。当许可是可用时,方法会立即返回,不会阻塞,反之就会阻塞当前线程直到下面3件事发生:
其他线程调用了unpark(此线程)
其他线程interrupts(终止)了此线程
调用时发生未知原因的返回
重入锁
重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下ReentrantLock和synchronized都是重入锁。
测试代码如下:
/**
* 测试ReentrantLock和synchronized
*/
@Test
public void testReentrantLock() {
// ReentrantLock test
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println("ReentrantLock:" + Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println("ReentrantLock:" + Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
}
}).start();
}
// synchronized test
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public synchronized void get() {
System.out.println("synchronized:" + Thread.currentThread().getId());
set();
}
public synchronized void set() {
System.out.println("synchronized:" + Thread.currentThread().getId());
}
@Override
public void run() {
get();
}
}).start();
}
}
2段代码的输出一致:都会重复输出当前线程id2次。
可重入锁最大的作用是避免死锁。以自旋锁作为例子:
/**
* 自旋锁原理简单示例
*
* @author zacard
* @since 2016-01-13 21:40
*/
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);
}
}
若有同一线程两调用lock(),会导致第二次调用lock位置进行自旋,产生了死锁说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
若1问题已经解决,当unlock()第一次调用时,就已经将锁释放了。实际上不应释放锁
自旋锁避免死锁的方法(采用计数次统计):
/**
* 自旋锁改进
*
* @author Guoqw
* @since 2016-01-14 14:11
*/
public class SpinLockImprove {
private AtomicReference<Thread> owner = new AtomicReference<>();
private int count = 0;
/**
* 获取锁
*/
public void lock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
count++;
return;
}
while (!owner.compareAndSet(null, current)) {
}
}
/**
* 释放锁
*/
public void unlock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
if (count != 0) {
count--;
} else {
owner.compareAndSet(current, null);
}
}
}
}
改进后自旋锁即为重入锁的简单实现。