• 并发-竞态条件


    本文所整理的知识点源自于 (Java 并发编程实战) 一书,有兴趣的童鞋可以去网上下载或者在评论区里向博主讨要。

    在并发编程中,由于不恰当的执行程序而出现不正确的结果是一种非常重要的情况,它的名字叫:竞态条件(RaceCondition).
    在UnsafeCountingFactorizer中存在多个竞态条件,从而使结果变得不可靠。当某个正确性取决于多个线程的交替执行时序时,
    就会发生竞态条件。相当于正确的结果要取决于运气。最常见的竞态条件类型是:先检查后执行(Check-Then-Act)操作,即通过
    一个可能失效的结果来决定下一步的动作。
    比如说:A和B约定在商城门口集合,A到了之后发现现场有两家商城,而他们并没有约定是哪一家,在1号商城门口没有看见B
    于是A走向2号商城,但是2号商城门口也没有。这时有两种结果:1、B迟到了,还没有到任何一家商城;2,B先到了2号商城,
    没看到A,于是走向1号商城,但此时A已经离开1号商城走向2号商城。这样下来,除非A和B还有别的见面方法,否则会一直
    互相寻找。
    在“去另一家商城寻找中”,问题有:A走向2号商城时,B离开了2号商城;同样,B从2号商城走向1号商城,但这可能不是
    同时发生。商城之间的路程需要几分钟,在这几分钟里,程序的状态可能会发生变化。
    这个例子就是一种竞态条件,想要获得正确的结果(A和B见面),必须取决于事情发生的时序(其中一个到达商城,在离开去另
    一家商城之前等待多长时间.....)。当A离开1号商城门口时,它在1号商城门口的观察结果将会无效,因为B可能从另一条路走来
    ,A不知道。这种观察结果的失效就是大多数竞态条件的本质-基于可能失效的结果来做出判断或某个计算。这类的竞态条件
    称为“先检查后执行”:首先观察某个条件为真(主线程判断文件A),A不存在,主线程创建A,但这之中,观察结果可能会无效
    另一个执行创建A文件的线程启动了,创建了文件A,主线程先前的判断结果失效,但是依然又创建了文件A,这就导致了可能
    出现的一系列问题(数据破坏,文件丢失,未知异常....)。

    使用“先检查后执行”的一种常见情况就是延迟初始化。延迟初始化的目的是将对象的初始化操作推迟到实际使用时再进行,同时
    要确保只被初始化一次:
    @NotThreadSafe
    public cass ObjFactory {
    private Obj instance;

    public Obj getInstance() {
    if(instance == null) {
    instance = new Obj();
    }
    return instance;
    }
    }
    在ObjFactory中有一个竞态条件,它可能会破坏这个类的正确性。如:线程A和线程B同时执行getInstance。A看到instance为空,
    创建一个Obj实例。B也需要判断instance是否为空。但此时instance是否为空取决于不可预测的时序,线程的调度,A创建Obj需要
    的时间。如果B检查时,instance为空,那么两次调用getInstance会得到不同的结果。
    在UnsafeCountingFactorizer的统计命中计数操作中还有一种竞态条件-“读取-修改-写入”(如:sun++),基于对象之前的状态来
    定义对象状态的转换。要正确递增数值,必须知道它之前的值,并保证在递增的同时没有其他线程来修改或使用这个值。
    与大多数并发错误一样,竞态条件不是总是产生错误,还需要某种不恰当的执行时序。但是如果在一些重要的操作中产生了竞态条件
    比如,初始注册表,那么多个行为就会返回不一样的视图。

    解决方法:
    1、同步锁:synchronized (也称:内置锁)
    在方法名前加上 synchronized 关键字,可以保证在同一时间内只能有一个线程调用本方法。当A执行 getInstance 判断完成到实例化 Obj期间,线程B或C、D等
    会按照各自的优先级在锁处等待,直到A完成方法,释放锁,getInstance 才可以被其他线程调用。

    弊端:
    Java的内置锁相当于一种互斥体,每次只能有一个线程能够持有这种锁,假设A先掌握锁,但由于别的原因一直没有释放锁,那么所有
    需要执行这个方法的线程B、C、D就要永远的等下去。
    这种同步机制使得确保Service方法变得很安全,但是,这种方法却过于极端了,因为每次只能有一个用户来进行访问,服务器响应
    会很慢。这时,我们就需要使用”重入“来解决这个弊端。

    重入
    当某个线程请求一个由其他线程持有的锁时,发出的请求会阻塞。但是,内置锁是可重入的,所以,如果某个线程视图获得一个已经
    由它自己持有的锁,请求就会成功。“重入”意味着获取锁的粒度是”线程“而不是”调用“。重入一种实现方法是:为每个所关联一个获取
    计数值和一个所有者线程。当计数值为0时,这个锁被认为没有被任何线程所执有。当一个线程请求一个未被执有的锁时,JVM会记下
    锁的执有者,计数值+1,如果同一个线程再次请求这个锁,计数值会++,当线程释放锁时,计数值会相应的--,当计数值为0是,锁
    将被释放。

    代码:

    //多个线程调用
    public class Service {
    public synchronized void service1() {
    System.out.println("1号启动");
    service2();
    }

    public synchronized void service2() {
    System.out.println("2号启动");
    service3();
    }

    public synchronized void service3() {
    System.out.println("3号启动");
    }
    }
    //主对象
    public class MyThread extends Thread {
    public void run() {
    Service service = new Service();
    service.service1();
    }
    }
    //测试
    class Test {
    public static void main(String[] args) throws InterruptedException {
    MyThread myThread = new MyThread();
    myThread.start();
    }
    }

    //结果
    1号启动
    2号启动
    3号启动

  • 相关阅读:
    hive.exec.parallel参数
    MySQL FEDERATED 提示
    mapreduce作业单元测试
    linux 更改mysql的数据库目录
    SQL Server 2008数据库邮件配置及应用
    mysql主键大小写不敏感的解决办法
    java遍历hashMap、hashSet、Hashtable
    Linux下命令行显示当前全路径方法
    通过SQL Server操作MySQL的步骤和方法
    Linux shell获取时间和时间间隔(ms级别)
  • 原文地址:https://www.cnblogs.com/zhuangfei/p/7209794.html
Copyright © 2020-2023  润新知