• 技术文章 >Java >java教程

    Java 竞态条件和临界段

    黄舟黄舟2017-02-28 10:35:36原创515
    一个竞态条件是一个特殊的条件,可能发生在一个临界部分的内部(critical section)。一个临界部分是一段正在被多线程执行的代码,以及线程执行的顺序对于临界部分并发执行的结果产生影响。

    当多线程执行一个临界段的结果依赖线程执行的顺序可能是不同的,这个临界段包含一个竞态条件。这个竞态条件的词条源于这个线程正在竞速通过这个临界段的暗喻,并且这个竞争的结果影响着执行这个临界段的结果。

    这个可能听起来有点复杂,以至于我将会在下面的部分详细阐述关于竞态条件和临界段。


    临界段(Critical Sections)

    在相同的应用内部运行不止一个线程不会被他自己引起问题。当多个线程访问相同的资源问题就会出现。例如相同的内存(变量,数组,或者对象),系统(数据库,web服务)或者文件。

    事实上,如果一个或者多个线程写这些资源的时候问题会出现。让多个线程读取相同的资源是安全的,只要资源不会改变。

    这里有一个例子,如果多个线程同事执行可能会失败:


     public class Counter {
    
         protected long count = 0;
    
         public void add(long value){
             this.count = this.count + value;
         }
      }


    想象下如果线程A和B正在执行相同的Counter类的实例的add方法。这里没有办法知道操作系统什么时间会在线程之间切换。add方法中的代码不会被java虚拟机作为单独的原子指令执行。而是作为一系列的更小的指令集执行,跟这个类似:

    1. 从内存中读取this.count值进入寄存器。

    2. 增加value值到寄存器。

    3. 将寄存器中的值写回内存。

    观察线程A和B混合执行会发生什么:

           this.count = 0;
    
       A:  Reads this.count into a register (0)
       B:  Reads this.count into a register (0)
       B:  Adds value 2 to register
       B:  Writes register value (2) back to memory. this.count now equals 2
       A:  Adds value 3 to register
       A:  Writes register value (3) back to memory. this.count now equals 3


    这两个线程想添加2和3到counter中。因此这两个线程执行完之后的值应该是5。然而,因为这两个线程执行时交叉的,因此结果以不同而结束。

    在上面提到的执行顺序的例子中,两个线程都从内存中读取到0这个值。然后他们添加他们各自的值,2和3到那个值中去,然后把这个结果写回到内存中。代替5,在this.count中留下的值将会是最后的那个线程写给他的那个值。在上面的例子中是线程A,但是他也可能是线程B。

    在临界段中的竞态条件

    在上面的那个例子中的add方法的代码中包含了一个临界段。当多个线程执行这个临界段的时候,竞态条件就会发生了。

    更正式的讲,两个线程的这种情形竞争着相同的资源,资源被访问的顺序是重要的,它被称为竞态条件。一个代码部分导致竞态条件就会被称之为临界段。

    预防竞态条件

    为了防止竞态条件的发生,你必须确保被执行的临界段作为一个原子指令被执行。那就意味着一旦一个单独的线程在执行它,其他的线程就不能执行它直到第一个线程已经离开了这个临界段。

    竞态条件可以通过在临界段中使用线程同步的方式去避免。线程同步可以使用一个Java代码的同步锁去获取。线程同步也可以使用其他的同步概念去获取,像锁或者像java.util.concurrent.atomic.AtomicInteger的原子变量。

    临界段的吞吐量

    对于更小的临界段使得整个临界段的一个同步锁可能会工作。但是,对于更大的临界段,去把它分解成更小的临界段是更有意义的,使得允许多线程去执行每一个更小的临界段。整个就可能降低共享资源的竞争,以及增加整个临界段的吞吐量。

    这里有一个非常简单的Java实例:

    public class TwoSums {
        
        private int sum1 = 0;
        private int sum2 = 0;
        
        public void add(int val1, int val2){
            synchronized(this){
                this.sum1 += val1;   
                this.sum2 += val2;
            }
        }
    }


    注意这个add方法是怎样往这两个sum变量中添加值得。为了预防竞态条件,在内部执行的求和有一个Java同步锁。伴随着这个实现,同时只能有一个线程可以执行这个求和。

    然而,因为这两个sum变量是相互独立的,你可以把他们分离成两个分离的同步锁,像这样:

    public class TwoSums {
        
        private int sum1 = 0;
        private int sum2 = 0;
        
        public void add(int val1, int val2){
            synchronized(this){
                this.sum1 += val1;   
            }
            synchronized(this){
                this.sum2 += val2;
            }
        }
    }


    注意,两个线程可以同时执行这个add方法。一个线程获取到第一个同步锁,另外一个线程获取第二个同步锁。这种方式,线程之间将会等待的更少时间。

    当然,这个例子是非常简单的。在真正的生活中,临界段分离的共享资源可能会更加复杂的,并且需要更多的执行顺序可能性的分析。


    以上就是Java 竞态条件和临界段的内容,更多相关内容请关注PHP中文网(m.sbmmt.com)!

    声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
    上一篇:创建以及启动一个Java线程 下一篇:Java 线程全和共享资源
    Web大前端开发直播班

    相关文章推荐

    • Java实例详解之Lambda表达式• 带你搞懂Java的接口(实例详解)• Java技巧总结之如何看Lambda源码• 实例详解JAVA抽象工厂模式• 一起聊聊Java多线程之线程安全问题

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网