最近的开发遇到了多线程的问题,由于之前总是使用Synchronized
这种方式,让人难以理解不说,而且这个字还不好记,真是反人类,最重要的是这种方式是Java 1.5
之前的方法,已经有很多人(我没统计过)证明这种方式在性能和使用上存在问题,关键是如果继续使用这种老技术,会被认为是不思进取的人(自己的看法,哈哈)。所以为了与时俱进(其实我们已经晚了一大步),我们还是使用Java 1.5
中新提供的技术吧!
由于之前也没有系统地研究过Java新提供的锁机制,所以从这篇开始算作自己的学习的一个笔记吧!
首先说明的一点,由于是学习笔记,所以可能对内部实现原理可能涉及不多,主要还是着重于应用!
首先总览一下并发框架的包,这里我们着重在java.util.concurrent.locks
,在这包内的主要关系(下面内容关注的,也是使用频率比较高的)如下:
所以根据上面这个图,主要的内容可以分为三个部分:
Condition
部分及使用;Lock
部分及使用;ReadWriteLock
部分使用以及与Lock
的区别;
本篇主要在Condition
部分,也是相对来说比较简单的一部分,个人感觉。
一、 Condition的简介
Condition
是Java 1.5
中用于线程之间协调同步的类,在Java 1.5 之前如果想要完成线程同步的工作,比较常见的有如下几种方法:
- 使用
Object
类中的wait/notify
等对象监视方法; - 使用
synchronized
方法或者代码块; - 使用
volatile
关键字 - 使用
ThreadLocal
关键字
既然有这么多种方法,为什么还要新增Condition
类呢?这里并不想比较这几种方式的优缺点,而是想主要说明一下Condition
自身具有的新特点,如果仔细看的话,Condition
中的新API
与Object
类中的wait/notify
等对象监视方法的作用几乎相同,但是如果深入剖析,差别还是很大的,从名字上来看Condition
是“条件”的意思,实际上可以简单理解为线程之间同步时如果满足一定条件则通知相关线程执行对应的操作,与之前的wait/notify
等对象监视方法的主要区别:
- 不使用
synchronized
关键字,而是使用Lock
接口进行保证线程之间的互斥,且所有的Condition
对象是来自于同一个Lock
实例,这样才能保证针对同一个对象,虽然在不同的条件下执行的并发操作也是互斥的; - 针对每个对象有多个动作线程等待,即针对一个对象同时维护多个线程等待队列,好吧,这句话有些拗口,也就是说针对一个对象的并发操作可以根据具体的环境和实际的条件进行。比如针对一个消息队列,加入说只有两种操作,一个是取消息,一个是放消息,每种动作可能有多个线程同时操作,使用Condition可以维护两个队列,当某种操作可以执行的时候则同时对应的队列;而如果使用
Object
的wait/notify
则只是维护一个队列,如果通知可以执行操作时,还需要到该队列中确定可以执行的是哪种操作。这个特点也是Condition
和之前的方法最主要的区别,在下面的使用中会详细说明。 - Condition中新提出两个API:
awaitUntil(Date deadline)
:在当前线程被通知或者中断之前会一直等待;awaitUninterruptibly()
:在当前线程被通知之前会一直等待;
二、 Condition的基本使用
Condition
的使用,这里以上面所说的消息队列为例,具体的过程看下图:
上图的整个过程如下,对于一个消息队列来说,它主要的操作就是放入消息和获取消息,针对一个消息队列对象来说,所有的并发操作都应该是互斥的,所以Condition
对象都是由同一个Lock
实例生成的,针对消息队列的两个不同的动作放在两个不同的线程队列中,这样在针对消息队列进行操作的时候,会根据具体的条件直接指定由哪个线程队列中的动作执行,这样让人看起来更加清晰、明了。Condition
中所说的会维持多个等待集,在这里指的就是多个线程队列。
那么使用传统的Object
的wait/notify
等对象监视方法是怎样的处理上述问题的呢?如下图:
从上图可以看到传统的方法只是维护了一个线程等待队列,那也就是说每当消息队列对象发生变化后,通知线程队列中的动作执行时还必须区分出是哪种动作线程,然后执行满足条件的对应的动作线程。
从上面两个图中可以看出,Condition
的方式可以省略消息队列去查询对应的动作线程的过程,更有效率,同时维护两个线程等待队列,更加清楚明白,即每当消息队列对象发生变化时,Condition
的await/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
,可以直接使用。
相关文章: