- 1、线程当中的基本概念
- 2、定义任务
- 3、驱动任务
- 4、驱动大量任务
- 5、任务产出
- 6、暂停任务
1、线程当中的基本概念
计算机的CPU发展到多个核心数的时候,就可以并发的跑程序,对于用户而言程序越来越快,对于开发者而言就是很大的挑战。并发编程当中,主要的概念就是任务、以及驱动任务这两个概念。
Java常用API也为我们提供了相应的类,来表述上面的两个概念,就是Runnable、Thread这两个概念,在这里我们需要明白的就是Runnable对应着我们新开辟的线程将要做什么样子的事情、而Thread就是去驱动也就是开启我们的定义好的任务。
2、定义任务
Runnable接口来定义一个任务,去实现这个类,然后重写这个类当中的run方法就可以很轻松的去定义一个任务,这里把我们要做的事情在Runnable的run方法当中表述出来即可,下面来看一个小例子,描述如何在Java当中定义一个任务。
1 | public class DefineTask implements Runnable{ |
上述代码,我们很轻松的创建了一个任务,我们通过几个变量来标示这个线程,比如当前线程的ID等,然后就是倒计时的变量。
关于Run方法的实现,在我们日常开发当中,比较常用的做法就是通过一个Looper来让一个任务一直的运行,然后通过一个标示位来控制这个Looper,当任务做完了以后我们主动的调用了yield方法,这个方法是对线程调度器的一种操作,大体的意思就是,我这边已经执行完了相应的任务,可以切换出去给别的线程使用了,需要明白的就是不是一调用这个方法立马就会把切出去,这完全取决于JDK。
3、驱动任务
上述定义了任务,但是如何开启我们的任务呢,这个时候,我们就需要使用Thread类来驱动我们的任务,上面我们定义好了,相应的任务,现在可以尝试的使用Thread来启动这个任务。
1 | public static void main(String[] args) { |
驱动我们的线程很简单,通过Thread的构造器,把定义好的任务传进去,然后调用start方法就开启了我们的任务,这里需要我们注意的一点就是start方法看起来像是一个长时间执行的方法,但是通过执行结果来看的话,却是立即返回了,原因就是后面的输出语句打印出来了。所以在此刻底层为我们重新创建了一个线程,去执行我们的任务,
这里有有一点需要我们注意一下,那就是关于run和start方法的区别,例如下面的这一条示例:
1 | public static void main(String[] args) { |
主要看两次的输出结果就可以发现,到底是如何才能区分它们两个。
当我们有大量的任务需要处理的时候,应该如何去做了,也许你已经想到了,那就是在main当中通过一个Looper来创建相应的线程,如下示例所示:
1 | public static void main(String[] args) { |
通过上述的输出结果,可以看到,线程是混在一起执行的,也就是每一次执行的结果都不会相同,这就说明了相应的线程执行的时候,将会存在很多的时许问题。
当main创建相应的线程的时候,当前的类并没有捕获任何对这些对象的引用。在使用普通对象的时候,会获取相应的引用,如果不需要了释放引用,那么垃圾回收就认为这个是一个垃圾,但是使用线程相应的对象的时候就不一样了,在使用的时候,每一个线程都会注册自己,这样就让一个引用都持有它,而且在它的任务退出之前,垃圾回收机制都无法清理它。
需要记住,在对start的调用完毕以后,这个对象将依旧会持续存在。
4、驱动大量任务
如果现在存在大量的任务需要我们去做的时候,应该怎么办,上述方式可以解决,但是如果有1000个任务,那么就需要创建1000一个Thread对象,通过上述知道了Thread对于垃圾回收机制的特殊性,那么我们的内存可想而知,并且我们也不能管理我们的任务。
有了上述的需求,这个时候我们就不能使用单个的Thread类去驱动任务了,需要Java为我们提供的另外一个叫做线程池的概念去驱动,和它的名字是一样的,那就是线程池就像是一个水池一样,当中放着大量的单个Thread,然后通过管理大量的Thread去驱动我们的任务。线程池这个概念是介于客户端和任务之间的一个中间层,与客户端直接执行任务不同,这个中间层允许你管理异步任务的执行,而无需显示的管理线程的生命周期。
Java为我们提供了四种常用的线程池
- newCacheThreadPool
- newFixedThreadPool
- newSingleThreadExecutor
- newScheduleThreadPool
基本的用法都差不多类似,sun为我们抽出来了特定的接口,我们只要看接口怎么使用就好了,只是具体实现不同而已。先来熟悉一下具体的用法吧,还是使用上述的例子
1 | /** |
具体的输出和上面一致,这里我们看到sun为我们提供的公共接口就是ExecutorService,我们这里使用了该接口的两个方法,一个是execute,类似于start方法,还有一个就是shutdown方法,使用这个方法防止新任务被提交到线程池,main线程将继续运行在shutdown被调用之间提交的所有任务,这个程序将在Executor中的所有任务完成之后尽快退出。
由于sun为我们抽离出来了共有的接口,所以将原有的实现类替换成为其他的实现类特别简单,下面我们试着替换一下
1 | public static void main(String[] args) { |
下面详细说一下这四种线程池的区别的使用场景吧。
4.1、newCacheThreadPool
创建一个可以缓存的线程池,如果线程池的长度超过处理的需要,可灵活回收,若无可回收,则新建线程。
特点
- 工作线程的创建数量几乎没有限制(通过源码可以知道是Int的最大值),这样就可以灵活的向线程池中添加线程
- 如果长时间没有往线程池当中提交任务,即如果工作线程空闲了指定的时间(默认是1分钟),则该工作线程将自动终止。终止后,如果你有提交了新的任务,则线程池重新创建一个工作线程
- 使用该类线程池需要注意:控制任务的数量,否则由于大量线程同时运行,可能会造成系统瘫痪
4.2、newFixedThreadPool
创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始化的最大数,则将提交的任务存入到池队列当中进行等待。
特点
- FixedThreadPool是一个典型并且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所消耗的开销的优点。
- 但是在空闲的时候,即线程池中没有运行任务的时候,它不会释放工作线程,并且还会占用一定的系统资源。
4.3、newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的一个工作线程来执行任务,它只会使用唯一的工作线程来执行任务,保证所有任务按照指定顺序(这里给出三种常用的顺序,FIFO,LIFO,优先级)来执行。
特点:
- 如果这个线程异常结束,遍会新建一个取代它保证顺序执行。
- 单工作线程可以保证顺序的执行各个任务,并且在任意给定的时间不会有多个线程活动
4.4、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期的任务执行,支持定时及周期性的任务执行
这个比较特殊,我们来看一个演示的Demo
1 | //延时5秒以后启动任务。 |
5、任务产出
我们知道在线程和线程之间是独立的,但是当开发程序的时候,需要两个线程要交互,比如我们有一个大计算需要在子线程当中做,然后反馈给主线程,这个时候就需要使用带有返回值的线程了,我们把这种需求叫做任务产出。
sun为我们定义了Runnable来表示任务,但是当需要返回值的时候,Runnable就感觉力不存心了,这个时候我们使用Callable接口,是sun公司在5.0的时候引入的,它是一种具有类型参数的范型,它的类型参数表示的是从call方法中返回的值,并且必须使用ExectorService.submit()方法驱动这种特殊的任务,下面是一个Demo
1 | /** |
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 | while(true){ |
当线程执行sleep(1000)之后会被立即阻塞,如果在阻塞时外面调用interrupt来中断这个线程,那么就会执行
System.err.println(“thread interrupted”);
这个时候其实线程并未中断,执行完这条语句之后线程会继续执行while循环,开始sleep,所以说如果没有对InterruptedException进行处理,后果就是线程可能无法中断
所以,在任何时候碰到InterruptedException,都要手动把自己这个线程中断。由于这个时候已经处于非阻塞状态,所以可以正常中断,最正确的代码如下:
1 | while(true){ |
在5.0以后sleep是可以带TimeUtil的,这也就方便了我们的使用。