您的位置:首页 > 编程语言 > Java开发

多线程

2015-12-08 17:39 387 查看
1.中断线程

当对一个线程调用interrupt方法时,线程的中断状态被置位。

判断线程是否被中断:

Thread.currentThread().isInterrupted();//Thread.currentThread():获得当前线程


如果线程被阻塞,就无法检测中断状态。

在中断状态被置位时调用sleep方法,它不会休眠,将会清除这一状态并抛出InterruptedException。

2.线程状态

六种状态:

New(新生),Runnable(可运行),Blocked(被阻塞),Waiting(等待),

Timed waiting(计时等待),Terminated(被终止)

2.1新生线程

new操作符创建一个新线程时,线程还没有开始运行。

2.2可运行线程

一旦调用start方法,线程处于Runnable状态。一个可运行的线程可能正在运行也可能没有运行。

抢占式调度系统给每一个可运行线程一个时间片来执行任务。时间片用完,选择下一个线程时,操作系统考虑线程的优先级。

具有多个处理器的机器上,每一个处理器运行一个线程,可有多个线程并行运行。

2.3被阻塞线程和等待线程

当一个线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。

当线程等待另一个线程通知调度器一个条件时,他自己进入等待状态。

有几个带有超时参数的方法,调用它们导致线程进入计时等待状态。

2.4被终止的线程

(1)因run方法正常退出而自然死亡;

(2)因一个没有捕获的异常终止了run方法而意外死亡。

3.线程属性

包括:线程优先级、守护线程、线程组及处理未捕获异常的处理器。

3.1线程优先级

默认一个线程继承它的父线程的优先级。可用setPriority方法设置优先级:MIN_PRIORITY(Thread类中定义为1)与MAX_PRIORITY(定义为10)间值,NORM_PRIORITY定义为5.

线程优先级高度依赖于系统,优先级个数会变。

Windows有7个优先级别;Sun为Linux提供的Java虚拟机,所有线程具有相同优先级。

static void yield():导致当前执行线程处于让步状态。如果有其他具有至少与此线程同样高优先级的可运行线程,那么这些线程接下来会被调度。

3.2守护线程

将线程转换为守护线程:调用
t.SetDaemon(true);//必须在线程启动之前调用
只剩下守护线程时,虚拟机就退出了。

3.3未捕获异常处理器

线程的run方法不能抛出任何被检测的异常,但不被检测的异常会导致线程终止。

在线程死亡前,异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。该接口只有一个方法:

