黑马程序员——多线程8:其他线程工具—上

本文详细介绍了Java中的定时器技术,包括传统定时器Timer和TimerTask的使用方法及应用场景,探讨了线程范围内的共享数据ThreadLocal的原理与实践,并介绍了线程池的概念及其在任务调度中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

------ Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

1  传统定时器技术

1.1  定时器技术简介

        定时器,顾名思义就是到达既定时间以后开始执行预先定义的任务的一种工具。由于利用这一工具执行操作时,底层会默认地开启一个单独的线程,因此也是一种多线程技术。那么定时器技术在实际开发中,尤其是在游戏开发中十分常见的,比如俄罗斯方块中不同方块保持匀速下降、贪吃蛇中蛇自动向前移动等等,都是通过定时器进行控制。简单来说,就是通过定时器,控制这些对象单位时间内移动的像素,即可实现物体移动的动画效果。

        在JDK1.5版本以前,Java标准类库中提供了两个类专门实现定时器功能——Timer和TimerTask。这两个类的关系就像是,Thread与Runnable的关系,前者负责执行存储在后者中的任务代码。这两个类均位于Java标准类库中java.util包中。下面我们简单看一下这两个类的API文档。

Timer类:

文档描述:一种工具,线程用其安排以后再后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。

构造方法:

        public Timer():创建一个新计时器。相关线程不作为守护线程运行。我们在之前的博客《多线程7:操作线程的其他方法》中曾经介绍过,当剩余的活动线程均为守护线程时,Java虚拟机将强制退出。那么通过空参数构造方法创建的Timer对象对应的线程并非是守护线程。

        public Timer(boolean isDaemon):如果在创建Timer对象时,传递布尔型常量true,则将该Timer对象对应的线程设置为守护线程。

方法:

        public void schedule(TimerTask task, long delay):安排在指定延迟后执行指定的任务。schedule方法的作用就相当于Thread类的start方法——启动一个线程。而定时器技术的特点则主要体现在该方法的delay参数上,该参数就是用于指定多长时间以后才开始执行存储在task对象中的任务。

        public void schedule(TimerTask task, long delay, long period):安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。以近似固定的时间间隔(由指定的周期分隔)进行后续执行。参数period实际就是指定任务重复执行的周期,因此该方法不仅可以实现任务的延迟执行,还是可以令其周期性地重复执行任务。

        public void schedule(TimerTask task, Date time, long period):安排在指定的时间执行指定的任务。如果此时间已过去,则安排立即执行该任务。该方法与以上两种方法的不同之处在于,并不是在调用schedule方法以后多长时间执行指定任务,而是在指定的日期时间执行指定任务,换句话说,并非指定相对时间,而是指定绝对时间。比如,可以通过该方法实现每天凌晨3点钟收取邮件的功能。参数time用于指定具体的年月日时分秒。

TimerTask抽象类:

文档描述:由Timer安排为一次执行或重复执行的任务。TimerTask是一个抽象类,就像Runnable接口一样,在实际使用时,需要定义TimerTask的子类,并复写其中的run方法。

构造方法:

        protected TimerTask():创建一个新的计时器任务。

方法:

public abstract voidrun():此计时器任务要执行的操作。该方法就是用于存储需要定时执行的任务代码,应由实现类去复写,因此是抽象方法。

1.2  定时器应用

(1)  简单应用

       我们通过下面的代码演示Timer以及TimerTask类的基本使用方法。

代码1:

import java.util.Calendar;
import java.util.Timer;
import java.util.TimerTask;

public class TraditionalTimerTest2 {
 
	public static void main(String[] args) {
		new Timer().schedule(new TimerTask(){
 
			//定义需要定时执行的任务
			@Override
			public void run() {
				System.out.println("Bombing!!!");
			}
           
		}, 5000);//定义延迟时间为5000毫秒,也就是5秒
 
		//以下代码用于每秒打印一次当前秒值
		while(true){
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
           
			System.out.println(Calendar.getInstance().get(Calendar.SECOND));
		}
	}
}
执行结果为:

56

58

59

0

Bombing!!!

1

2

从执行结果来看,打印“Bombing!!!”的语句确实在5秒以后执行了。由于计时代码时无线循环,因此需要手动停止执行。

代码说明:

       代码1中我们通过定义匿名内部类的方式创建了TimerTask子类对象。我们可以对定时器的应用进行一个简单总结:创建Timer对象,并调用schedule方法启动该定时器,并在调用schedule方法时,分别传递一个TimerTask的子类对象以及延迟时间。

       在代码1的基础上,我们还可以实现延时任务的周期性重复执行,只需在调用schedule方法时,再传递一个表示周期的long类型常量即可,代码如下。

代码2:

new Timer().schedule(new TimerTask(){
 
	@Override
	publicvoid run() {
		System.out.println("Bombing!!!");
	}
           
}, 5000, 3000);//除了指定延时任务的第一次执行时间,还指定重复执行周期
执行结果为:

16

17

18

19

Bombing!!!

20

21

22

Bombing!!!

23

24

25

Bombing!!!

26

从执行结果来看,除第一次延时执行5秒钟以外,以后每3秒钟重复执行一次。

(2)  复杂应用

       下面我们将利用定时器实现一个较为复杂的功能:第一次延时4秒钟执行,第二次延时2秒钟执行,接着再延时2秒钟执行,就这样不断交替着执行任务。这里我们介绍两种主要的方法。

