知识点
- 线程的创建和运行
- 线程信息的获取和设置
- 线程的中断
- 线程中断的控制
- 线程的休眠和恢复
- 等待线程的终止
- 守护线程的创建和运行
- 线程中不可控异常的处理
- 线程局部变量的使用
- 线程的分组
- 线程组中不可控异常的处理
- 使用工厂类创建线程
不得不讲的并发与并行
所有的并发处理都有排队等候,唤醒,执行至少三个这样的步骤.所以并发肯定是宏观概念,在微观上他们都是序列被处理的,只不过资源不会在某一个上被阻塞(一般是通过时间片轮转),所以在宏观上看多个几乎同时到达的请求同时在被处理。
并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。
线程的创建和运行
Java提供了两种方式来创建线程:
1)继承Thread类,并覆盖run()方法
2)创建一个是实现Runnable接口的类。
下面以第二种方式创建10个简单的线程,每个线程完成计算和打印乘以1-10后的结果。1
2
3
4
5
6
7
8
9
10
11
12
13public class Calculator implements Runnable{
private int number;
public Calculator(int number){
this.number = number;
}
public void run(){
for(int i = 1; i <= 10; i++){
System.out.printf("%s: %d * %d = %d\n", Thread.currentThread().getName(), number, i, i*number);
}
}
}
接着编写一个主类,进行测试。
1 | public class Main{ |
简单说明下,对一个实现了Runnable接口的类来说,创建Thread对象并不会创建一个新的执行线程,同样,调用它的run()方法,也不会创建一个新的执行线程。只有当Thread调用start()方法时,才会创建一个新的执行线程来调用run()方法。
当一个程序的所有线程都运行完成时,更明确的说,当所有非守护线程都运行完成的时候,这个Java程序将宣告结束。
如果main线程结束了,而其余的线程仍将继续执行它们的任务,直到运行结束。但如果某一个线程调用了System.exit()指令来结束程序的执行,则所有的线程都将结束。
线程信息的获取和设置
Thread类有一些保存信息的属性,这些属性可以用来标识线程,如显示线程的状态、控制线程的优先级。
ID:保存了线程的唯一标识。
Name:保存了线程的名称。
Priority:保存了线程的优先级。从1到10,由低优先级到高优先级。
Status:保存了线程的状态。在Java中,有6种状态,new \ runnable \ blocked \ waiting \ timewaiting \ terminated。
下面我们来打印下线程的这些标识。
1 | public class Calculator implements Runnable{ |
1 | public class Main{ |
这样每个线程的状态演变都记录在log.txt里。
Thread类的属性存储了线程的所有信息,JVM使用线程的Priority来决定某一时刻由哪个线程来使用CPU,并根据线程的情景为它们设置实际的状态。
如果Thread类没有取名字,JVM会自动分配一个名字给它,如Thread-XX。
Thread的ID和状态是只读的,不能自己set。另外,setPriority()方法的值必须是1~10之内,超过这个访问会报IllegalArgumentException异常。
线程的中断
前面提到过单个线程结束并不会使进程结束,只有当所有的线程都结束了,这个进程才会结束。中途结束进程可以调用System.exit(),那么中途结束线程呢?Java也提供了中断机制。这种机制要求线程检查它是否被中断了,然后决定是不是影响这个中断请求。换句话说,线程是允许忽略中断请求并继续运行的。
接下来,还是以代码来说明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class PrimeGenerator extends Thread{
public void run(){
long number = 1L;
while(true){
if(isPrime(number)){
System.out.println("Number %d is Prime\n", number);
}
if(isInterrupted()){
System.out.println("The Prime Generator has been Interrupted\n");
return;
}
number++;
}
}
/**
*是否是质数
*/
private boolean isPrime(long number){
if(number <= 2){
return true;
}
for(long i = 2; i < number; i++){
if((number % i )==0){
return false;
}
}
return true;
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13public class Main{
public static void main(String[] args){
Thread task = new PrimeGenerator();
task.start();
try{
TimeUnit.SECONDS.sleep(5);
}catch(InterruptedException e){
e.printStackTrace();
}
task.interrupt();
}
}
Thread类有一个表明线程被中断与否的属性,它存放的是布尔值。线程的interrupt()方法被调用时,这个属性就会被设置为true。isInterrupted()方法只是返回这个属性的值。
线程中断的控制
已经学会中断线程,也学会了在线程对象中去控制这个中断。在实际编码中,如果线程实现了复杂的算法并且分布在几个方法中,或者线程里有递归调用等,我们就得使用一个更好的机制来控制线程的中断。Java提供了InterruptedException异常。当检查到线程中断时,就抛出这个异常,然后在run()中捕获并处理这个异常。
主程序1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Main{
public static void main(String[] args){
FileSearch searcher = new FileSearch("C:\\", "autoexec.bat");
Thread thread = new Thread(searcher);
thread.start();
try{
TimeUnit.SECONDS.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
thread.interrupt();
}
}
这样不管递归调用多少次,只要抛出InterruptedException,就结束线程,释放资源。
线程的休眠和恢复
线程的休眠可以通过调用sleep()方法来实现。sleep()方法接受整型数值作为参数,以表明线程挂起执行的毫秒数。
sleep()方法的另一种使用方式是通过TimeUnit枚举类进行调用。这个方法也使用Thread类的sleep()方法来使当前线程休眠,但是它接受的参数单位是秒,最终会被转化成毫秒。1
2
3
4
5
6
7
8
9
10
11
12
13public class FileClock implements Runnable{
public void run(){
for(int i = 0; i < 10; i++){
System.out.println("%s\n", new Date());
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
System.out.println("The FileClock has been interrupted");
}
}
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Main{
public static void main(String[] args){
FileClock clock = FileClock();
Thread thread = new Thread(clock);
thread.start();
try{
TimeUnit.SECONDS.sleep(5);
}catch(InterruptedException e){
e.printStackTrace();
}
thread.interrupt();
}
}
如果休眠中线程被中断,该方法就会立即抛出InterruptedException异常,而不需要等待到线程休眠时间结束
等待线程的终止
有时候,我们必须等待某个线程的终止,这个时候可以使用Thread类的join()方法。当一个线程对象的join()方法被调用时,调用它的线程将被挂起,直到这个线程对象完成它的任务。1
2
3
4
5
6
7
8
9
10
11
12public class DataSourcesLoader implements Runnable{
public void run(){
System.out.println("Begining data sources loading: %s\n", new Date());
try{
TimeUnit.SECONDS.sleep(4);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Data sources loading has finished: %s\n", new Date());
}
}
1 | public class NetworkConnectionsLoader implements Runnable{ |
1 | public class Main{ |
运行发现,只有当DataSourcesLoader线程运行结束,NetworkConnectionsLoader线程也运行结束的时候,主线程对象才会继续运行并且打印出最终的信息。
另外,java还提供了另外两个形式的join()方法:join(long milliseconds) join(long milliseconds, long nanos)
这两种形式的join返回比join()方法多一个条件,就是指定的时间到时,线程也将继续执行。
守护线程的创建和运行
Java里有一个特殊的线程叫做守护(Daemon)线程。这种线程的优先级很低,通常当一个程序中没有其他线程运行的时候它才运行。当守护线程是程序中唯一运行的线程时,它的结束也就意味着整个程序的结束。
因为这种特性,守护线程一般用来作为其他普通线程的服务提供者,通常都是无限循环的,以等待服务请求或者执行线程任务。一个典型的守护线程就是Java的垃圾回收器(Garbage Collector)。
下面一个例子:创建两个线程,一个用户线程,它将事件写入到一个队列中;另一个是守护线程,它将管理这个队列,如果生成的事件超过10秒钟,就会被移除。
创建Event类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Event {
private Date date;
private String event;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
}
创建WriteTask类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class WriterTask implements Runnable {
Deque<Event> deque; //事件队列
public WriterTask (Deque<Event> deque){
this.deque=deque;
}
public void run() {
// Writes 100 events
for (int i=1; i<100; i++) {
//每次循环中都会创建一个新的Event对象,并放入队列
Event event=new Event();
event.setDate(new Date());
event.setEvent(String.format("The thread %s has generated an event",Thread.currentThread().getId()));
deque.addFirst(event);
try {
//休眠一秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
创建CleanerTask类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class CleanerTask extends Thread {
private Deque<Event> deque;
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
//设置为守护线程
setDaemon(true);
}
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
}
private void clean(Date date) {
long difference;
boolean delete;
if (deque.size()==0) {
return;
}
delete=false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n",e.getEvent());
deque.removeLast();
delete=true;
}
} while (difference > 10000);
if (delete){
System.out.printf("Cleaner: Size of the queue: %d\n",deque.size());
}
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Main {
public static void main(String[] args) {
Deque<Event> deque=new ArrayDeque<Event>();
WriterTask writer=new WriterTask(deque);
for (int i=0; i<3; i++){
Thread thread=new Thread(writer);
thread.start();
}
CleanerTask cleaner=new CleanerTask(deque);
cleaner.start();
}
}
对程序的分析发现,队列中的对象不断增长直到30个,然后到程序结束,队列的长度维持在27~30之间。
这3个WriteTask,每个线程写入一个事件,然后休眠1秒钟。在第一个10秒钟内,队列中有30个事件,直到3个WriteTask都休眠后,CleanerTask才开始执行,但由于事件都小于10秒并未删除任何事件。在接下来的运行中,CleanerTask每秒删除3个对象,同时WriterTask会写入3个对象,所以队列的长度一直介于27~30之间。
setDaemon()只能在start()之前才能被调用,一旦线程开始运行,将不能再修改守护状态。
isDaemon()用来检查一个线程是不是守护线程。
线程中不可控异常的处理
在java中有两种异常。
- 非运行时异常。 这种异常必须在方法声明的throws语句指定,或者在方法体内捕获。例如IOException和ClassNotFoundException。
- 运行时异常。 例如:NumberFormatException。
因为run()方法不支持throws语句,run()中抛出非运行异常是,必须捕获并且处理它。
创建处理运行时异常的类1
2
3
4
5
6
7
8
9
10
11public class ExceptionHandler implements UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n",t.getId());
System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
抛出异常的线程类1
2
3
4
5
6public class Task implements Runnable {
public void run() {
int numero=Integer.parseInt("TTT");
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Main {
public static void main(String[] args) {
Task task=new Task();
Thread thread=new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread has finished\n");
}
}
当一个线程抛出异常并且没有被捕获时(这种情况只可能是运行时异常),JVM检查这个线程是否被预置了未捕获异常处理器。如果找到,JVM将调用线程对象的这个方法,并将线程对象和异常作为传入参数。
如果线程没有被预置未捕获异常处理器,JVM将打印堆栈记录到控制台,并退出程序。
Thread类还有另一个静态方法setDefaultUncaughtExceptionHandler()。这个方法在应用程序中为所有的线程对象创建了一个异常处理器。
当线程抛出一个未捕获的异常时,JVM将为异常寻找以下3种可能的处理器。
- 查找线程对象的未捕获异常处理器
- (如果上面找不到)JVM将继续查找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器
- (如果上面也没找到)JVM将继续查找默认的未捕获异常处理器。
如果没有一个处理器存在,JVM打印记录,然后退出。线程局部变量的使用
共享数据是并发最核心的问题之一。
Java并发API提供了一个干净的机制,即线程局部变量(Thread-Local Variable)。
创建线程共享类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class UnsafeTask implements Runnable{
private Date startDate;
public void run() {
startDate=new Date();
System.out.printf("Starting Thread: %s : %s\n",Thread.currentThread().getId(),startDate);
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread.currentThread().getId(),startDate);
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Main {
public static void main(String[] args) {
UnsafeTask task=new UnsafeTask();
for (int i=0; i<3; i++){
Thread thread=new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这个每个线程有一个不同的开始时间,但当它们结束时,都有相同的startDate属性值。
用线程局部变量机制解决这个问题。
创建SafeTask类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class SafeTask implements Runnable {
private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
protected Date initialValue(){
return new Date();
}
};
public void run() {
System.out.printf("Starting Thread: %s : %s\n",Thread.currentThread().getId(),startDate.get());
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
// Writes the start date
System.out.printf("Thread Finished: %s : %s\n",Thread.currentThread().getId(),startDate.get());
}
}
1 |
|
它跟UnsafeTask类的run()方法实现了一样的功能,但是访问startDate属性的方式改变了。
现在,这3个线程对象都有它们自己的startDate属性值。
线程局部变量分别为每个线程存储了各自的属性值,并提供给每个线程使用。可以使用get()方法读取这个值,并用set()方法设置这个值。
如果线程第一次访问线程局部变量,线程局部变量可能还没有为它存储值,这时initialValue()被调用,并返回当前时间值。
线程局部变量也提供了remove()方法。
如果一个线程是从其他某个线程中创建的,这个类将提供继承的值。线程A在线程局部变量已有值,当它创建线程B,线程B的线程局部变量将跟线程A是一样的。可以覆盖childValue()方法,这个方法用来初始化子线程在线程局部变量中的值。它使用父线程在线程局部变量中的值作为传入参数。
线程的分组
Java并发API提供了把线程分组,对组内线程对象进行访问并操作它们。例如,对于一些执行同样任务的线程,不管组内多少线程在运行,只需要一个单一的调用,所有这些线程的运行都会被中断。
ThreadGroup类表示一组线程。 线程组可以包含线程对象,也可以包含其他线程组对象,它是一个树形结构。
下面创建10个线程查询相同的任务并让它们随机休眠一段时间,当其中一个线程查询到结果,将其他9个线程中断。
创建Result类1
2
3
4
5
6
7
8
9
10
11public class Result {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建SearchTask类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class SearchTask implements Runnable {
private Result result;
public SearchTask(Result result) {
this.result=result;
}
public void run() {
String name=Thread.currentThread().getName();
System.out.printf("Thread %s: Start\n",name);
try {
doTask();
result.setName(name);
} catch (InterruptedException e) {
System.out.printf("Thread %s: Interrupted\n",name);
return;
}
System.out.printf("Thread %s: End\n",name);
}
private void doTask() throws InterruptedException {
Random random=new Random((new Date()).getTime());
int value=(int)(random.nextDouble()*100);
System.out.printf("Thread %s: %d\n",Thread.currentThread().getName(),value);
TimeUnit.SECONDS.sleep(value);
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class Main {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("Searcher");
Result result=new Result();
SearchTask searchTask=new SearchTask(result);
for (int i=0; i<10; i++) {
Thread thread=new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("Number of Threads: %d\n",threadGroup.activeCount());
System.out.printf("Information about the Thread Group\n");
threadGroup.list();
Thread[] threads=new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
for (int i=0; i<threadGroup.activeCount(); i++) {
System.out.printf("Thread %s: %s\n",threads[i].getName(),threads[i].getState());
}
waitFinish(threadGroup);
threadGroup.interrupt();
}
private static void waitFinish(ThreadGroup threadGroup) {
while (threadGroup.activeCount()>9) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程组类存储了线程对象和关联的线程组对象,并可以访问它们的信息(例如状态),将执行的操作应用到所有成员上(例如中断)。
线程组中不可控异常的处理
Java提供了捕获和处理异常的机制。
有的异常必须被捕获,或者必须使用方法的throws声明再次抛出,这类异常叫做非运行时异常。
还有一类异常叫做运行时异常,它们不需要被什么或者捕获。
建立一个方法来捕获线程组中的任何线程对象抛出的非捕获异常。
创建一个MyThreadGroup类,并继承ThreadGroup。1
2
3
4
5
6
7
8
9
10
11
12
13public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("The thread %s has thrown an Exception\n",t.getId());
e.printStackTrace(System.out);
System.out.printf("Terminating the rest of the Threads\n");
interrupt();
}
}
创建Task类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Task implements Runnable {
public void run() {
int result;
Random random=new Random(Thread.currentThread().getId());
while (true) {
result=1000/((int)(random.nextDouble()*1000));
System.out.printf("%s : %f\n",Thread.currentThread().getId(),result);
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d : Interrupted\n",Thread.currentThread().getId());
return;
}
}
}
}
主类1
2
3
4
5
6
7
8
9
10
11public class Main {
public static void main(String[] args) {
MyThreadGroup threadGroup=new MyThreadGroup("MyThreadGroup");
Task task=new Task();
for (int i=0; i<2; i++){
Thread t=new Thread(threadGroup,task);
t.start();
}
}
}
前面说过,当线程抛出非捕获异常时,JVM将为这个异常寻找3种可能的处理器。
首先,寻找抛出这个异常的线程的非捕获异常处理器,如果这个处理器不存在,JVM继续查找这个线程所在线程组的非捕获异常处理器,上面的代码就是这种情况。
使用工厂类创建线程
使用工厂类,可以将对象的创建集中化,这样做的好处:
- 更容易修改类,或者改变创建对象的方式
- 更容易为有限资源限制对象的数目。例如,可以限制一个类型的对象不多于n个。
- 更容易为创建的对象生成统计数据。
Java提供了ThreadFactory接口。Java并发API的高级工具类也使用了线程工厂创建线程。
创建MyThreadFactory类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class MyThreadFactory implements ThreadFactory {
private int counter;
private String name;
private List<String> stats;
public MyThreadFactory(String name){
counter=0;
this.name=name;
stats=new ArrayList<String>();
}
public Thread newThread(Runnable r) {
Thread t=new Thread(r,name+"-Thread_"+counter);
counter++;
stats.add(String.format("Created thread %d with name %s on %s\n",t.getId(),t.getName(),new Date()));
return t;
}
public String getStats(){
StringBuffer buffer=new StringBuffer();
Iterator<String> it=stats.iterator();
while (it.hasNext()) {
buffer.append(it.next());
}
return buffer.toString();
}
}
创建Task类1
2
3
4
5
6
7
8
9
10public class Task implements Runnable {
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Main {
public static void main(String[] args) {
MyThreadFactory factory=new MyThreadFactory("MyThreadFactory");
Task task=new Task();
Thread thread;
System.out.printf("Starting the Threads\n");
for (int i=0; i<10; i++){
thread=factory.newThread(task);
thread.start();
}
System.out.printf("Factory stats:\n");
System.out.printf("%s\n",factory.getStats());
}
}
ThreadFactory接口只有一个方法,即newThread,它以Runnable接口对象作为传入参数并且返回一个线程对象。当实现ThreadFactory接口时,必须实现覆盖这个方法。
可以通过增加一些变化来强化实现方法覆盖。
- 创建一个个性化线程,如使用一个特殊的格式作为线程名,或者通过继承Thread类来创建自己的线程类
- 保存新创建的线程的统计数据
- 限制创建的线程的数量
- 对生成的线程进行验证
使用工厂设计模式是一个很好的编程实践,如果通过实现ThreadFactory接口来创建线程,以保证所有的线程都是使用这个工厂创建的。