分享

java锁的种类及研究

 孤独一兵 2016-11-07

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等等 ) 。这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及。

自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被当前线程改变时其他前程才能进入临界区。

自旋锁流程:获取自旋锁时,如果没有任何线程保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。

简单实现原理的代码如下:

1234567891011121314151617/** * 自旋锁原理简单示例*/public class SpinLock { private AtomicReference 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源码:

12345678910111213141516171819202122232425262728293031323334353637383940414243package 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 The type of object referred to by this reference */public class AtomicReference 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)其他线程才可获得锁。

阻塞锁

阻塞锁,与自旋锁不同,改变了线程的运行状态。阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。

阻塞锁和自旋锁最大的区别就在于,当获取锁是,如果锁有持有者,当前线程是进入阻塞状态,等待当前线程结束而被唤醒的。

简单实现原理的代码如下:

123456789101112131415161718192021222324/** * 阻塞锁原理简单示例 * * @author zacard * @since 2016-01-13 22:02 */public class BlockLock { private AtomicReference 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的源码:

1234567public 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类源码:

12345678910111213141516171819public 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语句里的判断,他表示如果不是受信任的类调用,会直接抛出异常。显然,我们平常编写的类都是不受信任的!

但是,我们有反射!既然他已经持有了一个实例,就能通过反射强行窃取这个私有的实例。

代码如下:

123456789public 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():

123456789101112131415/** * 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():

12345/** * Disables the current thread for thread scheduling purposes unless the * permit is available. * *

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: * *

  • *

  • Some other thread invokes {@link #unpark unpark} with the * current thread as the target; or * *

  • Some other thread {@linkplain Thread#interrupt interrupts} * the current thread; or * *

  • The call spuriously (that is, for no reason) returns. *

* *

This method does not 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都是重入锁。

测试代码如下:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * 测试ReentrantLock和synchronized */@Testpublic 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次。

可重入锁最大的作用是避免死锁。以自旋锁作为例子:

123456789101112131415161718192021222324/** * 自旋锁原理简单示例 * * @author zacard * @since 2016-01-13 21:40 */public class SpinLock { private AtomicReference 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()第一次调用时,就已经将锁释放了。实际上不应释放锁

自旋锁避免死锁的方法(采用计数次统计):

12345678910111213141516171819202122232425262728293031323334353637383940/** * 自旋锁改进 * * @author Guoqw * @since 2016-01-14 14:11 */public class SpinLockImprove { private AtomicReference 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); } } }}

改进后自旋锁即为重入锁的简单实现。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多