Java多线程 0x02

知识点

  • 使用synchronized实现同步方法
  • 使用非依赖属性实现同步
  • 在同步代码块中使用条件
  • 使用锁实现同步
  • 使用读写锁同步数据访问
  • 修改锁的公平性
  • 在锁中使用多条件

临界区(Critical Section)是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

Java提供了两种基本的同步机制

  • synchronized关键字机制
  • Lock接口及其实现机制


使用synchronized实现同步方法

如果一个对象已用synchronized关键字声明,那么只有一个执行线程被允许访问它。每一个用synchronized关键字声明的静态方法,同时只能被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法。
创建Account类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class  {
private double balance;

public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public void addAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp+=amount;
balance=tmp;
}

public void subtractAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp-=amount;
balance=tmp;
}
}

创建Bank类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bank implements Runnable {
private Account account;

public Bank(Account account) {
this.account=account;
}

public void run() {
for (int i=0; i<100; i++){
account.subtractAmount(1000);
}
}
}

创建Company类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Company implements Runnable {
private Account account;

public Company(Account account) {
this.account=account;
}

public void run() {
for (int i=0; i<100; i++){
account.addAmount(1000);
}
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) {
Account account=new Account();
account.setBalance(1000);
Company company=new Company(account);
Thread companyThread=new Thread(company);
Bank bank=new Bank(account);
Thread bankThread=new Thread(bank);

System.out.printf("Account : Initial Balance: %fn",account.getBalance());

companyThread.start();
bankThread.start();

try {
companyThread.join();
bankThread.join();
System.out.printf("Account : Final Balance: %fn",account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

以上代码演示了错误的场景
下面是改进的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class  {

private double balance;
public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public synchronized void addAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp+=amount;
balance=tmp;
}


public synchronized void subtractAmount(double amount) {
double tmp=balance;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
tmp-=amount;
balance=tmp;
}

}

synchronized关键字机制避免了这类错误的方法。
一个对象的方法采用synchronized进行声明,只能被一个线程访问。如果线程A正在执行一个同步方法syncMethodA(),线程B要执行这个对象的其他同步方法syncMethodB(),线程B将被阻塞直达线程A访问完。但如果线程B访问的是同一个类的不同对象,那么两个线程都不会被阻塞。
synchronized关键字降低了应用的性能,因此只能在并发情景中需要修改共享数据的方法上使用它。
可以递归调用被synchronized声明的方法。当线程访问一个对象的同步方法时,它还可以用这个对象的其他的同步方法,也包含正在执行的方法,而不必再次去获取这个方法的访问权。
可以通过synchronized关键字来保护代码块(而不是整个方法)的访问。应该利用synchronized关键字:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。临界区(即同一时间只能被一个线程访问的代码块)的访问应该尽可能的短。
通常来说,我们使用this关键字来引用正在执行的方法所属的对象。


使用非依赖属性实现同步

当使用synchronized来同步代码时,必须把对象引用作为参数传入。 通常情况下,使用this关键字来引用执行方法所属的对象。
下面模拟一个场景,有两个屏幕和两个售票处的电影院。
创建一个电影院类Cinema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Cinema{
private long vacanciesCinmea1;
private long vacanciesCinmea2;

private final Object controlCinema1, controlCinema2;

public Cinema(){
controlCinema1 = new Object();
controlCinema2 = new Object();
vacanciesCinema1 = 20;
vacanciesCinema2 = 20;
}

//第一个电影院卖票,使用controlCinema1对象控制同步代码块
public boolean sellTicket1(int number){
synchronized(controlCineam1){
if(number < vacanciesCinema1){
vacancieCinema1 -= number;
return true;
}else{
return false;
}
}
}

public boolean sellTicket2(int number){
synchronized(controlCinema2){
if(number < vacanciesCinema2){
vacanciesCinema2 -= number;
return true;
}else{
return false;
}
}
}

//第一个电影院退票,使用controlCinema1控制同步代码块
public boolean returnTickets1(int number){
synchronized(controlCinema1){
vacanciesCinema1 += number;
return true;
}
}

public boolean returnTicket2(int number){
synchronized(controlCinema2){
vacanciesCinema2 += number;
return true;
}
}

public long getVacanciesCinema1(){
return vacanciesCinema1;
}

public long getVacanciesCinema2(){
return vacanciesCinema2;
}
}

创建售票处类TicketOffice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TicketOffice1 implements Runnable{
private Cinema cinema;

public TicketOffice1(Cinema cinema){
this.cinema = cinema;
}


public void run(){
cinema.sellTickets1(3);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTicket1(3);
cinema.sellTickets1(5);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
cinema.sellTickets2(2);
}
}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TicketOffice2 implements Runnable{
private Cinema cinema;

public TicketOffice2(Cinema cinema){
this.cinema = cinema;
}


public void run(){
cinema.sellTickets2(2);
cinema.sellTickets2(4);
cinema.sellTickets1(2);
cinema.sellTickets1(1);
cinema.returnTickets2(2);
cinema.sellTicket1(3);
cinema.sellTicket2(2);
cinema.sellTicket1(2);
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main{
public static void main(String[] args){
Cinema cinema = new Cinema();

TicketOffice1 ticketOffice1 = new TicketOffice1(cinema);
Thread thread1 = new Thread(ticketOffice1, "TicketOffice1");

TicketOffice2 ticketOffice2 = new TicketOffice2(cinema);
Thread thread2 = new Thread(ticketOffice2, "TicketOffice2");

thread1.start();
thread2.start();

try{
thread1.join();
thread2.join();
}catch(InterruptedException e){
e.printStackTrace();
}

System.out.printf("Room 1 Vacancies: %dn", cinema.getVacanciesCinema1());
System.out.printf("Room 2 Vacancies: %dn", cinema.getVacanciesCinema2());
}
}

用synchronized同步代码块,JVM保证同一时间只有一个线程能够访问这个对象的代码保护块(对象,不是类)。
上面这个例子,使用一个controlCinema来控制对vacanciesCinema属性的访问,所以同一时刻只有一个线程能够修改这个属性。vacanciesCinema1和vacanciesCinema2有分别的control对象,所以允许同时运行两个线程,一个修改vacanciesCinema1,另一个修改vacanciesCinema2。


在同步代码中使用条件

在并发编程中一个典型的问题是生产者-消费者问题。
Java在Object类中提供了wait()、notify()和notifyAll()方法。 线程可以在同步代码块中调用wait()方法。如果在同步代码块外调用wait(),JVM将抛出IllegalMonitorStateException异常。
当一个线程调用wait()方法后,JVM将这个线程置入休眠,并释放控制这个同步代码块的对象,同时允许其他线程执行这个对象控制的其他同步代码块。 为了唤醒这个线程,必须在这个对象控制的某个同步代码块中调用notify()或者notifyAll()方法。
下面结合例子学习下生产者-消费者问题。
创建EventStorage类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class EventStorage{
private int maxSize;
private List<Data> storage;

public EventStorage(){
maxSize = 10;
storage = new LinkedList<>();
}

public synchronized void set(){
while(storage.size() == maxSize){
try{
wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
storage.add(new Date());
System.out.printf("Set: %d", storage.size());
notify();
}

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());
notify();
}
}

创建生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Producer implements Runnable{
private EventStorage storage;

public Producer(EventStorage storage){
this.storage = storage;
}


public void run(){
for(int i = 0; i < 100; i++){
storage.set();
}
}
}

创建消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Consumer implements Runnable{
private EventStorage storage;

public Consumer(EventStorage storage){
this.storage = storage;
}


public void run(){
for(int i = 0; i < 100; i++){
storage.get();
}
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main{
public static void main(String[] args){
EventStorage storage = new EventStorage();

Producer producer = new Producer(storage);
Thread thread1 = new Thread(producer);

Consumer consumer = new Consumer(storage);
Thread thread2 = new Thread(consumer);

thread2.start();
thread1.start();
}
}

有了wait()和notify()机制,两个线程之间就有了通信。
其次,当其他线程调用notifyAll()方法时,挂起的线程将被唤醒并且再次检查条件。但notifyAll()并不保证哪个线程会被唤醒。



使用锁实现同步

Java提供了同步代码块的另一种机制,它比synchronized更强大也更灵活。
这种机制基于Lock接口及其实现类(例如ReentrantLock),提供了更多的好处。

  • 支持更灵活的同步代码块结构。使用synchronized只能在同一个synchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构
  • 相比synchronized关键字,Lock接口提供了更多的功能。其中一个新功能是tryLock()方法的实现。这个方法试图获取锁,如果锁一杯其他线程获取,它将返回false并继续往下执行代码。使用锁的tryLock()方法, 通过返回值将得知是否有其他线程正在使用这个锁保护的代码块。
  • Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。
  • 相比synchronized,Lock接口具有更好的性能。
    好了,开始学习下ReentrantLock——Lock接口的实现类。
    创建PrintQueue类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class PrintQueue{
    private final Lock queueLock = new ReentrantLock();

    public void printJob(Object document){
    queueLock.lock();

    try{
    Long duration = (long) (Math.random() * 10000);
    System.out.printf("%s : PrintQueue: Printing a Job during %d secondsn", Thread.currentThread().getName(), (duration/1000));
    }catch(InterruptedException e){
    e.printStackTrace();
    }finally{
    queueLock.unlock();
    }
    }
    }

创建Job类并实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Job implements Runnable{
private PrintQueue printQueue;

public Job(PrintQueue printQueue){
this.printQueue = printQueue;
}


public void run(){
System.out.printf("%s: Going to print a documentn",Thread.currentThread().getName());
printQueue.printJob(new Object());
System.out.printf("%s: The document has been printedn",Thread.currentThread().getName());
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main(){
public static void main(String[] args){
PrintQueue printQueue = new PrintQueue();

Thread thread[] = new Thread[10];
for(int i = 0; i < 10; i++){
thread[i] = new Thread(new Job(printQueue), "Thread " + i);
}

for(int i = 0; i < 10; i++){
thread[i].start();
}
}
}

在这个临界区的开始,必须通过lock()方法获取对锁的控制。当线程A访问这个方法时,如果没有其他线程获取这个锁的控制,lock()方法将让线程A获取锁并且允许它立即执行临界区代码。否则,如果其他线程B正在执行这个锁保护的临界区代码,lock()方法将让线程A休眠直到线程B执行完临界区的代码。
在线程离开临界区的时候,必须使用unlock()方法来释放它持有的锁,以让其他线程来访问临界区。如果离开临界区的时候没有调用unlock()方法,其他线程将永久地等待,从而导致死锁(Deadlock)。
如果临界区使用了try-catch块,不要忘记将unlock()放入finally里。
Lock接口还提供了额另一个方法来获取锁,即tryLock(),跟lock()方法最大的不同是:线程使用tryLock()不能获取锁,tryLock()会立即返回,它不会讲线程置入休眠。tryLock()方法返回一个布尔值,true表示线程获取了锁,false表示没有获取锁。
ReentrantLock类也允许使用递归调用。如果一个线程获取了锁并且进行了递归调用,它将继续持有这个锁,因此调用lock()方法后也将立即返回,并且线程将继续执行递归调用。


使用读写锁实现同步数据访问

锁机制最大的改进之一就是ReadWriteLock接口和它的唯一实现类ReentrantReadWriteLock。
这个类有两个锁,一个是读操作锁,另一是写操作锁。
使用读操作锁可以允许多个线程同时访问,但是写操作锁只允许一个线程进行。
创建一PricesInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PricesInfo{
private double price1;
private double price2;

private ReadWriteLock lock;

public PricesInfo(){
price1 = 1.0;
price2 = 2.0;
lock = new ReentrantReadWriteLock();
}

public double getPrice1(){
lock.readLock().lock();
double value = price1;
lock.readLock().unlock();
return value;
}

public double getPrice2(){
lock.readLock().lock();
double value = price2;
lock.readLock().unlock();
return value;
}

public void setPrices(double price1, double price2){
lock.writeLock().lock();
this.price1 = price1;
this.price2 = price2;
lock.writeLock().unlock();
}
}

创建Reader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Reader implements Runnable{
private PricesInfo pricesInfo;

public Reader(PricesInfo pricesInfo){
this.pricesInfo = pricesInfo;
}

@Override
public void run(){
for(int i = 0 ; i < 10 ; i++){
System.out.printf("%s: Price 1: %fn",Thread.currentThread().getName(),pricesInfo.getPrice1());
System.out.printf("%s: Price 2: %fn",Thread.currentThread().getName(),pricesInfo.getPrice2());
}
}
}

创建Writer类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Writer implements Runnable{
private PricesInfo pricesInfo;

public Writer(PricesInfo pricesInfo){
this.pricesInfo = pricesInfo;
}

@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();
}
}
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main{
public static void main(String[] args){
PricesInfo pricesInfo = new PricesInfo();

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 writer = new Writer(pricesInfo);
Thread threadWriter = new Thread(writer);

for(int i = 0 ;i < 5; i++){
threadsReader[i].start();
}
threadWriter.start();
}
}



修改锁的公平性

ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数fair,它允许控制这两个类的行为。
默认fair是false,它称为非公平模式(Non-Fair Mode)。
在非公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,这个选择是灭有任何约束的。
fair是true,公平模式(Fair Mode),当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,而且选择的是等待时间最长的。
这两种模式只适合于lock()和unlock方法。而Lock接口的tryLcok()方法没有将线程置于休眠,fair属性并不影响这个方法。

在锁中使用多条件(Multiple Condition)

一个锁可能关联一个或者多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起直到某个线程唤醒它们。
Condition接口提供了挂起线程和唤起线程的机制。
还是以生产者消费者为例
创建FileMock类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 indict = (int)Math.random()*255;
buffer.append((char) indice);
}
content[i] = buffer.toString();
}
}

public boolean hasMoreLines(){
return index < content.length;
}

public String getLine(){
if(this.hasMoreLines()){
System.out.println("Mock: " + (content.length-index));
return content[index++];
}
return null;
}
}

实现数据缓冲类Buffer,它将被生产者和消费者共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class Buffer{
//存放共享数据
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;
}

public void insert(String line){
lock.lock();
try{
//缓冲区是否还有空位
while(buffer.size() == maxSize){
space.await(); //当space的signal()或singalAll()时会被唤醒
}
buffer.offer(line);
System.out.printf("%s: Inserted Line: %dn", Thread.currentThread()
.getName(), buffer.size());
lines.signalAll(); //唤醒等待缓冲区中有数据的线程
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}

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: %dn",Thread.currentThread().getName(),buffer.size());
space.signalAll();
}
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
return line;
}

public void setPendingLines(boolean pendingLines){
this.pendingLines = pendingLines;
}

public boolean hasPendingLines(){
return pendingLines || buffer.size() > 0;
}
}

消费者类Consumer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Consumer implements Runnable{
private Buffer buffer;

public Consumer(Buffer buffer){
this.buffer = buffer;
}

@Override
public void run(){
while(buffer.hasPendingLines()){
String line = buffer.get();
processLine(line);
}
}

private void processLine(String line){
try{
Random random = new Random();
Thread.sleep(random.nextInt(100));
}catch(InterruptedException e){
e.printStackTrace();
}
}
}

生成者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Producer implements Runnable{
private FileMock mock;
private Buffer buffer;

public Producer(FileMock mock, Buffer buffer){
this.mock = mock;
this.buffer = buffer;
}

@Override
public void run(){
buffer.setPendingLines(true);
while(mock.hasMoreLines()){
String line = mock.getLine();
buffer.insert(line);
}
buffer.setPendingLines(false);
}
}

主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main{
public static void main(String[] args){
FileMock mock = new FileMock(101, 10);

Buffer buffer = new Buffer(20);

Producer producer = new Producer(mock, buffer);
Thread threadProducer = new Thread(producer, "Producer");

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(consummers[i], "Consumser " + i);
}

threadProducer.start();
for(int i = 0; i < 3; i++){
threadConsumers[i].start();
}
}
}

与锁绑定的所有条件对象都是通过Lock接口声明的newCondition()方法创建的。
在使用条件的时候,必须获取这个条件绑定的锁。
当线程调用条件的await()方法时,它将自动释放这个条件绑定的锁,其他某个线程才可以获取这个锁并且执行相同的操作,或者执行这个锁保护的另一个临界区代码。
当一个线程调用了条件对象的signal()或者signalAll()方法后,一个或者多个在该条件上挂起的线程将被唤醒,但这并不能保证让它们挂起的条件已经满足,所以必须在while循环中调用await(),在条件成立之前不能离开这个循环,如果条件不成立,将再次调用await()。
因调用await()方法的线程可能会被中断,所以必须处理InterruptException异常。


Condition接口还提供了await()方法的其他形式
await(long time, TimeUnit unit):直到发生以下情况之一前,线程将一直处于休眠状态

  • 其他某个线程中断当前线程
  • 其他某个线程调用了将当前线程挂起的条件的signal()或signalAll()方法。
  • 指定的等待时间已经过去。
  • 通过TimeUnit类的常量DAYS HOURS MiCROSECONDS MILLISECONDS MINUTES ANOSECONDS SECONDS指定的等待时间已经过去


    awaitUniterruptibly():它是不能中断的。这个线程将休眠知道其他某个线程调用了将它挂起的条件的signal()或signalAll()方法。
    awaitUnitl(Date date):直到发生以下情况之一之前,线程将一直处于休眠状态
  • 其他某个线程中断当前线程
  • 其他某个线程调用了将它挂起的条件的signal()或signalAll()
  • 指定的最后期限到了

也可以将条件与读写锁ReadLock和WriteLock一起用。

原文:大专栏  Java多线程 0x02


猜你喜欢

转载自www.cnblogs.com/petewell/p/11601535.html