在本章中,我们将介绍:
- 同步方法
- 在同步类中排列独立属性
- 在同步代码中使用条件
- 将代码块与锁同步
- 使用读/写锁同步数据访问
- 修改锁公平性
- 在锁中使用多个条件
并发编程中最常见的情况之一是多个执行线程共享一个资源。在并发应用中,多个线程读取或写入相同的数据或访问相同的文件或数据库连接是正常的。这些共享资源可能引发错误情况或数据不一致,我们必须实现避免这些错误的机制。
这些问题的解决方案来自于临界截面的概念。关键部分是访问共享资源的代码块,不能由多个线程同时执行。
为了帮助程序员实现关键部分,Java(以及几乎所有编程语言)提供了同步机制。当一个线程想要访问一个关键部分时,它会使用其中一个同步机制来确定是否有其他线程执行该关键部分。否则,螺纹进入临界段。否则,线程将被同步机制挂起,直到执行临界段的线程结束它。当多个线程正在等待一个线程完成一个关键部分的执行时,JVM 会选择其中一个线程,其余线程等待轮到它们执行。
本章介绍了一些方法,教您如何使用 Java 语言提供的两种基本同步机制:
- 关键词
synchronized
Lock
接口及其实现
在本食谱中,我们将学习如何使用 Java 中最基本的同步方法之一,即使用synchronized
关键字来控制对方法的并发访问。只有一个执行线程将访问使用synchronized
关键字声明的对象的其中一个方法。如果另一个线程试图访问使用同一对象的synchronized
关键字声明的任何方法,它将被挂起,直到第一个线程完成该方法的执行。
换句话说,每个用synchronized
关键字声明的方法都是一个关键部分,Java 只允许执行对象的一个关键部分。
静态方法具有不同的行为。只有一个执行线程将访问使用synchronized
关键字声明的一个静态方法,但另一个线程可以访问该类对象的其他非静态方法。在这一点上你必须非常小心,因为两个线程可以访问两个不同的synchronized
方法,如果一个是静态的,另一个不是。如果两种方法更改相同的数据,则可能会出现数据不一致错误。
为了学习这个概念,我们将实现一个示例,其中两个线程访问一个公共对象。我们将有一个银行账户和两个线程;一个向账户转账的人和另一个从账户取款的人。如果没有同步方法,我们可能会得到错误的结果。同步机制确保帐户的最终余额是正确的。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Account
的类,该类将模拟我们的银行帐户。它只有一个double
属性,名为balance
。public class Account { private double balance;
-
执行
setBalance()
和getBalance()
方法写入和读取属性值。public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; }
-
实现一个名为
addAmount()
的方法,该方法将余额的值增加一定量,并传递给该方法。只有一个线程应该更改余额的值,因此使用synchronized
关键字将此方法转换为临界部分。public synchronized void addAmount(double amount) { double tmp=balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp+=amount; balance=tmp; }
-
实现一个名为
subtractAmount()
的方法,该方法将余额的值递减一定量,并传递给该方法。只有一个线程应该更改余额的值,因此使用synchronized
关键字将此方法转换为临界部分。public synchronized void subtractAmount(double amount) { double tmp=balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp-=amount; balance=tmp; }
-
实现一个模拟 ATM 的类。它将使用
subtractAmount()
方法减少账户余额。此类必须实现要作为线程执行的Runnable
接口。public class Bank implements Runnable {
-
向该类添加一个
Account
对象。实现初始化该Account
对象的类的构造函数。private Account account; public Bank(Account account) { this.account=account; }
-
执行
run()
方法。它对账户的subtractAmount()
方法进行100
调用,以减少余额。@Override public void run() { for (int i=0; i<100; i++){ account.sustractAmount(1000); } }
-
实现一个模拟公司的类,使用
Account
类的addAmount()
方法增加账户余额。此类必须实现要作为线程执行的Runnable
接口。public class Company implements Runnable {
-
将
Account
对象添加到此类。实现初始化 account 对象的类的构造函数。private Account account; public Company(Account account) { this.account=account; }
-
执行
run()
方法。它通过100
调用账户的addAmount()
方法来增加余额。
```java
@Override
public void run() {
for (int i=0; i<100; i++){
account.addAmount(1000);
}
}
```
- 通过创建一个名为
Main
的类来实现应用的主类,该类包含main()
方法。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个
Account
对象,并将其余额初始化为1000
。
```java
Account account=new Account();
account.setBalance(1000);
```
- 创建一个
Company
对象并Thread
运行它。
```java
Company company=new Company(account);
Thread companyThread=new Thread(company);
```
- 创建一个
Bank
对象并Thread
运行它。
```java
Bank bank=new Bank(account);
Thread bankThread=new Thread(bank);
```
- 将初始余额写入控制台。
```java
System.out.printf("Account : Initial Balance: %f\n",account.getBalance());
Start the threads.
companyThread.start();
bankThread.start();
```
- 使用
join()
方法等待两个线程完成,并在控制台中打印帐户的最终余额。
```java
try {
companyThread.join();
bankThread.join();
System.out.printf("Account : Final Balance: %f\n",account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
```
在这个配方中,您已经开发了一个应用,可以对模拟银行账户的类的余额进行增减。程序对addAmount()
方法进行100
调用,该方法在每次调用中将余额增加1000
,并对subtractAmount()
方法进行100
调用,该方法在每次调用中将余额减少1000
。您应该期望最终余额和初始余额相等。
您试图使用名为tmp
的变量来存储帐户余额的值,从而强制出现错误情况,因此您读取帐户余额,增加时间变量的值,然后再次建立帐户余额的值。此外,您使用Thread
类的sleep()
方法引入了一点延迟,将执行该方法的线程休眠 10 毫秒,因此如果另一个线程执行该方法,它可以修改引发错误的帐户余额。是synchronized
关键字机制避免了这些错误。
如果您想查看并发访问共享数据的问题,请删除addAmount()
和subtractAmount()
方法的synchronized
关键字并运行程序。如果没有synchronized
关键字,当线程在读取帐户余额值后处于休眠状态时,另一个方法将读取帐户余额,因此两个方法都将修改相同的余额,并且其中一个操作不会反映在最终结果中。
正如您在以下屏幕截图中所看到的,您可能会得到不一致的结果:
如果你经常运行这个程序,你会得到不同的结果。JVM 不能保证线程的执行顺序。因此,每次执行它们时,线程将以不同的顺序读取和修改帐户余额,因此最终结果将不同。
现在,按照之前的学习添加synchronize
关键字,然后再次运行程序。正如您在下面的屏幕截图中所看到的,现在您获得了预期的结果。如果你经常运行这个程序,你会得到同样的结果。请参阅以下屏幕截图:
使用synchronized
关键字,我们保证在并发应用中正确访问共享数据。
正如我们在这个配方的介绍中提到的,只有线程才能访问在声明中使用synchronized
关键字的对象的方法。如果一个线程(a)正在执行一个synchronized
方法,而另一个线程(B)想要执行同一对象的其他synchronized
方法,它将被阻塞,直到线程(a)结束。但如果 threadB 可以访问同一类的不同对象,则不会阻止任何对象。
synchronized
关键字会影响应用的性能,因此您只能在并发环境中修改共享数据的方法上使用它。如果有多个线程调用synchronized
方法,则一次只有一个线程执行它们,而其他线程将等待。如果操作不使用synchronized
关键字,则所有线程可以同时执行该操作,从而减少总执行时间。如果您知道一个方法不会被多个线程调用,请不要使用synchronized
关键字。
您可以将递归调用与synchronized
方法一起使用。由于线程可以访问对象的synchronized
方法,因此可以调用该对象的其他synchronized
方法,包括正在执行的方法。它不必再次访问synchronized
方法。
我们可以使用synchronized
关键字来保护对代码块的访问,而不是对整个方法的访问。我们应该以这种方式使用synchronized
关键字来保护对共享数据的访问,将其余操作排除在该块之外,从而获得更好的应用性能。目标是使关键部分(一次只能由一个线程访问的代码块)尽可能短。我们使用了synchronized
关键字来保护对更新大楼中人数的指令的访问,省去了该块不使用共享数据的长时间操作。以这种方式使用synchronized
关键字时,必须将对象引用作为参数传递。只有一个线程可以访问该对象的synchronized
代码(块或方法)。通常,我们将使用this
关键字引用正在执行该方法的对象。
synchronized (this) {
// Java code
}
使用关键字synchronized
保护代码块时,必须将对象引用作为参数传递。通常,您将使用this
关键字引用执行该方法的对象,但您可以使用其他对象引用。通常,将专门为此目的创建这些对象。例如,如果一个类中有两个独立的属性由多个线程共享,则必须同步对每个变量的访问,但如果一个线程访问其中一个属性,而另一个线程同时访问另一个属性,则没有问题。
在本食谱中,您将学习如何通过一个示例来解决这种情况,该示例模拟了一个具有两个屏幕和两个售票处的电影院。当售票处售票时,它们是两个电影院中的一个,但不是两个都有,因此每个电影院的免费座位数是独立的属性。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Cinema
的类,并向其添加两个名为vacanciesCinema1
和vacanciesCinema2
的long
属性。public class Cinema { private long vacanciesCinema1; private long vacanciesCinema2;
-
在
Cinema
类中添加两个名为controlCinema1
和controlCinema2
的额外Object
属性。private final Object controlCinema1, controlCinema2;
-
实现初始化类的所有属性的
Cinema
类的构造函数。public Cinema(){ controlCinema1=new Object(); controlCinema2=new Object(); vacanciesCinema1=20; vacanciesCinema2=20; }
-
实现第一家影院的部分门票售出时调用的
sellTickets1()
方法。它使用controlCinema1
对象控制对synchronized
代码块的访问。public boolean sellTickets1 (int number) { synchronized (controlCinema1) { if (number<vacanciesCinema1) { vacanciesCinema1-=number; return true; } else { return false; } } }
-
实现第二家影院的部分门票售出时调用的
sellTickets2()
方法。它使用controlCinema2
对象控制对synchronized
代码块的访问。public boolean sellTickets2 (int number){ synchronized (controlCinema2) { if (number<vacanciesCinema2) { vacanciesCinema2-=number; return true; } else { return false; } } }
-
实现返回第一家影院的一些票时调用的
returnTickets1()
方法。它使用controlCinema1
对象控制对synchronized
代码块的访问。public boolean returnTickets1 (int number) { synchronized (controlCinema1) { vacanciesCinema1+=number; return true; } }
-
实现返回第二家影院的一些票时调用的
returnTickets2()
方法。它使用controlCinema2
对象控制对synchronized
代码块的访问。public boolean returnTickets2 (int number) { synchronized (controlCinema2) { vacanciesCinema2+=number; return true; } }
-
实现另外两种返回每家影院空位数的方法。
public long getVacanciesCinema1() { return vacanciesCinema1; } public long getVacanciesCinema2() { return vacanciesCinema2; }
-
实现类
TicketOffice1
并指定它实现Runnable
接口。public class TicketOffice1 implements Runnable {
-
声明一个
Cinema
对象并实现初始化该对象的类的构造函数。
```java
private Cinema cinema;
public TicketOffice1 (Cinema cinema) {
this.cinema=cinema;
}
```
- 实施
run()
方法,模拟两个影院的一些操作。
```java
@Override
public void run() {
cinema.sellTickets1(3);
cinema.sellTickets1(2);
cinema.sellTickets2(2);
cinema.returnTickets1(3);
cinema.sellTickets1(5);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
}
```
- 实现类
TicketOffice2
并指定它实现Runnable
接口。
```java
public class TicketOffice2 implements Runnable {
```
- 声明一个
Cinema
对象,并实现初始化该对象的类的构造函数。
```java
private Cinema cinema;
public TicketOffice2(Cinema cinema){
this.cinema=cinema;
}
```
- 实施
run()
方法,模拟两个影院的一些操作。
```java
@Override
public void run() {
cinema.sellTickets2(2);
cinema.sellTickets2(4);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTickets2(2);
cinema.sellTickets1(3);
cinema.sellTickets2(2);
cinema.sellTickets1(2);
}
```
- 通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。
```java
public class Main {
public static void main(String[] args) {
```
- 声明并创建一个
Cinema
对象。
```java
Cinema cinema=new Cinema();
```
- 创建一个
TicketOffice1
对象并Thread
执行它。
```java
TicketOffice1 ticketOffice1=new TicketOffice1(cinema);
Thread thread1=new Thread(ticketOffice1,"TicketOffice1");
```
- 创建一个
TicketOffice2
对象并Thread
执行它。
```java
TicketOffice2 ticketOffice2=new TicketOffice2(cinema);
Thread thread2=new Thread(ticketOffice2,"TicketOffice2");
```
- 启动两个线程。
```java
thread1.start();
thread2.start();
```
- 等待线程的完成。
```java
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
```
- 向控制台写信告知两家电影院的空置情况。
```java
System.out.printf("Room 1 Vacancies: %d\n",cinema.getVacanciesCinema1());
System.out.printf("Room 2 Vacancies: %d\n",cinema.getVacanciesCinema2());
```
当您使用synchronized
关键字来保护代码块时,您使用对象作为参数。JVM 保证只有一个线程可以访问由该对象保护的所有代码块(注意,我们总是谈论对象,而不是类)。
在本例中,我们有一个对象控制对vacanciesCinema1
属性的访问,因此每次只有一个线程可以修改该属性,另一个对象控制对vacanciesCinema2
属性的访问,因此每次只有一个线程可以修改该属性。但是可能有两个线程同时运行,一个修改vacancesCinema1
属性,另一个修改vacanciesCinema2
属性。
运行此示例时,您可以看到最终结果始终是每个电影院的预期空缺数。在以下屏幕截图中,您可以看到应用执行的结果:
synchronize
关键字还有其他重要用途。请参见部分,另请参见部分,了解解释此关键字用法的其他食谱。
- 第 2 章基本线程同步中的同步码配方中的使用条件
并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个数据生产者将其保存在缓冲区中,一个或多个数据消费者将其从缓冲区中取出。
由于缓冲区是一个共享数据结构,我们必须使用同步机制(如synchronized
关键字)来控制对它的访问,但我们有更多的限制。如果缓冲区已满,生产者无法将数据保存在缓冲区中;如果缓冲区为空,消费者无法从缓冲区中提取数据。
对于这些类型的情况,Java 提供了在Object
类中实现的wait()
、notify()
和notifyAll()
方法。线程可以在synchronized
代码块内调用wait()
方法。如果在synchronized
代码块之外调用wait()
方法,JVM 将抛出IllegalMonitorStateException
异常。当线程调用wait()
方法时,JVM 将线程置于睡眠状态,并释放控制其正在执行的synchronized
代码块的对象,并允许其他线程执行受该对象保护的synchronized
代码块。要唤醒线程,必须在受同一对象保护的代码块内调用notify()
或notifyAll()
方法。
在本配方中,您将学习如何使用synchronized
关键字和wait()
、notify()
和notifyAll()
方法实现生产者-消费者问题。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
EventStorage
的类。它有两个属性:一个名为maxSize
的int
属性和一个名为storage
的LinkedList<Date>
属性。public class EventStorage { private int maxSize; private List<Date> storage;
-
实现初始化类属性的类的构造函数。
public EventStorage(){ maxSize=10; storage=new LinkedList<>(); }
-
执行
synchronized
方法set()
在存储器中存储事件。首先,检查存储是否已满。如果已满,则调用wait()
方法,直到存储空间为空。在方法的末尾,我们调用notifyAll()
方法来唤醒wait()
方法中睡眠的所有线程。public synchronized void set(){ while (storage.size()==maxSize){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } storage.offer(new Date()); System.out.printf("Set: %d",storage.size()); notifyAll(); }
-
执行
synchronized
方法get()
获取存储的事件。首先,检查存储器是否有事件。如果没有事件,则调用wait()
方法,直到存储器有一些事件。在方法的末尾,我们调用notifyAll()
方法来唤醒wait()
方法中睡眠的所有线程。public synchronized void get(){ while (storage.size()==0){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("Get: %d: %s",storage.size(),((LinkedList<?>)storage).poll()); notifyAll(); }
-
创建一个名为
Producer
的类,并指定它实现Runnable
接口。它将实现示例的生产者。public class Producer implements Runnable {
-
声明一个
EventStore
对象并实现初始化该对象的类的构造函数。private EventStorage storage; public Producer(EventStorage storage){ this.storage=storage; }
-
实现调用
100
乘以EventStorage
对象的set()
方法的run()
方法。@Override public void run() { for (int i=0; i<100; i++){ storage.set(); } }
-
创建一个名为
Consumer
的类,并指定它实现Runnable
接口。它将实现示例中的消费者。public class Consumer implements Runnable {
-
声明一个
EventStorage
对象,并实现初始化该对象的类的构造函数。private EventStorage storage; public Consumer(EventStorage storage){ this.storage=storage; }
-
run()
实现方法。调用EventStorage
对象的get()
方法的100
倍。
```java
@Override
public void run() {
for (int i=0; i<100; i++){
storage.get();
}
}
```
- 通过实现名为
Main
的类来创建示例的主类,并向其添加main()
方法。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个
EventStorage
对象。
```java
EventStorage storage=new EventStorage();
```
- 创建一个
Producer
对象并Thread
运行它。
```java
Producer producer=new Producer(storage);
Thread thread1=new Thread(producer);
```
- 创建一个
Consumer
对象并Thread
运行它。
```java
Consumer consumer=new Consumer(storage);
Thread thread2=new Thread(consumer);
```
- 启动两个线程。
```java
thread2.start();
thread1.start();
```
这个例子的关键是EventStorage
类的set()
和get()
方法。首先,set()
方法检查存储属性中是否有可用空间。如果已满,则调用wait()
方法等待可用空间。当另一个线程调用notifyAll()
方法时,该线程将唤醒并再次检查条件。notifyAll()
方法不能保证线程会被唤醒。重复此过程,直到存储器中有可用空间,它可以生成一个新事件并存储它。
get()
方法的行为类似。首先,它检查存储器上是否有事件。如果EventStorage
类为空,则调用wait()
方法等待事件。当另一个线程调用notifyAll()
方法时,该线程将唤醒并再次检查条件,直到存储器中出现一些事件。
您必须不断检查条件并在while
循环中调用wait()
方法。在条件为true.
之前,您无法继续
如果您运行此示例,您将看到生产者和消费者是如何设置和获取事件的,但存储器中的事件从不超过 10 个。
synchronized
关键字还有其他重要用途。请参见部分,另请参见部分,了解解释此关键字用法的其他食谱。
- 第 2 章基本线程同步中的在同步类配方中安排独立属性
Java 为代码块的同步提供了另一种机制。这是一种比synchronized
关键字更强大、更灵活的机制。它基于Lock
接口和实现它的类(如ReentrantLock
)。该机制具有以下优点:
- 它允许以更灵活的方式构造同步块。使用
synchronized
关键字,您必须以结构化的方式获得并释放对同步代码块的控制。Lock
接口允许您获得更复杂的结构来实现关键部分。 Lock
接口通过synchronized
关键字提供附加功能。其中一项新功能是通过tryLock()
方法实现的。这个方法试图获得锁的控制权,如果不能,因为它被其他线程使用,它将返回锁。使用synchronized
关键字,当一个线程(a)尝试执行一个同步的代码块时,如果有另一个线程(B)执行它,线程(a)将被挂起,直到线程(B)完成同步块的执行。使用锁,您可以执行tryLock()
方法。此方法返回一个Boolean
值,指示是否有另一个线程运行受此锁保护的代码。Lock
接口允许读写操作分离,具有多个读卡器和一个修饰符。Lock
接口提供比synchronized
关键字更好的性能。
在本配方中,您将学习如何使用锁来同步代码块,并使用接口和实现它的ReentrantLock
类创建关键部分,实现模拟打印队列的程序。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
PrintQueue
的类,该类将实现打印队列。public class PrintQueue {
-
声明一个
Lock
对象并用ReentrantLock
类的新对象初始化它。private final Lock queueLock=new ReentrantLock();
-
执行
printJob()
方法。它将接收Object
作为参数,不会返回任何值。public void printJob(Object document){
-
在
printJob()
方法中,获取调用lock()
方法的Lock
对象的控制权。queueLock.lock();
-
然后,包含以下代码来模拟文档的打印:
try { Long duration=(long)(Math.random()*10000); System.out.println(Thread.currentThread().getName()+ ": PrintQueue: Printing a Job during "+(duration/1000)+ " seconds"); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); }
-
最后,使用
unlock()
方法释放Lock
对象的控制。finally { queueLock.unlock(); }
-
创建一个名为
Job
的类,并指定它实现Runnable
接口。public class Job implements Runnable {
-
声明
PrintQueue
类的一个对象,并实现初始化该对象的类的构造函数。private PrintQueue printQueue; public Job(PrintQueue printQueue){ this.printQueue=printQueue; }
-
执行
run()
方法。它使用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()); }
-
通过实现名为
Main
的类来创建应用的主类,并向其添加main()
方法。
```java
public class Main {
public static void main (String args[]){
```
- 创建一个共享的
PrintQueue
对象。
```java
PrintQueue printQueue=new PrintQueue();
```
- 创建 10 个
Job
对象和 10 个线程来运行它们。
```java
Thread thread[]=new Thread[10];
for (int i=0; i<10; i++){
thread[i]=new Thread(new Job(printQueue),"Thread "+ i);
}
```
- 启动 10 个线程。
```java
for (int i=0; i<10; i++){
thread[i].start();
}
```
在下面的屏幕截图中,您可以看到此示例的一次执行的部分输出:
示例的关键在于PrintQueue
类的printJob()
方法。当我们想要使用锁实现一个关键部分并保证只有一个执行线程运行一个代码块时,我们必须创建一个ReentrantLock
对象。在临界段开始时,我们必须使用lock()
方法控制锁。当一个线程(a)调用此方法时,如果没有其他线程控制锁,则该方法会将锁的控制权交给线程(a),并立即返回以允许执行此线程的关键部分。否则,如果有另一个线程(B)执行此锁控制的关键部分,lock()
方法会将线程(A)置于睡眠状态,直到线程(B)完成关键部分的执行。
在临界段结束时,我们必须使用unlock()
方法释放锁的控制,并允许其他线程运行该临界段。如果不在关键部分末尾调用unlock()
方法,等待该块的其他线程将永远等待,从而导致死锁情况。如果您在关键部分使用 try-catch 块,请不要忘记将包含unlock()
方法的句子放在finally
部分中。
Lock
接口(和ReentrantLock
类)包含另一种获取锁控制的方法。这是tryLock()
方法。与lock()
方法最大的区别在于,如果使用该方法的线程无法获得Lock
接口的控制,则该方法会立即返回,并且不会使线程进入睡眠状态。此方法返回一个boolean
值,如果线程获得了锁的控制,则返回true
,否则返回false
。
考虑到程序员有责任考虑此方法的结果并采取相应的行动。如果该方法返回false
值,则您的程序将不会执行临界段。如果是这样,您的应用中可能会出现错误的结果。
ReentrantLock
类还允许使用递归调用。当线程控制锁并进行递归调用时,它将继续控制锁,因此对lock()
方法的调用将立即返回,线程将继续执行递归调用。此外,我们还可以调用其他方法。
您必须非常小心使用Locks
以避免死锁。当两个或多个线程被阻塞,等待永远不会解锁的锁时,就会发生这种情况。例如,线程(a)锁定锁(X),线程(B)锁定锁(Y)。如果现在线程(A)尝试锁定锁(Y),线程(B)同时尝试锁定锁(X),这两个线程将被无限期阻塞,因为它们正在等待永远不会释放的锁。请注意,出现这个问题是因为两个线程都试图以相反的顺序获得锁。附录并发编程设计解释了充分设计并发应用和避免这些死锁问题的一些好技巧。
锁提供的最重要的改进之一是ReadWriteLock
接口和ReentrantReadWriteLock
类,这是实现它的唯一类。这个类有两个锁,一个用于读操作,一个用于写操作。可以有多个线程同时使用读操作,但只有一个线程可以使用写操作。当线程执行写操作时,不能有任何线程执行读操作。
在本食谱中,您将学习如何使用ReadWriteLock
接口实现一个程序,该程序使用该接口控制对存储两种产品价格的对象的访问。
您应该阅读同步代码块与锁的配方,以便更好地理解此配方。
按照以下步骤来实现该示例:
-
创建一个名为
PricesInfo
的类,该类存储两种产品的价格信息。public class PricesInfo {
-
声明两个名为
price1
和price2
的double
属性。private double price1; private double price2;
-
声明一个名为
lock
的ReadWriteLock
对象。private ReadWriteLock lock;
-
实现初始化三个属性的类的构造函数。对于
lock
属性,我们创建一个新的ReentrantReadWriteLock
对象。public PricesInfo(){ price1=1.0; price2=2.0; lock=new ReentrantReadWriteLock(); }
-
实现返回
price1
属性值的getPrice1()
方法。它使用读锁控制对此属性值的访问。public double getPrice1() { lock.readLock().lock(); double value=price1; lock.readLock().unlock(); return value; }
-
实现返回
price2
属性值的getPrice2()
方法。它使用读锁控制对此属性值的访问。public double getPrice2() { lock.readLock().lock(); double value=price2; lock.readLock().unlock(); return value; }
-
执行
setPrices()
方法建立两个属性的值。它使用写锁来控制对它们的访问。public void setPrices(double price1, double price2) { lock.writeLock().lock(); this.price1=price1; this.price2=price2; lock.writeLock().unlock(); }
-
创建一个名为
Reader
的类,并指定它实现Runnable
接口。此类实现了PricesInfo
类属性值的读取器。public class Reader implements Runnable {
-
声明一个
PricesInfo
对象并实现初始化该对象的类的构造函数。private PricesInfo pricesInfo; public Reader (PricesInfo pricesInfo){ this.pricesInfo=pricesInfo; }
-
为这个类实现
run()
方法。它的读数是这两种价格的 10 倍。
```java
@Override
public void run() {
for (int i=0; i<10; i++){
System.out.printf("%s: Price 1: %f\n", Thread.currentThread().getName(),pricesInfo.getPrice1());
System.out.printf("%s: Price 2: %f\n", Thread.currentThread().getName(),pricesInfo.getPrice2());
}
}
```
- 创建一个名为
Writer
的类,并指定它实现Runnable
接口。这个类实现了PricesInfo
类属性值的修饰符。
```java
public class Writer implements Runnable {
```
- 声明一个
PricesInfo
对象并实现初始化该对象的类的构造函数。
```java
private PricesInfo pricesInfo;
public Writer(PricesInfo pricesInfo){
this.pricesInfo=pricesInfo;
}
```
- 执行
run()
方法。它修改两个价格值的三倍,这两个价格在修改之间休眠两秒钟。
```java
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.printf("Writer: Attempt to modify the prices.\n");
pricesInfo.setPrices(Math.random()*10, Math.random()*8);
System.out.printf("Writer: Prices have been modified.\n");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
- 通过创建名为
Main
的类来实现示例的主类,并向其添加main()
方法。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个
PricesInfo
对象。
```java
PricesInfo pricesInfo=new PricesInfo();
```
- 创建五个
Reader
对象和五个Threads
执行它们。
```java
Reader readers[]=new Reader[5];
Thread threadsReader[]=new Thread[5];
for (int i=0; i<5; i++){
readers[i]=new Reader(pricesInfo);
threadsReader[i]=new Thread(readers[i]);
}
```
- 创建一个
Writer
对象并Thread
执行。
```java
Writer writer=new Writer(pricesInfo);
Thread threadWriter=new Thread(writer);
```
- 启动线程。
```java
for (int i=0; i<5; i++){
threadsReader[i].start();
}
threadWriter.start();
```
在以下屏幕截图中,您可以看到此示例的一次执行的部分输出:
如前所述,ReentrantReadWriteLock
类有两个锁,一个用于读操作,一个用于写操作。读取操作中使用的锁是通过ReadWriteLock
接口中声明的readLock()
方法获得的。这个锁是实现Lock
接口的对象,所以我们可以使用lock()
、unlock()
和tryLock()
方法。写入操作中使用的锁是通过ReadWriteLock
接口中声明的writeLock()
方法获得的。这个锁是实现Lock
接口的对象,所以我们可以使用lock()
、unlock()
、和tryLock()
方法。程序员有责任确保这些锁的正确使用,使用它们的目的与设计它们的目的相同。当您获得Lock
接口的读取锁时,您不能修改变量的值。否则,您可能会有不一致的数据错误。
类的ReentrantLock
和ReentrantReadWriteLock
类的构造函数允许一个名为fair
的boolean
参数,允许您控制这两个类的行为。false
值为默认值,称为非公平模式。在此模式下,当有一些线程等待锁定(ReentrantLock
或ReentrantReadWriteLock
,并且锁必须选择其中一个线程才能访问关键部分时,它会选择一个没有任何条件的线程。该true
值称为公平模式。在该模式下,当有一些线程等待锁(ReentrantLock
或ReentrantReadWriteLock
)并且锁必须选择一个线程才能访问关键部分时,它会选择等待时间最长的线程。考虑到前面解释的行为仅用于lock()
和unlock()
方法。由于如果使用了Lock
接口,tryLock()
方法不会使线程休眠,因此 fair 属性不会影响其功能。
在此配方中,我们将修改在同步代码块与锁配方中实现的示例,以使用此属性,并查看公平模式和非公平模式之间的差异。
我们将修改在同步代码块与锁配方中实现的示例,因此请阅读该配方以实现此示例。
按照以下步骤来实现该示例:
-
实现同步代码块与锁配方中解释的示例。
-
在
PrintQueue
类中,修改Lock
对象的构造。新指令如下所示:private Lock queueLock=new ReentrantLock(true);
-
修改
printJob()
方法。将打印模拟分为两块代码,释放它们之间的锁。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"); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } queueLock.lock(); try { Long duration=(long)(Math.random()*10000); System.out.println(Thread.currentThread().getName()+": PrintQueue: Printing a Job during "+(duration/1000)+" seconds"); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } }
-
在
Main
类中修改启动线程的代码块。新的代码块如下所示:for (int i=0; i<10; i++){ thread[i].start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }
在以下屏幕截图中,您可以看到此示例的一次执行的部分输出:
所有线程的创建时间相差 0.1 秒。请求锁控制的第一个线程是线程 0,然后是线程 1,依此类推。当线程 0正在运行受锁保护的第一个代码块时,我们有九个线程正在等待执行该代码块。当线程 0释放锁时,它会立即再次请求锁,因此我们有 10 个线程试图获取锁。由于启用了公平模式,Lock
接口将选择线程 1,因此该线程等待锁的时间更长。然后选择线程 2,然后选择线程 3等等。在所有线程都通过锁保护的第一个块之前,它们都不会执行锁保护的第二个块。
一旦所有线程都执行了锁保护的第一个代码块,就再次轮到线程 0。然后,轮到线程 1了,依此类推。
要查看与非公平模式的差异,请更改传递给锁构造函数的参数,并输入false
值。在以下屏幕截图中,您可以看到一次执行修改后的示例的结果:
在这种情况下,线程按照创建的顺序执行,但每个线程执行两个受保护的代码块。但是,这种行为不能得到保证,因为正如前面所解释的,锁可以选择任何线程来访问受保护的代码。在这种情况下,JVM 不能保证线程的执行顺序。
读/写锁的构造函数中也有 fair 参数。此参数在此类锁中的行为与我们在介绍此配方时解释的相同。
锁可能与一个或多个条件相关联。这些条件在Condition
接口中声明。这些条件的目的是允许线程控制锁,并检查条件是否为true
,如果是false
,则暂停,直到另一个线程唤醒它们。Condition
接口提供挂起线程和唤醒挂起线程的机制。
并发编程中的一个经典问题是生产者-消费者问题。我们有一个数据缓冲区,一个或多个生产者将数据保存在缓冲区中,一个或多个消费者将数据从缓冲区中取出,如本章前面所述
在本教程中,您将学习如何使用锁和条件实现生产者-消费者问题。
您应该阅读同步代码块与锁的配方,以便更好地理解此配方。
按照以下步骤来实现该示例:
-
首先,让我们实现一个模拟文本文件的类。创建一个名为
FileMock
的类,该类有两个属性:名为content
的String
数组和名为index
的int
数组。它们将存储文件的内容和将被检索的模拟文件的行。public class FileMock { private String content[]; private int index;
-
实现用随机字符初始化文件内容的类的构造函数。
public FileMock(int size, int length){ content=new String[size]; for (int i=0; i<size; i++){ StringBuilder buffer=new StringBuilder(length); for (int j=0; j<length; j++){ int indice=(int)Math.random()*255; buffer.append((char)indice); } content[i]=buffer.toString(); } index=0; }
-
实现方法
hasMoreLines()
,如果文件有更多行要处理,则返回true
,如果我们已经完成了模拟文件的结尾,则返回false
。public boolean hasMoreLines(){ return index<content.length; }
-
执行方法
getLine()
返回索引属性确定的行并增加其值。public String getLine(){ if (this.hasMoreLines()) { System.out.println("Mock: "+(content.length-index)); return content[index++]; } return null; }
-
现在,实现一个名为
Buffer
的类,该类将实现生产者和消费者共享的缓冲区。public class Buffer {
-
此类有六个属性:
-
一个名为
buffer
的LinkedList<String>
属性,用于存储共享数据 -
一个名为
maxSize
的int
类型,用于存储缓冲区的长度 -
一个名为
lock
的ReentrantLock
对象,控制对修改缓冲区的代码块的访问 -
两个名为
lines
和space
的Condition
属性 -
一个名为
pendingLines
的boolean
类型,指示缓冲区private LinkedList<String> buffer; private int maxSize; private ReentrantLock lock; private Condition lines; private Condition space; private boolean pendingLines;
中是否有行
-
-
实现类的构造函数。它初始化前面描述的所有属性。
public Buffer(int maxSize) { this.maxSize=maxSize; buffer=new LinkedList<>(); lock=new ReentrantLock(); lines=lock.newCondition(); space=lock.newCondition(); pendingLines=true; }
-
执行
insert()
方法。它接收String
作为参数,并尝试将其存储在缓冲区中。首先,它获得锁的控制权。当它拥有它时,它会检查缓冲区中是否有空的空间。如果缓冲区已满,则在space
条件下调用await()
方法等待空闲空间。当另一个线程调用空间Condition
中的signal()
或signalAll()
方法时,该线程将被唤醒。当这种情况发生时,线程将该行存储在缓冲区中,并通过lines
条件调用signallAll()
方法。稍后我们将看到,这种情况将唤醒所有在缓冲区中等待行的线程。public void insert(String line) { lock.lock(); try { while (buffer.size() == maxSize) { space.await(); } buffer.offer(line); System.out.printf("%s: Inserted Line: %d\n", Thread.currentThread().getName(),buffer.size()); lines.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }
-
执行
get()
方法。它返回存储在缓冲区中的第一个字符串。首先,它获得锁的控制权。当它拥有它时,它会检查缓冲区中是否有行。如果缓冲区为空,则在lines
条件下调用await()
方法等待缓冲区中的行。当另一个线程在 lines 条件下调用signal()
或signalAll()
方法时,该线程将被唤醒。当它发生时,该方法获取缓冲区中的第一个行,通过空格条件调用signalAll()
方法并返回String
。public String get() { String line=null; lock.lock(); try { while ((buffer.size() == 0) &&(hasPendingLines())) { lines.await(); } if (hasPendingLines()) { line = buffer.poll(); System.out.printf("%s: Line Readed: %d\n",Thread.currentThread().getName(),buffer.size()); space.signalAll(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } return line; }
-
实现
setPendingLines()
方法建立属性pendingLines
的值。当它没有更多的生产线时,它将被生产商调用。
```java
public void setPendingLines(boolean pendingLines) {
this.pendingLines=pendingLines;
}
```
- 执行
hasPendingLines()
方法。如果有更多行需要处理,则返回true
,否则返回false
。
```java
public boolean hasPendingLines() {
return pendingLines || buffer.size()>0;
}
```
- 现在轮到制片人了。实现一个名为
Producer
的类,并指定它实现Runnable
接口。
```java
public class Producer implements Runnable {
```
- 声明两个属性:
FileMock
类的一个对象和Buffer
类的另一个对象。
```java
private FileMock mock;
private Buffer buffer;
```
- 实现初始化这两个属性的类的构造函数。
```java
public Producer (FileMock mock, Buffer buffer){
this.mock=mock;
this.buffer=buffer;
}
```
- 实现读取
FileMock
对象中创建的所有行的run()
方法,并使用insert()
方法将它们存储在缓冲区中。一旦完成,使用方法setPendingLines()
提醒缓冲区它不会生成更多行。
```java
@Override
public void run() {
buffer.setPendingLines(true);
while (mock.hasMoreLines()){
String line=mock.getLine();
buffer.insert(line);
}
buffer.setPendingLines(false);
}
```
- 接下来轮到消费者了。实现一个名为
Consumer
的类,并指定它实现Runnable
接口。
```java
public class Consumer implements Runnable {
```
- 声明一个
Buffer
对象并实现初始化它的类的构造函数。
```java
private Buffer buffer;
public Consumer (Buffer buffer) {
this.buffer=buffer;
}
```
- 执行
run()
方法。当缓冲区有挂起的行时,它会尝试获取一行并处理它。
```java
@Override
public void run() {
while (buffer.hasPendingLines()) {
String line=buffer.get();
processLine(line);
}
}
```
- 执行辅助方法
processLine()
。它只休眠 10 毫秒,以模拟对该行的某种处理。
```java
private void processLine(String line) {
try {
Random random=new Random();
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 通过创建名为
Main
的类来实现示例的主类,并向其添加main()
方法。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个
FileMock
对象。
```java
FileMock mock=new FileMock(100, 10);
```
- 创建一个
Buffer
对象。
```java
Buffer buffer=new Buffer(20);
```
- 创建一个
Producer
对象并Thread
运行它。
```java
Producer producer=new Producer(mock, buffer);
Thread threadProducer=new Thread(producer,"Producer");
```
- 创建三个
Consumer
对象和三个线程来运行它。
```java
Consumer consumers[]=new Consumer[3];
Thread threadConsumers[]=new Thread[3];
for (int i=0; i<3; i++){
consumers[i]=new Consumer(buffer);
threadConsumers[i]=new Thread(consumers[i],"Consumer "+i);
}
```
- 启动生产者和三个消费者。
```java
threadProducer.start();
for (int i=0; i<3; i++){
threadConsumers[i].start();
}
```
所有的Condition
对象都与一个锁关联,并使用Lock
接口中声明的newCondition()
方法创建。在我们可以对条件执行任何操作之前,您必须控制与条件相关联的锁,因此,对条件的操作必须在一个代码块中,该代码块以对Lock
对象的lock()
方法的调用开始,以同一Lock
对象的unlock()
方法结束。
当一个线程调用一个条件的await()
方法时,它会自动释放对锁的控制,以便另一个线程可以获得它并开始执行相同的或受该锁保护的另一个关键部分。
当一个线程调用某个条件的signal()
或signallAll()
方法时,等待该条件的一个或所有线程都会被唤醒,但这并不保证使它们睡眠的条件现在是true
,因此必须将await()
调用放入while
循环中。在条件为true
之前,您不能离开该循环。当条件为false
时,您必须再次呼叫await()
。
您必须小心使用await()
和signal()
。如果在某个条件下调用await()
方法,而在此条件下从未调用signal()
方法,则线程将永远处于休眠状态。
调用await()
方法后,线程在睡眠时可能会被中断,因此您必须处理InterruptedException
异常。
Condition
接口有await()
方法的其他版本,如下所示:
await(long time, TimeUnit unit)
:线程将处于休眠状态,直到:- 中断了
- 另一个线程调用条件中的
singal()
或signalAll()
方法 - 指定的时间过去了
TimeUnit
类是具有以下常量的枚举:DAYS
、HOURS
、MICROSECONDS
、MILLISECONDS
、MINUTES
、NANOSECONDS
和SECONDS
awaitUninterruptibly()
:该线程将一直处于休眠状态,直到另一个线程调用signal()
或signalAll()
方法,这是无法中断的awaitUntil(Date date)
:线程将休眠,直到:- 中断了
- 另一个线程调用条件中的
singal()
或signalAll()
方法 - 指定的日期到达
您可以使用读/写锁的ReadLock
和WriteLock
锁的条件。