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

  • 相关阅读:
    confluence --常用插件整合
    fuse--s3挂载到centos7.5服务器
    gvm--go版本管理工具
    等保1.0与等保2.0的区别
    postfix -- 发件调试
    postfix邮件服务器
    confluence -- 命令行备份还原
    浏览器使用小tip
    windows如何正确下载补丁包
    xwiki使用中的问题
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/6581287.html
Copyright © 2011-2022 走看看