Java并发实践: 显式锁之Condition使用

最近的开发遇到了多线程的问题,由于之前总是使用Synchronized这种方式,让人难以理解不说,而且这个字还不好记,真是反人类,最重要的是这种方式是Java 1.5 之前的方法,已经有很多人(我没统计过)证明这种方式在性能和使用上存在问题,关键是如果继续使用这种老技术,会被认为是不思进取的人(自己的看法,哈哈)。所以为了与时俱进(其实我们已经晚了一大步),我们还是使用Java 1.5中新提供的技术吧!

由于之前也没有系统地研究过Java新提供的锁机制,所以从这篇开始算作自己的学习的一个笔记吧!

首先说明的一点,由于是学习笔记,所以可能对内部实现原理可能涉及不多,主要还是着重于应用!

首先总览一下并发框架的包,这里我们着重在java.util.concurrent.locks,在这包内的主要关系(下面内容关注的,也是使用频率比较高的)如下:

这里写图片描述

所以根据上面这个图,主要的内容可以分为三个部分:

  • Condition部分及使用;
  • Lock部分及使用;
  • ReadWriteLock部分使用以及与Lock的区别;

本篇主要在Condition部分,也是相对来说比较简单的一部分,个人感觉。

一、 Condition的简介

ConditionJava 1.5中用于线程之间协调同步的类,在Java 1.5 之前如果想要完成线程同步的工作,比较常见的有如下几种方法:

  • 使用Object类中的wait/notify等对象监视方法;
  • 使用synchronized方法或者代码块;
  • 使用volatile关键字
  • 使用ThreadLocal关键字

既然有这么多种方法,为什么还要新增Condition类呢?这里并不想比较这几种方式的优缺点,而是想主要说明一下Condition自身具有的新特点,如果仔细看的话,Condition中的新APIObject类中的wait/notify等对象监视方法的作用几乎相同,但是如果深入剖析,差别还是很大的,从名字上来看Condition是“条件”的意思,实际上可以简单理解为线程之间同步时如果满足一定条件则通知相关线程执行对应的操作,与之前的wait/notify等对象监视方法的主要区别:

  • 不使用synchronized关键字,而是使用Lock接口进行保证线程之间的互斥,且所有的Condition对象是来自于同一个Lock实例,这样才能保证针对同一个对象,虽然在不同的条件下执行的并发操作也是互斥的;
  • 针对每个对象有多个动作线程等待,即针对一个对象同时维护多个线程等待队列,好吧,这句话有些拗口,也就是说针对一个对象的并发操作可以根据具体的环境和实际的条件进行。比如针对一个消息队列,加入说只有两种操作,一个是取消息,一个是放消息,每种动作可能有多个线程同时操作,使用Condition可以维护两个队列,当某种操作可以执行的时候则同时对应的队列;而如果使用Objectwait/notify则只是维护一个队列,如果通知可以执行操作时,还需要到该队列中确定可以执行的是哪种操作。这个特点也是Condition和之前的方法最主要的区别,在下面的使用中会详细说明。
  • Condition中新提出两个API:
    • awaitUntil(Date deadline):在当前线程被通知或者中断之前会一直等待;
    • awaitUninterruptibly():在当前线程被通知之前会一直等待;

二、 Condition的基本使用

Condition的使用,这里以上面所说的消息队列为例,具体的过程看下图:

这里写图片描述

上图的整个过程如下,对于一个消息队列来说,它主要的操作就是放入消息和获取消息,针对一个消息队列对象来说,所有的并发操作都应该是互斥的,所以Condition对象都是由同一个Lock实例生成的,针对消息队列的两个不同的动作放在两个不同的线程队列中,这样在针对消息队列进行操作的时候,会根据具体的条件直接指定由哪个线程队列中的动作执行,这样让人看起来更加清晰、明了。Condition中所说的会维持多个等待集,在这里指的就是多个线程队列。

那么使用传统的Objectwait/notify等对象监视方法是怎样的处理上述问题的呢?如下图:

这里写图片描述

从上图可以看到传统的方法只是维护了一个线程等待队列,那也就是说每当消息队列对象发生变化后,通知线程队列中的动作执行时还必须区分出是哪种动作线程,然后执行满足条件的对应的动作线程。

从上面两个图中可以看出,Condition的方式可以省略消息队列去查询对应的动作线程的过程,更有效率,同时维护两个线程等待队列,更加清楚明白,即每当消息队列对象发生变化时,Conditionawait/signal操作能够准确的指明要通知的线程。这种情况产生的原因在于,从编程的角度看,一个Lock实例可以对应多个Condition对象,但是一个synchronized方法或者代码块只能对应一个对象。

上面使用Condition实现消息队列操作的代码如下:

public class MessageQueue {
    private static final Lock lock = new ReentrantLock();
    private final Condition getAction = lock.newCondition();
    private final Condition putAction = lock.newCondition();
    private Object[] items = new Object[3];
    private int count = 0;
    private int putPosition = 0;
    private int getPosition = 0;

    public void put(Object object) throws InterruptedException {
        lock.lock();
        try{
            while(count == items.length){
                System.out.println("full");
                putAction.await();
            }
            items[putPosition] = object;
            if(putPosition == items.length-1){
                putPosition = 0;
            }
            putPosition++;
            count++;
            System.out.println("put time is: " + object);
            getAction.signal();
        }finally {
            lock.unlock();
        }
    }

    public Object get() throws InterruptedException {
        Object object = null;
        lock.lock();
        try{
            while(count == 0){
                System.out.println("empty");
                getAction.await();
            }
            object = items[getPosition];
            if (getPosition == items.length-1) {
                getPosition = 0;
            }
            getPosition++;
            count--;
            putAction.signal();
            return object;
        }finally {
            lock.unlock();
        }
    }

}

对这个消息队列进行测试如下:

final MessageQueue messageQueue = new MessageQueue();

ExecutorService service = Executors.newFixedThreadPool(20);
// 模拟多线程环境,且写数据的请求多于读数据的请求
// 也可以模拟读数据请求多于写数据请求,那么运行结果应该是会出现empty
for(int i=0; i<20; i++){
    Thread.sleep(200);
    if(i %3 == 0){
        service.submit(new Runnable() {
            public void run() {
                try {
                    Object object = messageQueue.get();
                    System.out.println(object);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }else{
        service.submit(new Runnable() {
            public void run() {
                try {
                    long time = System.currentTimeMillis();
                    messageQueue.put(String.valueOf(time));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

运行结果如下:

这里写图片描述

针对上述消息队列,JDK已经有官方实现ArrayBlockingQueue,可以直接使用。


相关文章:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值