我们已经了解了线程同步的基本概念以及使用synchronized
关键字的各种机制。Java提供了另一种基于Lock
接口和实现它的类(例如ReentrantLock
)的代码块同步机制。在本教程中,我们将看到Lock用于解决打印机队列问题的接口的基本用法。
Lock接口
java.util.concurrent.locks.Lock
是与synchronization
一样的线程同步机制。然而,Lock
比synchronization
更灵活,更复杂。由于Lock
是一个接口,您需要使用其中一个实现来在您的应用程序中使用Lock
。ReentrantLock
是Lock
接口的一个这样的实现。
下面Lock
接口的简单使用。
Lock lock = new ReentrantLock();
lock.lock();
//critical section
lock.unlock();
首先创建一个Lock
。然后调用它的lock()
方法。现在Lock
实例已被锁定了。在已锁定的线程调用unlock()
之前,将阻止任何其他线程调用lock()
。最后调用unlock()
,现在Lock
解锁了,所以其他线程就可以锁定它。
Lock接口和synchronized关键字之间的区别
Lock
和synchronized
块之间的主要区别是:
- 尝试访问
synchronized
块时超时是不可能的。使用Lock.tryLock(long timeout, TimeUnit timeUnit)
,这是可以的。 -
synchronized
块必须完全包含在一个方法中。Lock
可以在不同的方法中调用lock()
和unlock()
。
使用Locks模拟打印机队列
在此示例中,程序将模拟打印机的行为。您可以在不同的时间间隔内或同时向打印机提交许多打印作业。打印机将从打印机队列中取出作业并打印出来。其余的工作将在那里等待轮到他们。一旦打印机完成了打印作业,它将从队列中选择另一个作业并开始打印。保持这种情况不断循环。
PrintingJob.java
此类表示可以提交给打印机的独立打印。这个类实现了Runnable
接口,因此打印机可以在掉头时执行它。
public class PrintingJob implements Runnable {
private PrinterQueue printerQueue;
public PrintingJob(PrinterQueue printerQueue) {
this.printerQueue = printerQueue;
}
public void run() {
System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName());
printerQueue.printJob(new Object());
}
}
PrinterQueue.java
此类表示打印机队列/打印机。当前打印作业完成后,打印机将保持锁定以启动新的打印作业。
public class PrinterQueue {
private final Lock queueLock = new ReentrantLock();
public void printJob(Object document) {
queueLock.lock();
try {
Long duration = (long) (Math.random() * 10000);
System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
queueLock.unlock();
}
}
}
我们来测试我们的打印机程序:
public class LockExample {
public static void main(String[] args)
{
PrinterQueue printerQueue = new PrinterQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++)
{
thread[i] = new Thread(new PrintingJob(printerQueue), "Thread " + i);
}
for (int i = 0; i < 10; i++)
{
thread[i].start();
}
}
}
输出结果为:
Thread 1: Going to print a document
Thread 0: Going to print a document
Thread 9: Going to print a document
Thread 8: Going to print a document
Thread 7: Going to print a document
Thread 6: Going to print a document
Thread 5: Going to print a document
Thread 4: Going to print a document
Thread 3: Going to print a document
Thread 2: Going to print a document
Thread 1: PrintQueue: Printing a Job during 5 seconds :: Time - Sun Mar 03 01:29:27 CST 2019
Thread 1: The document has been printed
Thread 0: PrintQueue: Printing a Job during 6 seconds :: Time - Sun Mar 03 01:29:32 CST 2019
Thread 0: The document has been printed
Thread 9: PrintQueue: Printing a Job during 9 seconds :: Time - Sun Mar 03 01:29:39 CST 2019
Thread 9: The document has been printed
Thread 8: PrintQueue: Printing a Job during 6 seconds :: Time - Sun Mar 03 01:29:48 CST 2019
Thread 8: The document has been printed
Thread 7: PrintQueue: Printing a Job during 4 seconds :: Time - Sun Mar 03 01:29:54 CST 2019
Thread 7: The document has been printed
Thread 6: PrintQueue: Printing a Job during 6 seconds :: Time - Sun Mar 03 01:29:58 CST 2019
Thread 6: The document has been printed
Thread 5: PrintQueue: Printing a Job during 3 seconds :: Time - Sun Mar 03 01:30:05 CST 2019
Thread 5: The document has been printed
Thread 4: PrintQueue: Printing a Job during 2 seconds :: Time - Sun Mar 03 01:30:08 CST 2019
Thread 4: The document has been printed
Thread 3: PrintQueue: Printing a Job during 2 seconds :: Time - Sun Mar 03 01:30:10 CST 2019
Thread 3: The document has been printed
Thread 2: PrintQueue: Printing a Job during 1 seconds :: Time - Sun Mar 03 01:30:13 CST 2019
Thread 2: The document has been printed
该示例的关键在于PrinterQueue
类的printJob()
方法。当我们想要使用锁实现一个关键部分并保证只有一个执行线程运行一个代码块时,我们必须创建一个ReentrantLock
对象。在临界区的开头,我们必须使用lock()
方法来控制锁。
在关键部分的末尾,我们必须使用unlock()
方法来释放锁的控制并允许其他线程运行此关键部分。如果不在临界区末尾调用unlock()
方法,则等待该块的其他线程将永远等待,从而导致死锁情况。如果在关键部分使用try-catch
块,请不要忘记将包含unlock()
方法的句子放在finally
部分中。
阅读更多:如何创建死锁并在java中解决它
你必须非常小心使用Locks
以避免死锁。当两个或多个线程被阻塞等待永远不会被解锁的锁时会发生这种情况。例如,线程(A)
锁定Lock (X)
,线程(B)
锁定Lock (Y)
。如果现在,线程(A)
尝试锁定锁定(Y)
并且线程(B)
同时尝试锁定锁定(X)
,两个线程将无限期地被阻塞,因为它们正在等待永远不会被释放的锁定。请注意,这个问题出现,是因为两个线程都试图以相反的顺序获取锁。
参考资料
How to Use Locks in Java | java.util.concurrent.locks.Lock Tutorial and Example