Java中Lock接口解析

本文非原创,为转载文章,原文链接:https://www.jianshu.com/p/2344a3e68ca9



一、Lock

synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:

public interface Lock {
	void lock();
	void lockInterruptibly() throws InterruptedException;
	boolean tryLock();
	boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
	void unlock();
	Condition newCondition();
}

1.Lock一般的使用如下:
Lock lock= ...;//获取锁
lock.lock();
try{
	//处理任务
}catch(Exception e){

}finally{
	lock.unlock();//释放锁
}

lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的,其放在finally块里执行,可以保证锁一定被释放,newCondition方法下面会做介绍(通过该方法可以生成一个Condition对象,而Condition是一个多线程间协调通信的工具类)。


2.Lock接口的主要方法介绍:
  • lock():获取不到锁就不罢休,否则线程一直处于block状态。
  • tryLock():尝试性地获取锁,不管有没有获取到都马上返回,拿到锁就返回true,不然就返回false
  • tryLock(long time, TimeUnit unit):如果获取不到锁,就等待一段时间,超时返回false。
  • lockInterruptibly():该方法稍微难理解一些,在说该方法之前,先说说线程的中断机制,每个线程都有一个中断标志,不过这里要分两种情况说明:
    1. 线程在sleepwait或者join, 这个时候如果有别的线程调用该线程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException
    2. 如果线程处在运行状态, 则在调用该线程的interrupt()方法时,不会响应该中断。
      lockInterruptibly()和上面的第一种情况是一样的, 线程在获取锁被阻塞时,如果调用lockInterruptibly()方法,该线程会被唤醒并被要求处理InterruptedException。下面给出一个响应中断的简单例子:
public class Test{
	public static void main(String[] args){
		MyRunnable myRunnable = new Test().new MyRunnable();
		Thread thread1 = new Thread(myRunnable,"thread1");
		Thread thread2 = new Thread(myRunnable,"thread2");
		thread1.start();
		thread2.start();
		thread2.interrupt();
	}

	public class MyRunnable implements Runnable{
		private Lock lock=new ReentrantLock();
		@Override
		public synchronized void run() {
			try{
				lock.lockInterruptibly();
				System.out.println(Thread.currentThread().getName() +"获取了锁");
				Thread.sleep(5000);
			}catch(InterruptedException e) {
				e.printStackTrace();
				System.out.println(Thread.currentThread().getName() +"响应中断");
			}finally{
				lock.unlock();
				System.out.println(Thread.currentThread().getName() +"释放了锁");
			}
		}
	}
}

执行结果如下:

thread1获取了锁
thread1释放了锁
thread2响应中断

thread2在响应中断后,在finally块里执行unlock方法时,会抛出java.lang.IllegalMonitorStateException异常(因为thread2并没有获取到锁,只是在等待获取锁的时候响应了中断,这时再释放锁就会抛出异常)。


3.newCondition()方法

上面简单介绍了ReentrantLock的使用,下面具体介绍使用ReentrantLock的中的newCondition方法实现一个生产者消费者的例子。
生产者、消费者
例子:两个线程A、B,A生产牙刷并将其放到一个缓冲队列中,B从缓冲队列中购买(消费)牙刷(说明:缓冲队列的大小是有限制的),这样就会出现如下两种情况。

  1. 当缓冲队列已满时,A并不能再生产牙刷,只能等B从缓冲队列购买牙刷;
  2. 当缓冲队列为空时,B不能再从缓冲队列中购买牙刷,只能等A生产牙刷放到缓冲队列后才能购买。
