Zacard's Notes

java锁的种类及研究

背景

锁作为并发共享数据,保证一致性的工具,在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);
            }
        }
    }
}

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

坚持原创技术分享,您的支持将鼓励我继续创作!

热评文章