线程同步
线程同步用于协调相互依赖的线程的执行。如果一个共享资源被多个线程同时访问,可能会遭到破坏。假设创建并启动100个线程,每个线程都往同一个账户中添加一元钱。
package edu.uestc.avatar; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class AccountWithoutSync { //临界资源 private static Account account = new Account(); private static class Account{ private int balance = 0; public int getBalance() { return balance; } public void deposit(int amount) { //为了故意放大数据破坏的可能性,采用下列语句,其实可用 balance += amount;代替 int newBalance = balance + amount; try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } balance = newBalance; } } /** * 该任务向账户存入指定金额 * */ private static class AddPenyTask implements Runnable{ @Override public void run() { account.deposit(1); } } public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); for(int i = 0; i < 100; i++) executor.execute(new AddPenyTask()); executor.shutdown();//关闭执行器,不再接收新的任务 //等待线程池中所有的任务完毕 while(!executor.isTerminated()) {} System.out.println("账户余额:" + account.getBalance()); } }
运行结果:
那么,究竟是什么导致了程序的错误?下面给出了一个可能的场景
在步骤1中,任务1从账户中获取余额数目。在步骤2中,任务2从账户中获取同样数目的余额。在步骤3中,任务1向账户写入一个新余额。在步骤4中,任务2也向该账户写入一个新余额。
这个场景的效果就是任务1什么都没做,因为在步骤4中,任务4覆盖了任务1的结果。很明显,问题是任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程中的一个普遍问题,称为竞争状态(race condition)。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的(thread-safe).
Synchronized关键字
为避免竞争状态,应该防止多个线程同时进入程序的某一特定部分,程序中的这部分称为临界区(critical region)。上面程序中的临界区为整个deposit方法。可以使用关键字synchronized来同步方法,以便一次只有一个线程可以访问这个方法。
一个同步方法在执行之前需要加锁。锁是一种资源排他机制。对于实例方法,要给调用该方法的对象加锁。对于静态方法,需要给这个类加锁。如果一个线程调用一个对象上的同步实例方法(静态方法),首先给该对象(类)加锁,然后执行该方法,最后释放锁。在释放锁之前,其它调用该方法的线程将被阻塞,直到释放锁。
package edu.uestc.avatar; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AccountWithSync { //临界资源 private static Account account = new Account(); private static class Account{ private static Lock lock = new ReentrantLock(); private int balance = 0; public int getBalance() { return balance; } /** * */ public void deposit(int amount) { lock.lock();//获取锁 try { //可以使用balance += amount; int newBalance = balance + amount; Thread.sleep(5); balance = newBalance; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock();//释放锁 } } } /** * 该任务向账户存入指定金额 * */ private static class AddPenyTask implements Runnable{ @Override public void run() { account.deposit(1); } } public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); for(int i = 0; i < 100; i++) executor.execute(new AddPenyTask()); executor.shutdown();//关闭执行器,不再接收新的任务 //等待线程池中所有的任务完毕 while(!executor.isTerminated()) {} System.out.println("账户余额:" + account.getBalance()); } }
同步代码块
同步代码块允许设置同步方法中的部分代码,而不必是整个方法。这大大增加了程序的并发能力
package edu.uestc.monster; public class CustomThreadTest { public static void main(String[] args) { //创建10个线程,代表10个售票窗口 for(var i = 0; i < 10; i++) { var thread = new CustomThread("窗口-" + (i + 1)); thread.start();//启动线程 } } } /** * 由于Thread类实现了Runnable接口, * 所以可以定义一个类扩展自Thread类,并且覆盖run方法。 * */ class CustomThread extends Thread{ private static int ticket = 100;//共享资源,存在线程安全问题 public CustomThread() {} public CustomThread(String name) { super(name);//Thread(String):指定线程名称 } @Override public void run() { try { while(true) { synchronized (CustomThread.class) { if(ticket <= 0) break; //Thread.currentThread():获取当前运行的线程 System.out.println(Thread.currentThread().getName() + "正在售出编号为 " + ticket-- + " 的火车票"); Thread.sleep(10);//模拟销售一张火车票的时间为1秒钟 } } } catch (InterruptedException e) { e.printStackTrace(); } } }
利用加锁同步
基于synchronized关键字的锁机制有以下问题:
- 锁只有一种类型,而且对所有同步操作都是一样的作用
- 锁只能在代码块或方法开始的地方获得,在结束的地方释放
- 线程要么得到锁,要么阻塞,没有其他的可能性
Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:
- 可以添加不同类型的锁,例如读取锁和写入锁
- 可以在一个方法中加锁,在另一个方法中解锁
- 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作
显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:
- ReentrantLock - 比synchronized稍微灵活一些的重入锁
- ReentrantReadWriteLock - 在读操作很多写操作很少时性能更好的一种重入锁
package edu.uestc.avatar; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class AccountWithLock { //临界资源 private static Account account = new Account(); private static class Account{ private int balance = 0; public int getBalance() { return balance; } /** * 临界区:整个deposit * synchronized防止多个线程同时今日临界区——一次只有一个线程可以访问该方法 * @param amount 金额 * */ public synchronized void deposit(int amount) { //可以使用balance += amount; int newBalance = balance + amount; try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } balance = newBalance; } } /** * 该任务向账户存入指定金额 * */ private static class AddPenyTask implements Runnable{ @Override public void run() { account.deposit(1); } } public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); for(int i = 0; i < 100; i++) executor.execute(new AddPenyTask()); executor.shutdown();//关闭执行器,不再接收新的任务 //等待线程池中所有的任务完毕 while(!executor.isTerminated()) {} System.out.println("账户余额:" + account.getBalance()); } }
注意:解锁的方法unlock的调用最好能够在finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。
线程间通信
锁上的条件可用于协调线程间的通信。一个线程可以指定在某种条件下该做什么。条件是通过调用Lock对象的newCondition()方法而创建的对象。一旦创建了条件,就可以使用await()、signal()和signalAll()方法来实现线程之间的通信。
package edu.uestc.avatar; import java.util.concurrent.Executors; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 线程间相互协作——通过条件实现线程间的通信 * 条件是通过调用Lock对象的newCondition()方法创建的对象 * await():让当前线程进入等待,直到条件发生 * signal():唤醒一个等待的线程 * signalAll():唤醒所有等待的线程 * * 启动两个任务:一个任务向账户中存钱,另一个任务向账户中提款。当取钱金额大于账户余额,提款线程必须等待。 * 不管什么时候,只要向账户中存入一笔钱,存钱线程必须通知提款线程重新尝试 * * 使用一个有条件的锁 newDeposit 。一个条件对应一个锁。在等待和通知状态之前,线程必须先获取该条件的锁。 * */ public class ThreadCooperation { private static Account account = new Account(); private static class Account{ //创建锁 private static Lock lock = new ReentrantLock(); //创建条件 private static Condition newDeposit = lock.newCondition(); //余额 private int balance = 0; public void withdraw(int amount) { lock.lock(); try { while(balance < amount) {//当余额小于取款金额,必须等待存款任务条件的发生 System.out.println("等待存款任务的完成....."); newDeposit.await(); } balance -= amount; System.out.println("取款完成,取款金额:" + amount); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void deposit(int amount) { lock.lock(); try { balance += amount; System.out.println("存入一笔金额:" + amount); newDeposit.signalAll(); } finally { lock.unlock(); } } /* * public int getBalance() { return balance; } */ } /** * 存款任务 * */ private static class DepositTask implements Runnable{ @Override public void run() { try { while(true) { account.deposit((int)(Math.random() * 100) + 1); Thread.sleep(5); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static class WithdrawTask implements Runnable{ @Override public void run() { while(true) { account.withdraw((int)(Math.random() * 100) + 1); } } } public static void main(String[] args) { var executor = Executors.newFixedThreadPool(2); executor.execute(new WithdrawTask()); executor.execute(new DepositTask()); executor.shutdown(); } }
传统的线程通信方式
锁和条件是JDK 5中的新内容。在jdk 5之前,线程通信是使用对象的内置监视器实现的。锁和条件比内置监视器更加强大和灵活,因此无须使用内置监视器。然而,如果使用jdk 5.0以前的遗留代码,可能会使用到内置监视器。
监视器(monitor)是一个相互排斥且具备同步能力的对象。监视器中的同一个时间节点上,只能有一个线程执行一个方法。线程通过获取监视器上的锁进入监视器,并且通过释放锁退出监视器。任意对象都可以是一个监视器。一旦一个线程锁住对象,该对象就称为监视器。加锁是通过在方法上或者代码块上使用synchronized关键字来实现的。在执行同步方法或者代码块之前,线程必须先获取锁。如果条件不适合线程继续在监视器内执行,线程可能在监视器中等待。可以对监视器对象调用wait()方法来释放锁,这样其他的一些监视器中的线程就可以获取它,也就有可能改变监视器中的状态。当条件合适的时候,另一些线程可以调用notify()或notifyAll()方法来通知一个或所有的等待的线程重新获取锁并且恢复执行
package edu.uestc.canary.concurrent; import java.util.concurrent.Executors; public class TraditionalCooperation { private static Account account = new Account(); public static class Account{ private int balance; public synchronized void withdraw(int amount) { try { while(amount > balance) { System.out.println("余额不足,终止当前线程并且释放对象的锁"); wait(); //必须先获取锁(在同步代码块或者方法中进行调用,否则IllegalMonitorStateException) } balance -= amount; System.out.println("取款金额:" + amount + "当前余额:" + balance); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void deposit(int amount) { balance += amount; System.out.println("存款金额:" + amount + "当前余额:" + balance); notify();//通知一个等待的线程重新获取锁并恢复执行 } /** * 取款任务 * */ private static class WithdrawTask implements Runnable{ @Override public void run() { while(true) { int amount = (int)(Math.random() * 10000 + 1); account.withdraw(amount); } } } private static class DepositTask implements Runnable{ @Override public void run() { try { while(true) { int amount = (int)(Math.random() * 10000 + 1); account.deposit(amount); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { var executor = Executors.newFixedThreadPool(2); executor.execute(new DepositTask()); executor.execute(new WithdrawTask()); executor.shutdown(); } } }
生产者/消费者(另一篇)
CustomThread.class