使用Java5中Lock对象也能实现同步的效果,而且在使用上也更加方便。
使用ReentrantLock类的使用
在Java多线程中,可以使用synchronized关键字实现线程之间同步互斥,但在JDK1.5中增加了ReentranLock类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定,多路分支通知等功能,而且在使用上也比synchronized更加的灵活。
使用ReentranLock实现同步:测试1
既然ReentranLock类在功能上相比synchronized更多,那么就以一个初步的程序示例来介绍一下ReentranLock类的使用。
代码清单1 测试1
运行结果如图1所示,从运行结果来看,当前线程打印完毕之后将锁将进行释放,其他线程才可以继续打印。线程打印的数据是分组打印,因为当前线程已经持有锁,但线程之间的打印顺序是随机的。
图1 同步运行
使用ReentranLock实现同步:测试2
Lock下lock()持有对象监听作用。
代码清单2 测试2
运行结果如图2所示,,调用lock.lock()代码的线程就持有了“对象监视器”,其他线程只有等待锁被释放时再次抢夺。效果和使用synchronized关键字一样,线程之间还是顺序执行的。
图2 全部同步运行
使用Condition实现等待/通知:错误用法与解决
关键字synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。Condition类是在JDK5中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。
在使用notify()/notifyAll()方法进行通知时,被通知的线程却是由JVM随机选择的。但使用Reentrant Lock结合Condition类是可以实现前面介绍过的“选择性通知”,这个功能是非常重要的,而且在Condition类中是默认提供的。
而synchronized就相当于整个Lock对象中只有一个单一的Condition对象,所有的线程都注册在它一个对象的身上。线程开始notifyAll()时,需要通知所有的WAITING线程,没有选择权,会出现相当大的效率问题。
代码清单3 condition错误用法
运行结果如图3所示,出现异常没有监视对象。
图3 出现异常(无监视对象)
报错的异常信息是监视器出错,解决的办法是必须在condition.await()方法调用之前调用lock.lock()代码获得同步监视器。
代码清单4 获得同步监听
运行结果如图4所示,控制台中只打印一个字母A,原因是调用了Condition对象的await()方法,使当前执行任务的线程进入等待WAITING状态。
图4 只打印字母A
正确使用Condition实现等待/通知
代码清单5 实现等待通知
程序运行结果如图5所示,成功实现等待/通知模式。
图5 正常运行
Object类中的wait()方法相当于Condition类中的await()方法。
Object类中的wait(long timeout)方法相当于Condition类中await(long time,TimeUnit unit)方法。
Object类中的notify()方法相当于Condition类中的signal()方法。
Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。
使用多个Condition实现通知部分线程:错误用法
其实Condition对象也可以创建多个。那么一个Condition对象和多个Condition对象在使用上有什么区别呢?
代码清单6 错误用法
运行结果如图6所示,A和B线程都被唤醒了。
图6 线性A和B都被唤醒
如果想单独唤醒部分线程该怎么处理呢?这时就有必要使用多个Condition对象了,也就是Condition对象可以唤醒部分指定线程,有助于提升程序运行效率。可以先对线程进行分组,然后再唤醒指定组中的线程。
使用多个Condition实现通知部分线程:正确用法
代码清单7 正确用法
运行结果如图7所示,只有线程A被唤醒了。使用ReentrantLock对象可以唤醒指定种类的线程,这是控制部分线程行为的方便方式。
图7 线程B没被唤醒
实现生产者/消费者模式:一对一交替打印
代码清单8 一对一交替打印
运行结果如图8所示,通过使用Condition对象,成功实现交替打印的结果。
图8 交替打印
实现生产者/消费者模式:多对多交替打印
代码清单9 多对多交替打印
运行结果如图9所示,程序运行后出现假死。可以使用signalAll()方法来解决。将MyService.Java类中两处signal()代码改成signalAll()后,运行结果如图10所示,程序正常运行,不再出现假死状态。
图9 出现假死
图10 假死的状态解决了
“打印★”和“打印☆”是交替输出的,但是“有可能★★连续”和“有可能☆☆连续”却不是交替输出的,有时候出现连续打印的情况。原因是程序中使用一个Condition对象,再结合signalAll()方法来唤醒所有的线程,那么唤醒的线程就有可能是同类,所以就出现连续打印“有可能★★连续”和“有可能☆☆连续”的情况了。
公平锁与非公平锁
公平与非公平锁:锁lock分为“公平锁”和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,及先来先得的FIFO先进先出的顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
代码清单10 公平锁与非公平锁
公平锁运行结果如图11所示,打印的结果基本是呈有序的状态,这就是公平锁的特点。
图11 公平锁
非公平锁运行结果如图12所示,结果基本都是乱序的,说明先start()启动的线程不代表先获得锁。
图12 非公平锁
方法getHoldCount()、getQueueLenght()和getWaitQueueLength()的测试
1)方法int getHoldCount()的作用是查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
代码清单11 getHoldCount()方法
运行结果如图13所示,分别查询当前线程保持此锁定的个数。
图13 运行结果
2)方法int getQueueLength()的作用是返回正等待获取此锁定的线程估计数,比如有5个线程,一个线程首先执行await()方法,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待lock的释放。
代码清单12 getQueueLength()方法
允许结果如图14所示。
图14 运行结果
3)方法int getWaitQueueLength(Condition condition)的作用是返回等待此锁定相关的给定条件Condition的线程估计数,比如5个线程,每个线程都执行了同一个condition对象的await()方法,则调用getWaitQueueLength(Condition condition)方法时返回的int值是5。
代码清单13 getWaitQueueLength(Condition condition)
运行结果如图15所示。
图15 运行结果
方法hasQueuedThread()、hasQueueThreads()和hasWaiters()的测试
1)方法boolean hasQueueThread(Thread thread)的作用是查询指定的线程是否正在等待获取此锁定。
代码清单14 hasQueuedThread()方法
运行结果如图16所示。
图16 运行结果
2)方法boolean hasWaiters(Condition condition)的作用是查询是否有线程正在等待与此锁定有关的condition条件。
代码清单15 hasWaiters(Condition condition)方法
运行结果如图17所示。
图17 运行结果
方法isFair()、isHeldByCurrentThread()和isLocked()的测试
1)方法boolean isFair()的作用是判断是不是公平锁。
代码清单16 isFair()方法
运行结果如图18所示,在默认情况下,ReentrantLock类使用的是非公平锁。
图18 运行结果
2)方法boolean isHeldByCurrentThread()的作用是查询当前线程是否保持此锁定。
代码清单17 isHeldByCurrentThread()方法
运行结果如图19所示。
图19 运行结果
3)方法boolean isLocked()的作用是查询此锁定是否任意线程保持。
代码清单18 isLocked()方法
运行结果如图20所示。
图20 运行结果