写在前面的话
从这篇文章开始就正式进入了并发主题,该主题相关知识较前面的集合主题会比较晦涩难懂,需要不断回顾、整理,才能构建出自己的知识网络。
首先推荐一本讲并发的好书:JAVA并发编程实战。这本书非常完整的讲解了关于并发的知识点,是一本不可多得的好书,如果有时间一定要多看几遍。
下面放一张并发主题的思维导图(受限于网页大小,请童鞋们自行下载图片浏览):
从这张图中可以看到并发相关的知识点非常多并且非常杂,我们这个主题不可能把这里边所有罗列的知识点全部深入讲解一遍,所以我在这里只能挑选一些相对比较重要的知识点(大家可以认为是主干知识 : ))进行深入分析,至于剩下的一些枝枝叶叶就需要童鞋们自己去实践掌握啦。
OK,介绍就到这里,接下来正式开始知识点剖析,当然首先从基础知识开始(图中右下角线程部分)。
并发基础知识
这部分知识是相对来说比较简单并且容易理解的,我从这里开始写也是为了巩固童鞋们学习并发知识的信心,让大家能够更有动力的去学习。
1.什么是线程?什么是进程?它们之间有何联系?
关于这个问题,我准备用一个形象的例子来描述,这样比较容易理解:大家都有用过杀毒软件吧?当你打开杀毒软件,那么你就启动了一个进程。而现代的杀毒软件功能已不仅仅局限于查杀病毒了,比如还有清理垃圾文件、工具箱、修复漏洞、扫描驱动等等功能,而这一个个功能对应的其实就可以认为是一个个线程在执行任务(当然实际情况可能更复杂)。
从上面这个例子可以得出结论:进程是正在运行的程序的实例,而线程是程序中一个单一的顺序控制流程,它是程序执行流的最小单元。一个进程可以拥有许多个线程,这些线程可以共享该进程的全部资源,并且可以并发执行(比如你可以同时进行扫描病毒和修复漏洞的操作)。
2.如何创建并启动一个线程?
java中创建线程有两种方式:继承Thread基类 以及 实现Runnable接口。
继承Thread基类的方式:
1 class ExtendsThread extends Thread{ 2 3 @Override 4 public void run() { 5 System.out.println("create thread by extends Thread class........."); 6 } 7 }
实现Runnable接口的方式:
1 class ImplementsThread implements Runnable{ 2 3 @Override 4 public void run() { 5 System.out.println("create thread by implements Runnable interface........."); 6 } 7 }
创建线程后,就要启动线程去执行它的工作,两种创建线程方式有不同的启动方法:
1 public class CreateThread { 2 3 public static void main(String[] args) { 4 //使用 extends Thread 创建并启动线程 5 ExtendsThread et = new ExtendsThread(); 6 et.start(); 7 8 //使用 implements Runnable 创建并启动线程 9 Thread runThread = new Thread(new ImplementsThread()); 10 runThread.start(); 11 12 } 13 }
这里有一个很古老的面试题:线程创建有哪两种方式?哪种方式比较好?为什么?
这个问题的答案要根据具体业务情况来回答,如果确实是非常非常简单的业务场景,我只需要一个线程就能完成任务,那么当然是implements Runnable接口的方式比extends Thread类的方式好。
因为大家知道java是单继承的,如果用extends的方式创建了线程,那么这个线程类就无法再继承别的类,而implements的方式则不会有这个问题,相对来说扩展性更好。
但是,现代的企业(尤其是互联网企业)中,基本不可能看到用这两种方式来创建线程的,而是使用Executors类提供的许多类型的线程池来创建并管理一组线程,为什么?
因为在复杂的、对性能和实时性要求非常高的业务场景下,用这两种创建线程的方式会造成大量资源的消耗,并且线程的上下文切换也会浪费大量的时间,所以java很贴心的为我们提供了线程池,以便于更好的管理许许多多的线程以适应业务需要。
这里再说一个额外的小知识点,大家有没有想过,如果我对同一个线程调用两次start()方法,会出现什么情况呢?
可以看到java会抛出异常:非正常的线程状态,在java中是不允许调用两次同一个线程的start方法的。
3.线程有哪几种状态?
既然上面提到了“非正常的线程状态”,那么接下来就讲一下java中线程的状态,其实共有6种:
1.NEW 新建状态。
顾名思义,在此状态下的线程就是刚刚创建出来的新线程。
2.RUNNABLE 可运行状态。
调用线程的start()方法并获取到资源锁(关于锁的知识后文会详细讲解)的线程就会进入此状态。注意,此时的线程可能正在虚拟机中执行任务,也有可能在等待CPU时间分片。
3.BLOCKED 阻塞状态。
某些代码可能会被加锁,而需要执行这些代码的线程首先需要获取到锁对象,如果线程因为没有获取到监视器锁而无法执行任务,则其就处于阻塞状态。
4.WAITING 等待状态。
如果线程执行了不带超时时间的wait方法或者join方法,那么线程就会让出执行权,并进入等待状态。
5.TIME_WAITING 超时等待状态
如果线程执行了带超时时间的wait方法或者join方法或者sleep方法,那么线程就会进入超时等待状态
6.TERMINATED 终止状态
无论是响应中断而强制结束的线程或者是正常执行任务完成后退出的线程都会处于这个状态。处于终止状态的线程不具备继续运行的能力。
线程的六种状态需要好好理解,这对于后面知识的理解会有一定帮助。
4.如何优雅的结束线程?
最后我们来讲一下如何优雅的结束一个线程。有的同学可能会说这还不简单,Thread相关api中不是有个stop方法嘛,只要调用这个方法线程不就结束了吗?
这里我先明确一下结论:stop方法确实有,但是这个方法具有不确定性,jdk1.5及以后这个方法已经被标示为“Deprecated(废弃)”状态,即不赞成在代码中继续使用该方法。
然而java中并没有提供能够立即使一个线程转变为TERMINATED状态的方法,取而代之的是提供了一种叫做“中断”的协调机制,可以使一个线程终止另一个线程当前的工作。
1 /** 2 * 优雅终止线程的方式 3 */ 4 public class TerminateThread { 5 6 public static void main(String[] args){ 7 NeedStopThread nst = new NeedStopThread(); 8 try { 9 nst.start(); 10 Thread.sleep(3000); 11 nst.cancel(); //线程运行3秒后由主线程发出中断请求 12 } catch (Exception e) { 13 System.out.println("Exception in Main Thread....."); 14 } 15 } 16 } 17 18 class NeedStopThread extends Thread{ 19 20 private AtomicInteger count = new AtomicInteger(); 21 22 @Override 23 public void run() { 24 try { 25 while(!Thread.currentThread().isInterrupted()){ //当前线程未接收到中断信号 26 System.out.println("i'm running.........count now is " + count.getAndIncrement()); 27 } 28 } catch (Exception e) { 29 System.out.println("Exception in NeedStop Thread....."); 30 }finally { 31 System.out.println("try to close io or sql resource here........."); 32 } 33 } 34 35 public void cancel(){ 36 interrupt(); //由另一个线程调用发出中断信号,请求终止当前线程 37 } 38 }
大家可以多次执行这段代码,会发现结果肯定都是不一样的,这也证明了中断操作并不会让线程立即进入终止状态,最终进入终止状态的时间还是由当前线程自己判断的。
关于线程的基础知识讲解到这里就结束了。基础知识大家一定不能小看它,地基要扎稳,楼房才能盖的好。下篇文章就开始讲解线程间的通信和协作的几种方式,也是非常重要的内容。OK,我们下篇文章见。