小学徒成长系列—线程同步、死锁、线程池

  在前一篇博文《小学徒的成长系列—线程》中,我们已经讲解了关于线程的基本概念及其常用的方法,现在在本次博文中,我们就讲解关于守护线程,同步,及线程池的知识吧。

 1.守护线程(后台线程)

  在Java中,线程定义有两种:

  1> 非守护线程(有些教学书籍喜欢叫做非后台线程)

  2> 守护线程(有些教学书籍喜欢叫做后台线程),下面是摘自《Java编程思想》的说法:

  所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分,因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main()方法的就是一个后台线程。

  当然,并不是只有由JVM创建的才是守护线程啦,其实我们也可以定义守护线程,通过Thread类的setDaemon()定义即可。

  下面是sun公司提供的JConsole中的截图

2.线程同步问题

  我们访问很多网站的时候,往往都会有一个计数器,显示我们是第几个访问该网站的,下面我们来模拟一下,eg:

 1 package com.thread.tongbu;
 2 
 3 public class NumberAddThread implements Runnable {
 4     public static int number = 0;
 5     
 6     @Override
 7     public void run() {
 8         timmer();  //当多个线程访问该方法修改数据时,将会涉及数据安全问题
 9     }
10     //计算该线程第几个访问的
11     public void timmer() {
12         number++;
13      
14         try {
15             //让该线程睡眠0.1s
16             Thread.sleep(100);    
17         } catch (InterruptedException e) {
18             e.printStackTrace();
19         }
20         //输出该线程是第几个访问该变量的
21         System.out.println(Thread.currentThread().getName() + " : 你是第 " + number + " 个访问");
22     }
23     
24     public static void main(String[] args) {
25         NumberAddThread n = new NumberAddThread();
26         Thread t1 = new Thread(n);
27         Thread t2 = new Thread(n);
28         t1.start();
29         t2.start();
30     }
31 }

  本来正常的情况下输出结果应该是 : , 但我们发现结果竟然出乎意料是 :

  这究竟是为什么呢?

  其实原因在于,刚开始,第一个线程访问的时候,number已经自加为1,然后该线程睡眠了,在它睡眠期间,跌二个线程来了,也给number加1变成了2,这个时候第一个线程才睡眠结束继续执行下一行输出语句,然而此时的number的值已经改变了,输出的结果也不再是1了。换句话说,上面的问题就是run()方法体不具备同步安全性。

  为了解决这个问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的代码块就是同步代码块,同步代码块的格式如下:

  1>修饰对象:

synchronized(obj) {
    //....
    //此处的代码块就是同步代码块   
}

  2>修饰方法,表示整个方法为同步方法:

