线程安全性

作者:xcbeyond
疯狂源自梦想,技术成就辉煌!微信公众号:《程序猿技术大咖》号主,专注后端开发多年,拥有丰富的研发经验,乐于技术输出、分享,现阶段从事微服务架构项目的研发工作,涉及架构设计、技术选型、业务研发等工作。对于Java、微服务、数据库、Docker有深入了解,并有大量的调优经验。

线程的使用一直是难以把控掌握的。如果使用得当,线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。在GUI应用程序中,提高用户界面的响应灵敏度,在服务器应用程序中,提升资源利用率以及系统吞吐量。

     然而,如果使用不当,线程将会带来一系列不可预估的风险。Java对线程的支持其实就是一把双刃剑。虽然Java明确是一种跨平台(编写异常,随处运行)的语言,JDK并提供了相应的类库,简化了程序的开发,但更多的在处理一些复杂程序时,就需要使用线程,随之引入的“并发性”问题(线程安全性),就成为了开发人员实现考虑的难点。

    在多线程中的各个操作的顺序都是不可预测的,有时其执行结果简直出乎意料,令人惊讶。例如,通过下面代码片段的数值序列生成器,用来生成一个递增序列。

    package com.xcbeyond.thread;
     
    import net.jcip.annotations.NotThreadSafe;
     
    /**
     * 非线程安全的数值序列生成器
     * @author xcbeyond
     * 2018-5-6下午03:17:33
     */
    public class UnSafeThreadSequence {
        private int value;
        
        /**
         * 返回一个唯一的值
         * @return
         */
        public int getValue() {
            return value++;
        }
    }

       

     如果在单线程中执行,没有任何问题,结果也正如我们预期的一样。而在多线程并发的场景下操作,将会出现不同线程之间交替操作,不同线程很可能同时读取同一个值,得到相同值,导致不同线程返回了相同的序列值,这恰恰与我们的预期截然相反。

     这充分说明了一种常见的线程并发危险:竞争条件。因为线程共享了相同的内存空间地址,且并发的执行,它们可能访问或修改其他线程正在使用的变量,将会出现变量竞争情况。

       线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。


     如何编写线程安全的代码呢?

    编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。一个对象是否需要时线程安全的,取决于该对象是否被多线程访问。这指的是程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,要采用同步机制来协同对对象可变状态的访问。Java常用的同步机制是Synchronized,还包括 volatile类型的变量,显示锁以及原子变量。

原子性:

      假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

      竞态条件(Race Condition):某个计算/程序的正确性取决于多个线程的交替执行的时序。(线程的时序不同,产生的结果可能会不同)
  “先检查后执行”,即通过一个潜在的过渡值来决定下一步的操作。

     首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。

     “读取-修改-写入”,基于对象之前的状态来定义对象状态的转换。即使是volatile修饰的变量,在多线程的环境里面进行自增操作,同样会发生竞态条件,所以volatile不能保证绝对的线程安全。
加锁机制:

  在线程安全的定义中,多个线程间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。当不变性条件中涉及多个变量时,各个变量之间并不是互相独立的,一个变量发生变化会对其他变量的值产生约束。因此,一个变量发生改变,在同一个原子操作里面,其他相关变量也要更新。
   内置锁:同步代码块(Synchronized Block)包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。关键字Synchronized修饰方法就是一种同步代码块,锁就是方法调用所在的对象,静态的Synchronized方法以Class对象作为锁。内置锁或监视锁就是以对象作为实现同步的锁。

  内置锁在Java中扮演了互斥锁的角色,意味着最多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占用的锁时,线程A必须等待或者处于阻塞状态,知道B释放为止。如果B不释放锁,A将永远等待下去。

    虽然同步代码块解决了线程安全问题,但线程可能出现等待或者阻塞情况,导致响应性能非常低下。(解决了线程安全问题,但可能会出现性能问题)可以尽可能的将每个同步代码块的大小进行调整,即:同步代码块“足够小”,在一定程度上解决了线程安全问题,也解决了性能低下的问题。要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性( 必须得到满足)、简单性和性能。