方法一:

       定义两种不同的TimerTask子类,一种指定延时2秒钟执行(简称2秒钟任务),一种指定延时4秒钟执行(简称4秒钟任务)。首先启动2秒钟任务,在任务执行完毕以后,立即启动4秒钟任务;在4秒钟任务执行完毕后,再立即启动2秒钟任务,就这样两者互相嵌套在对方任务执行代码中,代码如下。

代码3:

private static void method1() {
 
	//首先定义2秒钟任务
	classTimerTask2000 extends TimerTask {
 
		@Override
		public void run() {
			System.out.println("Bombing!!!");
               
			//在2秒钟任务内部定义4秒钟任务
			class TimerTask4000 extends TimerTask{
 
				@Override
				public void run() {
					System.out.println("Bombing!!!");
               
					//4秒钟任务执行完毕后,立即启动2秒钟任务
					newTimer().schedule(new TimerTask2000(), 2000);
				}
                   
			}
			//2秒钟任务执行完毕后,立即启动4秒钟任务
			newTimer().schedule(new TimerTask4000(), 4000);               
		}
           
	}
	//执行2秒钟任务       
	newTimer().schedule(new TimerTask2000(), 2000);
        
	showCurrentTime();
}
//计时代码
private static void showCurrentTime() {
	while(true){
		System.out.println(Calendar.getInstance().get(Calendar.SECOND));
           
		try{
			Thread.sleep(1000);
		}catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
将以上代码存放到一个主函数内部执行的结果为:

5

6

Bombing!!!

7

8

9

10

Bombing!!!

11

12

Bombing!!!

13

14

15

16

Bombing!!!

17

18

根据以上执行结果,实现了2秒钟任务与4秒钟任务的交替执行效果。之所以以上代码看起来比较复杂是因为任务的定义代码和任务的启动代码同时定义在了一个run方法内,若想提高代码的阅读性,则可以在方法外部定义两个外部类,分别表示2秒钟任务和4秒钟任务,然后在各自run方法中,启动对方法的执行任务即可,大家可以自行尝试。

方法二:

       第二种思路是只定义一种TimerTask类,然后分别定义一个布尔型标记flag,和表示延迟时间的整型变量delay,两者均定义为主函数所在类的私有静态成员变量。不同任务的执行依靠标记的真假进行区分:若标记为真则执行2秒钟任务,反之执行4秒钟任务,每执行完一次任务以后,就将标记置反,同时更改延迟时间,代码如下。

代码4:

//定义为主函数所在类的私有静态成员变量
private static boolean flag = false;
private static long delay = 2000l;
 
private static void method2() {
	//只需定义一个TimerTask子类
	class MyTimerTask extends TimerTask {
 
		@Override
		public void run() {
			System.out.println("Bombing!!!");
               
			//判断标记
			if(flag){
				delay = 2000l;
				flag = false;
			}else{
				delay = 4000l;
				flag = true;
			}
               
			new Timer().schedule(new MyTimerTask(), delay);
		}
           
	}
	new Timer().schedule(new MyTimerTask(), delay);
   
	//计时方法,同代码3
	showCurrentTime();
}
执行结果为:

57

58

Bombing!!!

59

0

1

2

Bombing!!!

3

4

Bombing!!!

5

6

7

8

Bombing!!!

9

       第二种思路还可以有一种变体,同样只需要定义一个TimerTask子类,而标记和延迟时间不再定义为主函数所在类的静态成员变量,而是定义为TimerTask子类的私有成员变量。标记不再是布尔型变量,而是整型变量,那么区分2秒钟任务和4秒钟任务的方式为:对标记进行处2求余操作,余数为0(表示为偶数),则执行2秒钟任务;若余数为1(表示为奇数),则执行4秒钟任务。为此,标记应该是一个不断变化的变量,这可以在创建TimerTask对象时通过构造函数进行执行——每创建一次TimerTask对象,就在前一个标记基础上加1,代码如下。

代码5:

private static void method3() {
	class MyTimerTask extends TimerTask{
		private int flag = 0;
		private long delay = 4000;
           
		//构造函数用于初始化flag的值
		MyTimerTask(intflag){
			this.flag = flag;
		}
           
		@Override
		public void run() {
			System.out.println("Bombing!!!");
               
			//若标记为偶,则执行4秒钟任务;反之执行2秒钟任务
			if(flag% 2 != 0)
				delay -= 2000;
               
			//每创建一个TimerTask子类对象,指定的标记值加1,
			//以此改变标记的奇偶性
			newTimer().schedule(new MyTimerTask(flag+1), delay);
		}
           
	}
	new Timer().schedule(new MyTimerTask(0), 2000);
       
	showCurrentTime();
}
执行结果为:

18

19

Bombing!!!

20

21

22

23

Bombing!!!

24

25

Bombing!!!

26

27

28

29

Bombing!!!

30

上述需求是不能通过匿名内部类的方式实现的,因为匿名内部类只能创建某个类唯一的一个实例对象(这是因为匿名内部类没有类名),而我们需要反复创建某个类的若干对象,以实现指定任务的反复循环执行,因此必须为TimerTask子类定义类名。大家可以自行尝试使用匿名内部类的方式实现上述需求。

       对于定时器最后再提醒大家一点,已经启动的过的定时器任务,也就是被schedule方法使用过的某一特定TimerTask对象,是不能再重新启动的,否则将抛出异常,若想再次执行同一个任务,就要重新创建一个新的TimerTask对象。

       正如这一小节的标题所示,定时器技术(Timer和TimerTask)是在JDK1.5版本以前实现定时执行任务的实现方法,而在JDK1.5版本以后,Java标准类库提供了新的工具类来实现定时执行任务的功能,因此在实际开发中我们应尽可能是用新的工具类,这里对Timer和TimerTask进行介绍就是希望大家对定时器原理有一个初步的了解。

2  ThreadLocal

1.1  应用背景

       ThreadLocal类是用于解决线程范围内共享数据的线程安全问题,该技术在J2EE底层框架的设计中具有较广泛的应用。设想这样的一个场景,当线程1顺序执行模块A、模块B和模块C(可以将模块理解为一个对象,或者一个方法)的代码,这三个模块在执行时需要访问模块外部的一个数据(可以将其定义为静态,成为全局变量),我们称之为线程1绑定的数据,简称为数据1,换句话说,数据1具体的值取决于线程本身。如果又有另一个线程,比如线程2顺序执行模块A、B、C,那么此时三个模块调用同一个数据时——称之为线程2绑定的数据(其实指向的是同一个全局变量)——其值应该与数据1是不同的,因此简称为数据2。上述线程、模块与数据的相互关系可以通过下图表示。


       换句话说,当同一个线程访问这三个模块时,访问数据的值是相同的,如果换做另一个线程访问这个三个模块时,数据的值将发生变化,这就是所谓的线程范围内的共享数据——同一个数据的值随着访问线程的不同而不同。假如有5个线程同时访问同一个线程范围内的共享数据,那么就有5个对应的数据值。下面我们就通过代码演示,如何应用线程范围内的共享数据。

1.2  ThreadLocal应用演示

(1)  模拟

       为了方便大家理解,ThreadLocal类的工作原理,我们首先写一段代码来模仿ThreadLocal的功能。阅读下面代码。

代码6:

import java.util.Random;
 
public class ThreadScopeShareDataTest {   
	//共享数据
	private static int data = 0;   
	//定义两个模块
	private static class ModuleA {
		//获取共享数据
		public void get() {
			Thread thread = Thread.currentThread();           
			System.out.println("ModuleA : " + thread.getName() + "get data : " + data);
		}
	}
	private static class ModuleB {
		//获取共享数据
		public void get(){
			Thread thread = Thread.currentThread();           
			System.out.println("ModuleB : " + thread.getName() + " get data : " + data);
		}
	}
	public static void main(String[] args) {       
		//创建两个线程,先创建并存储数据,然后分别调用两个模块获取该数据
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
 
				@Override
				public void run() {
					Thread thread = Thread.currentThread();
 
					data = new Random().nextInt(500);
					System.out.println(thread.getName() + " has put data : " + data);
 
					new ModuleA().get();
					new ModuleB().get();
				}
			}).start();
		}       
	}
}
执行结果为:

