你的位置:首页 > Java教程

[Java教程]Java多线程(二) 多线程的锁机制


      当两条线程同时访问一个类的时候,可能会带来一些问题。并发线程重入可能会带来内存泄漏、程序不可控等等。不管是线程间的通讯还是线程共享数据都需要使用Java的锁机制控制并发代码产生的问题。本篇总结主要著名Java的锁机制,阐述多线程下如何使用锁机制进行并发线程沟通。

1、并发下的程序异常

  先看下下面两个代码,查看异常内容。

  异常1:单例模式

 1 package com.scl.thread; 2  3 public class SingletonException 4 { 5   public static void main(String[] args) 6   { 7     // 开启十条线程进行分别测试输出类的hashCode,测试是否申请到同一个类 8     for (int i = 0; i < 10; i++) 9     {10       new Thread(new Runnable()11       {12         @Override13         public void run()14         {15           try16           {17             Thread.sleep(100); 18           }19           catch (InterruptedException e)20           {21             e.printStackTrace();22           }23           System.out.println(Thread.currentThread().getName() + " " + MySingle.getInstance().hashCode());24         }25       }).start();26     }27   }28 }29 30 class MySingle31 {32   private static MySingle mySingle = null;33 34   private MySingle()35   {36   }37 38   public static MySingle getInstance()39   {40     if (mySingle == null) { mySingle = new MySingle(); }41     return mySingle;42   }43 }

view code

    运行结果如下:

      

  由上述可见,Thread-7与其他结果不一致,证明了在多线程并发的情况下这种单例写法存在问题,问题就在第40行。多个线程同时进入了空值判断,线程创建了新的类。

  异常2:线程重入,引发程序错误

     现在想模拟国企生产规则,每个月生产100件产品,然后当月消费20件,依次更替。模拟该工厂全年的生产与销售

      备注:举这个实例是为后面的信号量和生产者消费者问题做铺垫。可以另外举例,如开辟十条线程,每条线程内的任务就是进行1-10的累加,每条线程输出的结果不一定是55(线程重入导致)

 1 package com.scl.thread; 2  3 //每次生产100件产品,每次消费20件产品,生产消费更替12轮 4 public class ThreadCommunicateCopy 5 { 6   public static void main(String[] args) 7   { 8     final FactoryCopy factory = new FactoryCopy(); 9     new Thread(new Runnable()10     {11 12       @Override13       public void run()14       {15         try16         {17           Thread.sleep(2000);18         }19         catch (InterruptedException e)20         {21           e.printStackTrace();22         }23 24         for (int i = 1; i <= 12; i++)25         {26           factory.createProduct(i);27         }28 29       }30     }).start();31 32     new Thread(new Runnable()33     {34 35       @Override36       public void run()37       {38         try39         {40           Thread.sleep(2000);41         }42         catch (InterruptedException e)43         {44           e.printStackTrace();45         }46 47         for (int i = 1; i <= 12; i++)48         {49           factory.sellProduct(i);50         }51 52       }53     }).start();54 55   }56 }57 58 class FactoryCopy59 {60   //生产产品61   public void createProduct(int i)62   {63 64     for (int j = 1; j <= 100; j++)65     {66       System.out.println("第" + i + "轮生产,产出" + j + "件");67     }68   }69   //销售产品70   public void sellProduct(int i)71   {72     for (int j = 1; j <= 20; j++)73     {74       System.out.println("第" + i + "轮销售,销售" + j + "件");75     }76 77   }78 }

View Code

  结果如下:

    

   该结果不能把销售线程和生产线程的代码分隔开,如果需要分隔开。可以使用Java的锁机制。下面总结下如何处理以上两个问题。

 

2、使用多线程编程目的及一些Java多线程的基本知识

  使用多线程无非是期望程序能够更快地完成任务,这样并发编程就必须完成两件事情:线程同步及线程通信。

      线程同步指的是:控制不同线程发生的先后顺序。

      线程通信指的是:不同线程之间如何共享数据。 

   Java线程的内存模型:每个线程拥有自己的栈,堆内存共享 [来源:Java并发编程艺术 ],如下图所示。 锁是线程间内存和信息沟通的载体,了解线程间通信会对线程锁有个比较深入的了解。后面也会详细总结Java是如何根据锁的信息进行两条线程之间的通信。

          

2、使用Java的锁机制

    Java语音设计和数据库一样,同样存在着代码锁.实现Java代码锁比较简单,一般使用两个关键字对代码进行线程锁定。最常用的就是volatile和synchronized两个

     2.1 synchronized

       synchronized关键字修饰的代码相当于数据库上的互斥锁。确保多个线程在同一时刻只能由一个线程处于方法或同步块中,确保线程对变量访问的可见和排它,获得锁的对象在代码结束后,会对锁进行释放。

       synchronzied使用方法有两个:①加在方法上面锁定方法,②定义synchronized块。

        模拟生产销售循环,可以通过synchronized关键字控制线程同步。代码如下:

      

 1 package com.scl.thread; 2  3 //每次生产100件产品,每次消费20件产品,生产消费更替10轮 4 public class ThreadCommunicate 5 { 6   public static void main(String[] args) 7   { 8     final FactoryCopy factory = new FactoryCopy(); 9     new Thread(new Runnable() 10     { 11  12       @Override 13       public void run() 14       { 15         try 16         { 17           Thread.sleep(2000); 18         } 19         catch (InterruptedException e) 20         { 21           e.printStackTrace(); 22         } 23  24         for (int i = 1; i <= 12; i++) 25         { 26           factory.createProduct(i); 27         } 28  29       } 30     }).start(); 31  32     new Thread(new Runnable() 33     { 34  35       @Override 36       public void run() 37       { 38         try 39         { 40           Thread.sleep(2000); 41         } 42         catch (InterruptedException e) 43         { 44           e.printStackTrace(); 45         } 46  47         for (int i = 1; i <= 12; i++) 48         { 49           factory.sellProduct(i); 50         } 51  52       } 53     }).start(); 54  55   } 56 } 57  58 class Factory 59 { 60   private boolean isCreate = true; 61  62   public synchronized void createProduct(int i) 63   { 64     while (!isCreate) 65     { 66       try 67       { 68         this.wait(); 69       } 70       catch (InterruptedException e) 71       { 72         e.printStackTrace(); 73       } 74     } 75  76     for (int j = 1; j <= 100; j++) 77     { 78       System.out.println("第" + i + "轮生产,产出" + j + "件"); 79     } 80     isCreate = false; 81     this.notify(); 82   } 83  84   public synchronized void sellProduct(int i) 85   { 86     while (isCreate) 87     { 88       try 89       { 90         this.wait(); 91       } 92       catch (InterruptedException e) 93       { 94         e.printStackTrace(); 95       } 96     } 97     for (int j = 1; j <= 20; j++) 98     { 99       System.out.println("第" + i + "轮销售,销售" + j + "件");100     }101     isCreate = true;102     this.notify();103   }104 }

View Code

 

  上述代码通过synchronized关键字控制生产及销售方法每次只能1条线程进入。代码中使用了isCreate标志位控制生产及销售的顺序。

       备注:默认的使用synchronized修饰方法, 关键字会以当前实例对象作为锁对象,对线程进行锁定。

                 单例模式的修改可以在getInstance方式中添加synchronized关键字进行约束,即可。

                wait方法和notify方法将在第三篇线程总结中讲解。

    2.2 volatile

  volatile关键字主要用来修饰变量,关键字不像synchronized一样,能够块状地对代码进行锁定。该关键字可以看做对修饰的变量进行了读或写的同步操作。

  如以下代码:

 1 package com.scl.thread; 2  3 public class NumberRange 4 { 5   private volatile int unSafeNum; 6  7   public int getUnSafeNum() 8   { 9     return unSafeNum;10   }11 12   public void setUnSafeNum(int unSafeNum)13   {14     this.unSafeNum = unSafeNum;15   }16 17   public int addVersion()18   {19     return this.unSafeNum++;20   }21 }

View Code

 代码编译后功能如下:

 1 package com.scl.thread; 2  3 public class NumberRange 4 { 5   private volatile int unSafeNum; 6  7   public synchronized int getUnSafeNum() 8   { 9     return unSafeNum;10   }11 12   public synchronized void setUnSafeNum(int unSafeNum)13   {14     this.unSafeNum = unSafeNum;15   }16 17   public int addVersion()18   {19     int temp = getUnSafeNum();20     temp = temp + 1;21     setUnSafeNum(temp);22     return temp;23   }24 25 }

View Code

 由此可见,使用volatile变量进行自增或自减操作的时候,变量进行temp= temp+1这一步时,多条线程同时可能同时操作这一句代码,导致内容出差。线程代码内的原子性被破坏了。

 

  以上是对Java锁机制的总结,如有问题,烦请指出纠正。代码及例子很大一部分参考了《Java 并发编程艺术》[方腾飞 著]