知识点
使用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 ; } 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 ; } } } 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(); } 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