Thread-0 has put data : 202

Thread-1 has put data : 202

ModuleA : Thread-0 get data : 202

ModuleA : Thread-1 get data : 202

ModuleB : Thread-0 get data : 202

ModuleB : Thread-1 get data : 202

代码说明:

(1)  代码6中定义了两个静态内部类,代表两个访问共享数据的模块,私有静态整型变量data即为共享数据。为了体现共享数据值和线程之间的绑定关系,由线程本身先创建一个随机数据“存储”到data中,然后再令两个线程获取数据。

(2)  按理说,按照线程范围内共享数据的特点,不同线程访问同一个共享数据,应获取到不同的数据,但是由于该数据并非线程安全数据,当两个线程执行得非常快时,前一个线程刚为data赋完值,还没来得及打印,第二个线程就将该数据覆盖了,因此两线程获取的数据是同一个数据。当然可能还会发生其他各种由于线程安全造成的错误现象,大家可以自行尝试。

       之所以会发生代码6的错误,究其原因,就是没有真正将线程与数据绑定起来。所谓绑定应该就是一种一一对应的关系,,由此我们可以联想到一种存储键值对的集合——Map。由此我们可以在代码6基础上进行这样的修改:定义一个Map集合,用于存储若干共享数据,在存储的时候,同时将数据对应的线程对象存储进去,作为键,数据作为值。获取数据时,获取线程对象对应的数据,即可保证数据与线程间的一一对应。代码如下。

代码7:

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
 
public class ThreadScopeShareDataTest {     
	//将共享数据改为一个Map集合
	private static Map<Thread, Integer> threadDatas = newHashMap<Thread, Integer>();
   
	//定义两个模块
	private static class ModuleA {
		//获取共享数据
		public void get() {
			Thread thread = Thread.currentThread();
           
			Integer data = new Integer(threadDatas.get(thread));
			System.out.println("ModuleA : " + thread.getName() + "get data : " + data);
		}
	}
	private static class ModuleB {
	//获取共享数据
		public void get(){
			Thread thread = Thread.currentThread();
           
			Integer data = new Integer(threadDatas.get(thread));
			System.out.println("ModuleB : " + thread.getName() + "get data : " + data);
		}
	}
	public static void main(String[] args) {   
       
		//创建两个线程,先创建并存储数据,然后分别调用两个模块获取该数据
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
 
				@Override
				public void run() {
					Thread thread =Thread.currentThread();
 
					Integer data = newRandom().nextInt(500);
					System.out.println(thread.getName() + " has put data : " + data);
                   
					//将数据连同相关线程本身存储到Map集合中
					threadDatas.put(thread,data);
 
					new ModuleA().get();
					new ModuleB().get();
				}
 
			}).start();
		}       
	}
}
执行结果为:

Thread-0 has put data : 94

Thread-1 has put data : 177

ModuleA : Thread-0 get data : 94

ModuleA : Thread-1 get data : 177

ModuleB : Thread-0 get data : 94

ModuleB : Thread-1 get data : 177

从执行结果来看,很好的体现了线程范围内共享数据与线程之间的一一对应关系——不同线程访问共享数据得到不同的值。代码7的执行原理可以通过下图表示。


