Java多线程中Lock的实现

在Java 1.5之后,并发包中新增了Lock接口用来实现锁功能,它提供了Synchronized关键字类似的功能,只是在使用时需要显式地获取锁和释放锁。虽然它缺少了隐式获取锁释放锁的便捷性,但是却拥有了锁释放和获取的可操作性、可中断地获取锁以及超时获取锁等多种选择。

1 Lock接口

Lock接口的主要api如下:

1)void lock():获取锁,调用该方法的当前线程或获取锁,并从该方法返回,没有获取锁的将会自旋等待。

2)void lockInterruptibly () throws InterruptedException:可中断的获取锁,该方法会响应中断,即在获取锁的自旋等待中,可以中断当前线程。

3)boolean tryLock():尝试非阻塞的获取锁,调用该方法会立刻返回,获取则返回true。

4)Condition newCondition():返回等待通知组件,类似于Object.wait与Object.notify方法,可以使线程进入waiting状态。

5)相对应的unlock()方法。

在Lock的具体实现中,基本都是通过聚合了一个队列同步器(AbstractQueuedSynchronizer,AQS)的子类来完成线程访问控制的。下面我们将详细介绍AQS的使用和实现。

2 队列同步器AQS

队列同步器(AbstractQueuedSynchronizer,AQS)是构建锁或者其他同步组件的基础框架,它使用了一个int state变量来表示同步状态。通过内置的FIFO(先进先出)的同步队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承。子类通过对同步器的继承并实现它的抽象方法来管理同步状态。同时,它提供了三个方法(getState()、setState(int newState)和compareAndSetState(int expect , int update))来对同步状态进行更改,它们能够保证操作的安全性。同步器支持独占式和共享式地获取同步状态。

2.1 同步器的接口

同步器的设计是基于模板方法模式的,我们需要重写指定的方法,随后将同步器指定在自定义同步组件中,并调用同步器提供的模板方法,这些模板方法会调用使用者重写的方法来实现特定的同步规则。可重写的方法如下:
1)boolean tryAcquire(int arg):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
2)boolean tryRelease(int arg):独占锁释放同步状态,等待同步状态的线程将有机会获取同步状态。
3)int tryAcquireShared(int arg):共享式获取同步,返回大于0表示获取成功。
4)boolean tryRealeaseShared(int arg):共享是锁释放
5)boolean isHeldExclusively():当前线程同步器是否被该线程独占。 

上述的几个方法将被模板方法引用,我们在实现自定义方法时,将调用这些模板方法。模板方法如下:

1)void acquire(ing arg):独占式获取同步锁(调用tryAcquire()),成功的话,则同步器的锁被该线程拥有(同步器记录下该线程),方法返回。否则进入同步队列等待。

2)void acquireInterruptibly(int arg):与1)相同,但是能响应中断,在同步队列等待时,可以响应中断,抛出异常并返回。

3)boolean AcquireShared(int arg):共享式的获取锁。

4)Collection<Thread> getQueuedThreads():获取等待在同步队列上的线程。

5)其他独占、共享、中断的组合获取锁和释放锁的方法。

2.2 模板方法的实现

1.同步队列

同步器依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(NODE)并将其加入同步队列,同时会阻塞当前线程(自旋,不是系统级的线程阻塞),当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态(公平锁)。未取得锁的线程将被构造成节点加入同步队列尾端。

2.独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是线程失败后会进入同步队列,后续线程进行中断操作时,线程不会从同步队列中移除。方法代码如下:
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
上述代码主要完成了同步状态的获取、节点构造、加入同步队列和在同步队列中自旋等操作。首先调用tryAcquire()方法获取同步,如果失败,则使用addWaiter构造节点并加入同步队列。最后用acquireQueued方法,使节点自旋来获取同步锁。
tryAcquire()方法是需要我们重写的,下面展示一个简单的不可重入锁的tryAcquire()的例子:
	public boolean tryAcquire(int acquires){
		if(this.compareAndSetState(0, 1)){	//如果锁未被占用,则设置为1
			this.setExclusiveOwnerThread(Thread.currentThread());	//将此线程设为锁的拥有者
			return true;
		}
		return false;
	}
在这个方法中,同步器的状态只能为0和1,同样的线程不能重新入锁,因为加锁后状态为1,方法中只有0才可以入锁。同一个线程,如果拥有锁后继续入锁,则会被加入同步队列,发生阻塞。这是一个“不好”的设计。
那么,我们常用的ReentrantLock不公平重入锁是怎么设计的,请看下面的代码:
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
        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;
        }

可以看到,当状态为0时,会同上面的不可重入锁一样进行加锁并取得锁。但如果不为0,则对线程进行判断,如果当前线程是拥有锁的线程,则可以正确返回,执行同步方法。否则,将加入同步队列等待。
在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())	//会调用LockSupport.park()方法阻塞自己,等待前继节点唤醒自己
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

自旋获取锁的过程:

判断当前节点的前驱节点。

需要获取当前节点的前驱节点的状态,当前驱节点是头结点并且当前节点(线程)能够获取状态(tryAcquire方法成功),

代表该当前节点占有锁,如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点(setHead)。

如果没有满足上述条件,判断前一个节点的状态,并调用parkAndCheckInterrupt方法使得当前线程阻塞,直到unpark调用(前一个节点释放锁的时候会通知后继节点),Thread的interrupt调用,然后重新轮训去尝试上述操作。

在当前线程获取同步并执行完成后,就会释放同步状态,方法如下:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
该方法会唤醒头结点的后续节点。

3 共享式同步状态的获取与释放

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。其获取锁的代码如下:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)	
            doAcquireShared(arg);
    }
    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);
        }
    }

在acquireShared()放法中,先会tryAcquireShared(arg),如果大于0成功,则获取同步状态。否则,加入同步队列,阻塞,等待前继节点执行完同步方法后唤醒,重新尝试获取同步状态。共享锁的释放同独占锁,需要唤醒后继节点。

4 独占式超时获取同步状态

可以在指定时间内获取同步状态,如果获取成功,则返回true,否则,返回false。在acquireSharedInterruptibly()的基础是增加了时间限制。不再详述。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值