第21章——并发(二)
结束任务
前面的示例,cancel()和isCanceled()放到了所有任务都能看到的类中。通过检查isCanceled()来确定何时终止它们自己。这是一种合理的方式。但是,有些情况下,任务必须更加突然地终止。
装饰性花园
演示终止,演示资源共享。
该仿真程序,希望统计通过多个大门进入公园的总人数。每个大门都有一个十字转门计数器,任何一个十字转门的计数值递增时,表示公园中总人数的共享计数值也递增。
package com.example.demo.garden;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class Count {
private int count = 0;
private Random rand = new Random(47);
// Remove the synchronized keyword to see counting fail;
public synchronized int increment() {
int temp = count;
if (rand.nextBoolean())//Yield half the time
{
Thread.yield();
}
return (count = ++temp);
}
public synchronized int value() {
return count;
}
}
class Entrance implements Runnable {
private static Count count = new Count();
private static List<Entrance> entrances = new ArrayList<Entrance>();
private int number = 0;
//Doesn't need synchronization to read
private final int id;
private static volatile boolean canceled = false;
//Atomic operation on a volatile field;
public static void cancel() {
canceled = true;
}
public Entrance(int id) {
this.id = id;
//Keep this task in a list.Also prevents
//garbage collection of dead tasks
entrances.add(this);
}
@Override
public void run() {
while (!canceled) {
synchronized (this) {
++number;
}
System.out.println(this + "Total" + count.increment());
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("sleep interrupted");
}
}
System.out.println("Stopping" + this);
}
public synchronized int getValue() {
return number;
}
public String toString() {
return "Entrance" + id + ": " + getValue();
}
public static int getTotalCount(){
return count.value();
}
public static int sumEntrances(){
int sum=0;
for(Entrance entrance:entrances)
sum+=entrance.getValue();
return sum;
}
}
public class OrnamentalGarden {
public static void main(String[] args) throws Exception {
ExecutorService exec= Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new Entrance(i));
//Run for a while,then stop and collect the data;
}
TimeUnit.SECONDS.sleep(3);
Entrance.cancel();
exec.shutdown();
if(!exec.awaitTermination(250,TimeUnit.MILLISECONDS))
System.out.println("some tasks were not terminated");
System.out.println("Total:"+Entrance.getTotalCount());
System.out.println("Sum of Entrances:"+ Entrance.sumEntrances());
}
}
//结果:
Entrance0: 1Total1
Entrance3: 1Total4
Entrance2: 1Total3
Entrance1: 1Total2
Entrance4: 1Total5
Entrance1: 2Total6
Entrance0: 2Total10
Entrance4: 2Total7
Entrance3: 2Total9
Entrance2: 2Total8
。。。。。。。。。。省略
StoppingEntrance0: 28
StoppingEntrance1: 28
StoppingEntrance3: 28
StoppingEntrance2: 28
StoppingEntrance4: 28
Total:140
Sum of Entrances:140
这里使用单个的Count对象来跟踪花园参观者的主计数值,并且将其当作Entrance类中的一个静态域进行存储。Count.increment()和Count.value()都是synchronized的,用来控制对count域的访问。increment()方法使用了Random对象,目的是在从把count读取到temp中,到递增temp并将其存储回count的这段时间里,有大约一半的时间产生让步。如果将increment()上的synchronized关键字注释掉,那么这个程序就会崩溃,因为多个任务将同时访问并修改count(yield()会使问题更快地发生。)
每个Entrance任务都维护着一个本地值number,它包含通过某个特定入口进入的参观者数量。这提供了对count对象的双重检查,以确保其记录的参观者数量是正确的。Entrance.run()只是递增number和count对象,然后休眠100毫秒。
因为Entrance.canceled是一个volatile布尔标志,而它只会被读取和赋值(不会与其它域组合在一起被读取),所以不需要同步对其的访问,就可以安全地操作它。如果你对诸如此类的情况有任何疑虑,最好使用synchronized。
3秒钟之后,main()向Entrance发送static cancel()消息,然后调用exec对象的shutdown()方法,之后调用exec上的awaitTermination()方法。ExecutorService.awaitTermination()等待每个任务结束,如果所有的任务都在超时时间达到之前全部结束,则返回true,否则返回false,表示不是所有的任务都已经结束了。这会导致每个任务都退出其run()方法,并因此作为任务而终止,但是Entrance对象仍是有效的,因为在构造器中,每个Entrance对象都存储在称为entrances的静态List<Entrance)>(没有右括号)中。因此,sumEntrances()仍旧可以作用于这些有效的Entrance对象。
这个程序运行中,将会看到,人们在通过十字转门时,将显示总人数和通过每个入口的人数。如果移除Count.increment()上面的synchronized声明,总人数与期望会有差异,每个十字转门统计的人数将与count中的值不同。只要用互斥来同步对Count的访问,问题就可以解决了。Count.increment()通过使用temp和yield(),增加了失败的可能性。真正的线程中,失败的可能性从概率性的角度来说很小,但不代表不会出现错误。
在阻塞时终结
前面示例Entrance.run()在其循环中包含对sleep()的调用。sleep()最终会唤醒,而任务也将返回循环的开始部分,去检查canceled的标志,从而决定是否跳出循环。但是,sleep()一种情况,它使任务从执行状态变为阻塞状态,而有时必须终止被阻塞的任务。
线程状态
- 一个线程可以处于以下四种状态之一:
1.新建(new):当线程被创建时,它只会短暂地处于这个状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将这个线程转变为可运行状态或阻塞状态。
2.就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行,也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。
3.阻塞(Blocked):线程能够运行,但有某个条件阻止它运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
4.死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回。但是任务的线程还可以被中断。
进入阻塞状态
- 一个任务进入阻塞状态,可能有如下的原因:
1.通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
2.通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息(或JavaSE5的java.util.concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。
任务在等待某个输入/输出完成。
任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。
(suspend()和resume()方法已经被废止了,因为可能会导致死锁。stop()也被废止,因为它不释放线程获得的锁,并且如果线程处于不一致状态(受损状态),其他任务可以在这种状态下浏览并修改它们)
中断
推荐阅读
-
找出链表倒数第n个节点元素的二个方法
-
Python实现查找二叉搜索树第k大的节点功能示例
-
一个模仿oso的php论坛程序源码(之二)第1/3页
-
SQL Server 索引结构及其使用(二) 改善SQL语句第1/3页
-
asp.net下用url重写URLReWriter实现任意二级域名的方法第1/2页
-
【C++并发实战】(二)线程管理
-
java高并发系列 - 第10天:线程安全和synchronized关键字
-
java高并发系列 - 第11天:线程中断的几种方式
-
并发编程(二)—— CountDownLatch、CyclicBarrier和Semaphore
-
Python实现查找二叉搜索树第k大的节点功能示例