线程基础(前篇)

  • 1、线程当中的基本概念
  • 2、定义任务
  • 3、驱动任务
  • 4、驱动大量任务
  • 5、任务产出
  • 6、暂停任务

1、线程当中的基本概念

计算机的CPU发展到多个核心数的时候,就可以并发的跑程序,对于用户而言程序越来越快,对于开发者而言就是很大的挑战。并发编程当中,主要的概念就是任务、以及驱动任务这两个概念。

Java常用API也为我们提供了相应的类,来表述上面的两个概念,就是Runnable、Thread这两个概念,在这里我们需要明白的就是Runnable对应着我们新开辟的线程将要做什么样子的事情、而Thread就是去驱动也就是开启我们的定义好的任务。

2、定义任务

Runnable接口来定义一个任务,去实现这个类,然后重写这个类当中的run方法就可以很轻松的去定义一个任务,这里把我们要做的事情在Runnable的run方法当中表述出来即可,下面来看一个小例子,描述如何在Java当中定义一个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefineTask implements Runnable{

protected int countDown = 10 ;
private static int taskCount = 0 ;
private final int id = taskCount++ ;
@Override
public void run() {
while(countDown-- > 0){
System.out.println(status());
Thread.yield();
}
}

public String status(){
return "#" + id + "(" + (countDown > 0 ? countDown : "denfine task") + ")," ;
}
}

上述代码,我们很轻松的创建了一个任务,我们通过几个变量来标示这个线程,比如当前线程的ID等,然后就是倒计时的变量。

关于Run方法的实现,在我们日常开发当中,比较常用的做法就是通过一个Looper来让一个任务一直的运行,然后通过一个标示位来控制这个Looper,当任务做完了以后我们主动的调用了yield方法,这个方法是对线程调度器的一种操作,大体的意思就是,我这边已经执行完了相应的任务,可以切换出去给别的线程使用了,需要明白的就是不是一调用这个方法立马就会把切出去,这完全取决于JDK。

3、驱动任务

上述定义了任务,但是如何开启我们的任务呢,这个时候,我们就需要使用Thread类来驱动我们的任务,上面我们定义好了,相应的任务,现在可以尝试的使用Thread来启动这个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}
output:
main Thread
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(denfine task),

驱动我们的线程很简单,通过Thread的构造器,把定义好的任务传进去,然后调用start方法就开启了我们的任务,这里需要我们注意的一点就是start方法看起来像是一个长时间执行的方法,但是通过执行结果来看的话,却是立即返回了,原因就是后面的输出语句打印出来了。所以在此刻底层为我们重新创建了一个线程,去执行我们的任务,

这里有有一点需要我们注意一下,那就是关于run和start方法的区别,例如下面的这一条示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	public static void main(String[] args) {
DefineTask defineTask = new DefineTask();
defineTask.run();
System.out.println("main Thread ");
}
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(denfine task),
main Thread

主要看两次的输出结果就可以发现,到底是如何才能区分它们两个。

当我们有大量的任务需要处理的时候,应该如何去做了,也许你已经想到了,那就是在main当中通过一个Looper来创建相应的线程,如下示例所示:

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
41
42
43
44
45
46
public static void main(String[] args) {
for(int i = 0; i < 5; i++){
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}
}


public static void main(String[] args) {
for(int i = 0; i < 5; i++){
DefineTask defineTask = new DefineTask();
Thread thread = new Thread(defineTask);
thread.start();
System.out.println("main Thread ");
}

}
main Thread main Thread #0(9),
#1(9), main Thread
#2(9), #0(8),
#1(8), main Thread
main Thread #3(9),
#4(9), #2(8),
#4(8), #3(8),
#1(7), #0(7),
#4(7), #2(7),
#3(7), #0(6),
#3(6), #0(5),
#2(6), #0(4),
#4(6), #1(6),
#2(5), #3(5),
#0(3), #4(5),
#0(2), #4(4),
#3(4), #2(4),
#4(3),#3(3),
#1(5), #0(1),
#2(3), #4(2),
#3(2), #1(4),
#2(2), #0(denfine task),
#4(1), #3(1),
#1(3), #2(1),
#4(denfine task), #3(denfine task)
#1(2), #2(denfine task),
#1(1), #1(denfine task),

通过上述的输出结果,可以看到,线程是混在一起执行的,也就是每一次执行的结果都不会相同,这就说明了相应的线程执行的时候,将会存在很多的时许问题。

