并发是用于多处理器编程的工具。但是并发通常是提高运行在单处理器上的程序的将性能。实现并发的最直接方式是在操作系统级别使用进程。进程是运行在他自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性的CPU从一个进程切换到另一个进程,来实现同时运行多个进程。编写多线程最基本困难是协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
java的线程机制是抢占式的,这表示调度机制会周期的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他的任务。在协作式系统中,每个任务都会自动的放弃控制,这要求程序员有意识的在每个任务中插入某种类型的让步语句。协作式系统的优势:上下文切换的开销通常比抢占式系统要低廉,并且可以同时执行的线程数量在理论上没有限制。通常,线程使得我们能够创建更加松耦合的设计。
基本的线程机制
并发编程使我们可以将程序划分为多个分离、独立的运行的任务。通过使用多线程机制,这些独立任务中的每个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,所以单个进程都可以拥有多个并发执行任务,使得程序每个任务都好像有自己的CPU一样,其底层机制是切分CPU时间。
定义任务
你要执行的任务与驱动它的线程之间有差异,你创建任务,并通过某种方式将一个线程附到任务上,以使得这个线程可以驱动这个任务。Thread本身不执行任何操作,它只是驱动它的任务。
线程可以驱动任务,因此你要一种描述任务的方式。有两种实现方式
- 实现Runnable接口
- 继承Tread类
下面是用实现Runnable接口来创建任务
public class TreadTest implements Runnable{
protected int count = 10;
private static int task= 0;
private final int id = task++;
public TreadTest(){}
public TreadTest(int count){
this.count = count;
}
public String status(){
return "#"+id+(count > 0 ? count :"Liftoff");
}
@Override
public void run() {
while(count-->0){
System.out.print(status());
Thread.yield();
}
}
public static void main(String[] args) {
TreadTest test = new TreadTest();
test.run();
}
}
当从Runnable接口导出类时必须具有run()方法,但是它不会产生任何内在的线程能力。要实现线程行为,必须显式的将一个任务附在线程上。
下面是利用Thread类,将Runnable对象转变为工作任务的传统方式把它将给一个Thread构造器,调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个线程中启动任务。
public class ThreadTest {
public static void main(String[] args) {
Thread thread = new Thread(new TreadTest());
thread.start();
for(int i = 0; i<5;i++){
new Thread(new TreadTest()).start();
}
}
}
使用Executor
- CachedThreadPool 在程序执行中通常会创建与所需数量相同的线程。然后在它回收旧线程时停止创建新线程。是合理的首选。
- FixedThreadPool 可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程数量,可以节省时间,
- SingleThreadPool 就像线程数量为1的FixedThreadPool ,对于希望在另一个线程中连续运行的任何事物来,很有用。若向SingleThreadPool提交了任务,那么这些任务将排队,每个任务都会在下一个任务开始之前结束,所有的任务都是用相同的线程。
Executor为我们管理Thread对象,Executor在客户端和任务执行之间提供了一个间接层,与客户直接执行任务不同,这个中介执行任务,Executor允许你管理异步任务的执行,而无需显式的管理线程的生命周期,是启动任务的优选。
public class ThreadTest {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for(int i = 0;i<5;i++){
es.execute(new TreadTest());
}
es.shutdown();//可以防止新任务被提交给这个Executor
}
}
在任务中产生返回值
Runnable是执行独立的任务但它不会返回任何值。若希望产生返回值,那么可以实现Callable接口而不是Runnable。
submit()方法产生Future对象,他用Callable返回结果的特定类型进行类参数化
public class CallableTest implements Callable<String>{
private int id;
public CallableTest(int id){
this.id = id;
}
@Override
public String call() throws Exception {
return "result is :"+id;
}
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
ArrayList<Future<String>> result = new ArrayList<Future<String>>();
for(int i = 0;i <10;i++){
result.add(es.submit(new CallableTest(i)));
}
for(Future<String> fs : result){
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
休眠
影响任务的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。
如下代码片
public void run() {
while(count-->0){
System.out.print(status());
Thread.sleep(1000);
}
}
加入一个线程
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。对join()方法的调用可以被中断,做法是调用interrupt()。
共享受限资源
解决共享资源的竞争
- synchronized关键字 如果某些事物失败会抛出异常,就没机会做任何清理工作
- 使用显式的Lock对象 可以使用finally字句将系统维护在正确状态,可以有细粒度的控制。
对于并发工作,需要某种方式来阻止两个任务访问相同的资源。防止这种冲突的方法就是在当资源被一个任务使用时,在其上加锁。
基本上所有的并发模式在解决线程安全时,都是采取序列化访问共享资源的方案。通过在代码前面加上一条锁的语句,使得在一段时间只有一个任务可以运行这段代码。这种机制叫互斥量。
java已提供关键字synchronized的形式,为防止资源冲突提供内置支持。当任务要执行被synchronized修饰的代码片段时,他检查锁是否可用,然后获取锁,执行代码,释放锁。
共享资源一般是以对象的形式存在内存片段,要控制对共享资源的访问,得先把他包装进一个对象,然后把所有要访问的资源方法标记为synchronized。如果某个任务处于一个标记为synchronized的方法的调用中,那么在这个线程返回之前,其他要调用改方法的线程都会被阻塞。
synchronized void f(){/………/}
synchronized void g(){/……../}
所有对象都自动含有单一的锁。当在对象上调用其任意synchronized方法的时候,此对象被加锁。这时该对象上的其他synchronized方法只有等前一个方法调用完并释放了锁之后才可被调用。所以对于某个特定对象而言,其所有的synchronized方法共享同一个锁,可以防止多个任务同时访问被编码为对象内存。
原子性与易变性
原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么一定可以在可能发生的上下文切换之前执行完毕。
当定义long或double变量是如果使用volatile关键字,就会获得原子性。
所以原子操作可由线程机制来保证其不可中断。volatile还确保了应用中的可视性,如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么其他所有的读操作都可以看到这个修改。即便使用了本地缓存,情况也一样,volatile域会被立即写入到主存中。如果将一个域完全由synchronized方法或语句块保护,那就不必将其设置为是volatile的。
如果当一个域的值依赖于他之前的值时,volatile就无法工作了。
使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变域。记住,我们的第一选择是synchronized,这是最安全的方式。
临界区
如果只是希望防止多个线程同时访问方法内部的部分代码而不是防止整个方法,通过这种方式分离出来的代码段被称为临界区,他也是用synchronized关键字建立。synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。
这也被称为同步控制块,在进入此代码块前必须拿到syncObject的锁。
synchronized(syncObject){
// this code can be accessed by one task at a time
}
下面分别使用同步方法和同步代码块
public class syncObjectTest {
private Object syncObject = new Object();
public synchronized void f(){
for(int i = 0;i<5;i++){
System.out.println("f()");
Thread.yield();
}
}
public void g(){
synchronized(syncObject){
for(int i=0;i<5;i++){
System.out.println("g()");
Thread.yield();
}
}
}
public static void main(String[] args) {
final syncObjectTest so = new syncObjectTest();
new Thread(){
public void run(){
so.f();
}
}.start();
so.g();
}
}
线程状态
一个线程可以处于一下四种状态之一:
- 新建,此时它已经被分配了必须的系统资源,并执行了初始化。此刻线程已经获得了CPU时间,之后调度器将把这个线程转变为可变运行状态或阻塞状态。
- 就绪,只要调度器把时间片分配给线程,线程就可以运行了。只要调度器能分配时间片给线程,他就可以运行,与死亡和阻塞不同。
- 阻塞,线程能运行,但有某个条件阻止他运行。处于阻塞时调度器不会给他分配时间片。直到重新进入了就绪状态,它才可执行
- 死亡,将不再可调度,并且再也不会得到时间片,任务已经结束。死亡的通用方式是从run()方法返回,但是任务的线程还可以被中断。
进入阻塞状态,可能的原因:
- 任务在等待某个输入或输出完成.
- 通过调用sleep()使任务进入休眠状态,在指定时间内不会运行。
- 通过调用wait()使线程挂起。直到等到notify()或notifyAll()消息,线程才会进入就绪状态。
- 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用。
中断
interrupt()方法可以终止被阻塞的任务,这个方法将设置线程的中断状态,被调用时,中断状态将被复位。
检查中断
当在线程上调用interrupt()时,中断发生的唯一时刻是在任务要进入阻塞操作中,或者已经在阻塞操作内部时。我们可以通过中断状态来检查中断状态,用interrupted()来检查。不仅可以知道interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断通知你两次。
线程之间的协作
当任务协作时,关键问题是这些任务之间的握手,为了实现这种握手,使用相同的基础特性:互斥。这种情况下,互斥能够确保只有一个任务可以响应某个信号,这样就可以根除竞争条件。在互斥之上,我们为任务添加了一种途径,可以将自身挂起,直到某些外部条件发生变化,表示是时候让这个任务向前开动了为止。
wait()和notifyAll()
wait()(会释放锁,因此该对象中的其他synchronized方法可以在wait期间被调用)会等待外部产生变化时候将任务挂起,并且只有在notify(),notifyAll()发生时人物才会被唤醒。调用sleep()、yield()时候并没有释放锁。我们可以把wait()放进任何同步控制方法里而不考虑这个类是否继承自Runnable接口。
错失信号
synchronized(sharedMonitor){
sharedMonitor.notify();
}
//有缺陷,应防止someCondition产生竞争条件
while(someCondition){
synchronized(sharedMonitor){
sharedMonitor.wait();
}
}
//正确执行方式
synchronized(sharedMonitor){
while(someCondition){
sharedMonitor.wait();
}
}
死锁
同时满足以下四个条件就会发生死锁:
- 互斥条件。任务使用的资源至少有一个是不能共享的。
- 至少有个一任务必须持有一个资源且正在等候获取一个当前被别的任务持有的资源。
- 资源不能把任务抢占。
- 必须有循环等待。