共享数据连同对应的线程对象(实际是线程对象的引用)存储到一个Map集合中,不同模块从Map集合中获取数据时以本线程作为键进行查找,即可得到线程对应数据。

(2)  ThreadLocal演示

       在上述内容中,我们通过一个Map集合模拟了线程范围内共享数据的存储与获取,而这一方法实际就是ThreadLocal类实现线程范围内共享数据的线程安全的基本原理。下面我们就对该类进行简单介绍与应用演示。

API文档:

       该类提供了线程局部(thread-local)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的privatestatic字段,它们希望将状态与某一个线程(例如,用户ID或事物ID)相关联。

构造方法:

       public ThreadLocal():创建一个线程本地变量。

方法:

       public T get():返回此线程局部变量的当前线程副本中的值。

       public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

从ThreadLocal类的set和get方法可知,无论是设置数据还是获取数据都不需要明确的指明与数据绑定的那个线程对象,因为set、get方法底层会默认的通过本线程对象的引用(类似于调用Thread.currentThread()方法)进行操作,非常方便。我们利用ThreadLocal继续对代码7进行修改,代码如下。

代码8:

import java.util.Random;
 
public class ThreadLocalTest {
   
	//创建ThreadLocal对象,将其定义为共享数据
	private static ThreadLocal<Integer> threadDatas = newThreadLocal<Integer>();
 
	private static class ModuleA {
		public void get() {
			Thread thread = Thread.currentThread();
           
			//默认地根据本线程对象获取对应的数据
			Integer data = new Integer(threadDatas.get());
			System.out.println("ModuleA : " + thread.getName() + "get data : " + data);
		}
	}
	private static class ModuleB {
		public void get(){
			Thread thread = Thread.currentThread();
           
			//默认地根据本线程对象获取对应的数据
			Integer data = newInteger(threadDatas.get());
			System.out.println("ModuleB : " + thread.getName() + "get data : " + data);
		}
	}
 
	public static void main(String[] args) {   
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
 
				@Override
				public void run() {
					Thread thread = Thread.currentThread();
 
					Integer data = newRandom().nextInt(500);
					System.out.println(thread.getName() + " has put data : " + data);
                   
					threadDatas.set(data);//不必手动指定与数据对应的线程对象
 
					new ModuleA().get();
					new ModuleB().get();
				}
 
			}).start();
		}
	}
}
执行结果为:

Thread-0 has put data : 417

Thread-1 has put data : 59

ModuleA : Thread-0 get data : 417

ModuleA : Thread-1 get data : 59

ModuleB : Thread-0 get data : 417

ModuleB : Thread-1 get data : 59

同样实现了线程范围内共享数据的线程安全,并且相比使用Map对象存储线程-数据对更为方便。

       以上代码实现了单个数据与线程范围内共享数据的绑定,如果在实际开发中需要由多个数据与线程绑定,可以有两种思路:要么定义与共享数据数量相同的多个ThreadLocal对象,不同的ThreadLocal中存储同一个线程的多个数据;如果多个数据之间具有一定的相关关系,比如是一个类的多个属性,那么可以直接将该类的实例对象作为共享数据存储到一个ThreadLocal中这两种方法均可以实现多个数据与一个线程的绑定。下面我们来演示通过第二种思路来实现多个数据与一个线程的绑定。

代码9:

import java.util.Random;
 
public class ThreadLocalTest2 {
   
	//定义一个封装有两个数据的类
	private class MyThreadScopeData {
       
		private String name;
		private int age;
 
		public MyThreadScopeData(String name, int age) {
			this.name = name;
			this.age = age;
		}
       
		public String getName() {
			return name;
		}
		public int getAge() {
			return age;
		}
       
		public String toString(){
			return name + "=" + age;
		}
	}
   
	//专门用于存储线程范围内共享数据的ThreadLocal对象
	private staticThreadLocal<MyThreadScopeData> threadDatas = new ThreadLocal<MyThreadScopeData>();
   
	private static class ModuleA {
		public void get() {
			Thread thread = Thread.currentThread();
           
			MyThreadScopeData data = threadDatas.get();
			System.out.println("ModuleA : " + thread.getName() + "get data : " + data);
		}
	}
	private static class ModuleB {
		public void get(){
			Thread thread = Thread.currentThread();
           
			MyThreadScopeData data = threadDatas.get();
			System.out.println("ModuleB : " + thread.getName() + "get data : " + data);
		}
	}
   
	//定义标记,令两个线程存储不同的数据
	private static boolean flag = true;
 
	public static void main(String[] args) {
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
 
				@Override
				public void run() {
					Thread thread =Thread.currentThread();
                   
					synchronized(ThreadLocalTest2.class) {
						if(flag){
							MyThreadScopeData data = new ThreadLocalTest2().new MyThreadScopeData("David",31);
							System.out.println(thread.getName() + " has put data : " + data);
                       
							threadDatas.set(data);
                           
							flag = false;
						}
						else{
							MyThreadScopeDatadata = new ThreadLocalTest2().new MyThreadScopeData("Peter",23);
							System.out.println(thread.getName() + " has put data : " + data);
                       
							threadDatas.set(data);
						}
					}
 
					new ModuleA().get();
					new ModuleB().get();
				}
 
			}).start();
		}
	}
}
执行结果为:

Thread-0 has put data : David=31

Thread-1 has put data : Peter=23

ModuleA : Thread-1 get data : Peter=23

ModuleA : Thread-0 get data : David=31

ModuleB : Thread-1 get data : Peter=23

