多线程和多进程的本质区别在于,每个进程拥有自己的一整套变量,而线程则共享数据。与进程相比,线程通常更轻量级。创建/撤销一个线程比启动新进程的开销要小得多。
Thread类的静态sleep方法将暂停给定的毫秒数,调用Thread.sleep不会创建一个新线程,sleep使Thread类的静态方法,用于暂停当前线程的活动。
创建一个线程的一种方法:
Runnable接口中只有一个方法
public interface Runnable
{
void run();
}
范例如下:
Thread aThread = new Thread(new Runnable(){
int i=0;
long time = System.currentTimeMillis();
long now = System.currentTimeMillis();
public void run()
{
while(true)
{
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
now = System.currentTimeMillis();
System.out.println("output : " + (now - time) + " " +Thread.currentThread().getName());
time = now;
}
}
});
aThread.start();
不要调用Thread类或Runnable对象的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start方法。这个方法将创建一个执行run方法的新线程。
当线程的run方法执行方法体中最后一条语句后,并经由return语句返回时,或者出现了在方法中过没有捕获的异常时,线程将被终止。(不使用return语句,线程应也会终止,测试如下。)
aThread.start();
Thread.sleep(10000);
Set keySet = Thread.getAllStackTraces().keySet();
for(Thread thread : keySet)
{
System.out.println(thread.getName());
}
interrupt方法可以请求终止线程。当对一个线程调用interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志。
首先调用静态的Thread.currentThread方法获得当前线程,然后调用isIterrupted方法:
while(!Thread.currentThread().isInterrupt() && more work to do)
{
do more work
}
但是,如果线程被阻塞,就无法检查中断状态。这是产生InterruptException异常的地方。没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。
普遍的情况是,线程将简单地将中断作为一个终止的请求。如果在每次工作迭代之后都调用sleep方法(或者其他的可中断方法),isInterrupted检测既没必要也没用处。如果在中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态并抛出InterruptedException。
Interrupt方法是一个静态方法,它检测当前的线程是否被中断。调用interrupt方法会清除该线程的中断状态。
isInterrupted方法是一个实例方法,不会改变中断状态
线程可以有以下6种状态:
当用new操作符创建一个新线程时,该线程还没有开始运行,它的状态是new
一旦调用start方法,线程处于runnable状态。抢占式调度系统给每一个可运行线程一个时间片来执行任务。
现在所有的桌面以及服务器操作系统都使用抢占式调度。但是,像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用yield方法,或者被阻塞或等待时,线程才失去控制权。
在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行。
当线程处于被阻塞状态或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。
线程因为以下原因被终止
可以用stop方法杀死一个线程,但stop方法已过时,不要使用
java.lang.Thread 1.0中的重要方法
void join() 等待终止指定的线程
默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置在MIN_PRIORITY (在Thread类中定义为1)与MAX_PRIORITY(定义为10)之间的任何值。NORM_被定义为5。
Windows有7个优先级级别,一些Java优先级将映射到相同的操作系统优先级。在Sun为Linux提供的Java虚拟机,线程的优先级被忽略。
不要将程序构建为功能的正确性依赖于优先级。
可以通过调用t.SetDaemon(true);将线程转换为守护线程(daemon thread)。守护线程的唯一用途是为其他线程服务,如定时发送“时间滴答”信号给其他线程或清空过时的高速缓存项的计时线程。当只剩下守护线程时,虚拟机就退出了。
守护线程永远不应该去访问固有资源,如文件,数据库。因为它会在任何时候甚至一个操作的中间发生中断。
线程的run方法不能抛出任何已检查的异常,但是,不被检测的异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何catch子句类处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法
void uncaughtException(Thread t, Throwable e)
从Java SE 5.0起,可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组。不要在自己的程序中使用线程组。
ThreadGroup对象实现Thread.UncaughtExceptionHandler接口。它的uncaughtException方法做如下操作:
根据各线程访问数据的次序,可能会产生讹误的对象。这样的一个情况称为竞争条件(race condition)。
银行例程:多线程操作时,本应恒等的余额总值发生了变化。
写例程时的想法:错误检测和抛出异常的选择。
package learn.test.object;
public class BankTest
{
public static void main(String[] args)
{
int userCount = 20;
int initBalance = 10000;
Bank aBank = new Bank(userCount, initBalance);
for(int i=0; inew TransferUser(i, aBank);
Thread t = new Thread(aUser);
t.start();
}
}
}
class Bank
{
public Bank(int userCount, double initBalance)
{
if(userCount < 0)
return;
accounts = new double[userCount];
for(int i=0; ipublic String transfer(int from, int to, double balance)
{
String result = "tranfer from [" + from + "] to [" + to + "] " + " balance = [" + balance + "]";
if(from<0 || to<0)
return result + " FAILED";
if(from >= accounts.length || to >= accounts.length)
return result + " FAILED";
if(from == to )
return result + " FAILED";
if(accounts[from] < balance)
return result + " FAILED";
accounts[from] -= balance;
accounts[to] += balance;
return result + " SUCCESSED";
}
public int getUserCount()
{
return accounts.length;
}
public double getBalance(int i)
{
return accounts[i];
}
public String toString()
{
double total = 0;
// System.out.println("user count = " + accounts.length);
for(int i=0; i// System.out.println("user id = " + i + " ; balance = " + accounts[i]);
total += accounts[i];
}
return "total balance = " + total;
}
private double[] accounts = new double[0];
}
class TransferUser implements Runnable
{
public TransferUser(int id, Bank aBank)
{
if(id<0 || id>=aBank.getUserCount())
return;
this.id = id;
this.aBank = aBank;
}
public void run()
{
while(true)
{
try
{
Thread.sleep((int)(Math.random() * DELAY));
}
catch (InterruptedException e)
{
e.printStackTrace();
}
int target = (int)(Math.random()*aBank.getUserCount());
double transferBalance = Math.random() * aBank.getBalance(id);
System.out.println(aBank.transfer(id, target, transferBalance));
System.out.println(aBank);
}
}
private Bank aBank = null;
private int id = -1;
public static final int DELAY = 10;
}
假定两个线程同时执行指令
accounts[to] += amount;
这不是原子操作。该指令可能被处理如下:
1)将accounts[to]加载到寄存器
2)增加amount
3)将结果写回accounts[to]
假定第一个线程执行步骤1和2,然后,它被剥夺了运行权。假定第二个线程被唤醒并修改了accounts数组中的同一项。然后,第一个线程被唤醒并完成其第三步。这一动作擦去了第二个线程所做的更新。
更具体地,代码行accounts[to] += amount;
被转换为下面的字节码
aload_0
getfield #2; //Field accounts:[D
iload_2
dup2
daload
dload_3
dadd
dastore
从Java SE 5.0开始,有两种机制防止代码块受并发访问的干扰。Java语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类。
ReetrantLock保护代码块的基本结构如下:
public class Bank
{
public void transfer(int from, int to, int amount)
{
bankLock.lock();
try
{
System.out.printt(Thread.currentThread());
accounts[from] -= amount;
System.out.println("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
private Lock bankLock = new ReetrantLock(); // ReentrantLock implements the Lock interface
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果是两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这种特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
要留心临界区中的代码,即使在finally子句中释放了锁,也要注意对象是否处于受损状态。
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。(由于历史的原因,条件对象经常被称为条件变量(conditional variable)。)
注意不能使用这样的代码:
if(bank.getBalance(from) >= amount))
//thread might be deactivated at this point
bank.transfer(from, to , amount)
线程完全有可能在成功完成测试,且在调用transfer方法之前被中断。
通过使用锁来检查:
public void transfer(int from, int to, int amount)
{
bankLock.lock();
try
{
while(accounts[from] < amount)
{
//wait
...
}
...
}
finally
{
bankLock.unlock();
}
}
现在,当账户中没有足够的余额时,等待直到另一个线程向账户中注入了资金。但是,这一线程刚刚获得了对bankLock的排他性访问,因此别的线程没有进行存款的机会。这就是需要条件对象的原因。
一个锁对象可以有一个或多个相关的条件对象。你可以用newCondition方法获得一个条件对象。
class Bank
{
public Bank()
{
...
sufficientFunds = bankLock.newCondition();
}
...
private Condition sufficientFunds;
}
如果transfer发现余额不足,它调用
sufficientFunds.await()
使线程阻塞并放弃锁。等待获得锁的线程和调用await方法的线程存在本质的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法。当另一个线程转账时,它应该调用
sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程,调度器将再次激活它们,同时,它们将试图重新进入该对象,一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
但是signalAll方法只是通知正在等待的线程,可能满足条件,值得再次检测该条件。因此,await的调用通常在如下形式的循环中:
while(!(ok to proceed))
condition.await();
当一个线程调用await时,它没有办法重新激活自身。如果没有其他线程来重新激活等待的线程,它就永远不能再运行了。这将导致令人不快的死锁(deadlock)现象。
在对象的状态有利于等待线程的方向改变时调用signalAll。signalAll只是解除等待线程的阻塞,而非立即激活一个等待线程。
java.util.concurrent.locks.Condition 5.0 中的singal方法在该条件的等待集中随机选择一个线程,解除其阻塞状态。
从1.0版开始,Java中每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法,也即
public synchronized void method()
{
method body
}
等价于
public void method()
{
this.intrinsicLock.lock();
try
{
method body
}
finally
{
this.intrinsicLock.unlock();
}
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除线程的阻塞状态。调用wait或notifyAll等价于
intrinsicCondition.await();
intrinsicCondition.signalAll();
wait,notifyAll以及notify方法是Object类的final方法。Condition方法重新命名以避免发生冲突。
将静态方法声明为synchronized也是合法的。调用这个方法时,这个方法会获得相关的类对象的内部锁。如果Bank类有一个静态同步的方法,这个方法被调用时,Bank.class对象的锁被锁住。因此,没有任何其他线程可以调用同一个类的这个或任何其他的同步静态方法。
内部锁和条件存在的局限:
每个锁仅有单一的条件,可能是不够的
关于内部锁和条件的使用建议:
最好不使用Lock/Condition也不适用synchronized关键字。在许多情况下可以使用java.util.concurrent包中的一种机制处理加锁
每一个Java对象都有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized(obj) //this is the syntax for a synchronized block
{
critical section
}
于是它获得obj的锁。
有时会发现“特殊的”锁,例如:
public class Bank
{
public void transfer(int from, int to, int amount)
{
synchronized(lock) // an ad-hoc lock
{
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(...);
}
...
private double[] accounts;
private Object lock = new Object();
}
在此,lock对象被创建仅仅是用来使用这个对象持有的锁。
有时程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定(client-side locking)。如下:
public void transfer(Vector accounts, int from, int to, int amount)
{
synchronized(accounts)
{
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
}
System.out.println(...);
}
这个方法是否可以工作依赖于Vector类是否对自己的所有可修改的方法都使用内部锁。如此才能保证一个线程的set方法不会被另一个线程的set方法中断,丢失其中的一次修改。
监视器具有如下特性:
Java设计者以不是很精确的方式采用了监视器概念。每个对象有一个内部锁和内部条件。如果一个方法用synchronized声明,它的表现就像是一个监视器方法。然而,在下述的3个方面,Java对象不同于监视器,从而使线程的安全性下降:
使用现代的处理器和编译器,出错的可能性很大,原因在于:
volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
假定有一个布尔标记done,它的值被一个线程设置却被另一个线程查询,可以使用锁:
public synchornized boolean isDone() {return done;}
public synchornized boolean setDone() {done = true;}
private boolean done;
但是使用内部锁,如果另一个线程已经对该对象加锁,isDone和setDone方法可能被阻塞。一个线程可以为这一变量使用单独的Lock,但是,这也会带来许多麻烦。
在这种情况下,将域声明为volatile是合理的:
public boolean isDone() {return done;}
public void setDone() {done = true;}
private volatile boolean done;
Volatile变量不能提供原子性。例如方法
public void flipDone() {done = !done} // not atomic
不能确保改变域中的值。
在这种情况下,可以使用AtomicBoolean。这个类有方法get和set,且确保是原子的。该实现使用有效的机器指令,在不使用锁的情况下确保原子性。在java.util.concurrent.atomic中有许多包装器类用于原子的整数,浮点数,数组等。这些类是为编写并发实用程序的系统程序员提供使用的,而不是应用程序员。
在以下3个条件下,域的并发访问是安全的:
使用signal代替signalAll可能会导致死锁。因signal方法可能解锁另一个不可运行的线程,而导致所有的线程都阻塞。
Java中没有任何东西可以避免或者打破死锁现象。
tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,线程可以立即离开去做其他事情:
if(myLock.tryLock())
{
// now the thread owns the lock
try
{
...
}
finally
{
myLock.unlock();
}
}
else
{
//do something else
}
可以调用tryLock时使用超时参数
if(myLock.tryLock(100, TimeUnit.MILLISECONDS))
...
TimeUnit是一个枚举类型,可取SECONDS,MILLISECONDS,MICROSECONDS和NANOSECONDS。
lock方法不能被中断,然而,如果调用带有超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。这是一个非常有用的特性,允许程序打破死锁。
也可以调用lockInterruptibly方法,它相当于一个超时设置为无限的tryLock方法。
在等待一个条件时,也可以提供一个超时:
myCondition.await(100, TimeUnit.MILLISECONDS)
如果一个线程被另一个线程通过调用signalAll或signal激活,或者超时时限已到,或者线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出一个InterruptedException异常。在你希望出现这种情况时线程继续等待时,可以使用awaitUninterruptibly方法代替await。
java.util.concurrent.locks包定义了两个锁类,ReentrantLock类和ReentrantReadWriteLock类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是非常有用的。
//构造对象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//抽取读锁和写锁
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
//对所有的访问者加读锁
public double getTotalBalance()
{
readLock.lock();
try{...}
finally{readLock.unlock();}
}
//对所有的修改者加写锁
public void transfer(...)
{
writeLock.lock();
try{...}
finally{writeLock.unlock();}
}
当一个线程要终止另一个线程时,无法知道什么时候调用stop是安全的,什么时候导致对象被破坏。
suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用suspend的线程试图获得同一个锁,那么程序死锁;
对于实际贬称过来说,应该尽可能原理底层结构。使用由并发处理的专业人士实现的较高层次的结构要方便和安全的多。
许多线程问题可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。例如,转账程序中,转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。因此不需要同步。(当然,线程安全的队列类的实现者不能不考虑锁和条件。)
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列(blocking queue)导致线程阻塞。队列会自动地平衡负载。
阻塞队列方法:
方法 | 正常动作 | 特殊情况下的动作 |
---|---|---|
add | 添加一个元素 | 队列满时抛出IllegalStateException异常 |
element | 返回队列的头元素 | 队列空时抛出NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列满,则返回false |
peek | 返回队列的头元素 | 如果队列空,则返回null |
poll | 移出并返回队列的头元素 | 如果队列空,则返回null |
put | 添加一个元素 | 如果队列满,则阻塞 |
remove | 移出并返回头元素 | 队列空时抛出NoSuchElementException异常 |
take | 移出并返回头元素 | 如果队列空,则阻塞 |
poll和peek方法返回空来指示失败,因此,向这些队列中插入null值是非法的。
还有带有超时的offer方法和poll方法的变体。例如
boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);
尝试在100ms内在队列的尾部插入一个元素。如果成功返回true;否则,达到超时时返回false。类似地,下面的调用:
Object head = q.poll(100, TimeUnit.MILLISECONDS);
尝试在100ms内移除队列的头元素;如果成功返回头元素,否则,达到超时时返回false。
java.util.concurrent包提供了阻塞队列的几个变种。
LinkedBlockingQueue的容量在默认下是没有上边界的,也可设置之。
LinkedBlockingDeque是一个双端的版本。
ArrayBlockingQueue需要在构造时指定容量,并且有一个可选的参数来指定是否需要公平性,若设置了公平性,等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在必要时使用。
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照优先级顺序被移出。该队列没有容量上限,但是,如果队列为空,取元素的操作会阻塞。
最后,DelayQueue包含Delayed接口的对象:
interface Delayed extends Comparable
{
long getDelay(TimeUnit unit);
}
getDelay方法返回对象的残留延迟,负值表示延迟已经结束。元素只有在延迟用完的情况下才能从DelayQueue移除。还必须实现compareTo方法。DelayQueue使用该方法对元素进行排序。
仿照书中所做阻塞队列练习,开启一个线程读取目录结构,把文件添加到阻塞队列中,另外数个线程从队列中读取文件,并把文件中包含已设定关键字的行打印出来:
package learn.test.object;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueTest
{
public static void main(String[] args)
{
BlockingQueue q = new LinkedBlockingQueue(Q_SIZE);
FilePicker aFilePicker = new FilePicker(q, ROOT_DIR);
new Thread(aFilePicker).start();
for(int i=0; i< THREAD_COUNT; i++)
{
FileAnalyzer analyzer = new FileAnalyzer(q, KEY_WORD);
new Thread(analyzer).start();
}
}
public static final String ROOT_DIR = "/home/joseph/Documents/WorkSpace/Source/Java";
public static final String KEY_WORD = "public";
public static final int THREAD_COUNT = 10;
public static final int Q_SIZE = 10;
}
class FilePicker implements Runnable
{
public FilePicker(BlockingQueue q, String rootDir)
{
this.q = q;
this.rootDir = rootDir;
}
public void run()
{
try
{
pickFile();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
public void pickFile() throws InterruptedException
{
File root = new File(rootDir);
if(!root.exists())
System.out.println("the selected file/dir do not exists");
pickFile(root);
q.put(END_FLAG);
}
public void pickFile(File file) throws InterruptedException
{
if(file.isDirectory())
{
for(File subFile : file.listFiles())
pickFile(subFile);
}
else
{
q.put(file);
}
}
private BlockingQueue q;
private String rootDir;
public static final File END_FLAG = new File("");
}
class FileAnalyzer implements Runnable
{
public FileAnalyzer(BlockingQueue q, String key)
{
this.key = key;
this.q = q;
}
public void run()
{
try
{
while((curFile = q.take()) != FilePicker.END_FLAG)
{
Scanner in = new Scanner(new FileInputStream(curFile));
int lineCount = 1;
while(in.hasNext())
{
String str = in.nextLine();
if(str.contains(key))
System.out.printf("thread [%s] line [%d] in file [%s] : %s%n",Thread.currentThread().getName(), lineCount++, curFile.getName(), str);
}
}
q.put(FilePicker.END_FLAG);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
}
private File curFile;
private BlockingQueue q;
private String key;
}
java.util.concurrent包提供了映像,有序集和队列的高效实现:ConcurrentHashMap,ConcurrentSkipListMap和ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。
集合返回弱一致性(weakly consisteut)的迭代器。这意味着迭代器不一定能反映出它门被构造之后的所有修改,但是,它们不会同一个值返回两次,也不会抛出ConcurrentModificationException异常。
与之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出一个ConcurrentModificationException异常。
并发的散列映像表,可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达16个写线程同时执行。可以有更多的写线程,但是,如果同时多于16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而恐怕没有这种必要。
ConcurrentHashMap和ConcurrentSkipListMap类有相应的方法用于原子性的关联插入以及关联删除。
cache.putIfAbsent(key, value); //若原来没有这一关联,则删除
cache.remove(key, value); //原子性地删除键值对
cache.replace(key, oldValue, newValue); //原子性地替换
concurrent其他的类:
ConcurrentLinkedQueue, ConcurrentSkipListSet, ConcurrentHashMap,ConcurrentSkipListMap, ConcurrentSkipListSet(有序映射表)。
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组,但是,集合的数组已经被替换了。因而,旧的迭代器拥有一致的(可能过时的)视图,访问它无需任何同步开销。
Java SE 1.2中,Vector和Hashtable被弃用了,取而代之的使ArrayList和HashMap类。集合库提供了同步包装器(synchronization wrapper)来将任何集合类变成线程安全的。
使用同步包装器需要注意两点:
List synchArrayList = Collections.synchronizedList(new ArrayList());
Map synchHashMap = Collections.synchronizedMap(new HashMap());
synchronized( synchHashMap)
{
Iterator iter = synchHashMap.keySet().iterator();
while(iter.hasNext())...;
}
Runnable封装一个异步运行的任务,可以把它想象称为一个没有参数和返回值的异步方法。
Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call:
public interface Callable<V>
{
V call() throws Exception;
}
Future保存异步计算的结果,可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。
Future接口具有下面的方法:
public interface Future<V>
{
V get() throws ...; //阻塞至计算完成
V get(long timeout, TimeUnit unit) throws ...; //若超时,抛出TimeoutException异常
void cancel(boolean mayInterrupt); //若计算没有开始,则取消;若计算正在运行,mayInterrupt为true时中断
boolean isCancelled(); //
boolean isDone(); //计算还在进行则返回false
}
FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,它同时实现二者的接口:
Callable myComputation = ...;
FutureTask task = new FutureTask(myComputation);
Thread t = new Thread(task);
t.start;
...
Integer result = task.get();
仿照书中做练习如下,统计一目录下包含的文件中,含有“public”的行 数:
package learn.test.object;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class CallableFutrueTest
{
public static void main(String[] args)
{
File rootDir = new File(ROOT_DIR);
LineCounter lc = new LineCounter(rootDir, KEY_WORD);
FutureTask ft = new FutureTask(lc);
Thread t = new Thread(ft);
t.start();
try
{
System.out.println("line count = " + ft.get());
}
catch (InterruptedException | ExecutionException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static final String ROOT_DIR = "/home/joseph/Documents/WorkSpace/Source/Java";
public static final String KEY_WORD = "public";
}
class LineCounter implements Callable
{
public LineCounter(File dir, String key)
{
this.dir = dir;
this.key = key;
}
public Integer call() throws Exception
{
int counter = 0;
File[] files = dir.listFiles();
for(File f : files)
{
if(!f.isDirectory())
{
counter += countLine(f, key);
}
else
{
LineCounter lc = new LineCounter(f, key);
FutureTask ft = new FutureTask(lc);
Thread t = new Thread(ft);
t.start();
results.add(ft);
}
}
for(int i=0; ireturn counter;
}
public static int countLine(File f, String key) throws FileNotFoundException
{
int counter = 0;
Scanner sc = new Scanner(new FileInputStream(f));
while(sc.hasNextLine())
{
String line = sc.nextLine();
if(line.contains(key))
{
++counter;
}
}
return counter;
}
File dir = null;
String key = "";
List> results = Collections.synchronizedList(new ArrayList>());
}
构建一个新的线程涉及与操作系统的交互,是有一定代价的。如果程序中创建了大量的生命期很短的线程,应该使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数目。使用一个线程数固定的线程池以限制并发线程的总数。
执行器(Executor)类有许多静态工厂方法用来构建线程池,下表为汇总。
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程;空闲线程会被保留60秒 |
newFixedThreadPool | 该池包含固定数量的线程;空闲线程会被一直保留,无空闲线程时,得不到服务的任务放在队列中 |
newSingleThreadExecutor | 只有一个线程的池,顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行而构建的固定线程池,替代java.util.Timer |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程“池” |
前三个方法都返回了实现ExecutorService接口的ThreadPoolExecutor类的对象。
可用下面的方法之一将一个Runnable对象或者Callable对象提交给ExecutorService:
Future> submit(Runnable task); //可使用返回的Future对象调用isDone, cancel, isCancelled,但是get方法返回null
Future submit(Runnable task, T result); //get方法返回指定的result对象
Future submit(Callable task); //返回的Future对象将在计算结果准备好的时候得到它
调用submit时会得到一个Future对像,可用来查询该任务的状态。
当用完一个线程池时,调用shutdown,该方法启动该池的关闭序列,被关闭的执行器不再接受新的任务,当所有任务都完成以后,线程池中的线程死亡。另一种方法是调用shutdownNow。该池取消尚未开始的所有任务,并试图中断正在运行的线程。
总结使用连接池时应该做的事:
package learn.test.object;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolTest
{
public static void main(String[] args)
{
File rootDir = new File(ROOT_DIR);
ExecutorService pool = Executors.newCachedThreadPool();
LineCounterInPool lc = new LineCounterInPool(rootDir, KEY_WORD, pool);
Future result = pool.submit(lc);
try
{
System.out.println("line count = " + result.get());
}
catch (InterruptedException | ExecutionException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
System.out.println("the largest size = " + largestPoolSize);
pool.shutdown();
}
public static final String ROOT_DIR = "/home/joseph/Documents/WorkSpace/Source/Java";
public static final String KEY_WORD = "public";
}
class LineCounterInPool implements Callable
{
public LineCounterInPool(File dir, String key, ExecutorService pool)
{
this.dir = dir;
this.key = key;
this.pool = pool;
}
public Integer call() throws Exception
{
int counter = 0;
File[] files = dir.listFiles();
for(File f : files)
{
if(!f.isDirectory())
{
counter += countLine(f, key);
}
else
{
LineCounterInPool lc = new LineCounterInPool(f, key , pool);
Future result = pool.submit(lc);
results.add(result);
}
}
for(int i=0; ireturn counter;
}
public static int countLine(File f, String key) throws FileNotFoundException
{
int counter = 0;
Scanner sc = new Scanner(new FileInputStream(f));
while(sc.hasNextLine())
{
String line = sc.nextLine();
if(line.contains(key))
{
++counter;
}
}
return counter;
}
File dir = null;
String key = "";
List> results = Collections.synchronizedList(new ArrayList>());
ExecutorService pool = null;
}
ScheduledExecutorService接口具有为预定执行(Scheduled Execution)重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。
可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性地运行。
schedule方法预定在指定的时间之后执行任务。
scheduleAtFixedRate在初始的延迟之后,周期性地运行给定的任务。
scheduleWithFixedDelay在初始的延迟结束后,在一次调用完成和下一次调用开始之间有长度为delay的延迟。
有时,使用执行器控制一组相关任务。例如,可以在执行器中使用shutdownNow方法取消所有的任务。
invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。无法知道返回的究竟是哪个任务的结果,也许使最先完成的那个任务的结果,如果你愿意接受任何一种解决方案的话,你就可以使用这个方法。
invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以想下面这样对结果进行处理:
List> tasks = ...;
List> results = excutor.invokeAll(tasks);
for(Future result : results)
processFurther(result.get());
这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。可以用ExecutorCompletionService来排列,以使结果按可获得的顺序保存起来更有实际意义。
ExecutorCompletionService service = new ExecutorCompletionService(executor);
for(Callable task : tasks)
service.submit(task);
for(int i=0; i
java.tuil.concurrent.ExecutorCompletionService 5.0中的方法:
ExecutorCompletionService(Executor e) // 构建一个执行器完成服务来收集给定执行器的结果
Future< T> submit(Callable< T> task) //提交一个任务给底层的执行器
Future< T> submit(Runnable task, T result) //提交一个任务给底层的执行器
Future< T> take() //移除下一个已完成的结果,如果没有任何已完成的结果可用则阻塞
Future< T> poll() //移除下一个已完成的记过,如果没有任何已完成的结果返回null
Future< T> poll(long time, TimeUnit unit) //如果没有任何已完成的结果则等待给定的时间,返回null
java.util.concurrent包包含了管理相互合作的线程集的类,见下表:
类 | 它能做什么 | 何时使用 |
---|---|---|
CyclicBarrier | 允许线程集等待直至其中预定数目的线程到达一个公共障栅(barrier),然后可以选择执行一个处理障栅的工作 | 当大量的线程需要在它们的结果可用之前完成时 |
CountDownLatch | 允许线程集等待直到计数器减为0 | 当一个或多个线程需要等待指导指定数目的事件发生 |
Exchanger | 允许两个线程在要交换对象准备好时交换对象 | 当两个线程工作在同一数据结构的两个实例上的时候,一个向实例添加数据而另一个从实例清除数据 |
Semaphore | 允许线程集等待直到被允许继续运行为止 | 限制访问资源的总数。如果许可数是1,常常阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时 |
一个信号量管理许多的许可证(permits)。为了通过信号量,线程通过调用acquire请求许可。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用release释放许可。
许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可。如果释放的许可多于可用许可的最大数目,信号量只是被设置为可用许可的最大数目。
//Semaphore 常用方法
Semaphore(int permits)
Semaphore(int permits, boolean fair)
//用给定的许可数目为最大值构造一个信号量。如果fair为true,队列优先照顾等待了最长时间的线程
void acquire()
//等待获得一个许可
boolean tryAcquire()
//尝试获得一个许可,如果没有许可是可用的,返回false
boolean tryAcquire(long time, TimeUnit unit)
//尝试在给定时间内获得一个许可,如果没有许可是可用的,返回false
void release()
//释放一个许可
一个倒计时门栓(CountDownLatch)让一个线程集等待直到计数变为0。倒计时门栓是一次性的。一旦计数为0,就不能再重用了。
倒计时门栓的两种使用示例:
//CountDownLatch类的常用方法
CountDownLatch(int count)
//用给定的计数构建一个倒计时门栓
void await()
//等待这个门栓的计数降为0
boolean await(long time, TimeUnit unit)
//等待这个门栓的计数降为0或者时间超时。如果计数为0返回true,如果超时返回false
public void countDown()
//递减这个门栓的计数值
CyclicBarrier类实现了一个集结点(rendezvous)称为障栅(barrier)。
考虑大量的线程运行在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
下面是其细节:
CyclicBarrier barrier = new CyclicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用await:
public void run()
{
doWork();
barrier.await();
}
await方法有一个可选的超时参数:
barrier.await(100, TimeUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是因为它调用await时设置了超时,或者因为它被中断了)。在这种情况下,所有其他线程的await方法抛出BrokenBarrierException异常。那些已经在等待的线程立即终止await的调用。
可以提供一个可选的障栅动作(barrier action),当所有线程到达障栅的时候就会执行这一动作。
Runnable barrierAction = ...;
CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);
该动作可以收集那些单个线程的运行结果。
障栅被称为是循环的(cyclic),因为可以在所有等待线程被释放后重用。在这一点上,有别于CountDownLatch。
//障栅的常用方法
CyclicBarrier ( int parties);
CyclicBarrier ( int parties, Runnable barrierAction);
//构建一个线程数目使parties的循环障栅。当所有的线程都在障栅上调用await()之后,执行barrierAction
int await()
int await(long time, TimeUnit unit)
//等待直到所有的线程在障栅上调用await或者时间超时为止,在这种情况下会抛出TimeoutException异常,成功时,返回这个线程的序号。第一个线程的序号为parties-1,最后一个线程是0
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器。典型的情况是,一个线程向缓冲区填入数据,另一个线程消耗这些数据。当它们都完成以后,相互交换缓冲区。
//Exchanger 的常用方法
V exchange(V item)
V exchange(V item, long time, TimeUnit unit)
//阻塞直到另一个线程调用这个方法,然后,同其他线程交换item,并返回其他线程的item。第二个方法时间超时时抛出TimeoutException异常
同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用SynchronousQueue的put方法时,它会阻塞直到另一个线程调用take方法为止,反之亦然。与Exchanger的情况不同,数据仅仅沿一个方向传递,从生产者到消费者。
SynchronousQueue类实现了BlockingQueue接口,但是从概念上讲不是一个队列。它没有包含任何元素,它的size方法总是返回0。
//SynchronousQueue的常用方法
SynchronousQueue()
SynchronousQueue(boolean fair)
//构建一个允许线程提交item的同步队列,如果fair为true,队列优先照顾等待了最长时间的线程
void put(V item)
//阻塞直到另一个线程调用take来获取item
V take()
//阻塞直到另一个线程调用put。返回另一个线程提供的item
当程序需要做某些耗时的工作时,应该启动另一个工作器线程而不是阻塞用户接口。
Swing不是线程安全的,如果试图在多个线程中操纵用户界面的元素,那么用户界面可能崩溃。
在显示时对要操作的元素加锁可以避免这种情况的出现,但是Swing的设计者这么完成。
将线程与Swing一起使用时,必须遵循两个简单的原则:
EventQueue.invokeLater(new Runnable()
{
public void run()
{
label.setText(percent + "% complete");
}
});
当事件放入事件队列时,invokeLater方法立即返回,而run方法被异步执行。invokeAndWait方法等待直到run方法执行完成。
两种方法都是在事件分配线程中执行run方法。没有新的线程被创建。
//java.awt.EventQueue 1.1的主要方法
static void invokeLater(Runnable runnable)
static void invokeAndWait(Runnable runnable)
//在带处理的线程被处理后,让runnable对象的run方法在事件分配线程中执行
static boolean isDispatchThread()
//如果执行这一方法的线程是时间分配线程,返回true
典型的UI活动:
整个工作完成之后,对UI做最后的更新
SwingWorker类可以帮助完成这样的任务。覆盖doInBackground方法来完成耗时的工作,不时调用publish来报告工作进度,这一方法在工作器线程中执行。publish方法是的process方法在事件分配线程中执行来处理进度数据。当工作完成时,done方法在事件分配线程中被调用以便完成UI的更新。
每当要在工作器线程中做一些工作时,构建一个新的工作器(每一个工作器对象只能被使用一次)。然后调用execute方法。
假定工作器产生某种结果;SwingWorker< T, V>实现Future< T>。这一结果可以通过Future接口的get方法获得。由于get方法阻塞直到结果成为可用,因此不要在调用execute之后马上调用它。最明智的是只在知道工作完成时调用它,如,在done方法中调用get。
SwingWorker< T, V>产生类型为T的结果以及类型为V的进度数据。
要取消正在进行的工作,使用Future接口的cancel方法。当该工作被取消时,get方法抛出CancellationException异常。
工作器线程对publish的调用会导致在事件分配线程上的process的调用。
每一个Java应用程序都开始于主线程的main方法。在Swing程序中,main方法的生命期是很短的。它在事件分配线程中规划用户界面的构造然后退出。
对于单一线程规则存在一些例外情况:
JTextComponent.setText
JTextArea.insert
JTextArea.append
JTextArea.replaceRange
JComponent.repaint
JComponent.revalidate
revalidate方法在内容改变后强制执行组件布局。传统的AWT有一个validate方法强制执行组件布局,对于Swing组件,调用revalidate方法。但是,要强制执行JFrame的布局,仍然要调用validate方法,因为JFrame是一个Component而不是JComponent。