zoukankan      html  css  js  c++  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 }
    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             @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 }
    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标志位控制生产及销售的顺序。

           注意:即使代码不使用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 }
    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这一步时,多条线程同时可能同时操作这一句代码,导致内容出差。线程代码内的原子性被破坏了。

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

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

  • 相关阅读:
    SpringBoot项目部署与服务配置
    JDBC链接oracle已经mysql的测试
    Jedis工具类
    jsp&Sevelet基础详解
    Mysql和Oracle数据库concat()函数
    如何在maven工程中加载oracle驱动
    获取日期的相关方法
    SpringBoot配置Email发送功能
    spring中schedule注解的使用
    新建一个新的spring boot项目
  • 原文地址:https://www.cnblogs.com/doucheyard/p/5681217.html
Copyright © 2011-2022 走看看