ModuleB : Thread-0 get data : David=31

代码说明:

(1)  代码9中首先定义了一个封装有多个数据的类——MyThreadScopeData,为方便演示将其定义为了测试类的成员内部类。主函数中启动的两个线程,会根据标记创建存有不同数据的MyThreadScopeData对象,并将其与本线程绑定以后存储到ThreadLocal中,实现了多个线程范围内共享数据与线程的绑定。

(2)  虽然以上代码实现了最基本的要求,但是代码结构非常混乱——测试代码与实现共享功能的代码混在了一起,非常不利于维护与修改,实际上既然自定义数据MyThreadScopeData本身需要实现与线程对象的绑定功能,就应该将ThreadLocal同样封装到数据内部,换句话说,与线程对象的绑定功能应成为MyThreadScopeData本身的一个特点而存在,而不应将其定义到数据外部。因此下面我们按照上述思路,利用单利设计模式对MyThreadScopeData类进行修改,代码如下。

代码10:

public class ThreadLocalTest3 {
 
	private static class MyThreadScopeData {
       
		private String name;
		private int age;
       
		//将ThreadLocal直接定义到MyThreadScopeData内部
		private static ThreadLocal<MyThreadScopeData> threadDatas = newThreadLocal<MyThreadScopeData>();
       
		/*
		 * 先尝试从ThreadLocal中获取与当前线程绑定的数据
		 * 若数据为空,表示本线程从未存储过数据
		 * 此时创建一个新的数据,并存储到ThreadLocal中,并最终返回该数据
		 */
		public static MyThreadScopeData getInstance(){
			MyThreadScopeData _instatnce =threadDatas.get();
           
			if(_instatnce == null){
				_instatnce = new MyThreadScopeData();
				threadDatas.set(_instatnce);
			}
			return _instatnce;
		}
 
		private MyThreadScopeData() {}
       
		public void setName(String name) {
			this.name = name;
		}
		public String getName() {
			return name;
		}       
		public void setAge(int age) {
			this.age = age;
		}
		public int getAge() {
			return age;
		}
       
		public String toString(){
			return name + "=" + age;
		}
	}
   
	private static class ModuleA {
		public void get() {
			Thread thread = Thread.currentThread();
           
			MyThreadScopeData data = MyThreadScopeData.getInstance();
			System.out.println("ModuleA : " + thread.getName() + "get data : " + data);
		}
	}
	private static class ModuleB {
		public void get(){
			Thread thread = Thread.currentThread();
           
			MyThreadScopeData data = MyThreadScopeData.getInstance();
			System.out.println("ModuleB : " + thread.getName() + "get data : " + data);
		}
	}
   
	//存有若干备选姓名
	private static String[] nameLibrary = {"David", "Peter",
								  "Cook","George",
								  "Tim","William"};
 
	public static void main(String[] args) {
       
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
 
				@Override
				public void run() {
					Thread thread = Thread.currentThread();
					Random rand = new Random();
 
					/*
					 * 这里不必再手动创建数据对象
				 	* 只需手动设置封装在数据对象内部的数据即可
				 	*/
					MyThreadScopeData data = MyThreadScopeData.getInstance();
					data.setName(nameLibrary[rand.nextInt(nameLibrary.length)]);
					data.setAge(rand.nextInt(100));
                       
					System.out.println(thread.getName() + " has put data : " +data);
                   
					/*
				 	* 这里也不必再手动将数据对象存储到ThreadLocal中
				 	* 数据对象自动完成了这一操作
				 	*/
					//threadDatas.set(data);
 
					new ModuleA().get();
					new ModuleB().get();
				}
 
			}).start();
		}
	}
}
执行结果为:

Thread-1 has put data : George=36

Thread-0 has put data : Cook=12

ModuleA : Thread-1 get data : George=36

ModuleA : Thread-0 get data : Cook=12

ModuleB : Thread-1 get data : George=36

ModuleB : Thread-0 get data : Cook=12

代码说明:

       代码10中我们对MyThreadScopeData类进行了修改,将ThreadLocal定义在了其内部,当需要MyThreadScopeData对象时,只需调用静态方法getInstance,即可在获取到一个新的线程相关数据对象的同时,该对象也自动的存储到了ThreadLocal中。那么对于外部类来说,只需调用getInstane方法获取与该线程相关的MyThreadScopeData对象,并通过setName/setAge方法为其赋值即可,至于其内部如何创建数据对象、如何实现与线程的绑定等等,都不需要外部类手动实现,完成了业务逻辑代码与功能实现代码的分割,更便于代码的维护,体现了良好的面向对象思想。

       上述设计MyThreadScopeData类的思想在Struts2中是有类似应用的。比如有一个模块专门用于处理来自外部的请求,那么一个请求就是一个线程。在这个模块中事先定义好一个容器对象(类似于ThreadLocal),该容器就会将与此请求(或者与此线程)相关的所有数据存储起来,存储的时候请求与数据之间以键值对的形式存在,因此一个请求对应一个数据,不同的请求对应不同的数据。

(3) ThreadLocal的补充说明

       可能有朋友在使用ThreadLocal类时有这样的一个担忧,当长时间面对大量线程的访问时,一个ThreadLocal对象可能会存有成百上千的线程/数据对,这可能会占用大量的内存。这样的担心是不必要的,ThreadLocal的API文档有如下说明:每个线程都保持对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。换句话说,当线程本身完成了其所需要执行的代码,并死亡后(Thread对象被垃圾回收,调用的本地线程资源被释放),与此线程相关的所有共享数据也将被垃圾回收机制处理掉,当然如果其他地方还保有某个共享数据的引用,则该数据将暂时不被垃圾回收。因此简单来说,Java虚拟机将会自动清理ThreadLocal中的无用数据,优化内存分配。

       此外,ThreadLocal类在JDK1.5版本中提供了用于删除本线程绑定数据的方法——remove,因此我们也可以在需要时候手动删除线程共享数据。

