除了上篇讲的synchronized关键字来实现同步,java5中也提供了Lock对象来实现同步的效果,我们重点学习以下两个知识点。
- ReentrantLock类
- ReentrantReadWriteLock类
1. ReentrantLock 类的使用
java多线程中,synchronized关键字实现多线程之间的同步,但是ReentrantLock类也能达到同样的效果,并且在扩展功能上也更强大,比如嗅探锁定、多路分支通知等,也比synchronized更加灵活。
1.1 简单实例
创建类Service.java,如下:
public class Service {
// 创建ReentrantLock对象
private Lock lock = new ReentrantLock();
public void testMethod() {
// 获取锁
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " " + (i + 1));
}
// 释放锁
lock.unlock();
}
}
创建线程ThreadA和ThreadB类,如下
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
public class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
调试代码:
public class Main {
public static void main(String[] args) {
// 创建执行对象
Service service = new Service();
ThreadA[] threadAS = new ThreadA[2];
ThreadB[] threadBS = new ThreadB[2];
for (int i = 0; i < threadAS.length; i++) {
threadAS[i] = new ThreadA(service);
threadBS[i] = new ThreadB(service);
}
// 启动多线程
for (int i = 0; i < threadAS.length; i++) {
threadAS[i].start();
threadBS[i].start();
}
}
}
执行结果:
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-3 1
Thread-3 2
Thread-3 3
Thread-3 4
Thread-3 5
Thread-2 1
Thread-2 2
Thread-2 3
Thread-2 4
Thread-2 5
从结果上分析,当前线程执行结束后才释放锁,其他线程才可获取锁才可以继续打印,但是线程之间打印是随机的。
1.2 与synchronized同步一致
将1.1中Service.java,ThreadA.java和ThreadB.java修改如下:
public class Service {
private Lock lock = new ReentrantLock();
public void testMethodA() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " start...");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " end...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void testMethodB() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " start...");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " end...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethodA();
}
}
public class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethodB();
}
}
调试代码:
public class Main {
public static void main(String[] args) {
Service service = new Service();
ThreadA threadA = new ThreadA(service);
threadA.setName("A");
threadA.start();
ThreadA threadAA = new ThreadA(service);
threadAA.setName("AA");
threadAA.start();
ThreadB threadB = new ThreadB(service);
threadB.setName("B");
threadB.start();
ThreadB threadBB = new ThreadB(service);
threadBB.setName("BB");
threadBB.start();
}
}
调试结果:
A start...
A end...
AA start...
AA end...
B start...
B end...
BB start...
BB end...
从结果上看,lock.lock()方法的线程持有的是对象锁,效果和synchronized一样。
1.3 使用Condition实现等待/通知
synchronized使用的wait()和notify()/notifyAll()方法的结合可以实现线程之间的等待/通知,ReentrantLock类也有同样的功能实现,但是要使用Condition对象,Condition类是java5提供的,其灵活性更高,可以实现多路通知功能,也就是一个Lock对象可以创建多个Condition对象实例,线程对象可以注册在指定的Condition中从而可以选择性地给线程通知,调度上更加灵活。
示例:
public class MyService {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public void awaitA() {
lock.lock();
try {
System.out.println("waitA time:" + System.nanoTime());
conditionA.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("lockA released");
}
}
public void awaitB() {
try {
lock.lock();
System.out.println("waitB time:" + System.nanoTime());
conditionB.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("lockB released");
}
}
public void signalA() {
try {
lock.lock();
System.out.println("signalA time :" + System.nanoTime());
conditionA.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalB() {
try {
lock.lock();
System.out.println("signalB time :" + System.nanoTime());
conditionB.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
创建线程类:
public class ThreadA extends Thread {
private MyService myService;
@Override
public void run() {
myService.awaitA();
}
public ThreadA(MyService myService) {
this.myService = myService;
}
}
public class ThreadB extends Thread {
private MyService myService;
@Override
public void run() {
myService.awaitB();
}
public ThreadB(MyService myService) {
this.myService = myService;
}
}
调试程序:
public class Run {
public static void main(String[] args) throws InterruptedException {
MyService myService = new MyService();
ThreadA a = new ThreadA(myService);
a.start();
ThreadB b = new ThreadB(myService);
b.start();
TimeUnit.SECONDS.sleep(3);
myService.signalA();
myService.signalB();
}
}
调试结果:
waitA time:1506428352433847000
waitB time:1506428352435218000
signalA time :1506428355439920000
signalB time :1506428355440144000
lockA released
lockB released
从结果上看,已经实现了等待/通知模式,对此说明,Object类中的wait()方法相当于Condition中await()方法,Object类中的notify()方法相当于Condition类中的signal()方法。Object类中notifyAll()方法相当于Condition类中的signalAll()方法。
1.4 使用Lock对象实现生产者/消费者模式,交替打印数据
创建Service.java类,代码如下:
public class Service {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean hasValue = false;
public void set() {
try {
lock.lock();
while (hasValue == true) {
System.out.println("生产了数据: ## 连续");
condition.await();
}
System.out.println("生产了数据:#");
hasValue = true;
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get() {
try {
lock.lock();
while (hasValue == false) {
System.out.println("消费了数据: $$ 连续");
condition.await();
}
System.out.println("消费了数据:$");
hasValue = false;
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
创建生产者/消费者线程类,代码如下:
public class ProducerThread extends Thread {
private Service service;
public ProducerThread(Service service) {
this.service = service;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
service.set();
}
}
}
public class ConsumerThread extends Thread {
private Service service;
public ConsumerThread(Service service) {
this.service = service;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
service.get();
}
}
}
调试代码:
public class Run {
public static void main(String[] args) {
Service service = new Service();
ProducerThread[] producerThreads = new ProducerThread[10];
ConsumerThread[] consumerThreads = new ConsumerThread[10];
for (int i = 0; i < 10; i++) {
producerThreads[i] = new ProducerThread(service);
consumerThreads[i] = new ConsumerThread(service);
producerThreads[i].start();
consumerThreads[i].start();
}
}
}
调试结果:
生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
生产了数据: ## 连续
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
消费了数据:$
消费了数据: $$ 连续
生产了数据:#
生产了数据: ## 连续
生产了数据: ## 连续
从输出结果可以看出是交替输出的,但是出现了"生产了数据: ## 连续"和"消费了数据: $$ 连续"有时候是连续打印的,原因是程序中使用的Condition对象中signalAll()方法来通知所有await()的线程,那么有可能通知的是同类,所以就出现以上连续的情况了。
2. ReentrantReadWriteLock 类的使用
锁ReentrantLock是互斥的,同一时间只有一个线程可执行lock.lock()方法,这样保证了实例变量的线程安全,但是效率比较低,故JDK提供了另一种读写锁ReentrantReadWriteLock类。使其加速运行速度。
从字面上可以看出,读写锁包含了两个锁,一个是读锁,一个是写锁,读锁是共享锁,写锁则是排他锁。意思就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。也就是在没有现成进行写操作时,进行读操作的多个线程都可以获取读锁,而进行写入操作的线程只有在获取了写锁后才能进行写操作。多个线程可以同时进行读操作,但是同一时间只允许一个线程进行写操作。这就是ReentrantReadWriteLock类的全面语义理解。
还是通过代码示例来理解会更加容易
2.1 读读共享锁
创建Service.java类,代码如下:
public class Service {
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void read() {
try {
reentrantReadWriteLock.readLock().lock();
System.out.println("获得读锁 " + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.readLock().unlock();
}
}
}
创建读线程对象,代码如下:
public class ReadThread extends Thread {
private Service service;
public ReadThread(Service service) {
this.service = service;
}
@Override
public void run() {
service.read();
}
}
调试代码:
public class Main {
public static void main(String[] args) {
Service service = new Service();
ReadThread readThreadA = new ReadThread(service);
ReadThread readThreadB = new ReadThread(service);
readThreadA.start();
readThreadB.start();
}
}
调试结果:
获得读锁 Thread-0:1506430190938
获得读锁 Thread-1:1506430190938
通过结果可看出,两个线程几乎同时进入lock()方法后面的代码,说明lock.readLock()读锁可以提高运行效率,允许多个线程同时执行lock()后面的代码。
2.2 写写互斥
修改上例中的Service.java类,修改如下:
public class Service {
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void write() {
try {
reentrantReadWriteLock.writeLock().lock();
System.out.println("获得写锁 " + Thread.currentThread().getName() + ":" + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
创建写线程类,代码如下:
public class WriteThread extends Thread {
private Service service;
public WriteThread(Service service) {
this.service = service;
}
@Override
public void run() {
service.write();
}
}
调试代码:
public class Main {
public static void main(String[] args) {
Service service = new Service();
WriteThread writeThreadA = new WriteThread(service);
WriteThread writeThreadB = new WriteThread(service);
writeThreadA.start();
writeThreadB.start();
}
}
调试结果:
获得写锁 Thread-0:1506430557896
获得写锁 Thread-1:1506430562898
从结果可看到,两个线程获得锁时差为5s,是同步执行的,所以写写锁是互斥的。
2.3 读写锁互斥
按照上例,只需要修改调试程序即可,如下:
public class Main {
public static void main(String[] args) {
Service service = new Service();
ReadThread readThread = new ReadThread(service);
WriteThread writeThread = new WriteThread(service);
writeThread.start();
readThread.start();
}
}
调试结果:
获得写锁 Thread-1:1506430757186
获得读锁 Thread-0:1506430762191
可见,运行时间有差别,获取写锁的时候,是获取不到读锁的,所以读写锁互斥。
3 小结
通过几个简单的示例,我们在后续多线程同步代码时可使用Lock对象替换synchronized关键字,而且Lock对象比synchronized功能更强大。Lock对象是synchronized的进阶,掌握Lock有助于学习java并发包源代码的实现原理,在并发包中大量的类使用Lock接口作为同步的处理方式。