JAVA基础第一弹-JAVA同步器和锁机制

68 阅读9分钟

大家好,今天和大家分享一下同步器和锁机制

AQS

同步器和锁机制,就离不开AQS机制,首先介绍一下AQS基础知识:

AQS(AbstractQueuedSynchronizer)同步器,JUC(java.util.concurrent)里所有的锁机制都是基于AQS构建的。AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态

AQS的常用方法

1、 getState()

返回同步状态的当前值

2、 setState(int newState)

设置当前同步状态

3、 compareAndSetState(int expect, int update)

使用CAS设置当前状态,该方法能够保证状态设置的原子性

4、 tryAcquire(int arg)

独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态

5、 tryRelease(int arg)

独占式释放同步状态

6、 tryAcquireShared(int arg)

共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败

7、 tryReleaseShared(int arg)

共享式释放同步状态

8、 isHeldExclusively()

当前同步器是否在独占式模式下被线程占用

9、 acquire(int arg)

独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法

10、            acquireInterruptibly(int arg)

与acquire(int arg)相同,但是该方法响应中断

11、            tryAcquireNanos(int arg,long nanos)

超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true

12、            acquireShared(int arg)

共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态

13、            acquireSharedInterruptibly(int arg)

共享式获取同步状态,响应中断

14、            tryAcquireSharedNanos(int arg, long nanosTimeout)

共享式获取同步状态,增加超时限制

15、            release(int arg)

独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒

16、            releaseShared(int arg)

共享式释放同步状态

独占锁介绍

独占式,同一时刻仅有一个线程持有同步状态。

独占式同步状态获取

acquire(int arg)

该方法为独占式获取同步状态,但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。

