java中的锁(一)公平锁与非公平锁

在很多程序语言以及中间件中都存在“锁”的概念,在Java中同样根据不同的特性有不同的锁,其中我们常见的锁有以下几类:

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

一、Lock与syncronized

在jdk1.5之前,我们实现锁机制大多数情况下都是使用syncronized关键字,在jdk1.5之后,java引入了Lock机制,以修补syncronized存在的问题与缺陷。

类别 syncronized Lock
存在层次 Java的关键字,在jvm层次上
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 少量同步 大量同步

二、公平锁/非公平锁

公平锁顾名思义就是对每一个线程进行锁的分配时,在优先级上都持有比较公平的分配策略的锁,当线程在请求资源时,如果资源被其他线程占用,则将此线程挂起,置入请求队列中,在资源的锁被释放时,激活请求队列的头部线程,让他对资源进行操作。

非公平锁相较于公平锁具有更高的效率,在线程请求资源时,如果资源被占用,同样是将线程挂起置入请求队列,但当资源锁被释放时,如果恰好有新的线程对资源发起请求,则优先让新线程对资源进行操作,不去查找请求队列,这样就节省了切换上下文的开销。

公平锁与非公平锁的创建方式:

//创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);

//创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);

我们先实现一个公平锁

public class Main {
    public static void main(String[] args) throws InterruptedException {
        TestRes testRes = new TestRes();
        ReentrantLockTest t1 = new ReentrantLockTest(testRes);
        ReentrantLockTest t2 = new ReentrantLockTest(testRes);
        ReentrantLockTest t3 = new ReentrantLockTest(testRes);
        ReentrantLockTest t4 = new ReentrantLockTest(testRes);
        ReentrantLockTest t5 = new ReentrantLockTest(testRes);

        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        t4.setName("线程4");
        t5.setName("线程5");

        t1.start();
        Thread.sleep(100);
        t2.start();
        Thread.sleep(100);
        t3.start();
        Thread.sleep(100);
        t4.start();
        Thread.sleep(100);
        t5.start();
    }
}

class ReentrantLockTest extends Thread{

    private TestRes testRes;

    public ReentrantLockTest(TestRes testRes) {
        this.testRes = testRes;
    }

    @Override
    public void run() {
        while(true){
            testRes.resMethod(Thread.currentThread());
        }
    }
}

class TestRes{
    private Lock lock = new ReentrantLock(true);
    public void resMethod(Thread thread){
        lock.lock();
        try{
            System.out.println(thread.getName() + "获取了锁");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }
}

运行后的输出如下

/app/jdk-10.0.2/bin/java -javaagent:/app/idea-IU-182.4129.33/lib/idea_rt.jar=37233:/app/idea-IU-182.4129.33/bin -Dfile.encoding=UTF-8 -classpath /home/jacatch/IdeaProjects/JavaLock/out/production/JavaLock Main
Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
线程1获取了锁
线程2获取了锁
线程1释放了锁
线程2释放了锁
线程3获取了锁
线程3释放了锁
线程4获取了锁
线程4释放了锁
线程5获取了锁
线程5释放了锁
线程1获取了锁
线程1释放了锁
线程2获取了锁

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

可以看出,锁的获取与释放都是按照发起请求的顺序。接下来我们对代码进行修改,实现一个非公平锁

public class Main {
    public static void main(String[] args) throws InterruptedException {
        TestRes testRes = new TestRes();
        ReentrantLockTest t1 = new ReentrantLockTest(testRes);
        ReentrantLockTest t2 = new ReentrantLockTest(testRes);
        ReentrantLockTest t3 = new ReentrantLockTest(testRes);
        ReentrantLockTest t4 = new ReentrantLockTest(testRes);
        ReentrantLockTest t5 = new ReentrantLockTest(testRes);

        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        t4.setName("线程4");
        t5.setName("线程5");

        t1.start();
        Thread.sleep(1000); //线程太少,为了模拟,增加时长
        t2.start();
        Thread.sleep(1000);
        t3.start();
        Thread.sleep(1000);
        t4.start();
        Thread.sleep(1000);
        t5.start();
    }
}

class ReentrantLockTest extends Thread{

    private TestRes testRes;

    public ReentrantLockTest(TestRes testRes) {
        this.testRes = testRes;
    }

