多线程的4种实现方式
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常, 每一个任务称为一个线程(thread), 它是线程控制的简称。可以同时运行一个以上线程的程 序称为多线程程序(multithreaded)。
那么,多进程与多线程有哪些区别呢? 本质的区别在于每个进程拥有自己的一整套变 量, 而线程则共享数据。 这听起来似乎有些风险, 的确也是这样, 在本章稍后将可以看到这 个问题。然而,共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外, 在有 些操作系统中,与进程相比较, 线程更“ 轻量级”, 创建、撤销一个线程比启动新进程的开 销要小得多。
sleep方法可以抛出一个 InterrruptedException 异常。
下面是在一个单独的线程中执行一个任务的简单过程:
1 ) 将任务代码移到实现了 Runnable 接口的类的 run方法中。这个接口非常简单,只有 一个方法:
public interface Runnable { void run(); } 由于 Runnable 是一个函数式接口,可以用 lambda 表达式建立一个实例: Runnable r = () -> { taskcode};
2 ) 由 Runnable 创建一个 Thread 对象: Thread t = new Thread(r);
3 ) 启动线程: t.start();
package com;
import java.util.*;
import java.lang.*;
import java.io.*;
public class Welcome{
class a implements Runnable{
public void run() {
System.out.println("a");
}
}
public void x() {
Runnable i=new a();
Thread thread=new Thread(new a());
thread.start();
}
public static void main(String []args){
new Welcome().x();
}
}
当线程的 run 方法执行方法体中最后一条语句后, 并经由执行 return 语句返冋时, 或者出现了在方法中没有捕获的异常时,线程将终止。然而,interrupt 方法可以用来请求终止线程。当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。这是每一个线程都具有 的 boolean 标志。每个线程都应该不时地检査这个标志, 以判断线程是否被中断。 要想弄清中断状态是否被置位,首先调用静态的 Thread.currentThread方法获得当前线 程,然后调用 islnterrupted方法:
while(!Thread.currentThread().isInterrupted()) {
}
但是, 如果线程被阻塞, 就无法检测中断状态。这是产生 InterruptedExceptioii 异常的地 方。当在一个被阻塞的线程(调用 sleep 或 wait) 上调用 interrupt方法时,阻塞调用将会被 Interrupted Exception 异常中断。
没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它 的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常 后, 继续执行,而不理会中断。但是,更普遍的情况是,线程将简单地将中断作为一个终止 的请求:
Runnable r = () -> {
try {
while (!Thread.currentThread().islnterrupted0 && more work todo)
{ do morework }
}
catch(InterruptedException e)
{ // thread was interr叩ted during sleep or wait }
finally
{ cleanup,if required } // exiting the run method terminates the thread }
interrupt()不能中断在运行中的线程,它只能改变中断状态而已。
interruped用法
如果在每次工作迭代之后都调用 sleep方法(或者其他的可中断方法),islnterrupted 检测 既没有必要也没有用处。如果在中断状态被置位时调用 sleep方法,它不会休眠。相反,它 将清除这一状态(丨)并拋出 IntemiptedException。因此, 如果你的循环调用 sleep,不会检 测中断状态。相反,要如下所示捕获 InterruptedException 异常:
Runnable r = () -> {
try {
while more work todo)
{ do morework
Thread.sleep();}
}
catch(InterruptedException e)
{ // thread was interrupted during sleep or wait }
finally
{ cleanup,if required } // exiting the run method terminates the thread }
有两个非常类似的方法,interrupted 和 islnterrupted。Interrupted 方法是一个静态 方法, 它检测当前的线程是否被中断。 而且, 调用 interrupted 方法会清除该线程的中断状态。另一方面,islnterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调 用这个方法不会改变中断状态。
在很多发布的代码中会发现 InterruptedException 异常被抑制在很低的层次上。有两种合理 的选择:
•在 catch 子句中调用 Thread.currentThread().interrupt() 来设置中断状态。于是,调用者 可以对其进行检测。
void mySubTask() {
try { sleep(delay); }
catch (InterruptedException e)
{ Thread.currentThread()-interrupt(); }
•或者,更好的选择是,用 throws InterruptedException标记你的方法, 不采用 try语句 块捕获异常。于是,调用者(或者,最终的 run 方法)可以捕获这一异常。
void mySubTask() throws InterruptedException
{sleep(delay);}
线程可以有如下 6 种状态:
•New (新创建)
•Runnable (可运行)
•Blocked (被阻塞)
•Waiting (等待)
•Timed waiting (计时等待)
•Terminated (被终止)
当用 new 操作符创建一个新线程时,如 new Thread(r),该线程还没有开始运行。这意味 着它的状态是 new。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在 线程运行之前还有一些基础工作要做。
一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线桿可能正在运行也可能没 有运行, 这取决于操作系统给线程提供运行的时间。
抢占式调度系 统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行 权, 并给另一个线程运行机会(见图 14-4 )。当选择下一个线程时, 操作系统考虑线程的优 先级。
当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资 源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。
•当一个线程试图获取一个内部的对象锁(而不是java.util.concurrent 库中的锁),而该 锁被其他线程持有, 则该线程进人阻塞状态(我们在 14.5.3 节讨论java.util.concurrent 锁,在 14.5.5 节讨论内部对象锁)。当所有其他线程释放该锁,并且线程调度器允许 本线程持有它的时候,该线程将变成非阻塞状态。
■ 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。我们在第 14.5.4 节来讨论条件。在调用Object.wait方法或 Thread.join方法, 或者是等待 java, util.concurrent 库中的 Lock 或 Condition 时, 就会出现这种情况。实际上,被阻塞状态 与等待状态是有很大不同的。
•有几个方法有一个超时参数。调用它们导致线程进人计时等待(timed waiting) 状 态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait、Thread.join、Lock.tryLock 以及 Condition.await 的计时版。
图 14-3 展示了线程可以具有的状态以及从一个状态到另一个状态可能的转换。当一个线 程被阻塞或等待时(或终止时),另一个线程被调度为运行状态。当一个线程被重新激活(例 如, 因为超时期满或成功地获得了一个锁),调度器检查它是否具有比当前运行线程更高的优 先级。如果是这样,调度器从当前运行线程中挑选一个, 剥夺其运行权,选择一个新的线程 运行。
线程因如下两个原因之一而被终止:
•因为run方法正常退出而自然死亡。
•因为一个没有捕获的异常终止了 run方法而意外死亡
join的使用
下面将讨论线程的各种属性,其中包括:线程优先级、守护线程、线程组以及处理未捕 获异常的处理器。
在 Java 程序设计语言中,每一个线程有一个优先级。默认情况下,一+线程继承它的父 线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。可以将优先级设 置为在 MIN_PRIORITY (在 Thread类中定义为 1 ) 与 MAX_PRIORITY (定义为 10 ) 之间的 任何值。NORM_PRIORITY 被定义为 5。
Java 线程的优 先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
t.setDaemon(true); 将线程转换为守护线程(daemon thread)。这样一个线程没有什么神奇。守护线程的唯一用途 是为其他线程提供服务。计时线程就是一个例子,它定时地发送“ 计时器嘀嗒” 信号给其他 线程或清空过时的高速缓存项的线程。当只剩下守护线程时, 虚拟机就退出了,由于如果只 剩下守护线程, 就没必要继续运行程序了。 守护线程有时会被初学者错误地使用, 他们不打算考虑关机(shutdown) 动作。但是, 这是很危险的。守护线程应该永远不去访问固有资源, 如文件、 数据库,因为它会在任何时 候甚至在一个操作的中间发生中断。
线程的 run方法不能抛出任何受查异常, 但是,非受査异常会导致线程终止。在这种情 况下,线程就死亡了。
但是,不需要任何 catch子句来处理可以被传播的异常。相反,就在线程死亡之前, 异 常被传递到一个用于未捕获异常的处理器。 该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有— 个方法。
void uncaughtException(Thread t, Throwable e)
可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。替换 处理器可以使用日志 API 发送未捕获异常的报告到日志文件。 如果不安装默认的处理器, 默认的处理器为空。但是, 如果不为独立的线程安装处理 器,此时的处理器就是该线程的 ThreadGroup 对象。
注释: 线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组, 但是, 也可能会建立其他的组。现在引入了更好的特性用于线程集合的操作, 所以建议不要在自己的程序中使用线程组。
ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException方 法做如下操作:
1 ) 如果该线程组有父线程组, 那么父线程组的 uncaughtException方法被调用。
2 ) 否则, 如果 Thread.getDefaultExceptionHandler方法返回一个非空的处理器, 则调用 该处理器。
3 ) 否则,如果 Throwable 是 ThreadDeath 的一个实例, 什么都不做。
4 ) 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err 上。 这是你在程序中肯定看到过许多次的栈轨迹
yield方法
在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象, 并且每一个线程都调用了一个修改该对象状态的方法,将会发 生什么呢? 可以想象,线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生i化 误的对象。这样一个情况通常称为竞争条件(race condition)。
有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized关键字达 到这一目的,并且 Java SE 5.0引入了 ReentrantLock 类。synchronized 关键字自动提供一个 锁以及相关的“ 条件。
用 ReentrantLock 保护代码块的基本结构如下:
myLock.lock(); // a ReentrantLock object
try { critical section }
finally
{ myLock.unlock();// make sure the lock is unlocked even if an exception isthrown }
这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象, 其他任 何线程都无法通过 lock语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放 锁对象。
警告: 把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常, 锁必须被释放。否则, 其他线程将永远阻塞。
如果使用锁, 就不能使用带资源的 try语句。首先, 解锁方法名不是 close。不过, 即使将它重命名, 带资源的 try语句也无法正常工作。它的首部希望声明一个新变量。但 是如果使用一个锁, 你可能想使用多个线程共享的那个变量(而不是新变量) 。
同步,简单地理解,就是协同步调,一个完成了,另一个才能开始。
异步,就是你说的不同步,就是互不干扰,各干各的,多个线程可能同时进行
锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count) 来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。 由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法
通常, 可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完 成后, 另一个线程才能使用相同对象。
通常, 线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对 象来管理那些已经获得了一个锁但是却不能做有用工作的线程。在这一节里, 我们介绍 Java 库中条件对象的实现。
等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞 状态,直到另一个线程调用同一条件上的 signalAll 方法时为止。 这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时, 它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进人该对象。一旦 锁成为可用的,它们中的某个将从 await 调用返回, 获得该锁并从被阻塞的地方继续执行。 此时, 线程应该再次测试该条件。由于无法确保该条件被满足— —signalAll 方法仅仅是通知正在等待的线程 :此时有可能已经满足条件, 值得再次去检测该条件。
另一个方法 signal, 则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的 阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行, 那么它再次被 阻塞。如果没有其他线程再次调用 signal, 那么系统就死锁了。 当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 await、signalAll 或 signal 方法。
例:
package synch;
import java.util.*;
import java.util.concurrent.locks.*;
/**
* A bank with a number of bank accounts that uses locks for serializing access.
* @version 1.30 2004-08-01
* @author Cay Horstmann
*/
public class Bank
{
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
/**
* Constructs the bank.
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
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();
}
finally
{
bankLock.unlock();
}
}
/**
* Gets the sum of all account balances.
* @return the total balance
*/
public double getTotalBalance()
{
bankLock.lock();
try
{
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
finally
{
bankLock.unlock();
}
}
/**
* Gets the number of accounts in the bank.
* @return the number of accounts
*/
public int size()
{
return accounts.length;
}
}
package synch;
/**
* This program shows how multiple threads can safely access a data structure.
* @version 1.31 2015-06-21
* @author Cay Horstmann
*/
public class SynchBankTest
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_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(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; 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();
}
}
}
在进一步深人之前,总结一下 有关锁和条件的关键之处:
•锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
•锁可以管理试图进入被保护代码段的线程。
•锁可以拥有一个或多个相关的条件对象。
•每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
从 1.0 版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized关键字声明,那么对象的锁 将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁
例,下面两段代码等价:
public synchronized void method() { method body }
public void method(){
this.intrinsicLock.lock();
try{
}finally{
this.intrinsicLock.unlock();
}
}
内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAU /notify方 法解除等待线程的阻塞状态。换句话说,调用 wait 或 notityAll 等价于
intrinsicCondition.await(); intrinsicCondition.signalAll()
每一个对象有一个内部锁, 并且该锁有一个内部条件。由锁来管理那些试图进入 synchronized 方法的线程,由条件来管理那些调用 wait 的线程。
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对 象的内部锁。例如,如果 Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class 对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。 内部锁和条件存在一些局限。包括:
•不能中断一个正在试图获得锁的线程。
•试图获得锁时不能设定超时。
•每个锁仅有单一的条件, 可能是不够的。
在代码中应该使用哪一种? Lock 和 Condition 对象还是同步方法?下面是一些建议:
•最好既不使用 Lock/Condition 也不使用 synchronized 关键字。在许多情况下你可以使 用java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。
•如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代 码数量,减少出错的几率。程序清单 14-8 给出了用同步方法实现的银行实例。
•如果特别需要 Lock/Condition结构提供的独有特性时,才使用Lock/Condition.
例:
package synch2;
/**
* This program shows how multiple threads can safely access a data structure,
* using synchronized methods.
* @version 1.31 2015-06-21
* @author Cay Horstmann
*/
public class SynchBankTest2
{
public static final int NACCOUNTS = 100;
public static final double INITIAL_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(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; 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();
}
}
}
- synchronized锁住对象的时候锁住的是堆中的对象,而不是栈中对象的引用。
Object o=new Object(); synchronized(o){ } o=new Object(); synchronized(o){ } //这两个锁住的是不同的对象。
- 不要锁字符串常量,因为相同的字符串常量在一个地址,都在常量池里。不要去锁字符串常量。
正如刚刚讨论的,每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有 另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞: synchronized (obj) // this is the syntax for a synchronized block { critical section }
于是它获得 Obj 的锁。 有时会发现“ 特殊的” 锁,例如:
public class Bank
{ private double[] accounts;
private Object lock = new Object() ;
public void transfer(int from, int to, int amount)
{
synchronized (lock) // an ad-hoc lock
{ accounts[from] -= amount; accounts[to] += amount; }
System.out.print1n(.. }
}
在此,lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。 有时程序员使用一个对象的锁来实现额外的原子操作, 实际上称为客户端锁定。如你所见,客户端锁定是非常脆弱 的,通常不推荐使用。
用 Java 的术语来讲,监视器具有如下特性:
•监视器是只包含私有域的类。
•每个监视器类的对象有一个相关的锁。
•使用该锁对所有的方法进行加锁。
换句话说,如果客户端调用 obj.meth0d(), 那么 obj 对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有 的域是私有的,这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问 该域。
•该锁可以有任意多个相关条件
然而, 在下述的 3 个方面 Java 对象不同于监视器, 从而使得线程的安全性下降:
•域不要求必须是 private。
•方法不要求必须是 synchronized。
•内部锁对客户是可用的。
有时,仅仅为了读写一个或两个实例域就使用同步, 显得开销过大了。“ 如果向一个变量写入值, 而这个变量接下 来可能会被另一个线程读取, 或者,从一个变量读值, 而这个变量可能是之前被另一个 线程写入的, 此时必须使用同步。
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile, 那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
private volatile boolean done;
Volatile 变量不能提供原子性。例如, 方法 public void flipDone() { done = !done; } // not atomic 不能确保翻转域中的值。不能保证读取、翻转和写入不被中断。
volatile详解
还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。考虑以下声明:
final Map
其他线程会在构造函数完成构造之后才看到这个 accounts 变量。 如果不使用 final,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是 看到 null, 而不是新构造的 HashMap。 当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatile。java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用 锁)来保证其他操作的原子性。 例如, Atomiclnteger 类提供了方法 incrementAndGet 和 decrementAndGet, 它们分别以原子方式将一个整数自增或自减。例如,可以安全地生成一个 数值序列,如下所示:
public static AtomicLong nextNumber=new AtomicLong();
long id=nextNumber.incrementAndGet();
incrementAndGet 方法以原子方式将 AtomicLong 自增, 并返回自增后的值。也就是说, 获得值、增 1 并设置然后生成新值的操作不会中断。可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值。
例,设希望跟踪不同线程观察的最大值:
AtomicLong largest=new AtomicLong();
do {
oldValue=largest.get();
newValue=Math.max(oldvalue,observed);
}while(!largest.compareAndSet(oldValue, newValue));
在 Java SE 8中,不再需要编写这样的循环样板代码。实际上,可以提供一个 lambda 表 达式更新变量,它会为你完成更新。对于这个例子,我们可以调用:
largest.updateAndGet(x -> Math.max(x, observed)); 或
largest.accumulateAndCet(observed, Math::max); accumulateAndGet方法利用一个二元操作符来合并原子值和所提供的参数。 还有 getAndUpdate 和 getAndAccumulate 方法可以返回原值
有可能会因为每一个线程要等待更多的钱款存人而导致所有线程都被阻塞。这样的状态 称为死锁(deadlock)。
当程序挂起时, 键入 CTRL+\, 将得到一个所有线程的列表。每一个线程有一个栈 踪迹, 告诉你线程被阻塞的位置。
还有一种很容易导致死锁的情况: 在 SynchBankTest 程序中, 将 signalAll方法转换 为 signal, 会发现该程序最终会挂起(将 NACCOUNTS 设为 10 可以更快地看到结果) 。 signalAll 通知所有等待增加资金的线程, 与此不同的是 signa丨方法仅仅对一个线程解锁。遗憾的是,Java 编程语言中没有任何东西可以避免或打破这种死锁现象。必须仔细设计 程序,以确保不会出现死锁。
用空间换时间,不同的线程使用自己的对象。
前面几节中, 我们讨论了在线程间共享变量的风险。有时可能要避免共享变量, 使用 ThreadLocal 辅助类为各个线程提供各自的实例。 例如,SimpleDateFormat 类不是线程安全的。 假设有一个静态变量。
如果两个线程都执行以下操作: String dateStamp = dateFormat.format(new Date()); 结果可能很混乱,因为 dateFormat 使用的内部数据结构可能会被并发的访问所破坏。当 然可以使用同步,但开销很大; 或者也可以在需要时构造一个局部 SimpleDateFormat 对象, 不过这也太浪费了。
要为每个线程构造一个实例,可以使用以下代码:
public static final ThreadLocal dateformat=
new ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-mm-dd"));
要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new Date()); 在一个给定线程中首次调用 get 时, 会调用 initialValue方法。在此之后,get方法会返回 属于当前线程的那个实例。
在多个线程中生成随机数也存在类似的问题。java. .util.Random 类是线程安全的。但是如 果多个线程需要等待一个共享的随机数生成器, 这会很低效。 可以使用 ThreadLocal 辅助类为各个线程提供一个单独的生成器, 不过 Java SE 7 还另外 提供了一个便利类。只需要做以下调用:
int random = ThreadLocalRandom.current().nextlnt(upperBound):
ThreadLocalRandom.current()调用会返回特定于当前线程的 Random 类实例。
线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加 谨慎地申请锁。tryLock方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回 false, 而且线程可以立即离开去做其他事情:
if(mylock.tryLock()){
try{}
finally{mylock.unlock();}
}
可以调用 tryLock 时,使用超时参数,像这样:
if (myLock.tryLock(100, TineUnit.MILLISECONDS)) ...
TimeUnit 是一 枚举类型,可以取的值包括 SECONDS、MILLISECONDS, MICROSECONDS 和 NANOSECONDS。 lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之 前一直处于阻塞状态。如果出现死锁,那么,lock 方法就无法终止。 然而, 如果调用带有用超时参数的 tryLock, 那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。 也可以调用 locklnterruptibly方法。它就相当于一个超时设为无限的 tryLock 方法。 在等待一个条件时, 也可以提供一个超时: myCondition.await(100, TineUniBILLISECONDS)) 如果一个线程被另一个线程通过调用 signalAU 或 signal 激活, 或者超时时限已达到,或 者线程被中断, 那么 await 方法将返回。 如果等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你希望出 现这种情况时线程继续等待(可能不太合理), 可以使用awaitUninterruptibly方法代替 await.
ReentrantLock实现synchronized的所以功能。
这三个方法ReentrantLock都是有的,是与Lock的区别,更加灵活。
第三个函数,如果是那样锁定的话,线程就可以被打断了。其它线程打断。ReentrantLock可以指定为公平锁
。构造函数传true为公平所。公平锁效率比较低。
公平锁即谁先到达谁先得到锁。
如果很多线程从一个数据结构读取数据而很少线程修改其中数 据的话, 后者是十分有用的。在这种情况下, 允许对读者线程共享访问是合适的。当然,写 者线程依然必须是互斥访问的。
下面是使用读 / 写锁的必要步骤:
1 ) 构造一个 ReentrantReadWriteLock 对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():
2 ) 抽取读锁和写锁:
private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();
3 ) 对所有的获取方法加读锁:
public double getTotalBalance()
{
readLock.lock(); try { . . . } finally { readLock.unlock(); }
}
4 ) 对所有的修改方法加写锁:
public void transfer(. . .)
{
writeLock.lock(); try { . . . } finally { writeLock.unlock(); }
stop方法天生就不安全,经验证明 suspend方法 会经常导致死锁.当线程要终止另一个线程时, 无法知道什么时候调用stop方法是安全的, 什么时候导致 对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程, 被中断的线程 会在安全的时候停止.
接下来, 看看 suspend方法有什么问题。与 stop不同,suspend不会破坏对象。但是, 如果用 suspend 挂起一个持有一个锁的线程, 那么,该锁在恢复之前是不可用的。如果调用 suspend方法的线程试图获得同一个锁, 那么程序死锁: 被挂起的线程等着被恢复,而将其 挂起的线程等待获得锁.
对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生 产者线程向队列插人元素, 消费者线程则取出它们。使用队列,可以安全地从一个线程向另 一个线程传递数据。例如,考虑银行转账程序, 转账线程将转账指令对象插入一个队列中, 而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该 银行对象的内部。因此不需要同步。(当然, 线程安全的队列类的实现者不能不考虑锁和条 件,但是, 那是他们的问题而不是你的问题。)
当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队 列(blocking queue) 导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。
阻塞队列方法分为以下 3类, 这取决于当队列满或空时它们的响应方式。如果将队列当 作线程管理工具来使用, 将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列 中移出元素时,add、 remove 和 element 操作抛出异常。当然,在一个多线程程序中, 队列会 在任何时候空或满, 因此,一定要使用 offer、poll 和 peek方法作为替代。这些方法如果不能 完成任务,只是给出一个错误提示而不会抛出异常。
注释: poll和 peek 方法返回空来指示失败。因此,向这些队列中插入 null 值是非法的。
LinkedBlockingQueue 的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端 的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需 要公平性。若设置了公平参数, 则那么等待了最长时间的线程会优先得到处理。通常,公平 性会降低性能,只有在确实非常需要时才使用它。 PriorityBlockingQueue 是一个带优先级的队列, 而不是先进先出队列。元素按照它们的 优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的, 取元素的操作会阻 塞
JavaSE 7增加了一个 TransferQueue 接口,允许生产者线程等待, 直到消费者准备就绪 可以接收一个元素。如果生产者调用 q.transfer(iteni); 这个调用会阻塞, 直到另一个线程将元素(item) 删除。LinkedTransferQueue 类实现了这个接口。
例:
Collections.synchronizedMap()、Hashtable并发量小。
HashTable锁住所有数据,ConcurrentHashMap锁住一段。
ConcurrentHashMap 并发高 ConcurrentHashSet
ConcurrentSkipListMap 高并发有序。 ConcurrentSkipListSet
CopyOnWriteArrayList 和 CopyOnWriteArraySet
队列:
低并发:Collections.synchronizedList(),Vector
在高并发时可以使用两种队列
ConcurrentLinkedQueue //并发加锁的
ConcurrentLinkedDeque
BlokingQueue
LinkedBlokingQueue //堵塞式容器,轻易完成生产者消费者。 无界队列
ArrayBolokingQueue //有界队列
DelayQueue
TransferQueue
SynchronousQueue
LinkedTransferQueue 多了一个Transfer()生产者生产东西的时候,如果发现消费者需要,不往队列里扔了,直接给消费者消费。如果transfer()发现没有消费者,阻塞。add加入可以扔到队列。
SynchronousQueue<>(); 特殊的transferQueue. 容量为0. 装的任何东西都必须扔给消费者消费。
put()方法阻塞等待消费者消费,内部调用的是transfer
DelayQueue 每个元素记住自己还有多长时间能被消费者拿走。使用的时候要加入的元素要实现实现一个接口Delayed。Delayed接口继承comparable.
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.*;
import java.lang.*;
import java.io.*;
import java.lang.reflect.*;
public class Main implements Delayed{
long runningTime=0;
Queue q=new DelayQueue<>();
public Main(long rt)
{
this.runningTime=rt;
}
public int compareTo(Delayed arg0) {
// TODO Auto-generated method stub
if(this.getDelay(TimeUnit.MILLISECONDS)arg0.getDelay(TimeUnit.MILLISECONDS))
return 1;
else
return 0;
}
@Override
public long getDelay(TimeUnit arg0) {
// TODO Auto-generated method stub
return arg0.convert(runningTime-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
public static void main(String[] args) {
long now=System.currentTimeMillis();
Main t1=new Main(now+1000);
q.add(t1); //按照时间排序,执行定时任务
q.take();
}
}
java.util.concurrent 包提供了映射、 有序集和队列的高效实现:ConcurrentHashMap、 ConcurrentSkipListMap 、ConcurrentSkipListSet 和 ConcurrentLinkedQueue。 这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。 与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常 需要遍历。
而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(见之前的文章《JAVA API备忘---集合》)的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
集合返回弱一致性(weakly consistent) 的迭代器。这意味着迭代器不一定能反映出它 们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会拋出 Concurrent ModificationException 异常。 注释:与之形成对照的是, 集合如果在迭代器构造之后发生改变,java.util 包中的迭代器 将抛出一个 ConcurrentModificationException 异常。
并发的散列映射表, 可高效地支持大量的读者和一定数量的写者。默认情况下,假定 可以有多达 16 个写者线程同时执行。可以有更多的写者线程,但是, 如果同一时间多于 16 个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而,恐怕没有这种必要。并发散列映射将桶组织 为树, 而不是列表,键类型实现了 Comparable, 从而可以保证性能为 O(log(n))。
显然,下面的 代码不是线程安全的:
Long oldValue = map.get(word);
Long newValue = oldValue == null ? 1: oldValue + 1 ;
map.put(word, newValue); // Error-might not replace oldValue
有些程序员很奇怪为什么原本线程安全的数据结构会允许非线程安全的操作。不 过有两种完全不同的情况。如果多个线程修改一个普通的 HashMap,它们会破坏内部结 构(一个链表数组) 。有些链接可能丢失, 或者甚至会构成循环,使得这个数据结构不再 可用。对于 ConcurrentHashMap 绝对不会发生这种情况。在上面的例子中,get 和 put 代 码不会破坏数据结构。不过,由于操作序列不是原子的,所以结果不可预知。
传统的做法是使用 replace 操作,它会以原子方式用一个新值替换原值,前提是之前没有 其他线程把原值替换为其他值。必须一直这么做, 直到 replace 成功。
do{
oldValue = map.get(word);
newValue = oldValue = null ? 1 : oldValue + 1; }
while (!map.replace(word, oldValue, newValue);
调用 compute方法时可以提供 一个键和一个计算新值的函数。这个函数接收键和相关联的值(如果没有值,则为 mill), 它 会计算新值。例如,可以如下更新一个整数计数器的映射:
map.compute(word, (k, v) -> v = null ? 1: v +1 )
另外还有 computelfPresent 和 computelfAbsent方法,它们分别只在已经有原值的情况下计 算新值,或者只有没有原值的情况下计算新值.
首次增加一个键时通常需要做些特殊的处理。利用 merge 方法可以非常方便地做到这一 点。这个方法有一个参数表示键不存在时使用的初始值。否则, 就会调用你提供的函数来结 合原值与初始值。(与 compute 不同,这个函数不处理键。)
map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);
或者,更简单地可以写为:
map.merge(word, 1L, Long::sum);
如果传入 compute 或 merge 的函数返回 null, 将从映射中删除现有的条目.
Java SE 8 为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安 全地执行。批操作会遍历映射,处理遍历过程中找到的元素。无须冻结当前映射的快照。除非你恰好知道批操作运行时映射不会被修改,否则就要把结果看作是映射状态的一个近似。 有 3 种不同的操作:
•搜索(search) 为每个键或值提供一个函数,直到函数生成一个非 null 的结果。然后搜 索终止,返回这个函数的结果。
•归约(reduce) 组合所有键或值, 这里要使用所提供的一个累加函数。
•forEach 为所有键或值提供一个函数
每个操作都有 4 个版本:
•operationKeys: 处理键。
•operatioriValues: 处理值。
•operation: 处理键和值。
•operatioriEntries: 处理 Map.Entry对象
对于上述各个操作, 需要指定一个参数化阈值。如果映射包含的 元素多于这个阈值, 就会并行完成批操作。如果希望批操作在一个线程中运行,可以使用阈 值 Long.MAX_VALUE。如果希望用尽可能多的线程运行批操作,可以使用阈值 1。
String result=map.search(threhold,(k,v)->v>1000?k:null);
//forEach方法有两种形式。第一个只为各个映射条目提供一个消费者函数, 例如:
map.forEach(threhold,(k,v)->System.out.println(k+"->"+v));
//第二种形式还有一个转换器函数,这个函数要先提供,其结果会传递到消费者:
map.forEach(threhold,(k,v)->k+"->"+v,System.out::println);
Long sum=map.reduceValues(threhold,Long::sum);
Integer maxlength=map.reduceKeys(threshold,String::length,Integer::sum);
如果映射为空, 或者所有条目都被过滤掉, reduce 操作会返回 null。如果只有一 个元素, 则返回其转换结果, 不会应用累加器
对于 int、 long 和 double 输出还有相应的特殊化操作, 分别有后缀 Tolnt、ToLong和 ToDouble.
静态 newKeySet方法会生成一个 Set
Set words=ConcurrentHashMap.newKeySet();
这个集是可变的。 如果删除这个集的元素,这个键(以及相应的值)会从映射中删除。不过,不能向键集增加 元素,因为没有相应的值可以增加。Java SE 8 为 ConcurrentHashMap增加了第二个 keySet方 法,包含一个默认值,可以在为集增加元素时使用:
Set
words.add("java”); 如果 "Java”在 words 中不存在,现在它会有一个值 1.
要对原容器A做增删改,就先拷贝一份为B,在B中做增、删、改。此时其他线程读的是A的数据。修改完成之后把指向A的引用改变指向到B,这样完成操作。
CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数, 这样的安排是很有用的。当构建一个迭代器的时候, 它包含一个对当前数组的引用。如果数组后来被修改 了,迭代器仍然引用旧数组, 但是,集合的数组已经被替换了。因而,旧的迭代器拥有一致 的(可能过时的)视图,访问它无须任何同步开销。
在 Java SE 8中, Arrays类提供了大量并行化操作。静态 Arrays.parallelSort 方法可以对 一个基本类型值或对象的数组排序。例如:
parallelSetAll 方法会用由一个函数计算得到的值填充一个数组。这个函数接收元素索引, 然后计算相应位置上的值。
Arrays.parallelSort(words);
最后还有一个 parallelPrefix 方法,它会用对应一个给定结合操作的前缀的累加结果替换 各个数组元素.
Arrays.parallelPrefix(values, (x, y)-> x * y) 之后,数组将包含: [1,1*2,1*2*3,...]
Vector 和 Hashtable类就提供了线程安全的动态数组和散列表的 实现。现在这些类被弃用了, 取而代之的是 AnayList 和 HashMap类。这些类不是线程安全 的,而集合库中提供了不同的机制。任何集合类都可以通过使用同步包装器(synchronization wrapper) 变成线程安全的:
List list=Collections.synchronizedList(new ArrayList<>);
Map synchHashMap=Collections.synchronizeMap(new HashMap());
结果集合的方法使用锁加以保护,提供了线程安全访问
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“ 客户端” 锁定:
synchronized (synchHashMap)
{ Iterator
如果使用“ foreach” 循环必须使用同样的代码, 因为循环使用了迭代器。注意:如果在 迭代过程中,别的线程修改集合,迭代器会失效,抛出 ConcurrentModificationException异 常。同步仍然是需要的, 因此并发的修改可以被可靠地检测出来。 最好使用java.Util.COnciirrent 包中定义的集合, 不使用同步包装器中的。特别是, 假如它 们访问的是不同的桶, 由于 ConcurrentHashMap 已经精心地实现了,多线程可以访问它而且 不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的 ArrayList 可 以胜过 CopyOnWriteArrayList().
Runnable 封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable 与 Runnable 类似,但是有返回值。Callable 接口是一个参数化的类型, 只有一 个方法 call。类型参数是返回值的类型。例如, Callable
Future 保存异步计算的结果。可以启动一个计算,将 Future 对象交给某个线程,然后忘 掉它。Future 对象的所有者在结果计算好之后就可以获得它。
Future接口的方法:
public interface Future
{ V get() throws ..
V get(long timeout, TimeUnit unit) throws ..
void cancel(boolean maylnterrupt);
boolean isCancelled();
boolean isDone(); }
FutureTask 包装器是一种非常便利的机制, 可将 Callable转换成 Future 和 Runnable, 它 同时实现二者的接口.
例:
package future;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
/**
* @version 1.01 2012-01-26
* @author Cay Horstmann
*/
public class FutureTest
{
public static void main(String[] args)
{
try (Scanner in = new Scanner(System.in))
{
System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): ");
String directory = in.nextLine();
System.out.print("Enter keyword (e.g. volatile): ");
String keyword = in.nextLine();
MatchCounter counter = new MatchCounter(new File(directory), keyword);
FutureTask 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)
{
}
}
}
}
/**
* This task counts the files in a directory and its subdirectories that contain a given keyword.
*/
class MatchCounter implements Callable
{
private File directory;
private String keyword;
/**
* Constructs a MatchCounter.
* @param directory the directory in which to start the search
* @param keyword the keyword to look for
*/
public MatchCounter(File directory, String keyword)
{
this.directory = directory;
this.keyword = keyword;
}
public Integer call()
{
int count = 0;
try
{
File[] files = directory.listFiles();
List> results = new ArrayList<>();
for (File file : files)
if (file.isDirectory())
{
MatchCounter counter = new MatchCounter(file, keyword);
FutureTask task = new FutureTask<>(counter);
results.add(task);
Thread t = new Thread(task);
t.start();
}
else
{
if (search(file)) count++;
}
for (Future result : results)
try
{
count += result.get();
}
catch (ExecutionException e)
{
e.printStackTrace();
}
}
catch (InterruptedException e)
{
}
return count;
}
/**
* Searches a file for a given keyword.
* @param file the file to search
* @return true if the keyword is contained in the file
*/
public boolean search(File file)
{
try
{
try (Scanner in = new Scanner(file, "UTF-8"))
{
boolean found = false;
while (!found && in.hasNextLine())
{
String line = in.nextLine();
if (line.contains(keyword)) found = true;
}
return found;
}
}
catch (IOException e)
{
return false;
}
}
}
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.*;
import java.util.concurrent.*;
import java.lang.*;
import java.io.*;
import java.lang.reflect.*;
public class Main{
public static void main(String[] args) throws InterruptedException, ExecutionException {
//包装住Collable,这样可以用Thread执行这个任务
FutureTask task=new FutureTask<>(()-> {
TimeUnit.MILLISECONDS.sleep(500);
return 1000;
});
new Thread(task).start();
//get函数阻塞得到结果
System.out.println(task.get());
ExecutorService service=Executors.newFixedThreadPool(5);
//Future拿到返回值
Future f=service.submit(()->{
TimeUnit.MILLISECONDS.sleep(500);
return 1;
});
System.out.println(f.get());
}
}
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.*;
import java.util.concurrent.*;
import java.lang.*;
import java.io.*;
import java.lang.reflect.*;
//线程池和Callable实现的多线程完成质数计算。
public class Main{
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService service=Executors.newFixedThreadPool(5);
MyTask t1=new MyTask(1,80000);
MyTask t2=new MyTask(80001,13000);
MyTask t3=new MyTask(13001,17000);
MyTask t4=new MyTask(17001,20000);
Future> f1=service.submit(t1);
Future> f2=service.submit(t2);
Future> f3=service.submit(t3);
Future> f4=service.submit(t4);
f1.get();
f2.get();
f3.get();
f4.get();
}
static class MyTask implements Callable>{
int x;
int y;
public MyTask(int x,int y) {
this.x=x;
this.y=y;
}
@Override
public List call() throws Exception {
// TODO Auto-generated method stub
//返回一个计算素数的列表,省略
return null;
}
}
}
Executors操作Executor的一个工具类
Executor接口只有一个execute(Runnable r)函数.执行某一个任务
ExecutorService接口继承Executor接口 。在后台不停的运行,等待被扔任务。
boolean |
awaitTermination(long timeout, TimeUnit unit) 阻止所有任务在关闭请求完成后执行,或发生超时,或当前线程中断,以先到者为准。 |
|
invokeAll(Collection extends Callable 执行给定的任务,返回持有他们的状态和结果的所有完成的期货列表。 |
|
invokeAll(Collection extends Callable 执行给定的任务,返回在所有完成或超时到期时持有其状态和结果的期货列表,以先发生者为准。 |
|
invokeAny(Collection extends Callable 执行给定的任务,返回一个成功完成的结果(即没有抛出异常),如果有的话。 |
|
invokeAny(Collection extends Callable 执行给定的任务,返回一个已经成功完成的结果(即,不抛出异常),如果有的话在给定的超时之前过去。 |
boolean |
isShutdown() 如果此执行者已关闭(说我关了,但是有的任务可能还在跑),则返回 |
boolean |
isTerminated() 如果所有任务在关闭后完成,则返回 |
void |
shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。 |
List |
shutdownNow() 尝试停止所有主动执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。 |
|
submit(Callable 提交值返回任务以执行,并返回代表任务待处理结果的Future。 |
Future> |
submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来。 |
|
submit(Runnable task, T result) 提交一个可运行的任务执行,并返回一个表示该任务的未来。 |
构建一个新的线程是有一定代价的, 因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的 空闲线程。将 Runnable 对象交给线程池, 就会有一个线程调用 run方法。当 run 方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使 虚拟机崩溃。如果有一个会创建许多线程的算法, 应该使用一个线程数“ 固定的” 线程池以 限制并发线程的总数。 执行器(Executors) 类有许多静态工厂方法用来构建线程池
newScheduledThreadPool
newCachedThreadPool
newFixedThreadPool
newSingleThreadExecutor
newWorkStealingPool 工作窃取:偷工作的线程池。这个线程main函数结束了,可能线程还在运行,可能看不到输出。每个线程维护自己的队列,当某个任务完成自己的任务之后,去偷别线程的任务自动执行。使用ForkJoin实现。
ForkJoinPool 分叉合并
任务队列BlokingQueue
维护两个队列,一个等待执行的任务队列,一个已完成的线程队列。
newCachedThreadPool方法构建了一个线程池, 对于每个任务, 如果有空闲线程可用,立即让它执行 任务,如果没有可用的空闲线程, 则创建一个新线程。newFixedThreadPool 方法构建一个具 有固定大小的线程池。如果提交的任务数多于空闲的线程数, 那么把得不到服务的任务放 置到队列中。当其他任务完成以后再运行它们。newSingleThreadExecutor 是一个退化了的大小为 1 的线程池: 由一个线程执行提交的任务,一个接着一个。这 3 个方法返回实现了 ExecutorService 接口的 ThreadPoolExecutor 类的对象。
可用下面的方法之一将一个 Runnable 对象或 Callable 对象提交给 ExecutorService:
Future> submit(Runnable task)
Future
submit(Runnable task, T result) Future
submit(Callable task) 该池会在方便的时候尽早执行提交的任务。调用 submit 时,会得到一个 Future 对象, 可 用来查询该任务的状态。
候得到它。 当用完一个线程池的时候, 调用 shutdown。该方法启动该池的关闭序列。被关闭的执 行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。另一种方法是调用 shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。 下面总结了在使用连接池时应该做的事:
1) 调用 Executors 类中静态的方法 newCachedThreadPool 或 newFixedThreadPool。
2) 调用 submit 提交 Runnable 或 Callable对象。
3 ) 如果想要取消一个任务, 或如果提交 Callable 对象, 那就要保存好返回的 Future 对象。
4 ) 当不再提交任何任务时,调用 shutdown。
package threadPool;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
/**
* @version 1.02 2015-06-21
* @author Cay Horstmann
*/
public class ThreadPoolTest
{
public static void main(String[] args) throws Exception
{
try (Scanner in = new Scanner(System.in))
{
System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): ");
String directory = in.nextLine();
System.out.print("Enter keyword (e.g. volatile): ");
String keyword = in.nextLine();
ExecutorService pool = Executors.newCachedThreadPool();
MatchCounter counter = new MatchCounter(new File(directory), keyword, pool);
Future result = pool.submit(counter);
try
{
System.out.println(result.get() + " matching files.");
}
catch (ExecutionException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
}
pool.shutdown();
int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
System.out.println("largest pool size=" + largestPoolSize);
}
}
}
/**
* This task counts the files in a directory and its subdirectories that contain a given keyword.
*/
class MatchCounter implements Callable
{
private File directory;
private String keyword;
private ExecutorService pool;
private int count;
/**
* Constructs a MatchCounter.
* @param directory the directory in which to start the search
* @param keyword the keyword to look for
* @param pool the thread pool for submitting subtasks
*/
public MatchCounter(File directory, String keyword, ExecutorService pool)
{
this.directory = directory;
this.keyword = keyword;
this.pool = pool;
}
public Integer call()
{
count = 0;
try
{
File[] files = directory.listFiles();
List> results = new ArrayList<>();
for (File file : files)
if (file.isDirectory())
{
MatchCounter counter = new MatchCounter(file, keyword, pool);
Future result = pool.submit(counter);
results.add(result);
}
else
{
if (search(file)) count++;
}
for (Future result : results)
try
{
count += result.get();
}
catch (ExecutionException e)
{
e.printStackTrace();
}
}
catch (InterruptedException e)
{
}
return count;
}
/**
* Searches a file for a given keyword.
* @param file the file to search
* @return true if the keyword is contained in the file
*/
public boolean search(File file)
{
try
{
try (Scanner in = new Scanner(file, "UTF-8"))
{
boolean found = false;
while (!found && in.hasNextLine())
{
String line = in.nextLine();
if (line.contains(keyword)) found = true;
}
return found;
}
}
catch (IOException e)
{
return false;
}
}
}
出于信息方面的考虑, 这个程序打印出执行中池中最大的线程数。 但是不能通过 ExecutorService 这个接口得到这一信息。因此, 必须将该pool 对象强制转换为 ThreadPoolExecutor 类对象。
ScheduledExecutorService 接口具有为预定执行(Scheduled Execution) 或 重 复 执 行 任 务而设计的方法。它是一种允许使用线程池机制的java.util.Timer 的泛化。Executors 类的 newScheduledThreadPool 和 newSingleThreadScheduledExecutor方法将返回实现了 Scheduled-ExecutorService 接口的对象。 可以预定 Runnable 或 Callable 在初始的延迟之后只运行一次。也可以预定一个 Runnable 对象周期性地运行。
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.*;
import java.util.concurrent.*;
import java.lang.*;
import java.io.*;
import java.lang.reflect.*;
public class Main{
public static void main(String[] args) throws InterruptedException, ExecutionException {
ScheduledExecutorService service=Executors.newScheduledThreadPool(4);
service.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName());
},0,500, TimeUnit.MILLISECONDS);
}
}
有 时, 使用执行器有更有实际意义的原因, 控制一组相关任务。invokeAny方法提交所有对象到一个 Callable 对象的集合中,并返回某个已经完成了的 任务的结果。无法知道返回的究竟是哪个任务的结果, 也许是最先完成的那个任务的结果。 对于搜索问题, 如果你愿意接受任何一种解决方案的话,你就可以使用这个方法。
List>tasks=...;
list> results=executor.invokeAll(tasks);
for(Future:results){
processFurther(result.get(i));
}
这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结 果按可获得的顺序保存起来更有实际意义。可以用 ExecutorCompletionService 来进行排列。 用常规的方法获得一个执行器。然后, 构建一个 ExecutorCompletionService, 提交任务 给完成服务。更有效:
ExecutorCompletionService service = new ExecutorCompletionServiceo(executor);
for (Callable task : tasks)
service,submit(task);
for (int i = 0; i < tasks.size();i++)
processFurther(service.take().get());
ForkJoinTask的api
static |
adapt(Callable extends T> callable) 返回一个新的 |
static ForkJoinTask> |
adapt(Runnable runnable) 返回一个新的 |
static |
adapt(Runnable runnable, T result) 返回一个新的 |
boolean |
cancel(boolean mayInterruptIfRunning) 尝试取消执行此任务。 |
boolean |
compareAndSetForkJoinTaskTag(short e, short tag) 以原子方式有条件地设置此任务的标签值。 |
void |
complete(V value) 完成此任务,如果尚未中止或取消,返回给定的值作为后续调用的结果 |
void |
completeExceptionally(Throwable ex) 完成此任务异常,如果尚未中止或取消,将导致给定异常 |
protected abstract boolean |
exec() 立即执行此任务的基本操作,并返回true,如果从此方法返回后,此任务将保证已正常完成。 |
ForkJoinTask |
fork() 在当前任务正在运行的池中异步执行此任务(如果适用),或使用 |
V |
get() 等待计算完成,然后检索其结果。 |
V |
get(long timeout, TimeUnit unit) 如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。 |
Throwable |
getException() 返回由基础计算抛出的异常,或 |
short |
getForkJoinTaskTag() 返回此任务的标签。 |
static ForkJoinPool |
getPool() 返回托管当前任务执行的池,如果此任务在任何ForkJoinPool之外执行,则返回null。 |
static int |
getQueuedTaskCount() 返回当前工作线程已分配但尚未执行的任务数量的估计。 |
abstract V |
getRawResult() 返回由 |
static int |
getSurplusQueuedTaskCount() 返回当前工作线程保留的本地排队任务数量多于可能窃取它们的其他工作线程的估计值,如果该线程未在ForkJoinPool中运行,则返回零。 |
static void |
helpQuiesce() 可能执行任务,直到托管当前任务的池 |
static boolean |
inForkJoinPool() 返回 |
V |
invoke() 执行此任务后,如有必要,等待其完成,并返回其结果,或者如果基础计算执行此操作,则抛出(未选中) |
static |
invokeAll(Collection 叉指定集合中的所有任务,当 |
static void |
invokeAll(ForkJoinTask>... tasks) 叉出给定的任务,当每个任务保持isDone时 |
static void |
invokeAll(ForkJoinTask> t1, ForkJoinTask> t2) 叉出给定的任务,当每个任务保持isDone时 |
boolean |
isCancelled() 如果此任务在正常完成之前被取消,则返回 |
boolean |
isCompletedAbnormally() 如果此任务抛出异常或被取消,返回 |
boolean |
isCompletedNormally() 如果此任务完成而不抛出异常并且未被取消,则返回 |
boolean |
isDone() 返回 |
V |
join() 当 |
protected static ForkJoinTask> |
peekNextLocalTask() 返回,但不会取消调度或执行当前线程排队但尚未执行的任务(如果可以立即可用)。 |
protected static ForkJoinTask> |
pollNextLocalTask() 如果当前线程正在ForkJoinPool中运行,则不执行当前线程排队的下一个任务但尚未执行的时间并返回。 |
protected static ForkJoinTask> |
pollTask() 如果当前线程在ForkJoinPool中运行,则不执行下一个任务,返回当前线程排队的下一个任务,但尚未执行,如果一个可用,或者如果不可用,则由其他线程分派的任务,如果可供使用的话。 |
void |
quietlyComplete() 正常完成此任务而不设置值。 |
void |
quietlyInvoke() 执行此任务并等待其完成(如有必要),而不返回其结果或抛出异常。 |
void |
quietlyJoin() 加入此任务,而不返回其结果或抛出异常。 |
void |
reinitialize() 重置此任务的内部簿记状态,允许随后的 |
short |
setForkJoinTaskTag(short tag) 原子地设置此任务的标签值。 |
protected abstract void |
setRawResult(V value) 强制给定的值作为结果返回。 |
boolean |
tryUnfork() 尝试取消执行此任务。 |
forkjoinpoll的任务必须是forkjointask。一般使用forkjointask的子类RecuesiveAction(没有返回值)和RecursiveTask(有返回值)
有一个大任务,分成若干个小任务,将结果进行合并。线程的启动由切分的规则有线程池自己启动。
要采用框架可用的一种方式完成这种递归计算, 需要提供一个扩展 RecursiveTask() 的 类(如果计算会生成一个类型为 T 的结果)或者提供一个扩展 RecursiveAction 的类(如果不 生成任何结果)。再覆盖 compute方法来生成并调用子任务,然后合并其结果.
例:
package forkJoin;
import java.util.concurrent.*;
import java.util.function.*;
/**
* This program demonstrates the fork-join framework.
* @version 1.01 2015-06-21
* @author Cay Horstmann
*/
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.execute(counter);
System.out.println(counter.join());
}
}
class Counter extends RecursiveTask
{
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;
}
protected Integer compute()
{
//如果线程的数量小于THRESHOLD个
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();
}
}
}
待完成。。。。。
线程池多少个线程、最多装多少个线程、最多多长时间消失、BlockingQueue任务队列。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue 创建一个新的 |
java.util.concurrent 包包含了几个能帮助人们管理相互合作的线程集的类见表 14-5。这 些机制具有为线程之间的共用集结点模式(common rendezvous patterns) 提供的“ 预置功能” ( canned functionality)。 如果有一个相互合作的线程集满足这些行为模式之一, 那么应该直接 重用合适的库类而不要试图提供手工的锁与条件的集合。
概念上讲,一个信号量管理许多的许可证(permit)。为了通过信号量,线程通过调用 acquire请求许可。其实没有实际的许可对象, 信号量仅维护一个计数。许可的数目是固定 的,由此限制了通过的线程数量。其他线程可以通过调用 release 释放许可。而且,许可不是二 必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
一个倒计时门栓(CountDownLatch) 让一个线程集等待直到计数变为 0。倒计时门栓是 一次性的。一旦计数为 0, 就不能再重用了。 一个有用的特例是计数值为 1 的门栓。实现一个只能通过一次的门。线程在门外等候直 到另一个线程将计数器值置为 举例来讲, 假定一个线程集需要一些初始的数据来完成工作。工作器线程被启动并在门 外等候。另一个线程准备数据。当数据准备好的时候, 调用 cmmtDown, 所有工作器线程就 可以继续运行了。 然后,可以使用第二个门栓检査什么时候所有工作器线程完成工作。用线程数初始化门 栓。每个工作器线程在结束前将门栓计数减 1。另一个获取工作结果的线程在门外等待,一 旦所有工作器线程终止该线程继续运行。
CyclicBarrier 类实现了一个集结点(rendezvous) 称为障栅(barrier)。考虑大量线程运行 在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线 程完成了它的那部分任务后, 我们让它运行到障栅处。一旦所有的线程都到达了这个障栅, 障栅就撤销,线程就可以继续运行。 下面是其细节。首先, 构造一个障栅,并给出参与的线程数:
CyclicBarrier barrier = new CydicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用 await :
public void run(){ doWork(); bamer.await();
await 方法有一个可选的超时参数: barrier.await(100, TineUnit.MILLISECONDS); 如果任何一个在障栅上等待的线程离开了障栅, 那么障栅就被破坏了(线程可能离开是 因为它调用 await 时设置了超时,或者因为它被中断了) 。在这种情况下,所有其他线程的 await 方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await 的调用。
可以提供一个可选的障栅动作(barrier action), 当所有线程到达障栅的时候就会执行这 一动作。
Runnable barrierAction = ..
CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);
该动作可以收集那些单个线程的运行结果。 障栅被称为是循环的(cyclic), 因为可以在所有等待线程被释放后被重用。在这一点上, 有别于 CountDownLatch, CountDownLatch 只能被使用一次。 Phaser 类增加了更大的灵活性,允许改变不同阶段中参与线程的个数
当两个线程在同一个数据缓冲区的两个实例上工作的时候, 就可以使用交换器 ( Exchanger) 典型的情况是, 一个线程向缓冲区填人数据, 另一个线程消耗这些数据。当它 们都完成以后,相互交换缓冲区。
同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take方法为止,反之亦然。与 Exchanger 的情 况不同, 数据仅仅沿一个方向传递,从生产者到消费者。 即使 SynchronousQueue 类实现了 BlockingQueue 接口, 概念上讲, 它依然不是一个队 列。它没有包含任何元素,它的 size方法总是返回 0。
例子:实现一个集合,在第一个线程中加十个数,第二个线程在加到5个的时候发出响应。
volatile实现
import java.util.*; import java.util.concurrent.TimeUnit; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { public volatile List
l; public Main() { l=new LinkedList<>(); } public static void main(String[] args) { Main m=new Main(); Thread t1=new Thread(new Runnable() { public void run() { for(int i=0;i<10;i++) { m.l.add(i); System.out.println("add "+i); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }); t1.start(); Thread t2=new Thread(new Runnable() { public void run() { while(true) { if(m.l.size()==5) break; } System.out.println("5个了"); } }); t2.start(); } } 第二个实现方式,wait和notify
就是锁定一个对象,一个线程调用这个对象的wait,只有其它线程调用这个对象的notify时这个线程才能继续熏晕。wait释放锁,sleep和notify不释放锁。
使用一个对象的wait和notify必须对该对象进行锁定。斗则不能调用。
import java.util.*; import java.util.concurrent.TimeUnit; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { public volatile List
l; public Main() { l=new LinkedList<>(); } public static void main(String[] args) { Main m=new Main(); final Object o=new Object(); Thread t2=new Thread(new Runnable() { public void run() { synchronized(o) { if(m.l.size()!=5) { try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("5个了"); } } }); t2.start(); Thread t1=new Thread(new Runnable() { public synchronized void run() { synchronized(o) { for(int i=0;i<10;i++) { m.l.add(i); System.out.println("add "+i); if(i==4) o.notify(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }); t1.start(); } } 如上所示的代码,还是不能在5时停止。为什么呢,因为notify不能释放锁,t1不能在notify的时候释放锁
改正版本:
import java.util.*; import java.util.concurrent.TimeUnit; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { public volatile List
l; public Main() { l=new LinkedList<>(); } public static void main(String[] args) { Main m=new Main(); final Object o=new Object(); Thread t2=new Thread(new Runnable() { public void run() { synchronized(o) { if(m.l.size()!=5) { try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("5个了"); o.notify(); } } }); t2.start(); Thread t1=new Thread(new Runnable() { public synchronized void run() { synchronized(o) { for(int i=0;i<10;i++) { m.l.add(i); System.out.println("add "+i); if(i==4){ o.notify(); try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }); t1.start(); } } 上面代码加上了在t1线程上wait,释放锁。
第三个实现方法,倒计时门栓:
import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { public volatile List
l; public Main() { l=new LinkedList<>(); } public static void main(String[] args) { Main m=new Main(); final Object o=new Object(); CountDownLatch count=new CountDownLatch(1); Thread t2=new Thread(new Runnable() { public void run() { if(m.l.size()!=5) { try { count.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("5个了"); } }); t2.start(); Thread t1=new Thread(new Runnable() { public void run() { for(int i=0;i<10;i++) { m.l.add(i); System.out.println("add "+i); if(i==4) count.countDown(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }); t1.start(); } }
例2:
实现方式1:wait和notify
//生产者消费者问题 //写出一个容器,get,put方法。 import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.*; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { final private List l=new LinkedList<>(); public synchronized void get() { while(l.size()==0) { try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } l.remove(0); this.notifyAll(); } } public synchronized void put(int t) { //为什么用while。因为如果叫醒所有线程之后,所有线程都被 //唤醒,线程1向下运行,结果线程2已经向容器中扔了一个数,线程1 //再仍就会产生错误,应该用while,可以再次判断容器满没满。 while(l.size()==10) { try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } l.add(t); //为什么要notifyAll()?如果notify叫醒的线程又是一个生产者,就死锁 //了。 this.notifyAll(); } } public static void main(String[] args) { } }
方法二:Condition
//生产者消费者问题 //写出一个容器,get,put方法。 import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.*; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { private Lock lock=new ReentrantLock(); private Condition producer=lock.newCondition(); private Condition consumer=lock.newCondition(); private List
l=new LinkedList<>(); public void get() { lock.lock(); while(l.size()==0) { try { consumer.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } l.remove(0); producer.signalAll(); } lock.unlock(); } public void put(int t) { //为什么用while。因为如果叫醒所有线程之后,所有线程都被 //唤醒,线程1向下运行,结果线程2已经向容器中扔了一个数,线程1 //再仍就会产生错误,应该用while,可以再次判断容器满没满。 lock.lock(); while(l.size()==10) { try { producer.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } l.add(t); //为什么要notifyAll()? consumer.signalAll(); } lock.unlock(); } public static void main(String[] args) { } }
例子3:(引出队列什么的)
N张火车票,每张票一个编号。同时10个窗口对外售票,写一个模拟程序
import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.*; import java.lang.*; import java.io.*; import java.lang.reflect.*; public class Main { static Queue
tickets=new ConcurrentLinkedQueue<>(); static { for(int i=0;i<1000;i++) tickets.add("piaobianhao"+i); } public static void main(String[] args) { for(int i=0;i<10;i++) { new Thread(()-> { while(true) { String s=tickets.poll(); if(s==null) break; else System.out.println("销售了.."+s); } }).start(); } } }
并发容器:
ConcurrentHashMap
ConcurrentSkipListMap
HashTable 所有实现带锁。