0%

java中的锁

java中的锁

Lock接口

使用synchronized关键字会隐式的获取锁,但是他将锁的获取和释放固化了,Lock锁可以显示的获取锁和释放锁。

Lock lock = new ReentrantLock();
lock.lock();
try {

}finally {
    lock.unlock();
}

在finally中释放锁,目的是保证在获取到锁之后,最终能够被释放。

不要将获取锁的过程写到try 中,如果在获取锁的时候发生异常,会导致锁被无故释放。

Lock接口提供的synchronized不具备的特性

  • 尝试非阻塞的获取锁

  • 能够中断的获取锁

  • 超时获取锁

Lock的接口方法

  • lock() 获取锁,调用该方法当前线程获取锁,获取锁后,从该方法返回
  • lockInterruptibly(),中断的获取锁,该方法会相应中断,即在锁的获取过程中可以中断该线程
  • tryLock(),非阻塞的获取锁,调用后立即返回,获取到返回true,否则为false
  • unlock(),释放锁

队列同步器(aqs)

队列同步器AbstractQueuedSynchronized,是用来构建锁的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO队列完成资源获取线程的排队工作。

同步器的使用方法主要为继承,子类继承他的抽象方法来管理同步状态。

如何理解同步器和锁的关系:

  • 锁是面向使用者的,它定义了使用者于锁交互的接口,隐藏了实现细节
  • 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待和唤醒等底层操作。

同步器的接口和示例

同步器的设计是基于模板方法模式的,需要使用者继承同步器并重写指定的方法。

可重写的方法:

  • tryAcquire(int args):独占式的获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后使用CAS设置同步状态。
  • tryRelease(int args):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
  • tryAcquiredShared(int agrs):共享式的获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
  • tryReleaseShared(int args):共享式的释放同步状态
  • isHeldExclusively():当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占

模板方法:模板方法基本分为三类:独占式获取释放同步状态,共享式获取释放同步状态和查询同步队列的等待情况。

独占锁:在同一时刻只有一个线程获取到锁,而其他获取锁的线程之恶能处于同步队列中等待,只有获取锁的线程释放了锁,后记的线程才能获取锁。

public class Mutex implements Lock {

    public static class Sync extends AbstractQueuedSynchronizer {

        // 是否处于独占状态
        public boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 状态为0的时候获取锁
        @Override
        protected boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

    private final Sync sync = new Sync();

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

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

队列同步器的实现分析

同步队列

同步器内部以来同步队列(双向FIFO队列)完成同步状态的管理,当前线程获取同步状态失败时,会将当前线程的等待状态等信息构成节点加入同步队列,同时阻塞当前线程,当同步状态释放时,会把首节点线程唤醒,使其获取同步状态。

AQS记录首节点head和尾节点tail。没有成功获取同步状态线程的会成为节点变成尾部。尾节点的设置必须保证线程安全,所以使用compareAndSetTail。设置首节点的是获取同步状态成功的线程来完成的,只有一个线程能获取成功,所以不需要CAS来保证。

共享式获取与独占式获取最主要的区别就是在同一时刻能否有多个线程同时获取到同步状态。例如对一个文件读操作可以是共享的,写操作是独占的。

一个共享式同步锁的示例,同一时刻至多允许两个线程同时访问

public class TwinsLock implements Lock {

    // 设置资源数 2
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {

        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero");
            }
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int returnCount) {

            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }
    }

    @Override
    public void lock() {
        sync.acquireShared(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        sync.releaseShared(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    // 验证twinsLock是否有效
    public static void main(String[] args) {
        final Lock lock = new TwinsLock();
        class Worker extends Thread {
            @Override
            public void run() {
                while (true) {
                    lock.lock();

                    try {
                        ThreadStateSample.SleepUtils.second(1);
                        System.out.println(Thread.currentThread().getName());
                        ThreadStateSample.SleepUtils.second(1);
                    } finally {
                        lock.unlock();
                    }

                }
            }
        }
        // 启动10个线程
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            w.setDaemon(true);
            w.start();
        }
        // 每隔一秒换行
        for (int i = 0; i < 10; i++) {
            ThreadStateSample.SleepUtils.second(1);
            System.out.println();
        }

    }
}

设置初始时status为2,当一个线程获取时,status-1,线程释放,则sttus+1,如果为0,则阻塞该队列

重入锁ReetrantLock

重入锁即支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁,除此之外,该锁还支持获取锁的公平锁和非公平锁的选择。

前面的Mutex锁,当一个线程调用Mutex的lock()后,再次调用lock(),该线程会被自己所阻塞,因为Mutex在tryAcquire方法中没有考虑占有锁的线程再次获取锁的场景,而是返回了false,导致阻塞。synchronized关键字隐式的支持冲进入。

锁的公平性问题:如果先获取锁的请求线程先被满足,则称之为公平锁,否则的话是非公平锁。

实现重进入

重进入是指任意线程在获取到锁之后能够再次获取到锁而不被锁阻塞,需要解决两个问题

  • 线程再次获取锁:需要识别获取锁的线程是否是当前占有锁的线程。如果是则再次获取
  • 锁的最终释放:线程重复n次获取锁,则在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要对锁的获取进行自增,锁释放时自减,当计数器为0时,则表示成功释放。

ReentrantLock的nonfairTryAcquire方法

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;
        }

处理逻辑是:c==0 ,直接获取锁,否则判断当前线程是否是获取锁的线程,是的话状态值增加。

ReentrantLock的tryRelease方法

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

锁被获取了n次,那么前n-1次必须返回false,每次状态值自减。

非公平锁和公平锁的区别

公平性是针对获取锁而言的,公平锁的获取是按照FIFO的。

/**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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,加入同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则代表有前驱节点,有线程更早的请求获取锁,所以需要前驱线程先获取锁才行。

读写锁

ReentrantReadWriteLock读写锁维护了一对锁,读线程在同一时刻允许多个线程访问,但是写线程访问时,所有读写线程均被阻塞。

  • 支持公平性和非公平性的选择。
  • 支持重进入

一个使用的示例

public class Cache {

    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {

            return map.get(key);
        }finally {
            r.unlock();
        }
    }

    // 设置key对应的value,返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {

            return map.put(key, value);
        }finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

读写锁的实现分析

状态设计:状态按位切割,高16位表示读,低16位表示写

获取写锁:基本上和reetrantLock一致,加了一个条件,获取读锁,如果存在读锁则不能获取锁

LockSupport工具

提供了基本的线程阻塞和唤醒功能。

  • park():阻塞当前线程,调用unpark或者当前线程被中断才能从park()方法返回。
  • parkNanos():加了一个超时返回。
  • parkUntil:加了一个deadline
  • unpark:唤醒阻塞线程。

Condtion 接口

~~