public synchronized void timmer() {
      //......
      //此处的代码块就是同步代码块        

  注意:synchronized只能修饰对象和方法,不能用来修饰构造器、属性。

  上面代码块中,不管synchronized修饰的是方法还是对象,它始终锁定的是对象的实例变量,或者类变量。当执行同步代码块的时候,就会先获取该对象的同步监视器的锁定,直到线程执行完同步代码块之后才会释放对同步监视器的锁定。

  到现在大家应该知道怎么解决前面程序出现的问题了吧,没错,只要把run()方法修改成如下即可:

1     public void run() {
2         synchronized(this){
3             timmer();
4         }
5     }

 执行结果:

  啊哈,这下我们终于对啦,呵呵。

   3.死锁

  3.1基本概念

  3.1.1什么叫做死锁?

  多个线程在执行过程中因争夺资源而造成的一种僵局,若无外力作用,将无法向前推进。

  3.1.2产生死锁的原因

  1> 竞争资源。当系统中供多个线程共享的资源如打印机等,其数目不足以满足诸线程的需要时,会引起诸线程对资源的竞争而产生死锁;

  2> 线程间推进顺序非法,线程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。

  3.1.3产生死锁的必要条件

  1> 互斥条件,即一段时间某资源只由一个线程占用;

  2> 请求和保持条件,指进程已经保持了至少一个资源,但又提出了新的资源请求新的资源求情,而该资源又已经被其他线程占有,此时请求进程序阻塞,但又对自己已经获得的其他资源保持不放;

  3> 不剥夺条件,指线程已经获得的资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放;

  4> 环路等待,指在发生死锁时,必然存在一个进程—资源的环形链。如P1等待一个P2占用的资源,P2正在等待P3占用的资源,P3正在等待P1占用的资源。

  3.1.4死锁的解除方式

  1>剥夺资源,从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

  2>撤销进程,最简单的撤销进程的方法是使全部死锁进程都夭折掉,稍微温和一点的方法是按照某种顺序逐个撤销进程,直至有足够的资源可用,使死锁状态消除为止。

 3.2Java程序中的死锁状况及其调试方法

  首先我们来看一个程序,eg:

 1 package com.thread.tongbu;
 2 
 3 public class TestDeadLock implements Runnable{
 4     static class Pen {}
 5     static class Paper{}
 6     
 7     boolean flag = false;
 8     static Paper paper = new Paper();
 9     static Pen pen = new Pen();
10     
11     @Override
12     public void run() {
13         if(flag) {
14             synchronized (paper) {
15                 try {
16                     Thread.sleep(100);
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20                 synchronized (pen) {
21                     System.out.println("paper");
22                 }
23             }
24         } else {
25             synchronized (pen) {
26                 try {
27                     Thread.sleep(100);
28                 } catch (InterruptedException e) {
29                     e.printStackTrace();
30                 }
31                 synchronized (paper) {
32                     System.out.println("pen");
33                 }
34             }
35         }
36     }
37     
38     public static void main(String[] args) {
39         TestDeadLock td1 = new TestDeadLock();
40         TestDeadLock td2 = new TestDeadLock();
41         td1.flag = false;
42         td2.flag = true;
43         Thread tt1 = new Thread(td2);
44         Thread tt2 = new Thread(td1); 
45         tt1.start();
46         tt2.start();
47     }
48 }

  执行的时候,等待了好久,都一直没有出现输出结果,线程也一直没有结束,这是因为出现死锁了。

  那我们怎么确定一定是死锁呢?有两种方法。

  1>使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开。

    1)连接到需要查看的进程。

2)打开线程选项卡,然后点击左下角的“检测死锁”

    3)jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情:

     从上图中我们可以看出:

      ①在线程Thread-1中,从状态可以看出,它想申请Paper这个资源,但是这个资源已经被Thread-0拥有了,所以就堵塞了。

      ②在线程Thread-0中,从状态可以看出,它想申请Pen这个资源,但是这个资源已经被Thread-1拥有了,所以就堵塞了。

    Thread-1一直等待paper资源,而Thread--一直等待pen资源,于是这两个线程就这么僵持了下去,造成了死锁。

  2>直接使用JVM自带的命令

    1)首先通过 jps 命令查看需要查看的Java进程的vmid,如图,我们要查看的进程TestDeadLock的vmid号是7412;

    

    2)然后利用 jstack 查看该进程中的堆栈情况,在cmd中输入 jstack -l 7412 ,移动到输出的信息的最下面即可得到:

    

    至此,相信大家都会看了吧,具体就不说啦,根据输出,找到问题所在的代码,开始调试解决即可啦。

 4.线程池

  4.1简介

  我们都知道对象的创建和销毁都是很消耗性能的,所以为了最大程度的复用对象,降低性能的消耗,就出现了容器对象池,而线程池的本质也是对象池,所以线程池能够最大程度上的复用已有的线程对象,当然除此之外,他还能够最大程度上的复用线程,否则他就不叫线程池啦。我记得我在面试金山的时候,面试官百分百的肯定线程是不能复用的,我当时就不太赞同,当然我也没有理论,因为当时的我,在这块确实不太熟悉,那一次的面试,也第一次让我意识到了我的基础还是太薄弱了。好啦,扯淡了,不好意思,我们讲讲线程池是怎样复用线程的吧。

  本来线程在执行完毕之后就会被挂载或者销毁的,但是,不断的挂载或销毁,是需要一定开销的的,但是如果我们让线程完成任务后忙等待一会儿,就可以维持存在,根据调度策略分配任务给他,就又能复用该线程执行多个任务,减少了线程挂起,恢复,销毁的开销,当然啦,如果一直让线程长期忙等待的话,也是非常消耗性能的。

  下面这个线程类关系图摘自:www-35java-com的博客

  

  当然啦,实际定义线程池的是ThreadPoolExecutor类,但是Java官网的API强烈推荐我们使用Executors,因为它已经为大多数使用情景预定义了设置:

4.2ThreadPoolExecutor

  在这个类中,有一个关键的类Worker,所有的线程对象都要经过Worker的包装,这样才能够做到复用线程而无需创建新的线程,关于这个Worker类我们在以后的博文会介绍到,这次我们只是看看ThreadPoolExecutor类的构造方法并且解析一下吧

 1     public ThreadPoolExecutor(int corePoolSize,
 2                               int maximumPoolSize,
 3                               long keepAliveTime,
 4                               TimeUnit unit,
 5                               BlockingQueue<Runnable> workQueue,
 6                               ThreadFactory threadFactory,
 7                               RejectedExecutionHandler handler) {
 8         if (corePoolSize < 0 ||
 9             maximumPoolSize <= 0 ||
10             maximumPoolSize < corePoolSize ||
11             keepAliveTime < 0)
12             throw new IllegalArgumentException();
13         if (workQueue == null || threadFactory == null || handler == null)
14             throw new NullPointerException();
15         this.corePoolSize = corePoolSize;
16         this.maximumPoolSize = maximumPoolSize;
17         this.workQueue = workQueue;
18         this.keepAliveTime = unit.toNanos(keepAliveTime);
19         this.threadFactory = threadFactory;
20         this.handler = handler;
21     }

 

根据Java官网文档的解释,构造方法中每个变量的解释如下:

1> corePoolSize :线程池维护线程的最小数量,哪怕是空闲的

2>maximumPoolSize : 线程池维护的最大线程数量  
  由于ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整池大小,其调整规则如下:

  当新任务在方法 execute(java.lang.Runnable) 中提交时

  1) 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的;

  2) 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池是大小固定的,

    如果运行的线程与corePoolSize相同,当有新请求过来时,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理

  3) 如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程才创建新的线程去处理请求;

  4) 如果运行的线程多于corePoolSize 并且等于maximumPoolSize,若队列已经满了,则通过handler所指定的策略来处理新请求;

  5) 如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务

   结论(摘自网络):

也就是说,处理任务的优先级为:
1. 核心线程corePoolSize > 任务队列workQueue > 最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
2. 当池子的线程数大于corePoolSize的时候,多余的线程会等待keepAliveTime长的时间,如果无请求可处理就自行销毁。

3>keepAliveTime :线程池维护线程所允许的空闲时间

 

4>unit : 线程池维护线程所允许的空间时间的单位

 

5>workQueue :线程池所使用的缓冲队列,该缓冲队列的长度决定了能够缓冲的最大数量,缓冲队列有三种通用策略:

  1) 直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;

 

  2) 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;

 

  3) 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量.

 

6>被拒绝的任务:当Executor已经关闭(即执行了executorService.shutdown()方法后),并且Executor将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法execute()中提交的新任务将被拒绝.

  在以上述情况下,execute 方法将调用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四种预定义的处理程序策略:

    1) 在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException;
    2) 在 ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度

    3) 在 ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除;

    4) 在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

 

  7>创建新线程 : 使用 ThreadFactory 创建新线程。如果没有另外说明,则在同一个 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。通过提供不同的 ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

  4.3 Executors

  Executors已经为程序员们预定义了大多数使用情景适用的线程池配置,强烈推荐使用这个类来创建对应的线程池。下面我们来介绍一下,该类中常用的几个创建线程池方法。

  1>CachedThreadPool :该线程池比较适合没有固定大小并且比较快速就能完成的小任务,它将为每个任务创建一个线程。那这样子它与直接创建线程对象(new Thread())有什么区别呢?看到它的第三个参数60L和第四个参数TimeUnit.SECONDS了吗?好处就在于60秒内能够重用已创建的线程。下面是Executors中的newCachedThreadPool()的源代码:

  

  2> FixedThreadPool使用的Thread对象的数量是有限的,如果提交的任务数量大于限制的最大线程数,那么这些任务讲排队,然后当有一个线程的任务结束之后,将会根据调度策略继续等待执行下一个任务。下面是Executors中的newFixedThreadPool()的源代码:

  

  3>SingleThreadExecutor就是线程数量为1的FixedThreadPool,如果提交了多个任务,那么这些任务将会排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将会使用相同的线程。下面是Executors中的newSingleThreadExecutor()的源代码:

  

  好啦,了解了这三个配置的线程池,不知道大家有没有自习看他们调用ThreadPoolExecutor的构造方法呢?

  通过三个配置的线程池的创建方法源代码,我们可以发现:

  1> 除了CachedThreadPool使用的是直接提交策略的缓冲队列以外,其余两个用的采用的都是无界缓冲队列,也就说,FixedThreadPool和SingleThreadExecutor创建的线程数量就不会超过 corePoolSize。

  2> 我们可以再来看看三个线程池采用的ThreadPoolExecutor构造方法都是同一个,使用的都是默认的ThreadFactory和handler:

 1 private static final RejectedExecutionHandler defaultHandler =
 2     new AbortPolicy();
 3 
 4 public ThreadPoolExecutor(int corePoolSize,
 5                     int maximumPoolSize,
 6                     long keepAliveTime,
 7                     TimeUnit unit,
 8                     BlockingQueue<Runnable> workQueue) {
 9     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
10         Executors.defaultThreadFactory(), defaultHandler);
11 }

  也就说三个线程池创建的线程对象都是同组,优先权等级为正常的Thread.NORM_PRIORITY(5)的非守护线程,使用的被拒绝任务处理方式是直接抛出异常的AbortPolicy策略(前面有介绍)。

  大概了解到这里吧,下面我们给出一个运行例子,在方法中,分别给出了三种配置的线程池的测试方法,大家要测试哪种只要取消哪行的方法注释然后注释掉其他两个运行即可,由于篇幅问题,具体的运行结果就不贴出来啦,eg:

TaskThread.java
View Code
 1 package com.thread.pool;
 2 
 3 public class TaskThread implements Runnable {
 4     protected int countDown = 10;    //DEFAULT
 5     private static int taskCount = 0;    //任务的个数
 6     private final int id = taskCount++;    //以第几个作为当前任务的ID
 7     
 8     public TaskThread() { }
 9     
10     public TaskThread(int countDown) {
11         this.countDown = countDown;
12     }
13     
14     public String status() {
15         String name = Thread.currentThread().getName();
16         return name + "#" + id + "( " + (countDown > 0 ? countDown : "LifeOff") +" )" ;
17     }
18     
19     @Override
20     public void run() {
21         while (countDown-- > 0) {
22             System.out.println(status());
23             Thread.yield();
24         }
25     }
26 }

TestThreadPool.java

View Code
 1 package com.thread.pool;
 2  
 3  import java.util.concurrent.ExecutorService;
 4  import java.util.concurrent.Executors;
 5  
 6  public class TestThreadPool {
 7      
 8      public static void main(String[] args) {
 9  //        testCachedThreadPool();
10  //        testFixedThreadPool(0);
11          testSingleThread();
12      }
13      
14      /**
15       * 创建Runnable任务并添加到线程池中
16       * @param executorService    指定的线程池类型
17       */
18      public static void createTask(ExecutorService executorService) {
19          for (int i = 0; i < 5; i++) {
20              executorService.execute(new TaskThread());    //创建任务并交给线程池进行管理
21          }
22          executorService.shutdown();    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务
23      }
24      
25      /**
26       * CachedThreadPool将为每个任务创建一个线程
27       */
28      public static void testCachedThreadPool() {
29          ExecutorService executorService = Executors.newCachedThreadPool();    //创建CachedThreadPool
30          createTask(executorService);
31      }
32      
33      /**
34       * FixedThreadPool使用的Thrad对象的数量是有限的,如果提交
35       * 的任务数量大于限制的最大线程数,那么这些任务讲排队,然
36       * 后当有一个线程的任务结束之后,将会根据调度策略继续等待
37       * 执行下一个任务
38       * @param number 限制 FixedThreadPool 中的线程对象的数量
39       */
40      public static void testFixedThreadPool(int number) {
41          if (number == 0) {
42              number = 3;    //DEFAULT
43          }
44          ExecutorService executorService = Executors.newFixedThreadPool(number);
45          createTask(executorService);
46      }
47      
48      /**
49       * SingleThreadExecutor就是线程数量为1的FixedThreadPool。
50       * 如果提交了多个任务,那么这些任务将会排队,每个任务都会在
51       * 下一个任务开始之前运行结束,所有的任务将会使用相同的线程
52       */
53      public static void testSingleThread() {
54          ExecutorService executorService = Executors.newSingleThreadExecutor();
55          createTask(executorService);
56      }
57  }

 

 参考资料:

1.《Java编程思想》第4版 P656

2.诗剑书生的专栏 :http://blog.csdn.net/axman/article/details/1481197

3.狂飙的蜗牛:http://blog.csdn.net/xjtuse_mal/article/details/5687368

4.洞玄的博客:http://dongxuan.iteye.com/blog/901689

5.Java官网文档:http://docs.oracle.com/javase/6/docs/api/

  

posted @ 2013-05-08 00:35  小学徒V  阅读(3108)  评论(4编辑  收藏  举报