public final void acquire(int arg) {

        if (!tryAcquire(arg) &&

            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

            selfInterrupt();

}

各个方法定义如下:

1、tryAcquire:去尝试获取锁,获取成功则设置锁状态(state+1)并返回true,否则返回false。

2、addWaiter:如果tryAcquire返回false,则调用该方法将当前线程加入到CLH同步队列尾部。

3、acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。

final boolean acquireQueued(final Node node, int arg) {

        boolean failed = true;

        try {

            //中断标志

            boolean interrupted = false;

            /*

             * 自旋过程(死循环)

             */

            for (;;) {

                //当前线程的前驱节点

                final Node p = node.predecessor();

                //当前线程的前驱节点是头结点,且同步状态成功

                if (p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next = null; // help GC

                    failed = false;

                    return interrupted;

                }

                //获取失败,线程等待

                if (shouldParkAfterFailedAcquire(p, node) &&

                        parkAndCheckInterrupt())

                    interrupted = true;

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }

 

4、selfInterrupt:产生一个中断。

总结:在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的队尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。

独占式获取响应中断

acquireInterruptibly(int arg)方法

该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException。

public final void acquireInterruptibly(int arg)

            throws InterruptedException {

        if (Thread.interrupted())

            throw new InterruptedException();

        if (!tryAcquire(arg))

            doAcquireInterruptibly(arg);

    }

首先校验该线程是否已经中断,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。

private void doAcquireInterruptibly(int arg)

        throws InterruptedException {

        final Node node = addWaiter(Node.EXCLUSIVE);

        boolean failed = true;

        try {

            for (;;) {

                final Node p = node.predecessor();

                if (p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next = null; // help GC

                    failed = false;

                    return;

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())

                    throw new InterruptedException();

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }

doAcquireInterruptibly(int arg)方法与acquire(int arg)方法区别。

1.方法声明抛出InterruptedException异常

2.在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。

独占式超时获取

public final boolean tryAcquireNanos(int arg, long nanosTimeout)

            throws InterruptedException {

        if (Thread.interrupted())

            throw new InterruptedException();

        return tryAcquire(arg) ||

            doAcquireNanos(arg, nanosTimeout);

}

 

tryAcquireNanos(int arg, long nanosTimeout)方法超时获取最终是在doAcquireNanos(int arg, long nanosTimeout)中实现的,如下:

 

  private boolean doAcquireNanos(int arg, long nanosTimeout)

            throws InterruptedException {

        //nanosTimeout <= 0

        if (nanosTimeout <= 0L)

            return false;

        //超时时间

        final long deadline = System.nanoTime() + nanosTimeout;

        //新增Node节点

        final Node node = addWaiter(Node.EXCLUSIVE);

        boolean failed = true;

        try {

            //自旋

            for (;;) {

                final Node p = node.predecessor();

                //获取同步状态成功

                if (p == head && tryAcquire(arg)) {

                    setHead(node);

                    p.next = null; // help GC

                    failed = false;

                    return true;

                }

                /*

                 * 获取失败,做超时、中断判断

                 */

                //重新计算需要休眠的时间

                nanosTimeout = deadline - System.nanoTime();

                //已经超时,返回false

                if (nanosTimeout <= 0L)

                    return false;

                //如果没有超时,则等待nanosTimeout纳秒

                //注:该线程会直接从LockSupport.parkNanos中返回,

               

                if (shouldParkAfterFailedAcquire(p, node) &&

                        nanosTimeout > spinForTimeoutThreshold)

                    LockSupport.parkNanos(this, nanosTimeout);

                //线程是否已经中断了

                if (Thread.interrupted())

                    throw new InterruptedException();

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }

 

独占式同步状态释放

AQS提供了release(int arg)方法释放同步状态

public final boolean release(int arg) {

        if (tryRelease(arg)) {

            Node h = head;

            if (h != null && h.waitStatus != 0)

                unparkSuccessor(h);

            return true;

        }

        return false;

}

先调用tryRelease(int arg)方法来释放同步状态,释放成功后,调用unparkSuccessor(Node node)方法唤醒后继节点。

共享锁介绍

共享式同步状态获取

acquireShared(int arg)方法

共享式获取同步状态

public final void acquireShared(int arg) {

        if (tryAcquireShared(arg) < 0)

            //获取失败,自旋获取同步状态

            doAcquireShared(arg);

}

先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。如果返回值等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。如果返回值大于0,表示当前线程获取共享锁成功且它后续等待的节点也有可能继续获取共享锁成功,也就是说此时需要把后续节点唤醒让它们去尝试获取共享锁。自旋式获取同步状态如下:

private void doAcquireShared(int arg) {

        /共享式节点

        final Node node = addWaiter(Node.SHARED);

        boolean failed = true;

        try {

            boolean interrupted = false;

            for (;;) {

                //前驱节点

                final Node p = node.predecessor();

                //如果其前驱节点,获取同步状态

                if (p == head) {

                    //尝试获取同步

                    int r = tryAcquireShared(arg);

                    if (r >= 0) {

                        setHeadAndPropagate(node, r);

                        p.next = null; // help GC

                        if (interrupted)

                            selfInterrupt();

                        failed = false;

                        return;

                    }

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                        parkAndCheckInterrupt())

                    interrupted = true;

            }

        } finally {

            if (failed)

                cancelAcquire(node);

        }

    }

tryAcquireShared(int arg)方法

尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。

private void setHeadAndPropagate(Node node, int propagate) {

        Node h = head;

        setHead(node);

        if (propagate > 0 || h == null || h.waitStatus < 0 ||

            (h = head) == null || h.waitStatus < 0) {

            Node s = node.next;

            if (s == null || s.isShared())

                doReleaseShared();

        }

    }

 

    private void setHead(Node node) {

        head = node;

        node.thread = null;

        node.prev = null;

    }

private void doReleaseShared() {

        for (;;) {

            Node h = head;

            if (h != null && h != tail) {

                int ws = h.waitStatus;

                if (ws == Node.SIGNAL) {

                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

                        continue;    

                    unparkSuccessor(h);

                }

                else if (ws == 0 &&

                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

                    continue;               

            }

            if (h == head)                  

                break;

        }

    }

共享式获取响应中断

acquireSharedInterruptibly(int arg)

共享式超时获取

tryAcquireSharedNanos(int arg,long nanos)

共享式同步状态释放

获取同步状态后,需要调用release(int arg)方法释放同步状态,方法如下:

  public final boolean releaseShared(int arg) {

        if (tryReleaseShared(arg)) {

            doReleaseShared();

            return true;

        }

        return false;

}

独占锁与共享锁对比

当锁被头节点获取后,独占锁是只有头节点获取锁,其余节点的线程继续等待,锁被释放后,才会唤醒下一个节点的线程,而共享锁是只要头节点获取锁成功,唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。

CLH同步队列

       CLH同步队列(CHL指的是该算法的三位作者:Craig、Landin和Hagersten名字首字母的缩写)是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

       在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)

 

1、 入队

tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。

2、 出队

CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,head执行该节点并断开原首节点的next和当前节点的prev即可,注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态。

 

今天的分享就到这里,大家如果有问题可以在评论区留言,一起沟通~