疯狂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 @Override

  13 public void run()

  14 {

  15 try

  16 {

  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 MySingle

  31 {

  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 }

  复制代码

  运行结果如下:

  由上述可见,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 @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 FactoryCopy

  59 {

  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 }

  复制代码

  结果如下:

  该结果不能把销售线程和生产线程的代码分隔开,如果需要分隔开。可以使用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 }

  复制代码

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

  注意:即使代码不使用isCreate标志位进行控制,代码只会出现 :thread-0 生产---thread-0 生产--- thread-0 生产(生产完毕) ---thread-1 销售...这种情况,不会出现生产跟销售交替。原因:使用Synchronized关键字对方法进行约束,默认锁定的是对一个的object类,直到代码结束,才会把锁给释放。因此使用该关键字进行限制时不会出现线程交叠现象。

  备注:默认的使用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 }

  复制代码

  代码编译后功能如下:

  复制代码

  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 }

  复制代码

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

  单纯使用volatile来控制boolean或者某一个int类型的时候,感觉不出太大的作用。但当volatile在修饰一个对象的时候,对象必须按照步骤进行。在单线程的情况下new一个对象必须进行三步操作:①开辟存储空间 ②初始化 ③使用变量指向该内存。在并发的情况下,虚拟机创建对象可能依据这三步依次执行。可能③在②之前执行,那么就可能会导致程序抛出空指针异常。这时候可以使用volatile保证对象初始化原子性。