3  线程池

3.1  概述

       假设我们要自己定义一个类似Tomcat服务器的程序,那么这个程序最主要的功能的就是要处理大量的并发访问请求。那么为了能够高效的处理这些请求,不致使得用户等待过长的时间,应为每个请求分配一个单独的线程。那么一个最基本的思想就是,当某个请求连接到服务器以后,就要为其创建一个线程,接着通过这个线程处理完请求以后,这个线程也就是没有用处了。可想而知,如果这样设计服务器程序,当服务器长时间处于高并发访问状态时,在一段时间内,服务器中的内存中可能对堆积大量的无用线程对象,并且占用大量的系统底层资源(这是因为Java的线程,从代码角度来说是Thread对象,但是底层调用时系统本身的线程资源),这必然导致服务器过重的负载。

       解决上述问题,提高服务器运行效率的方法之一就是重复利用线程。换句话说,当某个线程处理完一个请求之后,不急于令该线程死亡,而是判断是否还有多余的请求需要处理,如果有就处理此请求,如果没有就令该线程在存活状态下等待新请求的到来——进入冻结状态,一旦有新请求需要处理,就唤醒该线程去处理请求,如此循环,则可避免创建大量线程造成内存、系统资源的浪费。那么这一循环反复利用有限个线程的技术,就称为线程池

线程池,顾名思义,就是容纳有多个活动线程的一个容器。当没有需要处理的请求时,线程就处于等待状态,一旦有请求连接进来时,就同时唤醒所有等待的线程,谁分配到执行权就由哪个线程处理请求。那么请求本身也是一个个对象,连接进行来的请求对象,并不急于分配给线程去处理,而是将其存储到一个队列中,然后随机分配给线程去处理。大家可以把这个机制和银行的等待叫号系统进行类比:在银行大厅等待的客户就相当于是请求,大厅等候区就是队列,每个客户在等待叫号前都会取号,方便柜台按顺序叫号。当有柜台空闲下来时,就按等待顺序呼叫下一个客户。

3.2  线程池分类

       上述内容中简单介绍了线程池的基本原理,在演示如何使用线程池技术之前,我们首先来介绍4种不同的线程池类型:

创建包含指定数量线程的线程池:

ExecutorService threadPool =Executors.newFixedThreadPool(int nThreads);

       其中ExecutorService是一种接口,是所有线程池类的父接口,由于java.util.concurrent包中未提供线程池类的API,因此我们无法手动创建线程池对象,只能通过接口类型的变量接收。Executors是一个工具类,最主要的功能就是创建各类线程池对象。Executors的静态方法newFixedThreadPool,能够创建并返回一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,其方法参数就是用于指定线程池中线程的数量。

创建一个线程数量不固定的线程池:

ExecutorService threadPool = Executors.newCachedThreadPool();

       静态方法newCachedThreadPool()创建并返回一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。如果现有线程没有可用的,则创建一个新线程并添加到池中。这类线程池将终止并从缓存中移除那些已有60秒钟未被使用的线程。因此长时间保持空闲的线程池不会使用任何资源。简单说,这类线程池中的线程数是随着请求数变化而变化的:请求多,相应的处理请求的线程就多一些;请求少,线程也就少一些。

创建只包含单一线程的线程池:

ExecutorService threadPool = Executors.newSingleThreadExecutor();

       创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务。

       关于单一线程池,有一个这样的面试题:当某个线程死掉之后,如何能让它再次执行起来?对于某一个确定的线程来说,这是不可能的,线程死掉以后不可能再次执行任务。但是可以利用单一线程池,当其中的线程死亡以后,再创建一个新的线程代替它即可。

创建可延迟或定期执行任务的固定数量线程池:

ScheduleExecutorService threadPool = Executors.newScheduleThreadPool(intcorePoolSize);

ScheduleExecutorService是ExecutorService的子接口,是这一类线程池类的父接口。该线程池可以说是Timer和TimerTask类的替代品,同样可以延迟或者定期执行任务,而且使用更为方便灵活。

       上述内容中提到的ExecutorService、ScheduleExecutorService、Executors等接口和类均包含在java标准类库java.util.concurrent包中。

3.3  线程池应用演示

       下面我们就来演示如何向线程池中提交任务。代码如下。

代码11:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class ThreadPoolTest {
 
	public static void main(String[] args) {
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		for (int i = 0; i < 10; i++) {
			final int index = i;
			//调用execute方法向线程池中提交任务
			threadPool.execute(new Runnable() {
 
				@Override
				public void run() {
					for (int j = 0; j < 10;j++) {
						System.out.println(Thread.currentThread().getName() + " isloop of " + index+" for task of "+j);
					}
				}
 
			});
		}
		//关闭线程池
		threadPool.shutdown();
	}
}
执行结果为:

pool-1-thread-1is loop of 0 for task of 0

pool-1-thread-3is loop of 2 for task of 0

pool-1-thread-2is loop of 1 for task of 5

pool-1-thread-3is loop of 2 for task of 5

pool-1-thread-1is loop of 0 for task of 3

pool-1-thread-3is loop of 6 for task of 3

pool-1-thread-3is loop of 6 for task of 4

pool-1-thread-3is loop of 7 for task of 8

pool-1-thread-3is loop of 7 for task of 9

