两种基本的同步机制:synchronized与锁
1. 同步方法
每一个被synchronized方法就是一个关键代码段,在同一时间,Java只能执行某个对象的其中一个关键代码块。然而,静态方法的行为不同,只有一个执行线程能够访问所有synchronized的静态方法中的一个,但是另一个线程可以访问该类的某个对象的非静态方法。也就是说,如果有两个同步方法,一个是静态的,另一个是非静态的,则两个线程是可以同时分别访问两个方法的。这一原则有可能导致两个方法去修改同一数据。例如,
class Account {
public static double balance;
public static synchronized void addAmount(double amount) {
balance += amount;
}
public synchronized void substractAmount(double amount) {
balance -= amount;
}
}
2. 不同属性用不同对象同步
通常地,this关键字会被用来作为同步代码块的控制变量,但是也可以用其他的对象引用作为同步代码块的关键字。例如,用两个对象类分别作为某个类的两个属性的同步控制变量,这样两个线程可以同时独立地访问两个属性。
class Cinema {
private long vacanciesCinema1;
private long vacanciesCinema2;
private final Object controlCinema1;
private final Object controlCinema2;
public Cinema() {
controlCinema1 = new Object();
controlCinema2 = new Object();
vacanciesCinema1 = 20;
vacanciesCinema2 = 20;
}
public boolean sellTickets1(long number) {
synchronized (controlCinema1) {
if (number <= vacanciesCinema1) {
vacanciesCinema1 -= number;
return true;
}
else {
return false;
}
}
}
public boolean sellTickets2(long number) {
synchronized (controlCinema2) {
if (number <= vacanciesCinema2) {
vacanciesCinema2 -= number;
return true;
}
else {
return false;
}
}
}
public long getVacanciesCinema1() {
return vacanciesCinema1;
}
public long getVacanciesCinema2() {
return vacanciesCinema2;
}
}
3. wait/notify/notifyAll
在生产者-消费者模型中,通常用一个buffer作为共享的数据结构,当buffer满的时候,生产者不能再放入数据;当buffer空的时候,消费者不能取数据。对于以上两种情况,java在Object类中提供了wait notify notifyAll方法。wait方法需要在同步代码块中被调用,否则,将会抛出IllegalMonitorStateException。 当被调用时,JVM将会使当前线程sleep,同时释放控制同步代码块的对象锁,必须使用nofity或者notifyAll来唤醒该线程。
class EventStorage {
int maxSize;
LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void set() {
while (storage.size() == maxSize) {
try {
wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.offer(new Date());
System.out.printf("Set: %d\n", storage.size());
notifyAll();
}
public synchronized void get() {
while (storage.size() == 0) {
try {
wait();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.poll();
System.out.printf("Get: %d\n", storage.size());
notifyAll();
}
}
4. Lock
Java提供了另一种同步代码块的机制,它是基于Lock接口和它的实现类(例如ReentrantLock)来实现的,这种机制更加强大和灵活,主要的优点表现在:
1)Lock接口允许更加复杂的结构,synchronized关键字必须要用结构化的方法来获取或者释放同步代码块;
2)Lock接口提供了一些额外的功能。例如tryLock()方法。
3)当只有一个写和多个读的线程时,Lock接口允许读写操作的分离
4)Lock接口的性能更高
1)Lock接口允许更加复杂的结构,synchronized关键字必须要用结构化的方法来获取或者释放同步代码块;
2)Lock接口提供了一些额外的功能。例如tryLock()方法。
3)当只有一个写和多个读的线程时,Lock接口允许读写操作的分离
4)Lock接口的性能更高
class PrintQueue {
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");
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
queueLock.unlock();
}
}
}
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());
}
}
5. 读写锁
ReadWriteLock是所有Lock接口中最重要的接口之一,ReentrentReadWriteLock是它的唯一实现类。该类有两个锁,一个是读操作另一个是写操作。它能够同时包括多个读操作,但是只能有一个写操作。当某个线程执行写操作时,其他任何线程都不能执行读操作。
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();
}
}
6. 公平锁
ReentrantLock和ReentrantReadWriteLock的构造函数可以传入一个boolean型的参数fair,该参数默认为false,如果设置为true,则在多个线程等待同一个锁时,选择等待时间最长的线程优先执行。
class MessageQueue implements MyQueue {
private Lock queueLock = new ReentrantLock(true);
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();
}
}
}
7. 一个锁的多个condition
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; //是否还会有lines insert到Buffer中
}
public void insert(String line) {
lock.lock();
try {
while(buffer.size() == maxSize) {
space.wait();
}
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();
}
}
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;
}
public boolean hasPendingLines() {
return pendingLines || buffer.size() > 0;
}
public void setPendingLines(boolean pendingLines) {
this.pendingLines = pendingLines;
}
}