当main创建相应的线程的时候,当前的类并没有捕获任何对这些对象的引用。在使用普通对象的时候,会获取相应的引用,如果不需要了释放引用,那么垃圾回收就认为这个是一个垃圾,但是使用线程相应的对象的时候就不一样了,在使用的时候,每一个线程都会注册自己,这样就让一个引用都持有它,而且在它的任务退出之前,垃圾回收机制都无法清理它。

需要记住,在对start的调用完毕以后,这个对象将依旧会持续存在。

4、驱动大量任务

如果现在存在大量的任务需要我们去做的时候,应该怎么办,上述方式可以解决,但是如果有1000个任务,那么就需要创建1000一个Thread对象,通过上述知道了Thread对于垃圾回收机制的特殊性,那么我们的内存可想而知,并且我们也不能管理我们的任务。

有了上述的需求,这个时候我们就不能使用单个的Thread类去驱动任务了,需要Java为我们提供的另外一个叫做线程池的概念去驱动,和它的名字是一样的,那就是线程池就像是一个水池一样,当中放着大量的单个Thread,然后通过管理大量的Thread去驱动我们的任务。线程池这个概念是介于客户端和任务之间的一个中间层,与客户端直接执行任务不同,这个中间层允许你管理异步任务的执行,而无需显示的管理线程的生命周期。

Java为我们提供了四种常用的线程池

  • newCacheThreadPool
  • newFixedThreadPool
  • newSingleThreadExecutor
  • newScheduleThreadPool

基本的用法都差不多类似,sun为我们抽出来了特定的接口,我们只要看接口怎么使用就好了,只是具体实现不同而已。先来熟悉一下具体的用法吧,还是使用上述的例子

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
/**
* 线程池的研究
* @author pengchengliu
*
*/
public class ThreadPool implements Runnable{
//当前线程的执行次数。
protected int countDown = 10 ;
private static int taskCount = 0;

//使用一个ID来唯一表示当前的一个任务是必须的!使用fainl是因为我们希望一旦被赋值就不会改变。
private final int id = taskCount ++ ;

public String status(){
return "#" + this.id + "(" + (countDown > 0 ? countDown : "Liftoff!" ) + ").";
}
@Override
public void run() {
//使用循环,使得任务一直运行下去直到不再需要。所以需要设定跳出循环的条件,
//通常run被写成无限循环的形式,这意味着,除非有某个条件是的线程终止,否则他就会永远运行下去
while(countDown-- > 0){
System.out.println(status());
//是对线程调度器的一种建议,它在声明“我已经执行完声明周期中最重要的部分,此刻正是切换给其他任务执行的大好时机”
Thread.yield();
}
}

public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++){
service.execute(new ThreadPool());
}
service.shutdown();
}

}

具体的输出和上面一致,这里我们看到sun为我们提供的公共接口就是ExecutorService,我们这里使用了该接口的两个方法,一个是execute,类似于start方法,还有一个就是shutdown方法,使用这个方法防止新任务被提交到线程池,main线程将继续运行在shutdown被调用之间提交的所有任务,这个程序将在Executor中的所有任务完成之后尽快退出。

由于sun为我们抽离出来了共有的接口,所以将原有的实现类替换成为其他的实现类特别简单,下面我们试着替换一下

1
2
3
4
5
6
7
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for(int i = 0; i < 5; i++){
newFixedThreadPool.execute(new ThreadPool());
}
newFixedThreadPool.shutdown();
}

下面详细说一下这四种线程池的区别的使用场景吧。

4.1、newCacheThreadPool

创建一个可以缓存的线程池,如果线程池的长度超过处理的需要,可灵活回收,若无可回收,则新建线程。

特点

  • 工作线程的创建数量几乎没有限制(通过源码可以知道是Int的最大值),这样就可以灵活的向线程池中添加线程
  • 如果长时间没有往线程池当中提交任务,即如果工作线程空闲了指定的时间(默认是1分钟),则该工作线程将自动终止。终止后,如果你有提交了新的任务,则线程池重新创建一个工作线程
  • 使用该类线程池需要注意:控制任务的数量,否则由于大量线程同时运行,可能会造成系统瘫痪

4.2、newFixedThreadPool

创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始化的最大数,则将提交的任务存入到池队列当中进行等待。

特点

  • FixedThreadPool是一个典型并且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所消耗的开销的优点。
  • 但是在空闲的时候,即线程池中没有运行任务的时候,它不会释放工作线程,并且还会占用一定的系统资源。

4.3、newSingleThreadExecutor

创建一个单线程化的Executor,即只创建唯一的一个工作线程来执行任务,它只会使用唯一的工作线程来执行任务,保证所有任务按照指定顺序(这里给出三种常用的顺序,FIFO,LIFO,优先级)来执行。