pool-1-thread-3is loop of 9 for task of 2

pool-1-thread-3is loop of 9 for task of 3

pool-1-thread-1is loop of 0 for task of 9

pool-1-thread-2is loop of 1 for task of 9

由于执行结果太长,这里只截取了一部分,大家可以自行尝试执行。

代码说明:

(1)  代码11中最为重要的就是execute方法,用于向线程池中提交任务对象,一个Runnable匿名内部类就代表一个任务。由于线程数量少于任务数量,因此大体上就是先提交的任务先被执行完毕,后提交的任务相对滞后一些处理。理论上无论线程池中有多少个活动线程,可接收的任务数量是不限的,但是每一时段内能被同时处理的任务数就是活动线程数,而剩余还未被处理的任务就在任务队列中等待。

(2)  shutdown方法就是当线程池中的所有线程都没有任务需要处理时,关闭线程池,杀死其中的所有线程。之所以要手动调用该方法是因为,线程池中的线程并不像手动创建的线程那样,执行完run方法中的代码后自行死去,而是进入冻结状态,直到下一个请求到来时唤醒。线程池对象中还有一个名为shutdownNow()的方法,一旦调用该方法,无论线程池中是否还有未被处理的任务,都会强制杀死所有线程。

(3)  此外,由于匿名内部类无法访问非final的局部变量,因此需要将变量i的值赋给final变量index,才能对其进行操作。

       由以上代码本身,以及代码的执行结果来看,线程池处理任务的方式与手动创建线程处理任务的方式有一个区别:手动创建线程时,必须要指定哪个任务(Runnable实现类对象),由哪个线程去执行,而当委托线程池处理任务时,我们只需要向线程池提交任务即可,而至于具体由哪个线程去执行,统一由线程进行调度,因此灵活性很大,而且执行效率高。

       以上代码演示了如何创建固定数量线程池,以及利用线程池处理任务的方法。由于缓存线程池(newCachedThreadPool())以及单一线程池(newSingleThreadExecutor())的创建与任务提交方式与代码11基本一致,因此这里不再给出具体代码,只需修改线程池创建部分代码即可,大家可以自行尝试。当然三者的执行结果会有一些差异,由于缓存线程池中的线程数量随任务数量的变化而变化,因此当同时提交10个任务时,就会有10个线程同时处理任务。而单一线程池,因仅有一个线程处理10个任务,因此从执行结果来看,总是只有一个线程在活动,类似于单线程。

3.4  延时任务执行线程池

1)  基本用法演示

       正如上文所说,调用Executors的静态方法newScheduleThreadPool(intcorePoolSize)可创建并返回能够延时或定时执行任务的固定数量线程池。由于这类线程的特殊性,因此我们单独对其进行介绍。基本应用代码如下。

代码12:

import java.util.Calendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
public class ThreadPoolTest2 {
 
	public static void main(String[] args) {
		ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);
		//调用schedule来提交任务
		threadPool.schedul(new Runnable(){
 
			@Override
			public void run() {
				System.out.println("Bombing!!!");
			}
               
		}, 5, TimeUnit.SECONDS);//需指定延迟时间,以及时间单位
       
		//计时
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Calendar.getInstance().get(Calendar.SECOND));
		}
	}
}
执行结果为:

7

8

9

10

Bombing!!!

11

代码说明:

(1)  ScheduleExecutorService是延迟任务执行线程的父接口,用于指向延迟任务执行线程池对象,这是与其他线程池所不同的。

(2)  相比较于前三种线程池,延时任务执行线程池用于提交任务的方法为schedule,而不是execute。不过,也可以调用execute方法提交任务,只不过就没有了延时执行的效果。在调用schedule方法时除了传递Runnable实现类对象以外,还要执行延迟时间,以及时间单位。时间单位TimeUnit是一个枚举,其中包含有若干常量元素,代码12中的SECONDS表示时间单位为秒。

(3)  除了可以调用schedule方法提交任务以外,scheduleAtFixedRate同样可以用于提交任务,除了可以指定延迟执行时间,还需要指定执行周期,这样就可以按照指定的时间间隔执行任务。大家可以自行尝试。

(4)  ScheduleExecutorService接口的API文档中有这样的一段描述:所有的schedule()方法都接受相对延迟和周期作为参数,而不是绝对的时间或日期。将以Date所表示的绝对时间转换成要求的形式很容易。例如,要安排在某个以后的Date运行,可以使用:schedule(task,data.getTime() – System.currentTimeMillis(), TimeUnit.MILLISECONDS)。换句话说,如果想要在指定的年月日时分秒执行任务时,无法直接指定具体的Date对象,而是给出指定日期相对现在时间的差值。

2)  练习

       这里我们可以利用延迟任务执行线程池来完成第一节传统定时器类的练习:第一次延时4秒钟执行,第二次延时2秒钟执行,接着再延时2秒钟执行,就这样不断交替着执行任务。代码如下。

代码13:

import java.util.Calendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
public class ThreadPoolTest3 {
   
	private static class MyTimerTask2000 implements Runnable {
		@Override
		public void run() {
			System.out.println("Bombing!!!");
           
			class MyTimerTask4000 implements Runnable {
				@Override
				public void run() {
					System.out.println("Bombing!!!");
                    
					ScheduledExecutorServicethreadPool = Executors.newScheduledThreadPool(1);
					threadPool.schedule(newMyTimerTask2000(), 2, TimeUnit.SECONDS);
                   
				}           
			}
           
			ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
			threadPool.schedule(newMyTimerTask4000(), 4, TimeUnit.SECONDS);           
		}           
	}
 
