Lock接口

Java se5之后,出现了Lock接口,提供了与Synchronized类似同步功能。与synchronized相比,他虽然少了隐式获取释放锁的便捷性,却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized不具备的同步特性。



Lock的使用方式非常简单,首先实例化Lock对象


Lock lock = new ReentrantLock();


然后在需要的地方加锁:

lock.lock(): 

 

  try{ 

 

  //do something 

 

  }finally{ 

 

  lock.unlock();//避免发生异常不能释放锁 

 

  }


队列同步器


AbstractQueueSynchronizer队列同步器(以下简称同步器),是用来构建锁或者其他同步组件的基本框架。同步器是实现锁的关键,锁和同步器的关系如下:锁是面向使用者的,隐藏了实现细节。而同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。


队列同步器的接口与示例


同步器的设计是基于模板方法模式的,因此,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法。而这些模板方法将会调用使用者重写的方法。


也就是说同步器的方法主要分为两类:模板方法和可重写方法。


如下所示

java开发中常用的加锁方式 java锁的用法_读写锁

java开发中常用的加锁方式 java锁的用法_读写锁_02


同时,你要重写可重写方法,需要使用下面三个方法来访问或者修改同步状态


getState():获取当前同步状态


setState(int newState):设置当前同步状态


compareAndSetState(int expect,int update):使用CAS操作设置当前状态,该方法能保证状态设置的原子性。


下面我们来看一个同步器的简单实现;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;

public class Mutex {
	private class Sync extends AbstractQueuedSynchronizer{
		//是否处于占用状态
		protected boolean isHeldExclusively(){
			return getState()==1;
		}
		//当状态为0的时候获取锁
		public boolean tryAcquire(int acquire){
			if(compareAndSetState(0, 1)){
				setExclusiveOwnerThread(Thread.currentThread());
				return true;
			}
			return false;
		}
		//释放锁,将状态设置为0
		protected boolean tryRease(int releases){
			if(getState()==0){
				throw new IllegalMonitorStateException();
			}
			setExclusiveOwnerThread(null);
			setState(0);
			return true;
		}
		public Condition newCondition() {
			// TODO Auto-generated method stub
			return new ConditionObject();
		}
	}
	public final Sync s = new Sync();
	public void lock(){s.acquire(1);}
	public boolean tryLock(){return s.tryAcquire(1);}
	public void unlock(){s.release(1);}
	public Condition newCondition(){return s.newCondition();}
	public boolean isLocked(){return s.isHeldExclusively();}
	public boolean hasQueueThreads(){return s.hasQueuedThreads();}
	//other methods as same as in Lock

}


队列同步器的实现分析


队列同步器的实现主要包括以下几个方面:同步队列、度展示同步状态获取与释放、共享式同步状态获取与释放、以及超时获取同步状态等同步器的核心数据结构和模板方法。


同步队列


同步器依赖同步队列---一个双向的FIFO队列----来完成同步状态的管理。当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息封装的成一个节点放入同步队列,同时会阻塞当前线程。当同步状态释放时,会把首节点的线程唤醒,使其再次尝试获得同步状态。


同步器拥有首节点和为节点,每当一个线程获取同步状态失败,都会加入这个同步队列的尾部,这个操作是要保证线程安全的,也就是说,必须使用CAS操作。同步器提供了一个CAS方法:compareAndSetTail(node expect,node update)



java开发中常用的加锁方式 java锁的用法_读写锁_03




同步器持有的同步队列的节点唤醒基本过程就是:首先,首节点是获取同步状态成功的节点,当同步状态被当前首节点释放时,后继的节点会在获取同步状态成功时将自己设置为首节点。同时这个过程是由获得同步状态成功的线程来执行的,因此不需要CAS操作。


独占式同步状态的获取和释放


通过调用同步器的acquire方法可以获得同步状态。该方法对中断不敏感,也就是和synchronized一样,对于获得锁之后的中断不会执行中断操作。同步器的acquire方法调用了我们在自定义同步器类Sync中的tryAcquire()方法。其源代码如下所示:

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

这个方法的if语句作了三个操作:获取同步状态,如果上一步不成功则进行节点构造并将节点加入同步队列(addWaiter()方法),并在同步队列中自旋(acquireQueued()方法)。我么前面已经实现了tryAcquire,下面我们来看看addWaiter的源代码


private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

上面的代码首先创建了一个保存当前节点和其状态的Node实例node,然后将当前尾节点存储(Node pred = tail)。接下来在if语句中,如果当前尾节点非空,表示队列非空,那么就让node的前一个节点指针pre指向pred,同时compareAndSetTail方法确保节点能够被独立的设置为尾节点。然后让pred的下一个节点指向node,也就是现在的尾节点。enq(node)通过死循环的方式来保证节点正确添加。


节点进入队列后,就保持自旋的状态来观察,当条件满足,获取到了同步状态,那么就可以从自旋中退出。如下面的代码所示:


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


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


共享式允许同一时刻多个线程同时获取同步状态。


共享式同步状态的acquireShared(int arg)可以共享的获得同步状态,当tryAcquaire方法返回值大于0,表示能够获得同步状态,否则调用doAcquireShare进行自旋该方法代码如下:




java开发中常用的加锁方式 java锁的用法_公平锁_04




释放的代码如下:



java开发中常用的加锁方式 java锁的用法_读写锁_05



重入锁


前面我们写的Mutex就是一个不可重入的锁,重入的锁就是当一个线程A获得一个锁之后,再次调用lock()方法,不会把自己阻塞,而是在当前锁继续加一。释放锁的时候,也会每释放一次就让state加一,直到加到0表示释放完成。



公平锁和非公平锁:在研究冲入锁的时候,ReentrantLokc实现了两个方法。TryAcquire是公平的锁,nonfairTryAcquire()方法是非公平锁。二者的区别是,公平锁能够是能够按照进入的队列顺序来进行锁的获取的,而非公平锁不管这个。可以通过调用ReentrantLock(Boolean flag)来设置开启。设置flag为true是公平锁,否则为非公平锁。默认是非公平锁




读写锁ReentrantReadWriteLock


读写锁不是实现的lock接口,因此不能用多态的方式实例化ReentrantReadWriteLock。读写锁能够获得读锁和写锁。读锁是共享的,写锁是排除的。




LockSupport工具


LockSupport工具定义了一组的公共静态方法,提供了基本的线程阻塞和唤醒功能。




java开发中常用的加锁方式 java锁的用法_java_06


Condition接口


condition接口也实现了类似的像监控器一样的方法。与lock配合可以实现等待通知模式。



java开发中常用的加锁方式 java锁的用法_模板方法_07