使用锁实现同步
Java提供了同步代码块的另一种机制,它是一种比synchronized关键字更强大也更灵活的机制。这种机制基于Lock接口及其实现类(例如ReentrantLock),提供了更多的好处。
- 支持更灵活的同步代码块结构。使用synchronized关键字时,只能在同一个syanchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构(即控制的获取和释放不出现在同一个块结构中)。
- 相比synchronized关键字,Lock接口提供了更多的功能。其中一个新的功能是tryLock()方法的实现。这个方法试图获取锁,如果锁已经被其他线程获取,它将返回false并继续往下执行代码。使用synchronized关键字时,如果线程A试图执行一个同步代码块,而线程B已在执行这个同步代码块,则线程A就会被挂起直到线程B运行完这个同步代码块。使用锁的tryLock()方法,通过返回值将得知是否有其他线程正在使用这个锁保护的代码块。
- Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。
- 相比synchronized关键字,Lock接口具有更好的性能。
下面我们将学习如何使用锁来同步代码,并且使用Lock接口和它的实现类——ReentrantLock类来创建一个临界区。这个范例将模拟打印队列。
1. 创建一个打印队列类PrintQueue。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class PrintQueue { //声明一个锁对象,并且用ReentrantLock类初始化 private final Lock queueLock = new ReentrantLock(); //实现打印方法 public void printJob(Object doucument){ queueLock.lock(); Long duration = (long) (Math.random()*10000); System.out.println(Thread.currentThread().getName()+": PrintQueue: Printing a Job during "+(duration/1000)+" seconds"); try { Thread.sleep(duration); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { queueLock.unlock(); } } }
2. 创建打印工作类Job并且实现Runnable接口。
public class Job implements Runnable { private PrintQueue printQueue; public Job(PrintQueue printQueue){ this.printQueue = printQueue; } @Override public void run() { System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName()); printQueue.printJob(new Object()); System.out.printf("%s: The Document has been printed\n", Thread.currentThread().getName()); } }
3. 创建范例的主类Main
public class Main { public static void main(String[] args) { //创建一个共享的打印队列对象 PrintQueue printQueue = new PrintQueue(); //创建10个打印工作Job对象 Thread threads[] = new Thread[10]; for(int i=0;i<10;i++){ threads[i] = new Thread(new Job(printQueue), "Thread"+i); } //启动10个线程 for(int i=0;i<10;i++){ threads[i].start(); } } }
4. 程序执行结果如下所示
Thread0: Going to print a document Thread8: Going to print a document Thread6: Going to print a document Thread8: PrintQueue: Printing a Job during 5 seconds Thread4: Going to print a document Thread2: Going to print a document Thread9: Going to print a document Thread7: Going to print a document Thread5: Going to print a document Thread1: Going to print a document Thread3: Going to print a document Thread8: The Document has been printed Thread6: PrintQueue: Printing a Job during 2 seconds Thread6: The Document has been printed Thread4: PrintQueue: Printing a Job during 4 seconds Thread4: The Document has been printed Thread0: PrintQueue: Printing a Job during 1 seconds Thread0: The Document has been printed Thread2: PrintQueue: Printing a Job during 6 seconds Thread2: The Document has been printed Thread9: PrintQueue: Printing a Job during 6 seconds Thread9: The Document has been printed Thread7: PrintQueue: Printing a Job during 1 seconds Thread5: PrintQueue: Printing a Job during 5 seconds Thread7: The Document has been printed Thread5: The Document has been printed Thread1: PrintQueue: Printing a Job during 2 seconds Thread1: The Document has been printed Thread3: PrintQueue: Printing a Job during 0 seconds Thread3: The Document has been printed
这个范例中,在printJob()这个临界区的开始,必须通过lock()方法获得对锁的控制。当线程A访问这个方法时,如果没有其他线程获取对这个锁的控制,lock()方法将让线程A获得锁并且允许它立即执行临界区代码。否则,如果其他线程B正在执行这个锁保护的临界区代码,lock()方法将让线程A休眠直到线程B执行完临界区的代码。
在线程离开临界区的时候,我们必须使用unlock()方法来释放它持有的锁,以便其它线程可以访问临界区。如果在离开临界区的时候没有调用unlock()方法,其它线程将永远等待,从而导致了死锁(Deadlock)情景。如果在临界区使用了try-catch块,不要忘记将unlock()方法放入finally部分,以便避免死锁。
ReentrantLock类也允许使用递归调用。如果一个线程获取了锁并且进行了递归调用,它将继续持有这个锁,因此调用lock()方法后也将立即返回,并且线程将继续执行递归调用。再者,我们还可以调用其他的方法。
必须很小心的使用锁,以避免死锁的发生。