void UncaughtException(Thread t,Throwable e


SE 5.0开始可用setUncaughtExceptionHandler方法为任何线程安装一个处理器;也可用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。

若默认处理器为空,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。

4.同步

4.1详解竞争条件

4.2锁对象

(1)显示锁:synchronized关键字自动提供一个锁及相关的“条件”。

(2)SE 5.0引入了ReentrantLock类。

//ReentrantLock保护代码块的基本结构:
mylock.lock();
try{
...
}
finally{
mylock.unlock();//确保释放锁
}
!!这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。
当其他线程调用lock时,他们被阻塞,直到第一个线程释放锁对象。


“如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞(线程在操作不同Bank实例时,线程之间不会相互影响)”。

锁是可重入的,线程可以重复获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每次调用lock都要调用unlock来释放锁。由此,被一个锁保护的代码可以调用另一个使用相同锁的方法。

“transfer方法调用getTotalBalance方法,也会封锁bankLock对象,此时bankLock对象的持有计数为2.当getTotalBalance方法退出时,持有计数变为1.当transfer方法退出时,持有计数变为0.线程释放锁。”

4.3条件对象

一个锁对象可以有一个或多个相关的条件对象。可用newCondition方法获得一个条件对象:

private Condition sufficientFunds = bankLock.newCondition();
//条件不满足时,阻塞线程并释放锁
sufficientFunds.await();
//当锁可用时,该线程不能马上解除阻塞,直到另一个线程调用同一条件上的signalAll方法时为止。
//另一个线程转账时调用:
sufficientFunds.signalAll();//激活因这一条件而等待的所有线程。
//一旦某个等待的线程的锁成为可用的,该线程将从await调用返回,获得该锁,并从被阻塞的地方继续执行。
!!调用signalAll()并不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出
同步方法后,通过竞争实现对对象的访问。
!!另一个方法signal可以随机解除等待集中某个线程的阻塞状态。它更有效,但危险:随机选择的线程发现自己仍然
不能运行,再次被阻塞(没有其他线程再次调用signal,系统就死锁了)。


死锁:如果没有其他线程(调用signalAll)激活等待(调用await)的线程,它就不能再运行了。

如果没有任何线程可以解除其他线程的阻塞,那么该程序就被挂起了。

4.4synchronized关键字

1.0版开始,Java中每个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法(要调用该方法,线程必须获得内部的对象锁)。

内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。

!!wait、notifyAll/notify方法是Object类的final方法。

将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。

内部锁和条件存在一些局限:

(1)不能中断一个正在试图获得锁的线程;

(2)试图获得锁时不能设定超时;

(3)每个锁仅有单一的条件,可能是不够的。

4.5同步阻塞

每个Java对象有一个锁,线程可通过调用同步方法获得锁,还有另一种机制可以获得锁:通过进入一个阻塞队列。

客户端锁定:使用一个对象的锁来实现额外的原子操作。

//错误,
public void transfer(Vector<Double> accounts,int from,int to,int amount){
accounts.set(from,accounts.get(from)-amount);
accounts.set(to,accounts.get(to)+amount);
}
//Vector类的get和set方法是同步的。但在第一次对get的调用完成之后,一个线程有可能在transfer方法中被
剥夺运行权,于是另一个线程可在相同存储位置存入不同值。
//但是,我们可以截获这个锁,下面是正确的
public void transfer(Vector<Double> accounts,int from,int to,int amount){
synchronized(accounts){
accounts.set(from,accounts.get(from)-amount);
accounts.set(to,accounts.get(to)+amount);
}
}


4.6监视器

不需要考虑如何加锁时,就可保证多线程的安全性,最成功的解决方案之一是监视器。

监视器特性:

(1)监视器是只包含私有域的类;

(2)每个监视器类的对象有一个相关的锁;

(3)使用该锁对所有的方法进行加锁。

(4)该锁可以有任意多个相关条件。

如果一个方法用synchronized关键字声明,那么他表现的就像是一个监视器方法,通过调用wait、notifyAll/notify方法来访问条件变量。但在下述3个方面Java对象不同于监视器,使线程的安全性下降:

(1)域不要求是private;

(2)方法不要求必须是synchronized;

(3)内部锁对客户可用。

4.7Volatile域

!! Brian Goetz给出的“同步格言”:如果向一个变量写入值,而此变量接下来可能会被另一个线程读取;或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。

volatile关键字为实例域的同步访问提供了一种免锁机制。若声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

//使用内部锁可能会发生阻塞
private boolean done;
public synchronized boolean isDone(){return done;}
public synchronized void setDone(){done=true;}
//将域声明为volatile是合理的:
private volatile boolean done;
public boolean isDone(){return done;}
public void setDone(){done=true;}
!!volatile变量不能提供原子性。
public void flipDone(){done = !done;}//非原子操作,不能确保改变域中的值
!!在这种非常简单的情况下,存在第三种可能性,使用AtomicBoolean。这个类有方法get和set,且确保是原子的。


在以下3个条件下,域的并发访问是安全的:

(1)域是final,且在构造器调用完成之后被访问;

(2)对域的访问由公有的锁进行保护;

(3)域是volatile的。

4.8死锁

死锁:某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间相互等待的连续循环,没有哪个线程能继续。

4.9锁测试与超时

tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情。

if(myLock.tryLock()){  //myLock.tryLock(100,TimeUnit.MILLISECONDS):超时参数
try{
...
}
finally{
myLock.unLock();
}
else{
...
}


如果调用带有超时参数的tryLock,那么如果一个线程在等待期间被中断,将抛出InterruptedException异常。这一特性允许程序打破死锁。

4.10读/写锁

java.util.concurrent.locks包定义了两个锁类,ReentrantLock和ReentrantReadWriteLock。

如果很多线程从一个数据结构读取数据而很少线程修改其中数据,ReentrantReadWriteLock类是很有用的。这种情况下,允许对读者线程共享访问是合适的,然而写者线程依然必须是互斥访问的。

使用读/写锁的必要步骤:
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();}
}


