多线程之重入锁ReentrantLock及原理

读前必看AQS原理——http://blog.csdn.net/qq_31957747/article/details/74910939

一、重入锁ReentrantLock

重入锁是synchronized、Object.wait()和Object.notify()方法的替代品(或者说是增强版),在JDK5.0的早期版本,重入锁的性能远远好于synchronized,但从JDK6.0开始,JDK在synchronized上做了大量的优化,使得两者的性能差距并不大。但ReentrantLock也有一些synchronized没法实现的特性。

重入锁,为啥要叫重入锁呢,很多文章是这么解释的:


同一个线程连续两次获得同一把锁,这是允许的,否则同一个线程将在第二次获得锁的时候会和自己死锁。道理我都懂,但是这个例子也太抽象了,看了也不知道用处啊,还是看下面这个例子吧:

public class ReentrantTest implements Runnable {
    ReentrantLock lock = new ReentrantLock();
    public void doSomething() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId()+" doSomething()");
            System.out.println("进入第一个lock,当前的lock数:"+lock.getHoldCount());
            doAnotherThing();
        } finally {
            lock.unlock();
            System.out.println("第一个lock释放,当前的lock数:"+lock.getHoldCount());
        }
    }

    private void doAnotherThing() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId()+" doAnotherThing()");
            System.out.println("进入第二个lock,当前的lock数:"+lock.getHoldCount());
        } finally {
            lock.unlock();
            System.out.println("第二个lock释放,当前的lock数:"+lock.getHoldCount());
        }
    }

    @Override
    public void run() {
        doSomething();
    }
    public static void main(String[] args) {
        ReentrantTest rt = new ReentrantTest();
        ExecutorService es = Executors.newFixedThreadPool(10);
        for(int i = 0;i<5;i++) {
            es.submit(rt);
        }
        es.shutdown();
    }
}



部分运行结果:

这样一看不就明白了重入的意义。

注意:同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同的次数。如果释放多了,会抛出java.lang.IllegalMonitorStateException异常;反之,释放少了,那这个线程还占用这把锁,别的线程就没法进入临界区。


重入的实现源码:

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



ReentrantLock的几个重要方法:

1、lock():获得锁,如果锁已经被占用,则等待。

2、lockInterruptibly():获得锁,但优先响应中断。

3、tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。

4、tryLock(long time,TimeUnit unit):在给定时间内尝试获得锁。

5、unlock():释放锁。


ReentrantLock的特性:

1、公平锁和非公平锁