	public static void main(String[] args) {
       
		new Thread(new Runnable() {
 
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(1000);
					} catch(InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Calendar.getInstance().get(Calendar.SECOND));
				}
			}
           
		}).start();
       
		ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(1);
		threadPool.schedule(newMyTimerTask2000(), 2, TimeUnit.SECONDS);
 
	}
}
以上代码的与Timer/TimerTask版本的方法一是同样的思路,只不过将Timer换成了延时任务执行线程池,将TimerTask换成了Runnable实现类对象,其他两中思路也可以按照这样的方式实现,这里不再给出具体代码。

4  Callable & Future

4.1  简介  

       Callable和Future同样是java.util.concurrent包中提供的用于实现多线程功能的工具类(实际是接口),它们与上面提到的线程池具有紧密联系。简单说,Callable就相当于是一个任务对象,与Runnable类似,而Future代表任务执行完毕以后的一个结果。以下是两个接口的API文档。

Callable:

介绍:

       返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做call的方法。

Callable接口类似于Runnable,两者都是为了那些其实例可能被另一个线程执行的类设计的。但是Runnable不会返回结果,并且无法抛出经过检查的异常。

方法:

       V call()throws Exception:计算结果,如果无法计算结果,则抛出一个异常。注意到该方法的返回值是泛型类型参数,该类型参数需要在定义Callable实现类时指定,代表了计算返回结果的类型。与Runnable接口的run方法不同,call方法带有返回值类型。

Future:

介绍:

       Future表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用get方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由cancel方法来执行。一旦计算完成,就不能再取消计算。

方法:

       V get()throws InterruptedException, ExecutionException:如有必要,等待计算完成,然后获取其结果。该方法的返回值类型同样是一个泛型类型参数,表示任务执行返回结果的类型。

       Vget(long timeout, TimeUnit unit) throws InterruptedException,ExecutionException, TimeoutException:如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。如果超过给定,任务还未被执行完毕,则抛出TimeoutException。

4.2  应用演示

       下面我们就来演示Callable与Future的基本用法。代码如下。

代码14:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class CallableAndFutureTest {
 
	public static void main(String[] args) {
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		//向线程池中提交Callable任务,并接受Future对象作为计算结果
		Future<String> result =
			threadPool.submit(newCallable<String>(){
   
				@Override
				public String call() throws Exception {
					Thread.sleep(3000);
					return "HelloWorld!";
				}
               
			});
       
		System.out.println("等待结果...");       
		try {
			System.out.println("结果:"+result.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
       
		threadPool.shutdown();
	}
}
执行结果为:

等待结果...

结果:Hello World!

代码说明:

(1)  Callable的基本用法与Runnable是类似的,向线程池提交任务时传递Callable的实现类对象,并复写call方法。二者的不同之处有三点:一,提交任务使用submit,而不是execute,或者schedule;二,call方法带有返回值类型,而该返回值同样是submit方法的返回值;三,创建Callale实现类对象,以及通过Future类型变量接收返回值时,均需要定义为参数化类型,而类型参数就是任务执行的返回值。

(2)  需要强调的是,Future实现类对象的get方法是一个阻塞式方法,在线程执行完任务返回结果以前,该方法一直处于等待状态,直到获得结果,接触冻结。

4.3  同时提交多个Callable任务对象

       利用Callable任务对象还可以实现这样的一个功能:向一个线程池中同时提交多个Callable任务对象,提交完毕以后,按照任务的处理速度获取任务执行结果——哪个线程先执行完任务,就先获取哪个线程的任务执行结果。就好比同时种了几块麦地,然后等待麦子成熟。收割麦子时,最为简单的原则就是,哪块地先成熟,就先收割哪块地,而不是按照麦地的种植顺序。

       这里提到的线程池并非真正意义上的线程池对象,而是利用现有线程池对象,完成任务执行功能的类,叫做ExecutorCompletionService,是CompletionService接口的实现类。该类的构造方法中需要传递一个线程池对象,并依赖于这个线程池对象,来完成任务的执行。该类所提供的两个主要方法分别是,submit(Callable<V>task)和take(),前者用于提交任务对象,后者用于获取已完成任务所返回的结果。演示代码如下。

代码15:

import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CallableAndFutureTest2 {
 
	public static void main(String[] args) {
		//首先创建一个线程池
		ExecutorService threadPool = Executors.newFixedThreadPool(10);
		//构造函数中传递以上线程池对象
		CompletionService<Integer> complietionService = new ExecutorCompletionService<Integer>(threadPool);
		//提交10个Callable任务对象
		for(int i=0; i<10; i++) {
			final int seq = i;
			complietionService.submit(newCallable<Integer>(){
 
				@Override
				public Integer call() throws Exception {
					//令每个任务的执行时间为一个随机数
					//以此模拟执行不同需要不同时间的现象
					Thread.sleep((long)(Math.random()*10000));
					return seq;
				}
               
			});
		}
       
		//任务提交完毕后,按照任务完成顺序获取任务执行结果
		try {
			for(int i=0; i<10; i++){
				//take方法首先返回一个Future对象,再调用其get方法获得执行结果
				System.out.println(complietionService.take().get());
			}
		} catch (InterruptedException |ExecutionException e) {
			e.printStackTrace();
		}
       
		threadPool.shutdown();       
	}
}
执行结果为:

6

2

3

5

1

4

8

0

9

7

执行结果表明,6号任务首先完成,7号任务最后执行完毕。

代码说明:

       take方法同样是阻塞式方法,当没有任务被执行完毕时,就处于等待状态,直到至少有一个任务执行完毕,则返回该任务返回的Future对象。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值