5.阻塞队列

当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空时,阻塞队列导致线程阻塞。

协调多个线程间的合作时,阻塞队列是一个有用的工具。



6.线程安全的集合

6.1高效的映像、集合和队列

java.util.concurrent包提供了映像、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。

这些集合通过允许并发的访问数据结构的不同部分来使竞争极小化。

并发的散列映射表,可高效的支持大量的读者和一定数量的写者(默认16个写者线程同时执行)。

6.2写数组的拷贝

CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。

7.Callable与Future

Runnable封装一个异步运行的任务,可把他想象成一个没有参数和返回值的异步方法。

Callable与Runnable类似,但是有返回值。Callable是一个参数化的类型,只有一个方法call。

public interface Callable<V>{
V call() throws Exception;
}


Future保存异步计算的结果。

//Future接口有下面的方法
public interface Future<V>{
V get() throws ...;
V get(long timeout,TimeUnit unit) throws ...;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDone();
}


FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,同时实现二者的接口。

8.执行器

如果要创建大量的生命期很短的线程,应该使用线程池。线程池还可以减少并发线程的数目。

线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法,当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

执行器类(Executor)有许多静态工厂方法用来构建线程池:



8.1线程池

//可以以下方法之一将一个Runnable对象或Callable对象提交给ExecutorService:
Future<?> submit(Runnable task);//可用来调用isDone、cancel或isCancelled.get方法在完成时仅简单返回null。
Future<T> submit(Runnable task,T result);//Future的get方法在完成时返回指定的result对象
Future<T> submit(Callable<T> task);//返回的Future对象将在计算结果准备好时得到他。


shutdownNow:线程池取消尚未开始的所有任务并试图中断正在运行的线程。

shutdown:被关闭的执行器不再接受新的任务,当所有任务都完成后,线程池中的线程死亡。

8.2预定执行

ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法。

可预定Runnable或Callable在初始的延迟之后只运行一次,也可预定一个Runnable对象周期性的运行。

8.3控制任务组

invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。

invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可像下面这样对结果进行处理:

List<Callable<T>> tasks = ...;
List<Future<T>> results = executor.invokeAll(tasks);
for(Future<T> result:results){
processFurther(result.get());
}
!!缺点:如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。


常规方法获得一个执行器,然后构建一个ExecutorCompletionService,提交任务给完成服务(completion service)。该服务管理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());
}


9.同步器



9.1信号量

9.2倒计时门栓

9.3障栅

SyclicBarrier类实现了一个集结点称为障栅。

考虑大量线程运行在一次计算的不同部分的情形:当所有部分都准备好时,需把结果组合在一起。当一个线程完成了它的那部分任务后,让他运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。

实现过程:

//构造一个障栅,给出参与的线程数:
SyclicBarrier barrier = new SyclicBarrier(nthreads);
//每个线程做一些工作,完成后在障栅上调用await:
public void run(){
doWork();
barrier.await();//barrier.await(100,TimeUnit.MILLISECONDS)
...
}
!!可提供一个可选的障栅动作,当所有线程到达障栅时就会执行:
Runnable barrierAction = ...;
SyclicBarrier barrier = new SyclicBarrier(nthreads,barrierAction);


9.4交换器

9.5同步对列

同步队列是一种将生产者与消费者线程配对的机制。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  多线程 java