可以同时运行一个以上的程序称为多线程。
多线程或多进程的本质区别再议每一个进程拥有自己的一整套变量,而线程则共享数据。然而,共享变量使线程之间的通信比进程之间的通信更有效,更容易。而且,与进程相比。限次恒更轻,开销更小。
创建线程
第一种
- 将任务代码移动到实现了 Runnable 接口的类的 run 方法。
public interface Runnable{ void run();} # 也可以使用 lambda 表达式 RUnnable r = () ->{...};
- 由 Runnable 创建一个 Thread 对象
Thread t = new Thread(r);
- 启动线程
t.start();
第二种
通过构建一个 Thread 的子类定义一个线程
public MyThread extends Thread{
pubic void run(){
...
}
}
Tips:
不要调用 Thread 类或 Runnable 对象的 run方法。直接调用 run 方法,只会执行 同一个线程中的任务,而不会启动新线程。调用 Thread.start 方法将会创建一个执行 run 方法的新线程。如果有很多线程,更建议使用线程蹭池。
中断线程
在 Java 的早期版本中,有一个 stop 方法,可以用来终止线程。但是该方法现在已经弃用。现在没有可以强制线程终止的方法。但是 interrupt 方法可以用来请求终止线程。
当对一个线程调用 interrupt 方法,线程的中断状态将被置位。这是每一个线程就具有的 boolean 标志。每个线程对应该不时检查这个标志,以判断线程是否被中断。
判断中断状态是否被置位,需要调用 Thread.currentThread 方法获取当前的线程,然后低啊用 isInterrupted 方法。
while(!Thread.currentThread().isInterrupted() && more work to do){
do more work
}
但是,如果线程被阻塞,就无法检测中断状态。当在一个被阻塞的进程(调用 sleep 或 wait)上调用 interrupt 方法,阻塞调用将会被 Interrupted Exception 异常中断。
处理 InterruptedException 建议:
- 在 catch 子句中调用 Thread.currentThread().interrupt(0 来设置中断状态
- 用 throws InterruptedException 标记你的方法,不采用 try 语句块捕获异常
线程状态
线程具有以下六种状态
- New (新创建)
- Runnable (可运行)
- Blocked (被阻塞)
- Waiting (等待)
- Timed waiting (计时等待)
- Terminated (被终止)
新建线程
当使用 new 操作符创建一个新线程时,该线程还没有开始运行。在线程运行之前还有一些基础工作要做。
可运行线程
一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行,也可能没有运行,这取决于操作系统给线程提供运行的时间。
被阻塞线程和等待线程
当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗做少的资源,知道线程调动器重新激活它。细节取决于它是怎么达到非活动状态的。
- 当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrent 中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许该线程持有它的时候,该线程变成非阻塞状态。
- 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调度 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 Lock 或 Condition 时,就会出现这种情况。
- 有几个方法有一个超时参数。调用他们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait , Thread.join, Lock.tryLock 以及 Condition.await的计时版。
被终止的线程
线程因如下两个原因之一而被终止:
- 因为 run 方法正常退出而自然死亡
- 因为一个没有捕获的异常终止了 run 方法而意外死亡
线程属性
线程优先级
每一个线程都有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以使用 setPriority 方法提高或降低任何一个线程的优先级(在 MIN_PRIORITY 1 到 MAX_PRIORITY 10 之间)。
守护线程
t.setDaemon(true);
将线程转换为守护线程(daemon thread)。守护线程的唯一用途是为其他线程提供服务。
守护线程应该永远不要访问固有资源,如文件,数据库,因为他们会在任何时间甚至一个操作的额中间发生中断。
未捕获异常处理器
线程的 run 方法不能抛出任何受查异常,但是,非受查异常会导致线程终止。这种情况下,线程就死亡了。
但是,就在线程死亡之前,异常被传递到一个用与未捕获异常的处理器。
该处理器必须属于一个实现了 Thread.UncatchExceptionHandler 接口的类,这个接口只有一个方法。
void uncatchException(Thread t, Throwable e)
可以用 setUncatchExceptionHandler 方法为任何一个线程安装一个处理器。也可以使用 Thread 类的静态方法 setDefaultUncatchExceptionHandler 为所有线程安装一个默认处理器。(替换处理器可以使用日志 APAI 发送未捕获异常的报告到日志文件)
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。
Tips:
线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会创建其他的组。但是不建议在自己的程序中使用线程组。
ThreadGroup 类实现了 Thread.UncatchExceptionHandler 接口。它的 uncatchException 方法做如下操作:
- 如果该线程组有父线程组,那么父线程组的 uncatchException 方法被调用
- 否则,如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器,则调用该处理器
- 否则,如果 Throwable 是 ThreadDeath 的一个实例,什么都不做
- 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err上
同步
两个或两个以上的线程在某些时候需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生什么?你争我抢?根据线程访问的次序,可能会产生错误的对象。这样的情况称为竞争条件。
Bank.java
public class Bank {
private final double[] accounts;
public Bank(int n, double initalBalance) {
accounts = new double[n];
Arrays.fill(accounts, initalBalance);
}
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) return;
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
}
public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
UnsynchBankTest.java
public class UnsynchBankTest {
public static final int NACCOUNT = 100;
public static final double INITAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNT, INITAL_BALANCE);
for (int i = 0; i < NACCOUNT; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
}
};
Thread t = new Thread(r);
t.start();
}
}
}
...
Thread[Thread-83,5,main] 26.01 from 83 to 2Total Balance: 100000.00
Thread[Thread-52,5,main] 612.92 from 52 to 42Total Balance: 100000.00
Thread[Thread-66,5,main] 724.95 from 66 to 54Total Balance: 100000.00
Thread[Thread-29,5,main] 296.26 from 29 to 34Total Balance: 100000.00
....
Thread[Thread-4,5,main]Thread[Thread-86,5,main] 983.41 from 86 to 74Total Balance: 99690.46
Thread[Thread-9,5,main] 68.68 from 9 to 50Total Balance: 99690.46
Thread[Thread-79,5,main] 292.29 from 79 to 14Total Balance: 99690.46
Thread[Thread-2,5,main] 774.18 from 2 to 66Total Balance: 99690.46
Thread[Thread-44,5,main] 563.00 from 44 to 60Total Balance: 99690.46
Thread[Thread-7,5,main] 842.83 from 7 to 61Total Balance: 99690.46
Thread[Thread-36,5,main] 117.68 from 36 to 80Total Balance: 99690.46
Thread[Thread-26,5,main] 107.12 from 26 to 77Total Balance: 99690.46
...
309.54 from 4 to 3Total Balance: 100000.00
Thread[Thread-71,5,main] 235.53 from 71 to 83Total Balance: 100000.00
Thread[Thread-33,5,main] 309.09 from 33 to 31Total Balance: 100000.00
Thread[Thread-92,5,main] 299.54 from 92 to 80Total Balance: 100000.00
Thread[Thread-99,5,main] 429.22 from 99 to 49Total Balance: 100000.00
在最初的交易中,银行的余额保持在 100000,这是因为总共 100 个账户,每个账户 1000。但是过了一段时间后,余额总量发生变化。
产生错误的原因在于修改对象的操作不是原子操作,修改对象的方法在执行过程中可能被中断。如果能够确保线程在失去控制之前方法运行完成,那么就不会产生错误的对象。
锁对象
有两种机制防止代码快受到并发访问的干扰。Java 提供了一个 synchronized 关键字来达到这一目的,并且在 JavaSE 5.0 后引入了 ReentrantLock类。
使用 ReentrantLock 保护代码块的的基本结构如下:
myLock.locke();
try{
critical section
}finally{
myLock.unlock();
}
这种结构确保任何时可只有一个线程进入临界区。
Tips:解锁操作必须放在 finally 中,确保解锁操作被执行。
条件对象
通常,线程进入临界区,却发现在某一条件满足后才能执行。要使用一个条件对象来管理那些已经获得了一个锁但不能做有用工作的线程。
细化银行的模拟程序。为了避免选择没有足够资金的账户作为转出账户,我们需要进行一些判断处理。但是,不能使用下面的代码
if (accounts[from] >= amount)
bank.transfer(from,to,amount);
当前线程完全可能在成功地完成判断,且在调用 transfer 方法之前被中断。
必须确保没有其他线程在本次检查余额与转账之间修改余额。通过使用锁来保护检查和转账动作来做到这一点。
public void transfer(int from,int to,int amount){
bankLock.lock();
try{
while(account[from < amount){
//wait
....
}
//transfer money
...
}finally{
bankLock.unlock();
}
}
下载账户没有余额,应该做什么呢?等待直到另一个线程向账户中注入资金。但是,这一线程刚刚获得了对 bankLock 的排他性访问,因此别的线程没有进行存款的操作。这就是为什么需要条件对象的原因。
一个锁对象可以有一个或多个相关的条件对象。可以用 newCondition 方法获得一个条件对象。
class Bank{
private Condition sufficientFunds;
...
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}
如果 transfer 方法发现余额不足,就调用
sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。这样可以使另一个线程可以进行增加账户余额的操作。
等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 signalAll 方法时为止。
当另一个线程转账时,它应该调用:
sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足 — signalAll 方法仅仅是通知正在等待的线程 : 此时有可能已经满足条件,值的再次去检测该条件。
Tips:
通常,对 await 的调用应该在如下形式的循环体中
while(!(ok to peoceed)){
condition.await();
}
至关重要的是最终需要某个其他线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法,那么它也被阻塞,没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
应该何时调用 signalAll 呢?经验上讲,在对象的状态有利于等待线程的方法改变时调用 signalAll。例如,当一个账户余额发生改变时,等待的线程会应该有机会检查余额。在例子中,当完成转账时,调用 signalAll 方法。
public void transfer(int from,int to,int amount){
bankLock.lock();
try{
while( accounts[from] < amount){
sufficientFunds.await();
...
sufficientFunds.signalAll();
}finally{
bankLock.unlock();
}
}
}
注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法后,通过竞争实现对对象的访问。
另一个方法 signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用 signal,那么系统就死锁了。
public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initalBalance) {
accounts = new double[n];
Arrays.fill(accounts, initalBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf("Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bankLock.unlock();
}
}
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
...
Thread[Thread-81,5,main] 408.82 from 81 to 40Total Balance: 100000.00
Thread[Thread-74,5,main] 831.95 from 74 to 51Total Balance: 100000.00
Thread[Thread-54,5,main] 432.95 from 54 to 93Total Balance: 100000.00
Thread[Thread-41,5,main] 559.19 from 41 to 39Total Balance: 100000.00
Thread[Thread-64,5,main] 291.67 from 64 to 88Total Balance: 100000.00
Thread[Thread-64,5,main] 174.36 from 64 to 71Total Balance: 100000.00
Thread[Thread-93,5,main] 795.03 from 93 to 8Total Balance: 100000.00
...
synchronized 关键字
Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要这样的控制,并且可以使用一种嵌入到 Java 语言内部的机制。从 1.0 开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized 关键字声明,那么对象将保护整个方法。
pubilc synchronized void method(){
...
}
等价于
public void method(){
this.intrinsiclock.lock();
try{
...
}finally{
this.intrinsiclock.unlock();
}
}
内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中, notifyAll /notify 方法解除等待线程的阻塞状态。换句话说,调用 await 或 notifaAll 等价于
intrinsicCondition.await();
intrinsicCondition.signalAll();
Tips:
wait, notifyAll 以及 notify 都是 Object 类的 final 方法。Condition 方法必须命名为 await, singalAll 和 signal 以便于它们不会和那些方法发生冲突。
内部锁和条件存在存在一些局限:
- 不能中断一个正在试图获得锁的线程
- 试图获得锁不能设定超时
- 每个锁仅有但一个条件,可能是不够的
使用建议
- 最好不要将 Lock/Condition 和 synchronized 一起使用
- 尽量使用 sychronized,如果合适的话。因为它会为你处理所有的加锁。
- 只有在特别需要 Lock/Condition 的独有特性时,才使用
同步阻塞
每一个 Java 对象都有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized (obj){
...
}
于是它获得 obj的锁。
有时会发现 “特殊的”锁。
public class Bank{
private double[] accounts;
private Object lock = new Object;
...
public void transfer(int from, int to,int amount){
synchronized(lock){
account[from]-= amount;
account[to] += amount;
}
System.out.println("...")
}
}
在此, lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。
有时,程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定(client-side lock)。例如,Vector 类,它的方法是同步的。
public void transfer(Vertor<Double> accounts,int from,int to, int amount){
accounts.set(from,accounts.get(from)-amount);
accounts.set(to,accounts.get(to)+=amount);
...
}
Vector 的 get 和 set 方法是同步的。但是,在第一次调用 get 完成后,一个线程完全可以在 transfer 方法中被剥夺运行权,于是另一个线程可能在相同位置存入不同的值。
public void transfer(Vertor<Double> accounts,int from,int to, int amount){
sychronized(accounts){}
accounts.set(from,accounts.get(from)-amount);
accounts.set(to,accounts.get(to)+=amount);
}
...
}
这个方法就可以保证数据的正确,但是它完全依赖于: Vertor 类对自己的所有可修改方法都是用了内部锁。
客户端锁定是非常脆弱的,不建议使用。
监视器概念
锁和条件对象是线程同步的强大工具,但是严格来讲,他们不是面向对象的。多年来,研究人员在寻找一种可以在不需要加锁的情况下,就可以保证多线程的安全性。最成功的的解决方案之一就是监视器,由 Per Brinch Hansen 和 Tony Hoare 在 20 世纪 70 年代提出的。
监视器具有以下特性:
- 监视器是只包含私有域的类
- 每个监视器的对象都有一个相关的锁
- 使用该锁对所有的方法加锁。就是说:在调用 obj.method() ,那么 obj 的对象的的锁是在方法调用开始自动获得,并在方法返回是自动释放该锁。因为所有的域是私有的,这样可以确保一个线程在对象操作时没有其他线程你能访问该域。
- 该锁有任意多个相关条件
Java 设计者以不是很精确的方式采用了监视器概念,Java 中的每一个对象都有一个内部的锁和内部的条件。如果一个方法使用 synchronized 关键字声明,那么它表现得就像是一个监视器方法。通过调用 wait/notify/notifyAll 来访问条件变量。
然而,在下面的 3 个方面, Java 对象不同于监视器方法,从而使线程的安全性下降:
- 域不要求必须是 private
- 方法不要求必须是 synchronized
- 内部锁对客户是可用的
Volatile 域
volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域是 volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发更新。
假设有一个对象有一个 boolean 标记 done,它的值被一个线程设置却被另一个线程查询,如果使用锁:
private boolean dome;
public synchronized boolean isDone(){ return done;}
pubic synchronized void setDone(){ done =true;}
使用内部锁不是一个好主意。如果另一个线程已经对对象加锁,isDone 和 setDone 方法可以被阻塞。如果注意这方面,一个线程可以为这歌变量使用独立的 Lock。但是,这样会很麻烦。这时候,将域声明为 volatile 是合理的。
private volatile boolean dome;
public boolean isDone(){ return done;}
pubic void setDone(){ done =true;}
Tips:Volatile 变量不能提供原子性。
final 变量
一个域被声明为 final 时,也是可以安全地访问该域。
final Map<String,Double> accounts = new HashMap<>();
其他线程会在构造函数完成构造后才能看到 accounts 变量。
如果不使用 final,就不能保证其他线程看到的是 accounts 更新后的值。可能只是看到 null,而不是新构造的 HashMap。
当然,对于这个映射表并不是线程安全的。如果多线程在读写这个映射表,仍然需要进行同步。
原子性
java.util.concurrent.atomic 包中有很多类使用了很高效的机器指令来保证其他操作的原子性。例如, AtomicInteger 类提供了方法 incrementAndGet 和 decrementAndGet,他们分别以原子方式将一个整数自增或自减。也就是说,获得值,增 1 并设置然后生成新值的操作不会中断。可以保证多个线程并发地访问同一个实例,也会计算并返回正确的值。
不过,如果希望完成更复杂的更新,就必须使用 compareAndSet 方法。
do{
oldValue = largest.get();
newValue = Math.max(oldValue,observerd);
}while(! largest.compareAndSet(oldValue,newValue));
如果另一个线程也在更新 largest,就可能阻止这个线程更新。这样一来, compareAndSet 会返回 false,而不会设置新值。这种情况下,循环会再次尝试,读取更新后的值,并尝试修改。最终,它会成功地用新值替换原来的值。
在 Java SE 8 中,可以使用 lambda 表达式更新变量。
largest.updateAndGet(x -> Math.max(x,observed));
或
largest.accumulateAndGet(observed,Math::max);
accumulateAndGet 方法利用一个二元操作符来合并原子值和所提供的参数。
如果大量线程访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试。Java SE 8 提供了 LongAdder 和 LongAccumulator 类来解决这个问题。
死锁
每个线程都在等待其他的的线程释放资源而导致所有线程都被阻塞,这种情况称为死锁。
遗憾的是,Java 编程语言中没有任何东西可以避免或打破死锁,必须仔细设计程序,以确保不会出现死锁。
线程局部变量
有时为了避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。
public static final SimpleDateFormat ddateFormat= new SimpleDateFormat("yyyy-mm-dd");
如果两个线程都执行以下操作
String dateStamp = ddateFormat.format(new Date());
结果可能很混乱,因为 dateFormat 使用的内部数据结构可能会被并发的访问所破坏。也可使使用同步,但开销比较大;也可以在需要的时候构造一个局部 SimpleDateFormat 对象,不过就太浪费了。
要为每个线程构造一个实例,可以这样实现;
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-mm-dd"));
要访问具体的格式化方法,可以调用:
String dateFormat = dateFormat.get().format(new Date());
在一个给定线程中首次调用 get 时,会调用 initialValue 方法。在此之后,get 方法会返回属于当前线程的那个实例。
锁测试与超时
线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。tryLock 方法试图申请一个锁,在成功获得锁后返回 true,否则立即返回 false,而且线程可以立即离开去做其他事情。
if(myLock.tryLock()){
try{...}
finally{myLock.unlock();}
}else{
...
}
可以调用 tryLock 时,使用超时参数。
if(myLock.tryLock(100,TimeUnit.MILLISECONDS)) ...
TimeUnit 是一个枚举类型,可以取得值包括 SECONDS,MILLISECONDS,MICROSECONDS 和 NANOSECONDS。
调用带有超时参数的tryLock,那么如果线程在等待期间被中断,就会抛出 InterruptException 异常。
读写锁
1.构造一个 ReentranReadWriteLock 对象
private ReentranreadWriteLock rwl = new ReentranReadWriteLock();
2.抽取读锁和写锁
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
3.对所有的获取方法加读锁
public doublee getTotalBalance(){
readLock.lock();
try{...}
finally{ readLock.unlock();}
}
4.对所有的修改方法加写锁
public void transfet(...){
writeLock.lock();
try{...}
finally{ writeLock.unlock(); }
}
为什么启用 stop 和 suspend 方法
stop 方法终止所有未结束的方法,包括 run 方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于一个不一致的状态。例如,money 从一个账户转出后被终止,导致 money 没有转入目标用户,导致数据不一致。
当线程要终止另一个线程时,无法知道什么时候调用 stop 方法是安全的,什么对象被破坏。所以该方法被弃用了。
suspend 方法不会破坏对象。但是 suspend 方法会挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用 suspend 方法的线程试图获得同一个锁,那么程序死锁;被挂起的线程等待被恢复,而将其挂起的线程等待获得锁。
阻塞队列
对于许多多线程问题,可以通过一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。当试图向队列中添加元素而队列已满,或时向从队列移除元素而队列为空的时候,阻塞队列导致线程阻塞。
在协调多个线程之间的合作时,阻塞队列其实是一个有用的工具。工作者线程可以周期性将中间结果存储在阻塞队列中,其他工作者线程移除中间结果并进一步加以修改,队列会自动地平衡负载。第一个线程集运行的比第二个线程集快,第二个线程集会在等待结果时会阻塞。
阻塞队列方法分为以下 3 类,这取决于当队列满或空时它们的响应方式。如果将队列当做线程管理工具来使用,将要用到 put 和 take 方法。当试图向满的队列中添加元素或从空的队列中移除元素,add、remove 和 element 操作会抛出异常。当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用 offer、poll 和 peek 方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
java.util.concurrent 包提供了阻塞队列的几个变种。
默认情况下, LinkedBlockingQueue 的容量是没有上限的。但是,也可以选择指定最大容量。LinkedBlockingQueue 是一个双端的版本。
ArrayBlockingQueue 在构造时需要制定容量,并且有一个可选的参数来指定是否需要公平性。如果设置了公平参数,那么等待了最长的线程会优先获得处理。通常,公平性会降低性能,只有在确实非常需要的时候才使用它。
PriorityBlockingQueue 是一个带优先级的队列,而不是先进先出队列。元素按照优先级顺序被移除。该队列没有容量上限,但是如果队列是空的,取元素的操作会阻塞。
DelayQuque 包含实现 Delayed 接口的对象:
interface Delayed extends Comparable<Delayed>{
long getDelay(TimeUnit unit);
}
getDelay 方法返回对象的残留延迟。负值表示延迟已结束。元素只有在延迟用完的情况下才能从 DelayQueue 排除。还必须实现 compareTo 方法,DelayQueue 使用该方法对元素进行排序。
接口TransferQueue 允许生产者等待,直到消费者准备就绪可以接受一个元素。如果生产者调用:
q.transfer(item);
这个调用会阻塞,直到另一个线程将元素 item 删除。LinkedTransferQueue 类实现了这个接口。
public class BlockingQueueTest {
private static final int FILE_QUEUE_SIZE = 10;
private static final int SRARCH_THREADS = 100;
private static final File DUMMY = new File("");
private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.println("Enter base directory (e.g. /opt/jdk1.8/src) :");
String directory = in.nextLine();
System.out.println("Enter keyword (e.g. volatile):");
String keyword = in.nextLine();
Runnable enumerator = () -> {
try {
enumerate(new File(directory));
queue.put(DUMMY);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(enumerator).start();
for (int i = 1; i <= SRARCH_THREADS; i++) {
Runnable searcher = () -> {
try {
boolean done = false;
while (!done) {
File file = queue.take();
if (file == DUMMY) {
queue.put(file);
done = true;
} else {
search(file, keyword);
}
}
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
};
new Thread(searcher);
}
}
}
public static void enumerate(File directory) throws InterruptedException {
File[] files = directory.listFiles();
for (File file : files) {
if (file.isDirectory()) enumerate(file);
else queue.put(file);
}
}
public static void search(File file, String keyword) throws IOException {
try (Scanner in = new Scanner(file, "UTF-8")) {
int lineNumber = 0;
while (in.hasNext()) {
lineNumber++;
String line = in.nextLine();
if (line.contains(keyword)) {
System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber, line);
}
}
}
}
}
生产者线程枚举在所有子目录下的所有文件并把它们放到一个阻塞队列中,这个操作很快,如果没有设置上限的话,很块就包含了所有找到的文件。
启动了大量的搜搜线程,每个搜索线程从队列中取出一个文件,打开它,打印所有包含该关键字的行,然后取出下一个文件。使用一个小技巧在工作结束后终止这个应用程序,为了发出完成信号,枚举线程放置一个虚拟对象到队列中。到搜索线程取到这个虚拟对象时,将其放回并终止。
这个程序中,我们使用队列作为一种同步机制。
线程安全的集合
可以通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易。
高效的映射,集合队列
java.uti.concurrent 包提供了映射,有序集合队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet 和 CpncurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争最小化。
Tips:这样的集合确认集合的大小通常需要遍历,szie方法可能不准确。
集合返回弱一致性的迭代器,即迭代器不一定能反映出它们被构造之后的所有修改,但是不会将同一个值返回两次,也不会出现 ConcurrentModificationException异常。
映射条目的原子更新
Java SE 8 提供了一些可以更方便完成原子更新的方法,调用 compute 方法时可以提供一个键和一个计算新值的函数。这个函数接收键和相关联的值(如果没有值,默认为 null),他会计算新值。
map.compute(word,(k,v)-> v==null?1:v+1);
另外还有 computeIfPresent 和 computeIfAbsent 方法,他们分别只在已经有原值的情况下计算新值或只有没有原值的情况下计算原值。
map.computeIfAbsent(word,k-> new LongAdder()).increment();
对并发散列映射的批操作
Java SE 8 为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全执行。
有三种不同操作:
- 搜索(search) 为每个键或值提供一个函数,直到函数生成一个是非 null 的结果,然后搜索终止。返回这个函数的结果。
- 归约(reduce) 组合所有的键或值,这里要使用提供的一个累加哈拿书
- forEach 为所有的键或值提供一个函数
每个操作有四个版本:
- operationKeys : 处理键
- operationValues: 处理值
- operation :处理键值
- operationEntries :处理 Map.Entry 对象
对于上述操作,需要指定一个参数化阈值。如果映射包含的元素大于阈值,就会并行完成批操作。如果希望批操作在一个线程中运行,可以使用阈值 Long.MAX_VALUE。如果希望尽可能多的线程来完成,可以设置为 1。
search找出第一个出现次数超过 1000 次的单词:
String result = map.search(threshold,(k,v)-> v>1000?k:null);
forEach 有两种方式:
map.forEach(threahold,(k,v)->System.out.println(k+" -> "+v));
or
map.forEach(threshold,(k,v)-> k+" -> "+v,System.out:println)
reduce 计算所有值的总和:
Long sum = reduceValues(threshold,Long::sum)
对于 Int,long 和 double 输出还有响应的特殊化操作,分别后缀有 ToInt,ToLong 和ToDouble。
写数组的拷贝
CopeWriteArrayList 和 CopyOnWriteArrayList 是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程超过修改线程,这样很有用。因为,当构建一个迭代器时,它包含一个对当前数组的引用,如果其他线程修改该数组,迭代器使用的还是旧数组,但是数组已经被修改了。
并行数组算法
Java SE 8 中,Arrays 类提供了大量的并行化操作。
- 静态 Arrays.parallelSort() 方法可以对一个基本类型或对象的数组排序。
Arrays.parallelSort(words,Comparator.comparing(String::length))
- parallelSetAll 方法会用由一个函数计算得到的值填充一个数据
Arrays.parallelSetAll(values, i-> i%10);
- parallelPrefix 会用对应一个给定集合的操作的前缀的累加结果替换各个数组元素
# [1,2,3,4,5...]
Arrays.parallelePrefix(values,(x,y)-> x*y);
#[1,1*2,1*2*3,1*2*3*4...]
Callable 和 Future
Runnable 封装一个异步运行的任务,可以看成一个没有参数和返回值的异步方法。Callable 与Runnable类似,但是有返回值。
public interface Callable<V>{
V call() throws Exception;
}
Future 保存异步计算的结果。可以启动一个计算,将 Future 对象交给某个线程,然后忘掉它。Future 对象的所有者在结果计算好后就可以获得它。
public interface Future<V>{
V get() throws ...;
V get(long timeout,TimeUnit unit) throws ...;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDone();
}
第一个 get 方法的调用被阻塞,知道计算完成。如果计算完成之前,第二个方法的调用超时,抛出一个 TimeoutException 异常。如果运行计算线程被中断,两个方法抛出 InterruptedException。如果计算完成,那么 get 方法立即返回。
如果计算在进行,isDone 返回 false;完成计算,isDone 返回 true。
可以使用 cancel 取消计算。如果计算没开始,直接取消。如果就算开始,并且 mayInterrupt 为 true,计算中断。
FutureTask 包装器是一种便利机制,可以将 Callable 转为 Future 和 Runnable,同时实现两个接口。
public class FutureTest {
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.println("Enter the base directory:");
String directory = in.nextLine();
System.out.println("Enter keywords:");
String keyword = in.nextLine();
MatchCounter counter = new MatchCounter(new File(directory), keyword);
FutureTask<Integer> task = new FutureTask<>(counter);
Thread t = new Thread(task);
t.start();
try {
System.out.println(task.get() + " matching files.");
} catch (ExecutionException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MatchCounter implements Callable<Integer> {
private File directory;
private String keyword;
public MatchCounter(File directory, String keyword) {
this.directory = directory;
this.keyword = keyword;
}
@Override
public Integer call() throws Exception {
int count = 0;
try {
File[] files = directory.listFiles();
List<Future<Integer>> results = new ArrayList<>();
for (File file : files) {
if (file.isDirectory()) {
MatchCounter counter = new MatchCounter(file, keyword);
FutureTask<Integer> task = new FutureTask<>(counter);
results.add(task);
Thread t = new Thread(task);
t.start();
} else {
if (search(file)) count++;
}
}
for (Future<Integer> result : results) {
try {
count += result.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return count;
}
public boolean search(File file) {
try (Scanner in = new Scanner(file, "UTF-8")) {
boolean found = false;
while (!found && in.hasNext()) {
String line = in.nextLine();
if (line.contains(keyword)) found = true;
}
return found;
} catch (Exception e) {
return false;
}
}
}
执行器
构建一个线程是有代价的,因为涉及和操作系统的交互。如果要创建大量生命周期短的线程,应使用线程池。一个线程池中包含很多准备运行的空白线程,将 Runnable 对象交给线程,就会有一个线程调用 run 方法,当 run 方法退出后,线程不会死亡,而是在池中等待为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数据,创建大量线程会降低性能甚至导致系统崩溃。如果有个创建许多线程的算法,应该使用一个线程数 固定 线程数以限制并发线程总数。
执行器 Executor 类有很多静态工厂方法用来创建线程
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程;空线程会被保留 60 秒 |
newFixedThreadPool | 该池包含固定数量的线程;空闲线程会一直保留 |
newSingleThreadExecuter | 只有一个线程的池,该线程顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行而构建的固定线程池 |
newSingleTheadScheduledExecutor | 用于预定执行而构建的单线程池 |
线程池
将一个 Runnable 对象或一个 Callable 对象提交到线程池:
Future<?> submit(Runnable task)
Future<?> submit(Runnable task,T result)
Future<?> submit(Callable<T> task)
当使用完一个线程池时,调用 shutdown。该方法自带关闭线程池的关闭序列。被关闭线程池不接受新线程,并在所有任务完成后,线程池中线程死亡。或者使用 shutdownNow,取消尚未开始的任务并试图中断正在运行的线程。
预定执行
ScheduleExecutorService 接口具有为预定执行或重复执行任务而设计的方法。它是一种允许使用线程池机制的 java.util.TImer 的泛化。可以预定 Runnable 或 Callable 在初始的延迟之后只运行一次。
控制任务组
invokeAny 方法提交所有对象到一个 Callable 对象的集合中,并返回某个已完成的任务的结果。
invokeAll 方法提交所有对象到一个 Callable对象的集合中,并返回一个 Future 对象的列表,代表所有任务的解决方案。
Fork-Join 框架
Fork-Join 框架转为用来支持对每个处理器内核分别使用一个线程来完成计算密集型任务,如图像或音频处理。
public class ForkJoinTest {
public static void main(String[] args) {
final int SIZE = 10000000;
double[] numbers = new double[SIZE];
for (int i = 0; i < SIZE; i++) numbers[i] = Math.random();
Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5);
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(counter);
System.out.println(counter.join());
}
}
class Counter extends RecursiveTask<Integer> {
public static final int THRESHOLD = 1000;
private double[] values;
private int from;
private int to;
private DoublePredicate filter;
public Counter(double[] values, int from, int to, DoublePredicate filter) {
this.values = values;
this.from = from;
this.to = to;
this.filter = filter;
}
public Integer compute() {
if (to - from < THRESHOLD) {
int count = 0;
for (int i = from; i < to; i++) {
if (filter.test(values[i])) count++;
}
return count;
} else {
int mid = (from + to) / 2;
Counter first = new Counter(values, from, mid, filter);
Counter second = new Counter(values, mid, to, filter);
invokeAll(first, second);
return first.join() + second.join();
}
}
}
同步器
java.util.concurrent 包含几个帮助管理相互合作的线程集的类。 这些机制具有为线程之间的共用集结点模式提供的“预制功能”。如果有一个相互合作的线程集满足这些行为模式之一,那么应该直接重用合适的库类而不要试图提供手工的锁与条件的集合。
类 | 做什么 | 说明 |
---|---|---|
CyclicBarrier | 允许线程集等待直至其中预定数目的线程达到一个公共障栅,然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在它们的结果可用之前完成时 |
Phaser | 类似于循环障栅,不过有一个可变的技术 | Java SE 7 引入 |
CountDownLatch | 允许线程集等待直到计数器减为 0 | 当一个或多个线程需要等待直到指定数目的事件发生 |
Exchanger | 允许两个线程在要交换的对象准备好时交换对象 | 当两个线程工作在同一个数据结构的两个实例上的时候,一个向实例添加数据而另一个从实例中清除数据 |
Semaphore | 允许线程集等待直到被允许继续运行为至 | 限制访问资源的线程总数。如果许可数是 1,常常阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显示同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时 |
信号量
概念上说,一个信号量管理许多的许可证。为了通过信号量,线程通过调用 acquire 请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制通过的线程数量。其他线程通过调用 release 释放许可。而且,任何线程可以释放任何数目的许可,这可能会导致许可数目超过初始数目。
倒计时门栓
一个倒计时门栓(CountDownLatch)让一个线程集等待直到计数变为 0。倒计时门栓是一次性的,一旦计时为 0 ,就不能再重用了。
一个有用的特例是计数值为 1的门栓。实现一个只能通过一次的门,线程在门外等待直到另一个线程将计数器的值置位 0。
例如,一个线程需要一些初始化的数据来完成工作。工作线程被启动并在门外等候,另一个线程准备好的时候低啊用 countDown,所有工作线程就可以运行了。
然后,可以使用第二个门栓检查什么时候所有工作器线程完成工作。用线程数初始化门栓。每个工作线程在结束前将门栓计数减 1,另一个获取工作结果的线程在门外等待。一旦所有的工作线程终止该线程继续运行。
障栅
CyclicBarrier 类实现了一个集结点成为障栅。考虑大量线程运行在一次计算的不同部分的情形。当所有部分都准备好的时候,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
-
构造一个障栅,并给出参与的线程数:
CyclicBarrier barrier = new CyclicBarrier(nthreads);
-
每个线程做一些工作,完成后在障栅处调用 await
public voud run(){ doWork(); barrier.await(); ... }
Tips : await 方法有一个可选的超时参数
barrier.await(1000,TimeUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是因为它调用 await 时设置了超时,或者因为它被中断了)。在这种情况下,所有其他想线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await 的调用。
可以提供一个可选的障栅动作,当所有线程到达障栅的时候就会执行这一动作。
Runnable barrierAction = ...;
CyclicBarrier barrier = new CyclicBarrier(nthreads,barrierAction);
该动作可以收集那些单个线程的运行结果。
障栅被称为是循环的,因为可以在所有等待线程被释放后被重用。在这一点上,有别于 CountDownLatch。
Phaser 类增加了更大的灵活性,允许改变不同阶段中参与线程的个数。
交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器(Exchanger)。典型的情况是,一个线程向缓冲区填入数据,另一个线程消耗这些数据。当他们完成以后,相互交换缓冲区。
同步队列
同步队列是一种将生产者和消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take 方法为止,反之亦然。与 Exchanger不同的是,数据仅仅沿一个方向传递,从生产者到消费者。
即使 SynchronousQueue 类实现了 BlockingQueue 接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的 size 方法总是返回 0。