如今,单台计算机拥有多个CPU,但是并发执行的进程数并不是由CPU数目制约的,操作系统将CPU的时间片分配给每一个进程,给人并发处理的感觉
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务;通常每一个任务称为一个线程(thread)【同时运行一个以上线程的程序称为多线程程序】
多进程和多线程的本质区别:每个进程拥有自己的一整套变量,而线程则共享数据(共享变量使线程之间的通信比进程之间的通信更有效、更容易)
1.什么是线程
如何通过运行一个线程中的关键代码来保持用户对程序的控制权
参数:
- 1.millis 毫秒数
- 2.nanos 纳秒数
使用线程给其他任务提供机会
如果需要执行一个比较耗时的任务,应该使用独立的线程
下面是在一个独立的线程中执行一个任务的简单过程:
1.将任务代码移到实现了Runable接口类的run方法中,这个接口非常简单,只有一个方法:
public interface Runable
{
void run();
}
可以如下实现一个类:
class MyRunable implements Runable
{
public void run()
{
task code
}
}
2.创建一个类对象
Runable r = new MyRunable();
3.由Runable创建一个Thread对象
Thread t = new Thread(r);
4.启动线程:
t.start();
在一般情况下,线程在中断时被终止,因此,当发生InterruptedException时,run方法将结束执行
警告:不要调用Thread类或Runable对象的run方法,因为直接调用run方法只会执行同一个线程中的任务,而不会启动新线程,应该调用Thread.Start方法,这个方法将创建一个执行run方法的新线程
2.中断线程
当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止
没有可以强制线程终止的方法,然而,interrupt方法可以用来请求终止线程;当一个线程调用interrupt方法时,线程的中断状态将被置位,这是每一个线程都具有的boolean标志,每个线程都应该不时地检查这个标志,以判断线程是否被中断
要想弄清楚中断状态是否被置位,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted 方法:
while(!Thread.currentThread().isInterrupted() && more work to do)
{
do more work
}
但是,如果线程被阻塞,就无法检测中断状态(这是产生InterruptedException的地方),当一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被InterruptedException中断
中断一个线程不过是引起它的注意,被中断的线程可以决定如何响应中断【某些线程很重要,应该处理完异常后继续执行而不理会中断】
但是更普遍的情况是,线程将简单地将中断作为一个终止的请求,这种线程的run方法具有如下形式:
public void run()
{
try
{
...
while(!Thread.currentThread().isInterrupted() && more work to do)
{
do more work
}
}
catch(InterruptedException e)
{
//thread was interrupted during sleep or wait
}
finally
{
cleanup,if required
}
//exiting the run method terminates the thread
}
如果在每次工作之后都调用sleep方法(或者其他的可中断方法),isInterrupted检测既没有必要也没有用处;如果在中断状态被时调用sleep方法,他不会休眠,相反,它将会清除这一状态(!)并抛出InterruptedException;因此如果循环调用sleep,不会检测中断状态,相反应该如下所示捕获InterruptedException:
public void run()
{
try
{
...
while(more work to do)
{
do more work
Thread.sleep(delay);
}
}
catch(InterruptedException e)
{
//thread was interrupted during sleep
}
finally
{
cleanup,if required
}
//exiting the run method terminates the thread
}
interrupted和isInterrupted方法:
- 1.interrupted方法是一个静态方法,它检测当前的线程是否被中断,调用该方法会清除线程中断状态
- 2.isInterrupted方法是一个实例方法,用来检验是否有线程被中断,调用该方法不会改变线程中断状态
3.线程对象
线程有如下6种状态:
- 1.New(新建)
- 2.Runable(可运行)
- 3.Blocked(被阻塞)
- 4.Waiting(等待)
- 5.Timed Waiting(计时等待)
- 6.Terminated(被终止)
1.新创建线程
当new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行,这意味着它的状态是new,当一个线程处于新创建状态时,程序还没有运行线程中的代码(在线程运行之前还有一些基础工作要做)
2.可运行线程
一旦调用start方法,线程处于runable状态,一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间(Java规范说明没有将它作为一个单独状态,一个正在运行中的线程仍然处于可运行状态)
一旦一个线程开始运行,它不必始终保持运行,事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会(线程调度的细节依赖于操作系统提供的服务)
在具有多个处理器的机器上,每个处理器运行一个线程,可以有多个线程并行运行,如果线程的数目多于处理器的数目,调度一般采用时间片机制
3.被阻塞线程和等待线程
当线程处于被阻塞或等待状态时,它暂时不活动,不运行任何的代码且消耗最少的资源,直到线程调度器重新激活它(细节取决于它是怎样达到非活动状态的):
- 1.当一个线程视图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态;当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态
- 2.当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态(被阻塞状态和等待状态有很大不同)
- 3.有几个方法有一个超时参数,调用它们导致线程进入计时等待状态,这一状态一直保持到超时期或者接受到适当的通知(带有超时参数的方法有Thread.sleep和Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版)
当一个线程被重新激活(因为超时期满或成功获得一个锁),调度器检查它是否具有比当前运行线程更高的优先级,如果是,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行
4.被终止的线程
线程因如下2个原因之一被终止:
- 1.因为run方法正常退出而自然死亡
- 2.因为一个没有捕获的异常终止了run方法而意外死亡
4.线程属性
1.线程优先级
每一个线程都有一个优先级,默认情况下一个线程继承它的父线程的优先级(可以用setPriority方法提高或降低任何一个线程的优先级)【可以将优先级设置为MIN_PRIORITY(在Thread类中定义为1)与MAX_PRIORITY(在Thread类中定义为10)之间的任何值,NORM_PRIORITY被定义为5】
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程,线程优先级是高度依赖于系统的(当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多也许更少)
2.守护线程
通过调用t.setDaemon(true);
(这一方法必须在线程启动之前调用)将线程转换为守护线程(daemon thread),守护进行的唯一用途是为其他线程提供服务;当只剩下守护线程时,虚拟机就退出了
守护线程应该永远不去访问固有资源、如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断
3.未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是,不被检测的异常会导致线程终止。在这情况下,线程就死亡了;但是不需要任何catch子句来处理可以被传播的异常;相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器
该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法:void uncaughtExceptionH(Thread t, Throwable e)
- 1.可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器;
- 2.也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器(如果不安装默认的处理器,默认的处理器为空)
如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象
线程组是一个可以统一管理的线程集合,默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组
ThreadGroup类实现Thread.UncaughtExceptionHandler接口,它的UncaughtException方法坐如下操作:
- 1.如果该线程组有父线程组,那么父线程组的UncaughtException方法被调用
- 2.否则,如果Thread.getDefaultUncaughtExceptionHandler方法返回一个非空的处理器,则调用该处理器
- 3.否则,如果Throwable是ThreadDeath的一个实例,什么都有不做
- 4.否则,线程的名字以及Throwable的栈踪迹被输出到System.err上
5.同步
在大多数实际 线程应用中,2个或2个以上的线程需要共享对同一个数据的存取,这样的情况通常称为竞争条件(rece condition)
1.竞争条件的一个例子
为了避免多线程引起的对数据的讹误,必须学习如何同步存取
2.竞争条件详解
假设2个线程同时执行指令:accounts[to] += amount;
,问题在于这不是原子操作,该指令可能被处理如下:
- 1.将accounts[to] 加载到寄存器
- 2.增加amount
- 3.将结果写会accounts[to]
在这里增值命令是由几条指令组成的,执行它们的线程可以在任何一条指令点上被中断
对于程序员来说,很少有比无规律出现错误更糟糕的事情了!!!
3.锁对象
有2种机制防止代码块受并发访问的干扰:
- 1.Java提供一个synchronized关键字达到这一目的,synchronized关键字自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的
- 2并且在Java SE 5.0中引入了ReentrantLock类
用ReentrantLock保护代码块的基本结构如下:
myLick.lock();//a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock();//make sure your lock is unlocked even if an exception is thrown
}
这一结果确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句;当其他线程调用lock时,被阻塞,直到第一个线程释放锁对象
把解锁操作放在finally子句之内是至关重要的,如果在临界区的代码抛出异常,锁必须被释放,否则,其他的线程将永远被阻塞
如果使用锁,就不能用带资源的try语句
使用一个锁来保护Bank类的transfer方法,每一个Bank对象有自己的ReentrantLocl对象,如果2个线程试图访问同一个Bank对象,那么锁以串行的方式提供服务;如果2个线程试图访问不同Bank对象,每个线程得到不同的锁对象,2个线程都不会发生阻塞(线程在操纵不同的实例的时候,线程之间不会相互影响)
锁是可以重入的(线程可以重复地获得已经持有的锁)锁保持一个持有计数器(hold count)来跟踪对lock方法的嵌套调用;线程在每一次调用lock都要调用unlock来释放锁【被一个锁保护的代码可以调用另一个使用相同的锁的方法】
注意临界区中的代码,不要因为异常的抛出而跳出了临界区,如果在临界区代码结束之前抛出了异常,finally子句将释放锁,但会使对象可能处于一个受损状态
4.条件对象
通常,线程进入临界区,却发现在某一个条件满足之后它才能执行,要使用一个条件对象(由于历史原因,条件对象常被称为条件变量)来管理那些已经获得了一个锁但是却不能做有用工作的线程
一个锁对象可以有一个或多个相关的条件对象(用newCondition方法获得一个条件对象),习惯上给每一个条键对象命名为可以反映它所表达的条件的名字,例如设置一个条件对象来表达“余额充足”条件:
class Bank
{
private Condition sufficientFounds;
...
public Bank()
{
...
sufficientFounds
}
}
如果transfer方法发现余额不足,它会调用sufficientFounds.await();
,当前线程现在被阻塞了,并放弃了锁,我们希望这样可以使得另一个线程可以进行增加账号余额的操作;
等待获得锁的线程和调用await方法的线程存在本质上的不同,一旦一个线程调用await方法,它进入该条件的等待集,当锁可以用时,该线程不能马上解除阻塞,相反,它处于阻塞状态,直道另一个线程调用同一条件上的signalAll方法为止
当另一个线程转账时,它应该调用signalAll();这一调用重新激活因为这一条件而等待的所有线程;一旦锁成为可用的,它们中的某个将从await调用中返回,获得该锁并从被阻塞的地方继续执行
此时线程应该再次测试该条件,由于无法确保该条件被满足—signalAll方法仅仅是通知正在等待的线程,此时有可能已经满足条件,值得再次去检测该条件
通常对于await的调用形式如下:
while(!(ok to proceed))
{
condition.await();
}
至关重要的是最终需要某个其他线程调用signalAll方法,当一个线程调用await时,它没有办法重新激活自身,它寄希望于其他线程,如果没有其他线程来重新激活等待的线程,它就永远不再允许了,这将导致令人不快的死锁(deadlock)现象
如果所有线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用await方法,那么它也被阻塞,没有任何线程可以解除其他线程的阻塞,那么该程序就被挂起
在对象的状态有利于等待线程的方向改变时,调用signaAll
signalAll不会立即激活一个等待线程,仅仅解除等待线程的阻塞,一边这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问
signal方法则是随机解除等待集中某个线程的阻塞状态,这比解除所有线程的阻塞更加有效,但是也存在危险(如果随机选择的线程仍然不能运行,那么再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了)
当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await,signalAll或signal
5.synchronized关键字
锁和条件的关键之处:
- 1.锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
- 2.锁可以管理试图进入被保护代码段的线程中
- 3.锁可以拥有一个或多个相关的条件对象
- 4.每个条件对象挂链那些已经进入被保护的代码段但还不能运行的线程
Java中的每一个对象都有一个内部锁,如果一个方法用synchronized关键字声明,那么对象的锁键保护这个方法(调用该方法,线程必须获得内部的对象锁)
public synchronized void method()
{
method body
}
等价于
public void method()
{
this.intrinsicLock.lock();
try
{
method body
}
finally
{
this.intrinsicLock.unlock();
}
}
内部对象锁只有一个相关条件,wait方法添加一个线程到等待集中,notiyAll/notify方法解除等待线程的阻塞状态
wait、notiyAll、notify是Object的final方法,Condition方法必须被命名为await、signalAll、signal以便他们不会与那些方法发生冲突
使用synchronized关键字来编写代码要简洁的多,每一个对象有一个内部锁,并且该锁有一个内部条件,由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程
将静态方法声明为synchronized也是合法的,如果调用这种方法,该方法获得相关的类对象的内部锁【没有其他线程可以调用同一个类的这个或任何其他的同步静态方法】
内部锁和条件存在一些局限,包括:
- 1.不能中断一个正在试图获得锁的线程
- 2.试图获得锁时,不能设置为超时
- 3.每个锁仅有一个单一的条件,可能是不够的
在代码中应该使用Lock和Condition对象还是synchronized方法?建议如下:
- 1.最好都不使用(在许多情况下可以使用java.util.cincurrent包中的一种机制,它会为你处理所有的加锁)
- 2.如果synchronized关键字适合,尽量使用,减少代码量,减少出错概率
- 3.如果特别需要Lock/Condition结构提供的独有特性时,才使用
6.同步阻塞
每一个Java对象有一个锁,线程可以通过调用同步方法获得锁,还有另一个机制可以获得锁,通过进入一个同步阻塞;当线程进入如下形式的阻塞:
synchronized (obj)//this is the syntax for a synchronized block
{
critical section
}
于是它获得obj的锁
7.监视器概念
锁和条件是线程同步的强大工具,但是它们不是面向对象的
监视器是一种不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性
监视器具有如下特性
- 1.监视器是只包含私有域的类
- 2.每个监视器类的对象有一个相关的锁
- 3.使用该锁对所有的方法进行加锁(即如果客户端调用obj.method(),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁)
- 4.该锁可以有任意多个相关条件
每个条件变量管理一个独立的线程集
Java中的每一个对象有一个内部的锁和内部的条件,如果一个方法使用synchronized关键字声明,那么,它表现的就像是一个监视器方法,通过调用wait/notify/notifyAll来访问条件变量
在下面的3个方面Java对象不同于监视器,从而使得线程的安全性下降:
- 1.域不要求必须是private
- 2.方法不要求必须是synchronized
- 3.内部锁对客户是可用的
8.Volatile域
多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值,结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值
编译器可以改变指令的执行顺序以使吞吐量最大化,这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令才会改变,然而内存的值可以被另一个线程改变
编译器被要求通过在必要的时候刷新本地缓存来保持锁的效应,并且不能不正当地重新排序指令
同步格言:如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读取值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步
volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为volatile那么编译器和虚拟机就知道该域可能被另一个线程并发更新
Volatile变量不能提供原子性
9.final变量
除非使用锁或者volatile修饰符,否则无法从多个线程安全地读取一个域,还有一种情况可以安全的访问一个共享域,即这个域声明为final时,考虑以下声明:final Map<String, Double> accounts = new HashMap();
其他线程或者构造函数完成构造之后才能看到这个accounts变量
如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap
对这个映射表的操作并不是线程安全的,如果多个线程在读写这个映射表,仍然需要进行同步
10.原子性
假设对共享变量除了赋值之外,并没有完成其他操作,那么可以将这些共享变量声明为volatile
java.util.concurrent.atomic包中有很多类使用了高效的机器级指令(而不是锁)来保证其他操作的原子性
程序员一般不使用,主要是系统程序员使用
11.死锁
有可能因为每一个线程都要等待而导致所有的线程都被阻塞,这样的状态称为死锁
调试技巧:当程序挂起时,键入Ctrl+\,将得到一个所有线程的列表,每一个线程有一个栈踪迹,告诉你线程被阻塞的位置
造成死锁的另一个原因可能是调用了signal,它仅仅为一个线程解锁,而且它很可能选择一个不能继续运行的线程
12.线程局部变量
有时候可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例
13.锁测试与超时
线程在调试lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞,应该更加谨慎地申请锁,tryLock方法试图申请一个锁,在成功得获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做别的事情
if(myLock.tryLock())
{
//now the thread owns the lock
try{...}
finally
{
myLock.unlock();
}
}
else
{
//do somgthing else
}
可以调用tryLock时,使用超时参数:if(myLock.tryLock(100,TimeUnit.MILLISECONDS))...
,TimeUnit是一个枚举类型,可以取的值包括SECONDS、MULLISECINDS、MICRISECINDS、NANOSECONDS
lock方法不能被中断;如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态;如果出现死锁,那么lock方法就无法终止
如果调用带有用超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException,这是一个非常有用的特性,因为运行程序打破死锁
也可以调用lockInterruptibly方法,它就相当于一个超时设为无限的tryLock方法
在等待一个条件时,也可以提供一个超时:myCondition.await(100,TimeUnit.MILLISECONDS)
如果一个线程被另一个线程通过调用signalAll或signal激活,或者超时时限已到达,或者线程被中断,那么await方法将返回
如果等待的线程被中断,await方法将抛出一个InterruptedException
14.读/写锁
java.util.concurrent.locks包定义了2个锁类,:ReentrantLock类和ReentrantReadWritLock类,如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者十分有用,在这种情况下,允许对读者线程共享访问是适合的,当然写线程依然必须是互斥访问
下面是使用读或写锁的必要步骤:
- 1.构造一个ReentrantReadWritLock对象:
private ReentrantReadWritLock rwl = new ReentrantReadWritLock();
- 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();}
}
15.为什么弃用stop和suspend
初始的Java版本定义了一个stop方法用来终止一个线程,以及一个suspend方法用来阻塞一个线程直到另一个线程调用resume;stop和suspend方法有一个共同特点:都试图控制一个给定线程的行为
stop方法天生不安全,suspend方法经常导致死锁
stop方法终止所有未结束的对象,包括run方法,当线程被终止,立即释放被它锁住的所有对象的锁,这会导致对象处于不一致的状态
当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏,因此,该方法被弃用,在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止
如果用suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的,如果调用suspend的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁
6.阻塞队列
对于实际编程来说,应该尽量远离底层结构,使用由并发处理的专业人士实现的较高层次的结构要方便得多,要安全得多
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化,生产者线程向队列插入元素,消费者线程则读取它们,使用队列,可以安全地从一个线程向另一个线程传递数据
当视图向队列添加元素而队列已满,或是想从队列移除元素而队列为空的时候阻塞队列导致线程阻塞,在协调多个线程之间的合作时,阻塞队列是一个有用的工具
队列会自动地平衡负载
阻塞队列的方法
方法 | 正常动作 | 特殊情况下的动作 |
---|---|---|
add | 添加一个元素 | 如果队列满,抛出IllegelStateException |
element | 返回队列的头元素 | 如果队列空,抛NoSuchElementException |
offer | 添加一个元素并返回tue | 如果队列满,返回false |
peek | 返回队列的头元素 | 如果队列空,返回null |
poll | 移除并返回队列的头元素 | 如果队列空,返回null |
put | 添加一个元素 | 如果队列满,阻塞 |
remove | 移除并返回头元素 | 如果队列空,抛出NoSuchElementException |
take | 移除并返回头元素 | 如果队列空,阻塞 |
如果将队列当作线程管理工具来使用,将要用到put(添加)和take(移除)方法,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer(添加)、poll(移除队头)、和peek(返回队头)方法
poll和peek返回空来指示失败,因此向这些队列插入null值是非法的
带有超时的offer和poll方法:
boolean success = q.offer(x,100,TimeUnit.MILLISECONDS);
Object head = q.poll(100,TimeUnit.MILLISECONDS);
尝试在100毫秒的时间内在队列的尾部插入一个元素;尝试用100毫秒的时间移除队列的头元素
java.util.concurrent包提供了阻塞队列的几个变种,默认情况下:
- 1.LinkedBlockQueue的容量是没有上边界的,但是可以选择指定最大容量
- 2.LinkedBlockDuque是一个双端的版本
- 3.ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性
- 4.PriorityBlockingQueue是一个带优先级的队列,不是先进先出队列,没有容量上限
- 5.DelayQueue包含一个实现Delayed接口的对象:
interface Delayed extends Comparable<Delayed>
{
long getDelay(TimeUnit unit);
}
getDelay方法返回对象的残留延迟,负值表示延迟已经结束(元素只有在延迟用完的情况下才会从DelayQueue移除,还必须实现CompareTo方法)
7.线程安全的集合
通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易些【阻塞队列就是线程安全集合】
1.高效的映射表、集合和队列
java.util.concurrent包提供了映射表、有序集合队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化
集合返回弱一致性迭代器,这意味着迭代器不一定能反映出它们被构造之后的所有修改,但是它们不会将同一个值返回2次,也不会抛出ConcurrentModificationException
【与弱一致性集合形成对照,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出ConcurrentModificationException】
2.写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制,如果在集合上进行迭代的线程数超过修改的线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用,如果数组后来被修改了,迭代器仍然引用旧数组,但是,集合的数组已经被替换了
3.较早的线程安全集合
任何集合通过使用同步包装器变成线程安全的:
Lsit<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());
Map<K.V>synchHashMap = Collections.synchronizedMap(new HashMap<K,V>());
结果集合的方法使用锁加以保护,提供了线程的安全访问
应该确保没有任何线程通过原始的非同步方法访问数据结构,最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并立即传递给包装器
8.Callable和Future
Runnable封装一个异步运行的任务,可以把它想象成一个没有参数和返回值的异步方法;Callable与Runable类似,但是有返回值,Callable接口是一个参数化的类型,只有一个call方法:
public interface Callable<V>
{
V call() throws Exception;
}
类型参数V是返回值的类型
Future保存一个异步计算的结果(可以启动一个计算,将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();
}
- 1.第一个get方法的调用被阻塞,直到计算完成;
- 2.如果在计算完成之前,第二个方法的调用超时,抛出TimeoutException,
- 3.如果运行该计算的线程被中断,2个方法都将派出InterruptedException,
- 4.如果计算完成,那么get方法立即返回;
- 5.如果计算还在进行,isDone返回false,否则返回true
- 6.可以用cancel方法取消该计算,如果还没开始计算则直接取消,如果在运行中,那么mayInterrupt参数返回true
FutureTask包装器是一种非常便利的机制,可以将Callable转换成Future和Runable,同时实现两者的接口,如:
Callable<Integer> myComputation = ...;
FutureTask<Interger> task = new FutureTask<Interger>(myComputation);
Thread t = new Thread(task);//it is a Runable
t.start();
...
Interger result = task.get();//it is a Future
9.执行器
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互,如果程序中创建看大量的生命周期很短的线程,应该使用线程池;一个线程池中包含了许多准备运行的空闲线程,将Runable对象交给线程池,就会有线程调用run方法,当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务
另一个使用线程池的理由是减少并发线程的数目,创建大量线程会大大降低性能甚至使虚拟机崩溃,如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数
执行器类有许多静态工程方法来构建线程池
执行器工厂方法
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程;空闲线程会被保留60秒 |
newFixedThreadPool | 该池包含固定数量的线程;空闲线程会被一直保留 |
newSingleThreadExecutor | 只有一个线程的池,该线程顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行而构建的固定线程池,替代java.util.Timer |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程池 |
1.线程池
1.newCachedThreadPool方法构建了一个线程池,对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程
2.newFixedThreadPool方法构建一个具有固定大小的线程池,如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置在队列中
3.newSingleThreadExecutor是一个退化了大小为1的线程池,由一个线程执行提交的任务,一个接着一个
这3个方法实现了ExcutorService接口的ThreadPoolExecutor类的对象
当用完一个线程池的时候,调用shutdown,该方法启动该池的关闭序列,被关闭的执行器不在接受新的任务,当所有任务都完成以后,线程池中的线程死亡
另一个方法是调用shutdownNow,该池取消尚未开始的所有任务并试图中断正在运行的任线程
在使用线程池的时候应该做的事情:
- 1.调用Executor类中静态的方法newCacheedThreadPool或newFixedThreadPool
- 2.调用submit提交Runnable或Callable对象
- 3.如果想要取消一个任务,或入股提交Callable对象,那就保持好返回的Future对象
- 4.当不在提交任何任务是,调用shutdown
2.预定执行
ScheduledExecutorService接口具有预定执行或重复执行任务而设计的方法,它是一种允许使用线程池机制的java.util.Timer的泛化;Executors类的newSchedulerThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象
可以预定Runnable或Callable在初始的延迟之后只允许一次,也可以预定一个Runable对象周期性地运行
3.控制任务组
有时候使用执行器有更有实际意义的原因,控制一组相关任务。例如可以在执行器中使用shutdownNow方法取消所有的任务
invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果;无法知道返回的究竟是哪个任务的结果,也许是最先完成的那个任务的结果
invokeAkk方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案:
List<Callable<T>> tasks = ...;
List<Furure<T>> results = executor.invokeAll(tasks);
for(Future<T> result : results)
{
processFuther(result.get());
}
这个方法的缺点是如果第一个任务蹊跷花去了很多时间,则可能不得不进行等待;将结果按可获得的顺序保存起来更有意义,可以用ExecutorCompletionService来进行排序,用常规的方法获得一个执行器,然后构建一个ExecutorCompletionService,提交任务给完成服务,该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果:
ExecutorCompletionService service = new ExecutorCompletionService(executor);
for(Callable<T> task : tasks)
{
service.submit(task);
}
for(int i = 0; i < tasks.size(); i++)
{
processFurther(service.take().get());
}
4.Fork-Join框架
有些应用使用了大量线程,但其中大多数都是空闲的,Java SE 7中引入了for-join框架来支持计算密集型任务(对每个处理器内核分别使用一个线程)
在后头,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取 ;每个工作线程都有一个双端队列来完成任务,一个公祖线程将子任务压入其双端队列的队头(只有一个线程可以访问队头,因此不需要锁);一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务,由于大的子任务都在队尾,这种密取很少出现
10.同步器
java.util.concurrent包含了几个能帮助我们管理相互合作的线程集的类,这些机制具有线程之间的共用集结点模式提供的“预置功能”
同步器
类 | 功能 | 使用场景 |
---|---|---|
CyclicBarrier | 允许线程集等待直至预定数目的线程到达一个公共障栅,然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在他们的结果可用之前完成时 |
CountDownLatch | 允许线程集等待直到计数器减为0 | 当一个或多个线程需要等待直到指定书目的事件发生 |
Exchanger | 允许2个线程在要交换的对象准备好时交换对象 | 当2个线程工作在同一数据结构的2个实例上的时候,一个像实例添加数据而另一个从实例清除数据 |
Semaphore | 允许线程集等待直到被允许袭击允许为止 | 限制访问资源的线程总数,如果许可数是1,常常阻塞线程知道另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下,当2个线程准备好将一个对象从一个线程传递到另一个时 |
1.信号量
一个信号量管理许多的许可证,为了通过信号量,形成通过调用acquire请求许可,其实没有实际的许可对象,信号量仅仅维护一个计数,学科的数目是固定的,由此限制了通过的线程数量,其他线程可以通过调用relase释放许可,而且许可不是必须由获得它的线程释放,任何线程都可以释放任意数目的许可,这可能会增加学科数目以主要超过初始数目
2.倒计时门栓
一个倒计时门栓让一个线程集等到知道计数变为0,倒计时门栓是一次性的,一旦计数为0,就不在重用
一个有用的特例是计数值为1的门栓,实现一个只能通过一次的门,线程在门外等候知道另一个线程将计数值置为0
3.障栅
CyclincBarrier类实现了一个集节点称为障栅,考虑大量线程运行在一次计算的不同部分的情形,当所有部分都准备好时,需要把结果组合在一起,当一个线程完成了它的那部分任务后,我们让它运行到障栅处,一旦所有的线程都到达了这个障栅,障栅就撤销了线程可以继续运行
构造一个障栅,并给出参数:
CyclicBarrier barrier = new CyclicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用await:
public void run()
{
doWork();
barrier.await();
}
await方法有一个可选的超时参数:barrier.await(100,TimeUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可以离开是因为它调用await时设置了超时,或者因为它被中断了);在这种情况下,所有其他线程的await方法抛出BrokenBarrierException,那些已经在等待的线程立即终止await的调用
可以提供一个可选的障栅动作,当所有线程到达障栅的是就会执行这一动作
Runnable barrierAction = ...;
CyclicBarrier barrier = new CyclicBarrier(nthreads,barrierAction);
该动作可以收集那些单个线程的运行结果
障栅被称为循环的,因为可有在所有等待线程被释放之后重用,在这一点上,有别于CountDownLatch,CountDownLatch只能使用一次
4.交换器(Exchanger)
当2个线程在同一个数据缓冲区的2个实例上工作时,就可以使用交换器,一个线程向缓冲区填入数据,另一个线程消耗这些数据,当人们都完成以后,互相交换缓冲区
5.同步队列
同步队列是一种将生产者和消费者线程配对的机制
当一个线程调用SynchronousQueue的put方法,它会阻塞直到另一个线程调用take方法位置