zoukankan      html  css  js  c++  java
  • java多线程(五)-访问共享资源以及加锁机制(synchronized,lock,voliate)

          对于单线程的顺序编程而言,每次只做一件事情,其享有的资源不会产生什么冲突,但是对于多线程编程,这就是一个重要问题了,比如打印机的打印工作,如果两个线程都同时进行打印工作,那这就会产生混乱了。再比如说,多个线程同时访问一个银行账户,多个线程同时修改一个变量的值。这个时候,就很容易产生冲突了。

    看一个例子:src hread_runnableEvenTest.java

     1 class EvenChecker implements Runnable{
     2     private IntGenerator generator;
     3 
     4     public EvenChecker(IntGenerator generator) {
     5         super();
     6         this.generator = generator;
     7     }
     8 
     9 
    10     public void run() {
    11         // TODO Auto-generated method stub
    12         int val = 0;
    13         while (!generator.isCanceled()){
    14             val = generator.next();
    15             if (val%2 != 0){
    16                 System.out.println("Error info --->" + val + " not even, threadInfo=" + Thread.currentThread().getName());
    17                 generator.cancel();
    18             }
    19         }
    20     }
    21     
    22     public static void  test(IntGenerator gp, int count) {
    23         System.out.println("start test " + count + "  thread") ;
    24         ExecutorService exec = Executors.newCachedThreadPool();
    25         for (int i=0; i<count; i++){
    26             exec.execute(new EvenChecker(gp));
    27         }
    28         exec.shutdown();
    29     }
    30     
    31     public static void  test(IntGenerator gp) {
    32         test(gp, 5);
    33     }
    34 
    35 }//end of "class EventChecker"
    36 
    37 
    38 class IntGenerator {
    39     private int currentEvenValue = 0;
    40     private volatile boolean canceled = false;
    41     
    42     /**
    43      * 对于顺序执行的程序,该方法内的 currentEvenValue 的值每次都增加2,所以 该方法的返回值用于都为偶数,不可能为奇数。 
    44      * @return
    45      */
    46     public  int next(){
    47         ++currentEvenValue;
    48 //        Thread.yield();
    49         ++currentEvenValue;
    50         return currentEvenValue;
    51     }
    52     public void cancel(){
    53         canceled = true;
    54     }
    55     public boolean isCanceled(){
    56         return canceled;
    57     }
    58 }//end of "class IntGenerator"
    59 
    60 
    61 public class EvenTest {
    62     public static void main(String[] args) {
    63         // TODO Auto-generated method stub
    64         EvenChecker.test(new IntGenerator());
    65     }
    66 
    67 }

    先来分析这个代码 ,
    在IntGenerator对象中, currentEvenValue值初始值为0,在next()方法里每次加2,然后返回,所以next()方法返回的永远都为偶数,不可能为奇数。而EvenChecker对象默认开启了5个线程,循环获取 IntGenerator对象的next()方法产生的值,并进行判断,如果为奇数,则打印Error info,并停止循环。

    当然,这个程序如果是顺序程序,那么永远不可能打印出Error Info,但是实际运行程序,某一次的输出结果如下:

    很快的就产生了奇数的情况,原因就是因为 多个线程以交叉的顺序来修改了 currentEvenValue的值(当然对于多核cpu,可能就是在不同的核上同时运行),在 IntGenerator对象的next()方法中,有可能当currentEvenValue刚加一次时,另一个线程就又进入该方法进行修改。所以导致了产生了 奇数。
    这就是多线程共享受限资源,而引起bug的一个明显的例子,我们可以想到,如果在 next()方法的两句++操作语句之间,加一句 Thread.yield()语句,就像下面这样,

    1 ++currentEvenValue;
    2 Thread.yield();
    3 ++currentEvenValue;

    那么 next()将会更快的产生奇数。
    实际运行某一次输出结果如下:

    解决共享资源竞争
    要想避免类似上面demo中出现的不同步问题,做法就是当某一个受限资源在使用过程中加锁,每个线程在访问该资源前,都先检查一下该资源是否加锁了。没有则访问并加锁,否则就等待着,直到锁被(占有该资源的线程)释放了。

    这种某个时刻只允许一个线程访问某个共享资源的方法,称为 序列化访问共享资源 的方案,通常这都是通过在代码片段开始时 加入特殊语句来实现的,然后同一时刻,只允许一个线程来访问这个代码片段。因为锁语句产生了一种相互排斥的效果,所以这种机制也常常被称为 互斥量(mutex)。


    打印机的例子是很明显的,好几个人都挤在打印机前,都争着抢着打印自己的东西。但是如果某个人使用过程中,能随时被其他人打断抢走,那么最后的结果肯定是乱成一团。而通过加锁机制就可以避免这种情况。第一个挤上去的人,给打印机加了锁,然后开始打印自己的东西,这个时候其他人 虽然围在打印机周围,但是是没办法使用的,只有当第一个人使用完毕了,解锁后,才会有第二个人获得打印机资源,加锁,并开始使用打印机,不过谁会是第二个获得打印机的人,这就不确定了。
    Java提供了 synchronized 关键字 来提供加锁支持,当某个线程执行某个被synchronized关键字保护的代码片段的时候,它将先检查其是否加锁,如果没有,则加锁,执行完毕后,再释放锁。如果已经加锁了,那就无法使用这个资源了。

    在java中,一切都是对象,不管是要访问打印机,还是输入输出语句,都是要通过调用对象的方法来实现的,所以我们使用synchronized的方式可以是 在定义方法时加锁。
    比如

    1 class ClassA{
    2         synchronized  void g(){ /**  do something  */}
    3         synchronized  void f(){ /**  do something  */}
    4         void m(){ /**  do something  */}
    5 }

    我们对ClassA的g()和f()方法进行了加锁。但是需要注意的是,synchronized 加锁,是加在整个ClassA对象上的,也就说,某个线程操作g()方法时,因为g()方法加锁了,其实是ClassA加锁了,所以f()方法也不能被其他线程调用,当然m()方法是可以被其他线程调用的。加锁都是加在对象上,而不是 某个方法上,这样设计是合理的,因为f()和g()既然都是一个对象的方法,那么从设计理念上来讲,他们都应该是属于和同一个受限资源有关系的方法。

    具体的加锁,释放锁是JVM来负责的。

    我们将上一个 demo中的 IntGenerator对象的next()方法进行加锁。

    1 public   synchronized   int   next(){
    2         ++currentEvenValue;
    3         ++currentEvenValue;
    4         return currentEvenValue;
    5 }

    然后运行代码,输出结果如下

    从控制台可以看出,程序一直在运行,但是不会再出现奇数,打印出Error info了。
    使用 synchronized 关键字可以比较方便的来加锁,而java 5之后,引入了新的对象来加锁。例子如下:

    1 void func(){
    2 Lock lock = new ReentrantLock();
    3         lock.lock();
    4         try{
    5             //do something
    6         }finally{
    7             lock.unlock();
    8         }
    9     }

    Lock对象可以更加灵活,也可以提供更细粒度的控制,不过synchronized 写起来更加简单方便一些。
    如果我们希望加锁的只是方法的部分代码而不是全部(这段代码被称为临界区 critical section),那么也可以使用 synchronized 关键字来操作。

    1      void func(){
    2         //do something
    3         synchronized (this) {
    4             //临界区
    5         }
    6         
    7         //do something
    8     }

    我们采用synchronized 来加锁除了防止争夺受限资源这个重要方面之外,其实还有一个方面,那就是:内存可见性.我们不仅希望防止线程A在访问某个对象状态时,另一个线程B同时也在修改该对象状态的这种情况的发生。同时也希望,当线程A修改完该对象的状态后,其他的线程在访问该对象时,都能看到这个变化。这就叫做内存可见性。

    而实现内存可见性的方式,除了加锁方式,还有一个 volatile 关键字。
    在java当中有个原子操作的概念,原子操作的意思就是 不能被线程调度机制所中断的操作。一般开始该操作,那么在它执行完之前,是不可能进行上下文切换的。比如对于 除了long,double之外的基本类型进行简单操作,就可以称为原子操作。(long,double都是64位,jvm在使用他们的时候,都是将他们当做两个32位的)。原子操作既然不会被线程调度机制中断,那么看起来不需要对它们进行同步控制。但是这种想法对于单核cpu也许使用,但是对于多核cpu,就不是这个样子了。
    假设线程A,线程B 都需要访问一个int类型变量count,线程A在cpu的1号核上先执行任务,修改变量count的值,然后存储在了1号核本身的寄存器或者缓存上,然后访问完之后,线程B在cpu的2号核上开始运行,但是请注意这个时候,B读取的count的值是从 主存中读取的(有可能是内存,或者L1 ,L2 cache等).所以线程B读取到的值 和1号核的count值不同了。此时虽然对于count的修改是原子操作,没有被线程中断。但是却不同步。这也被称为 可视性问题。一个线程做出的修改,虽然是原子性的,没有被中断,但是对于其他线程也可能是不可视的。
    Volatile关键字就是确保了可视性,当声明一个变量为volatile的,一个线程修改了该变量的值,其他线程也可以看到该修改。添加了volatile关键字的属性,会立刻被写入到主存中,这样就避免了不同步的问题。

    synchronized和volatile有什么区别呢。
    (1) volatile是一种比synchronized更加轻量级的同步机制。volatile不会执行加锁操作,也不会阻塞线程。
    (2) 如果代码当中过度依赖volatile,那么将会使代码更脆弱,也更难以理解。
    (3) 加锁机制既可以保证可见性又可以保证原子性。而volaitle只确保可见性。
    总体来说,需要同步的时候,第一选择应该是synchronized,这是最安全的方式,虽然它可能性能差一些,不过随着jdk本身的优化,加锁机制的性能也在不断提升。

    这几篇java多线程文章的demo代码下载地址 http://download.csdn.net/detail/yaowen369/9786452

    -------
    作者: www.yaoxiaowen.com
    github: https://github.com/yaowen369

  • 相关阅读:
    Linnia学习记录
    漫漫考研路
    ENS的学习记录
    KnockoutJS 3.X API 第四章 数据绑定(4) 控制流with绑定
    KnockoutJS 3.X API 第四章 数据绑定(3) 控制流if绑定和ifnot绑定
    KnockoutJS 3.X API 第四章 数据绑定(2) 控制流foreach绑定
    KnockoutJS 3.X API 第四章 数据绑定(1) 文本及样式绑定
    KnockoutJS 3.X API 第三章 计算监控属性(5) 参考手册
    KnockoutJS 3.X API 第三章 计算监控属性(4)Pure computed observables
    KnockoutJS 3.X API 第三章 计算监控属性(3) KO如何实现依赖追踪
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/6581287.html
Copyright © 2011-2022 走看看