在大多数情况下,锁的申请都是非公平的。比如,线程1首先请求了锁A,线程2再请求了锁A,当锁A释放了之后,是线程1拿到锁还是线程2拿到锁是不一定的。系统只会在这个锁的等待队列中随机挑选一个。重入锁允许我们对公平性进行设置。

   public ReentrantLock() {     //默认为非公平锁
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {	//通过boolean类型参数true设置成公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }


之前我们谈到AQS

同步类在实现时,一般将自定义同步器(Sync)定义为内部类。而同步类就依赖Sync实现一系列接口供外部使用。而ReentrantLock中对Sync进一步扩展,实现了NonfairSync(非公平锁)和FairSync(公平锁)。

非公平锁就不说了,平时也一直在用,下面看看公平锁的使用:

public class FairLockTest implements Runnable{
    ReentrantLock fairLock = new ReentrantLock(true);

    @Override
    public void run() {
        while(true){
            try{
                fairLock.lock();
                System.out.println(Thread.currentThread().getId()+" 获得锁");
            }finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLockTest ft = new FairLockTest();
        ExecutorService es = Executors.newFixedThreadPool(3);
        for(int i = 0;i<3;i++) {
            es.submit(ft);
        }
    }
}
运行结果:


可以看到挺公平的,你一下我一下的。。。

2、轮询锁和定时锁

这两种都是由tryLock方法实现,两者都能避免死锁的发生。只不过轮询锁用的是tryLock(),而定时锁用的是tryLock(long timeout, TimeUnit unit)。


轮询锁:

tryLock:当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,直接返回true。如果被其他线程占用,则不会等待,直接返回false。

public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
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;
        }
可以看到tryLock也就是调用了非公平版本的获取资源的方法,获取不到直接返回false。

再看下lock()方法(就看非公平版本的lock()吧):

final void lock() {
            if (compareAndSetState(0, 1))  //直接抢占
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
可以看到lock()方法资源被占用的情况下调用的是acquire(1),之前在AQS那篇文章里说过,acquire(1)在AQS里是这样实现的(不清楚的可以看下这里: http://blog.csdn.net/qq_31957747/article/details/74910939):

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
也就是他会先尝试获取资源,获取不到,再加入等待队列,等待前驱唤醒自己。

而tryLock()则少了等待的过程,获取不到,不会等待,直接不干了。

while(true)配上tryLock(),就是一个轮询,顾名思义,一遍遍的循环尝试获取资源,失败继续循环,这就跟我们的CAS有点像了。

例子如下:

public class TryLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    public TryLock(int lock){
        this.lock = lock;
    }
    @Override
    public void run() {
        if(this.lock == 1){
            while (true) {
                if (lock1.tryLock()) {
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                        }
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + ":My Job Done");
                                return ;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            }
        }else {
            while (true) {
                if (lock2.tryLock()) {
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                        }
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + ":My Job Done");
                                return;
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        TryLock r1 = new TryLock(1);
        TryLock r2 = new TryLock(2);
        new Thread(r1).start();
        new Thread(r2).start();
    }
}


这段程序用两个线程一个占用lock1锁,一个占用lock2锁,然后又相互请求获取对方的锁,如果是用lock()的话,则就相互等待,产生死锁。而tryLock()这种轮询的方式,让他不停的尝试获取,最终两个线程都能成功退出,避免了死锁(可能轮询的时间有点长,不过最终两个线程是能正常执行结束的)。

运行结果:



定时锁:

tryLock(long timeout, TimeUnit unit)接收两个参数,第一个表示等待时间,第二个表示计时单位。当前线程会尝试获得锁,当前锁没有被线程占用,则获取成功返回true。被其他线程占用,则等待参数设置的时间,超过这个时间则不等待,立即返回false。

看看tryLock(long timeout, TimeUnit unit)的源码:

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
再看tryAcquireNanos这个方法:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||                
            doAcquireNanos(arg, nanosTimeout);   
    }

return后面的代码意思就很明确,尝试获取资源成功就返回true,获取失败就执行doAcquireNanos方法,那就再看这个方法:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();     //获取当前系统的时间数值
        final Node node = addWaiter(Node.EXCLUSIVE); //将当前线程节点标记为独占模式,放入队尾
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {//前驱线程节点为head节点,且刚好释放资源
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                if (nanosTimeout <= 0) //剩余等待时间<=0,退出等待
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&  //满足等待条件,那就等待喽
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                long now = System.nanoTime();//等待结束再次获取当前系统时间数值
                nanosTimeout -= now - lastTime; //剩余等待时间减少
                lastTime = now;
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
意思还是很明确。

来个例子:

public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try{
            System.out.println(Thread.currentThread().getName()+"正在运行。并尝试在5秒内获得锁");
            if(lock.tryLock(5, TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName()+"获取锁,并睡眠6秒");
                Thread.sleep(6000);
            }else{
                System.out.println(Thread.currentThread().getName()+"等待超过5秒。获得锁失败");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        TimeLock tl = new TimeLock();
        new Thread(tl).start();
        new Thread(tl).start();
    }
}

一个线程占用了锁,然后sleep6秒,sleep是不会释放锁的,所以另一个线程等待5秒后拿不到锁,在放弃等待了,方法返回false。

运行结果:



3、可中断锁

lockInterruptibly()方法在获得锁的同时保持对中断的响应。看下源码:

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
继续跟踪acquireInterruptibly()方法。
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
再看doAcquireInterruptibly()方法:

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();   //跟之前AQS那个非中断的系列的方法相比,不同就是这里throw了中断异常。
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
差不多就可以看出为什么会响应中断了,下面看个例子:
public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /**
     * 控制加锁顺序,方便构造死锁
     * @param lock
     */
    public IntLock(int lock){
        this.lock = lock;
    }
    @Override
    public void run() {
        try{
            if(lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            }else{
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
            }
            System.out.println(Thread.currentThread().getId()+":线程执行完任务");
        }catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getId()+":线程被中断");
        }finally {
            if(lock1.isHeldByCurrentThread())
                lock1.unlock();
            if(lock2.isHeldByCurrentThread())
                lock2.unlock();
            System.out.println(Thread.currentThread().getId()+":线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        //中断其中一个线程
        t2.interrupt();
    }
}

也是两个线程相互等待锁的问题,这时候中断了其中一个线程,另一个线程也就能顺利的拿到锁并执行结束。

运行结果:

可以看到,中断后两个线程双双退出。但真正完成任务的只有一个,另一个则放弃任务直接退出,释放资源。


二、Condition条件

ReentrantLock和Condition,就和synchronized和wait()、notify()很像。


Condition接口提供的基本方法如下:

void await() throws InterruptedException;

void awaitUninterruptibly();

long awaitNanos(long nanosTimeout) throws InterruptedException;

boolean await(long time,TimeUnit unit)throws InterruptedException;

void signal();

void signalAll();


await()方法会使当前线程等待,同时释放当前锁,当其他线程使用signal()或signalAll()方法的时候,线程会重新获得锁并继续执行。或者当线程被中断的时,也能跳出等待。跟Object.wait()方法很像。

awaitUninterruptibly()跟await方法基本相同,区别仅仅在于它不会在等待过程中响应中断。

signal()方法用于唤醒一个在等待中的线程。相对的signalAll()方法会唤醒所有在等待中的线程,跟Object.notify()很像。


用Condition来改造以前讲的notify()和wait()的生产者消费者的例子:

//仓库类
public class Store2 {
    private ReentrantLock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    private final int MAX_SIZE;  //仓库的最大容量
    private int count;           //当前的仓库数量
    public Store2(int n){
        MAX_SIZE = n;
        count = 0;
    }
    //往仓库加货物的方法
    public void add(){       //使用了wait()或notify()方法,需要加上syncronized关键字
        try {
            lock.lock();
            while (count >= MAX_SIZE) {          //否则可能会抛出java.lang.IllegalMonitorStateException
                System.out.println("已经满了");
                try {
                    notFull.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count++;
            //打印当前仓库的货物数量
            System.out.println(Thread.currentThread().toString() + " put" + count);
            //仓库中已经有东西可以取了,则通知所有的消费者线程来拿
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }
    //从仓库拿走货物的方法
    public  void remove(){
        try {
            lock.lock();
            while (count <= 0) {
                System.out.println("空了");
                try {
                    notEmpty.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //打印当前仓库的货物数量
            System.out.println(Thread.currentThread().toString() + " get" + count);
            count--;
            //仓库还没装满,通知生产者添加货物
            notFull.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Store2 s = new Store2(5);         //创建容量为5的仓库
        //创建两个生产者和两个消费者
        Thread pro = new Producer2(s);
        Thread con = new Consumer2(s);
        Thread pro2 = new Producer2(s);
        Thread con2 = new Consumer2(s);
        pro.setName("producer");
        con.setName("consumer");
        pro2.setName("producer2");
        con2.setName("consumer2");
        //启动各个线程
        pro.start();
        pro2.start();
        con.start();
        con2.start();
    }
}
class Producer2 extends Thread{          //生产者线程类
    private Store2 s;
    public Producer2(Store2 s){
        this.s = s;
    }
    public void run(){      //线程方法
        while(true){        //永久循环
            s.add();        //往仓库加货物
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
class Consumer2 extends Thread{          //消费者线程类
    private Store2 s;
    public Consumer2(Store2 s){
        this.s = s;
    }
    public void run(){      //线程方法
        while(true){        //永久循环
            s.remove();        //往仓库取走货物
            try{
                Thread.sleep(1500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}





 













  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值