Java语言线程安全性相关的概念讲解,包括原子性,竞态条件,复合操作,内置锁等
小标 2018-12-04 来源 : 阅读 921 评论 0

摘要:本文主要向大家介绍了Java语言线程安全性相关的概念讲解,包括原子性,竞态条件,复合操作,内置锁等,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了Java语言线程安全性相关的概念讲解,包括原子性,竞态条件,复合操作,内置锁等,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。


本文介绍线程安全性相关的概念,包括原子性,竞态条件,复合操作,内置锁等,通过这些术语的介绍逐步铺开线程安全的相关知识,了解在哪些情况下应当用内置锁,哪些情况下用线程安全类就足够了。同时,说明应过多的同步会引起程序的活跃度和性能问题。


对于要编写线程安全的代码,核心在于对状态的访问操作进行管理,特别是对共享的和可变的状态的访问。


共享,表示可以由多个线程同时访问;可变,表示变量的值在其生命周期内可以发生变化。


Java 中的主要同步机制是关键字 synchronized,它提供了一种独占的加锁方式。当然,同步还包括 volatile 类型的变量,显示锁以及原子变量。


什么是线程安全性


在线程安全性的定义中,最核心的概念就是正确性。当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的。无状态对象一定是线程安全的。比如下面例子,不包含任何域,也不包含任何对其他类中域的引用:


@TreadSafe

public class StatelessFactorizer implements Servlet{

    public void service(ServletRequest req, ServletResponse resp){

        BigInteger i = extractFromRequest(req);

        BigInteger [] factors = factor(i);

        encodeIntoresponse(resp, factors)

    }

}

   


原子性


众所周知,原子是构成物质的基本单位,所以原子代表的意思是[不可分]。在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制。


竞态条件


当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。常见的竞态条件类型就是[先检查后执行]操作,即通过检查一个可能失效的结果来决定下一步的动作。比如下面例子,延迟初始化中的竞态条件:


@NotThreadSafe

public class LazyInitRace{

    private ExpensiveObject instance = null;

 

    public ExpensiveObject getInstance(){

        if(instance == null)

            instance = new ExpensiveObject()

        return instance;

    }

}

   


假如线程 A 和 B,同时执行 getInstance() ,并都判断 instance 为空,那么就会创建出两个不同的对象,不是我们预期想要的结果,可能会产生严重的问题。当然,这种需要某种不恰当的执行时序才会产生。


复合操作


对于 value++ 这种递增复合操作,即[读取-修改-写入],必须以原子方式执行,才能确保线程安全性。如何确保是原子操作呢?一种方式是加锁,另一种是采用一个线程安全类来实现。先来看线程安全类的方式:


@ThreadSafe

public class SafeSequene{

    private final AtomicLong value = new AtomicLong(0);

    //返回一个唯一的数值

    public long getNext(){

        return value.incrementAndGet();

    }

}

   


在 java.util.concurrent.atomic 包中,包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用 AtmoicLong 来代替 long 类型的计数器,能够确保所有对计数器状态的访问都是原子的。因此也是线程安全的。


在实际情况中,应尽可能地使用现有的线程安全对象来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。


加锁机制


假设我们创建一个类,包含两个变量并且让其自增,目的是让两个变量的值一直相等,并通过 isEqual() 来检验。如下所示,我们理想结果是每次调用 isEqual() 都返回 true。然而,我们能够保证返回的就是 true 吗?


@NotThreadSafe

public class CompareValue{

    private final AtomicLong value1 = new AtomicLong(0);

    private final AtomicLong value2 = new AtomicLong(0);

    //返回连个数值是否相等

    public boolean isEqual(){

        return value1.incrementAndGet() == value2.incrementAndGet();

    }

}

   


实际上,这种方式并不能保证 isEqual 一定返回 true,即使是采用了线程安全类 AtmoicLong。原因是这里涉及多个变量,各个变量是彼此独立的,我们只能保证变量各自的自增操作是原子的;不能保证两个变量同时以原子的方式一起自增。线程 A 在获取 value1 后,可能线程 B 又同时修改了 value2。导致 A 获取到的是 B 修改后的值。


所以,要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。


内置锁


Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包含两部分:一个作为锁的对象引用;一个作为有这个锁保护的代码块。如:


synchronized (lock) {

    //访问或修改有锁保护的共享状态

}

   


每个 Java 对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视锁(Monitor Lock)。线程进入同步代码块自动获得锁,退出同步代码块自动释放锁。


Java 的内置锁是一种互斥锁,这表示最多只有一个线程能持有这种锁。当线程 A 尝试获取线程 B 持有的锁时,线程 A 必须等待或者阻塞,知道 B 释放了这个锁。


这种同步机制,使得上节提到的例子,CompareValue 变得安全,能确保两个变量永远相等。线程 A 调用 isEqual() 时,线程 B 必须等待知道线程 A 调用完成释放锁。


@NotThreadSafe

public class CompareValue{

    private final AtomicLong value1 = new AtomicLong(0);

    private final AtomicLong value2 = new AtomicLong(0);

    //返回一个唯一的数值

    public synchronized boolean isEqual(){

        return value1.incrementAndGet() == value2.incrementAndGet();

    }

}

   


重入


内置锁是可重入的,当某个线程试图获取一个已经有它持有的锁时,他是可以成功获取的。换句话说,重入意味着获取锁的操作粒度是[线程],而不是[调用]。


重入进一步提升了加锁行为的封装性,简化了面向对象并发代码的开发。举个例子,子类改写了父类的 synchronized 方法,然后调用父类的方法,此时如果不支持可重入锁,那么这段代码将产生死锁!!


//如果内置锁不是可重入的,那么这段代码将发生死锁

public class Base {

    public synchronized void doSomething() {

        ....

    }

}

 

public class Child extends Base{

    public synchronized void doSomething() {//重写

        super.doSomething();//直接调用父类方法

    }

}

   


用锁来保护状态


由于锁能使其保护的代码路径以串行的形式来访问,因此可以通过所来构造一些协议以实现对共享状态的独占访问。对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。在这种情况下,我们称状态变量是由这个锁来保护的。并且,每个共享和可变的状态变量应该只由一个锁来保护。


一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。许多线程安全类都使用这种模式,比如 Vector 和其他同步容器类。


另外,如果将每个方法都声明为 sychronized,也不足以保证在 Vector 上执行的符合操作是原子的!!


if (!vector.contains(element))

    vector.add(element);

   


如上所示,尽管 contains 和 add 两个方法都是原子的,但是尝试[缺少-加入]操作仍然存在竞争条件。所以,把多个操作组成一个复合操作时,仍然需要额外的锁来保证线程安全性。


活跃度与性能


如上所说,如果对于所有方法,我们都声明为 sychronized,那么每次只能有一个线程去执行它,而其他所有线程都必须等待。这会导致活跃度和性能问题。


解决的办法可以是,通过缩小 synchronized 块的范围,来提升并发性。同时,也要防止将一个原子操作分配到多个 synchronized 块中。


决定 sychronized 块的大小需要权衡各种设计要求,包括安全性、简单性、性能。通常简单性和性能之间是互相牵制的,实现一个同步策略时,不要过早的为了性能而牺牲简单性。


当使用锁时,你应该清楚块中的代码的功能,以及他的执行是否会耗时。如果长时间的占有锁,就会引起活跃度和性能风险的问题。有些耗时的操作,比如网络或控制台I/O,这些难以快速完成的任务,这期间不要占有锁。


          

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 1 不喜欢 | 0
看完这篇文章有何感觉?已经有1人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程