特点:

  • 如果这个线程异常结束,遍会新建一个取代它保证顺序执行。
  • 单工作线程可以保证顺序的执行各个任务,并且在任意给定的时间不会有多个线程活动

4.4、newScheduleThreadPool

创建一个定长的线程池,而且支持定时的以及周期的任务执行,支持定时及周期性的任务执行

这个比较特殊,我们来看一个演示的Demo

1
2
3
4
5
6
7
8
9
10
11
12
//延时5秒以后启动任务。
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.schedule(new ThreadPool(), 5, TimeUnit.SECONDS);
}


//延时2秒启动任务,以后每3秒执行一次
public static void main(String[] args) {
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
newScheduledThreadPool.scheduleAtFixedRate(new ThreadPool(), 2, 3, TimeUnit.SECONDS);
}

5、任务产出

我们知道在线程和线程之间是独立的,但是当开发程序的时候,需要两个线程要交互,比如我们有一个大计算需要在子线程当中做,然后反馈给主线程,这个时候就需要使用带有返回值的线程了,我们把这种需求叫做任务产出。

sun为我们定义了Runnable来表示任务,但是当需要返回值的时候,Runnable就感觉力不存心了,这个时候我们使用Callable接口,是sun公司在5.0的时候引入的,它是一种具有类型参数的范型,它的类型参数表示的是从call方法中返回的值,并且必须使用ExectorService.submit()方法驱动这种特殊的任务,下面是一个Demo

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
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 线程当中的带有返回值进行交互
* @author pengchengliu
*
*/
public class CallableDemo{

public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ArrayList<Future<String>> array = new ArrayList<Future<String>>();
//获取数据
for(int i = 0; i < 10; i++){
Future<String> future = newCachedThreadPool.submit(new TaskWithResult(i));
array.add(future);
}

//打印数据
for(Future<String> temp : array){
try {
System.out.println(temp.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
newCachedThreadPool.shutdown();
}
}
}
}
class TaskWithResult implements Callable<String>{
private int id ;

public TaskWithResult(int id){
this.id = id ;
}

public String call() {
return "result of TaskWithResult" + id ;
};

}

result of TaskWithResult0
result of TaskWithResult1
result of TaskWithResult2
result of TaskWithResult3
result of TaskWithResult4
result of TaskWithResult5
result of TaskWithResult6
result of TaskWithResult7
result of TaskWithResult8
result of TaskWithResult9

submit方法会产生一个Future的对象,使用get方法获取里面的值,也可以使用isDone来查看Future是否已经完成。当任务完成的时候,它具有一个结果,我们也可以直接调用get方法获取值,但是需要注意的是,如果结果还没有产生,当前线程将阻塞,知道结果产生。

6、暂停任务

影响任务行为最简单的方式就是sleep了,这将是任务中止执行给定的时间.

调用sleep方法的时候会抛出InterruptedException异常,并且这个异常直接在run当中进行捕获,没有跨越线程到达main,这是因为线程之间异常不能传递,当然上述的Callable却是可以的,这个以后再说,这里需要我们铭记于心的就是,我们必须在本地处理所有任务内部产生的异常。

这里的InterruptedException是在什么情况下触发的呢。

InterruptedException的意思是说当一个线程处于等待,睡眠,或者占用,也就是说阻塞状态,而这时线程被中断就会抛出这类错误。Java6之后结束某个线程A的方法是A.interrupt()。如果这个线程正处于非阻塞状态,比如说线程正在执行某些代码的时候,不过被interrupt,那么该线程的interrupt变量会被置为true,告诉别人说这个线程被中断了(只是一个标志位,这个变量本身并不影响线程的中断与否),而且线程会被中断,这时不会有interruptedException。但如果这时线程被阻塞了,比如说正在睡眠,那么就会抛出这个错误。请注意,这个时候变量interrupt没有被置为true,而且也没有人来中断这个线程。例如:

1
2
3
4
5
6
7
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
System.err.println("thread interrupted");
}
}

当线程执行sleep(1000)之后会被立即阻塞,如果在阻塞时外面调用interrupt来中断这个线程,那么就会执行

System.err.println(“thread interrupted”);

这个时候其实线程并未中断,执行完这条语句之后线程会继续执行while循环,开始sleep,所以说如果没有对InterruptedException进行处理,后果就是线程可能无法中断

所以,在任何时候碰到InterruptedException,都要手动把自己这个线程中断。由于这个时候已经处于非阻塞状态,所以可以正常中断,最正确的代码如下:

1
2
3
4
5
6
7
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.interrup();
}
}

在5.0以后sleep是可以带TimeUtil的,这也就方便了我们的使用。