public class ToothBrushDemo {
    public static void main(String[] args) {
        final ToothBrushBusiness toothBrushBusiness =
                new ToothBrushDemo().new ToothBrushBusiness();
        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, true);
            }
        }, "牙刷生产者1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, false);
            }
        }, "牙刷消费者1").start();
    }

    //循环执行50次
    public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
                                       boolean isProducer) {
        for (int i = 0; i < 50; i++) {
            if (isProducer) {
                toothBrushBusiness.produceToothBrush();
            } else {
                toothBrushBusiness.consumeToothBrush();
            }
        }
    }
    
    public class ToothBrushBusiness {
        //定义一个大小为10的牙刷缓冲队列
        private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
        private int number = 1;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notEmpty = lock.newCondition();
        private final Condition notFull = lock.newCondition();
        public ToothBrushBusiness() {
        }
        
        //生产牙刷
        public void produceToothBrush() {
            lock.lock();
            try {
                //牙刷缓冲队列已满,则生产牙刷线程等待
                while (toothBrushQueue.isFull()) {
                    notFull.await();
                }
                ToothBrush toothBrush = new ToothBrush(number);
                toothBrushQueue.enQueue(toothBrush);
                System.out.println("生产: " + toothBrush.toString());
                number++;
                //牙刷缓冲队列加入牙刷后,唤醒消费牙刷线程
                notEmpty.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        //消费牙刷
        public void consumeToothBrush() {
            lock.lock();
            try {
                //牙刷缓冲队列为空,则消费牙刷线程等待
                while (toothBrushQueue.isEmpty()) {
                    notEmpty.await();
                }
                ToothBrush toothBrush = toothBrushQueue.deQueue();
                System.out.println("消费: " + toothBrush.toString());
                //从牙刷缓冲队列取出牙刷后,唤醒生产牙刷线程
                notFull.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public class ToothBrush {
        private int number;
        public ToothBrush(int number) {
            this.number = number;
        }
        @Override
        public String toString() {
            return "牙刷编号{" +
                    "number=" + number +
                    '}';
        }
    }
}

这里缓冲队列的大小设成了10,定义了一个可重入锁lock,两个状态标记对象notEmpty,notFull,分别用来标记缓冲队列是否为空,是否已满。

  1. 当缓冲队列已满时,调用notFull.await方法用来阻塞生产牙刷线程。
  2. 当缓冲队列为空时,调用notEmpty.await方法用来阻塞购买牙刷线程。
  3. notEmpty.signal用来唤醒消费牙刷线程,notFull.signal用来唤醒生产牙刷线程。

4.Object和Conditon对应关系如下:
ObjectCondition
休眠waitawait
唤醒特定线程notifysignal
唤醒所有线程notifyAllsignalAll

对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。



二、ReadWriteLock

ReentrantLock(可重入锁)是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

synchronized和ReentrantLock都是可重入锁,可重入性举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。


ReentrantReadWriteLock简介

上面的响应中断的例子已经地使用到了ReentrantLock,下面来介绍另外一种锁,可重入读写锁ReentrantReadWriteLock,该类实现了ReadWriteLock接口,该接口的源码如下:

public interface ReadWriteLock {
	Lock readLock();
	Lock writeLock();
}

ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。

  • 线程进入读锁的前提条件:
    1. 没有其他线程的写锁
    2. 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程
  • 进入写锁的前提条件:
    1. 没有其他线程的读锁
    2. 没有其他线程的写锁
  • 需要提前了解的概念:
    • 锁降级:从写锁变成读锁;
    • 锁升级:从读锁变成写锁。

读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。

ReadWriteLock接口只有获取读锁和写锁的方法,而ReentrantReadWriteLock是实现了ReadWriteLock接口,接着对其应用场景做简单介绍。


应用场景:

假设一个共享的文件,其属性是可读,如果某个时间有100个线程在同时读取该文件,如果通过synchronized或者Lock来实现线程的同步访问,那么有个问题来了,当这100个线程的某个线程获取到了锁后,其它的线程都要等该线程释放了锁才能进行读操作,这样就会造成系统资源和时间极大的浪费,而ReentrantReadWriteLock正好解决了这个问题。下面给一个简单的例子,并根据代码以及输出结果做简要说明:

public class Test {
    public static void main(String[] args) {
        MyRunnable myRunnable = newTest().new MyRunnable();
        Thread thread1 = new Thread(myRunnable, "thread1");
        Thread thread2 = new Thread(myRunnable, "thread2");
        Thread thread3 = new Thread(myRunnable, "thread3");
        thread1.start();
        thread2.start();
        thread3.start();
    }

    public class MyRunnable implements Runnable {
        private ReadLock lock = new ReentrantReadWriteLock().readLock();

        @Override
        public synchronized void run() {
            try {
                lock.lock();
                int i = 0;
                while (i < 5) {
                    System.out.println(Thread.currentThread().getName() + "正在进行读操作");
                    i++;
                }
                System.out.println(Thread.currentThread().getName() + "读操作完毕");
            } finally {
                lock.unlock();
            }
        }
    }
}

输出结果:

thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1正在进行读操作
thread1读操作完毕
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3正在进行读操作
thread3读操作完毕
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2正在进行读操作
thread2读操作完毕

从输出结果可以看出,三个线程并没有交替输出,这是因为这里只是读取了5次,但将读取次数i的值改成一个较大的数值如100000时,输出结果就会交替的出现。


看了好多人的博文,在我看来,这个Lock的用处就是可以细化加锁和解锁的操作,使锁操作更加直观,可控

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值