    @Override
    public void run() {
        while(true){
            testRes.resMethod(Thread.currentThread());
        }
    }
}

class TestRes{
    private Lock lock = new ReentrantLock(false);  //设置为false
    public void resMethod(Thread thread){
        lock.lock();
        try{
            System.out.println(thread.getName() + "获取了锁");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }
}

这时的运行结果如下

/app/jdk-10.0.2/bin/java -javaagent:/app/idea-IU-182.4129.33/lib/idea_rt.jar=43359:/app/idea-IU-182.4129.33/bin -Dfile.encoding=UTF-8 -classpath /home/jacatch/IdeaProjects/JavaLock/out/production/JavaLock Main
Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
线程1获取了锁
线程2获取了锁
线程1释放了锁
线程2释放了锁
线程3获取了锁
线程3释放了锁
线程4获取了锁
线程4释放了锁
线程1获取了锁
线程1释放了锁
线程5获取了锁

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

我们可以看到,在4释放锁之后,并不是5获取了锁,而是1,因为此时1发起请求,5正挂起在等待队列中,非公平锁优先将资源给了线程1。

以上两个例子详细展示了公平锁与非公平锁的区别,我们再来深入研究一下在ReentrantLock中是如何实现公平锁与非公平锁的。

2.1、ReentrantLock中的FairSync与NonfairSync

我们打开java的源代码,找到ReentrantLock的构造方法,可以看到如下代码

    private final Sync sync;
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

在此我们可以看出,在ReentrantLock中的公平性与非公平性分别是由FairSync与NonfairSync两个方法实现的。

2.1.1、NonfairSync
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

我们可以看出,jdk中对NonfairSync的定义主要是对Sync的继承,所以我们再来看Sync的源代码。

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /*
        *获取非公平锁,直接将非公平锁的获取在Sync中进行实现
        */
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        /*
        *释放锁
        */
        @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            booleadengdain free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        /*
        *判断当前线程是否为锁的持有者
        */
        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        /*
        *获取锁的持有者线程
        */
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        /*
        *返回当前线程status的状态,如果持有锁就读取status,没有就0
        */
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        /*
        *判断是否上锁
        */
        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

在这里我们并没有找到lock()方法,我们在ReentrantLock中找到lock的实现

public void lock() {
    sync.acquire(1);
}

注意,在JDK8中,lock()方法分别定义在公平锁与非公平锁的具体实现中,然后在ReentrantLock中调用锁对象的lock()方法,而在JDK10中将lock()方法进行了拆分,这里主要写一下JDK10中的实现,JDK8的实现将在附录中补充。

其中的acquire为Sync类继承的AQS的方法,其主要作用是获取线程的同步状态,若获取同步状态失败,则将线程放入同步队列。

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

将节点放入同步队列

    private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }
2.1.2、FairSync
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        @ReservedStackAccess
        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;
        }
    }

与非公平锁相比,公平锁的实现上增加了hasQueuedPredecessors()方法,其主要作用是检测是否有线程比当前线程更早发起锁获取申请,如果有,则从同步队列中取出这个线程,对其进行操作。

参考文章
java中锁的分类
一张图读懂公平锁与非公平锁
ReentrantLock与公平锁、非公平锁的实现
详解sycronized与Lock的区别与使用


附录

  1. JDK1.8中实现lock()的代码
static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
}

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
}

public void lock() {
    sync.lock();
}
  1. 同步队列结构是双向链表,在运行过程中为FIFO
  2. 处于等待的线程为休眠状态

 上一篇
java中的锁(二)AQS java中的锁(二)AQS
一、概述在我们上一章中,我们可以看到FairSync与NonfairSync均继承自Sync,而Sync又继承了AbstractQueuedSynchronizer,在这一章,我们就主要了解一下AbstractQueuedSynchroni
2018-09-02
下一篇 
编舟记 编舟记
“辞典,是横渡词汇海洋的船,”荒木仿佛倾吐灵魂之声一般娓娓道来,“人们乘坐辞典这艘船,搜集漂浮在漆黑海面上的点点星光。只为了能用最恰当的措辞,准确地把自己的所思所想传达给他人。如果没有辞典,我们只能伫立在这片浩瀚的大海前,驻足不前。”
2018-08-26
  目录