Java锁的种类以及辨析 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。本系列文章将分析JAVA中常见的锁以及其特性,为大家答疑解惑。
1、自旋锁
2、自旋锁的其他种类
3、阻塞锁
4、可重入锁
5、读写锁
6、互斥锁
7、悲观锁
8、乐观锁
9、公平锁
10、非公平锁
11、偏向锁
12、对象锁
13、线程锁
14、锁粗化
15、轻量级锁
16、锁消除
17、锁膨胀
18、信号量
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被当前线程改变时其他前程才能进入临界区。
自旋锁流程:获取自旋锁时,如果没有任何线程保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
简单实现原理的代码如下:
/** * 自旋锁原理简单示例 */ 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件事发生:
o 其他线程调用了unpark(此线程)
o 其他线程interrupts(终止)了此线程
o 调用时发生未知原因的返回
重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在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); } }
o 若有同一线程两调用lock(),会导致第二次调用lock位置进行自旋,产生了死锁说明这个锁并不是可重入的。(在lock函数内,应验证线程是否为已经获得锁的线程)
o 若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); } } } }
改进后自旋锁即为重入锁的简单实现。