欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java并发编程实战(chapter_1)原子性

程序员文章站 2022-08-02 15:22:23
这是阅读《Java编发编程实战》这本Java多线程领域的宝典书籍的自我总结与融汇贯通的过程。现在看到了第二部分的第七章,我自己先在我们几个人中,做一个开头,把自己学习到的分享出来。现在只是多线程原子性总结了出来,今天陆续吧可见性和不变性都总结出来,贴上来。这些学习,都算是基础夯实的过程,再多的框架,... ......

混混噩噩看了很多多线程的书籍,一直认为自己还不够资格去阅读这本书。有种要高登大堂的感觉,被各种网络上、朋友、同事一顿外加一顿的宣传与传颂,多多少少再自我内心中产生了一种敬畏感。2月28好开始看了之后,发现,其实完全没这个必要。除了翻译的烂之外(一大段中文下来,有时候你就会骂娘:这tm想说的是个shen me gui),所有的,多线程所必须掌握的知识点,深入点,全部涵盖其中,只能说,一书在手,万线程不愁那种!当然,你必须要全部读懂,并融汇贯通之后,才能有的效果。我推荐,看这本书的中文版本,不要和哪一段的冗长的字句在那过多的纠缠,尽量一段一段的读,然后获取这一段中最重要的那句话,否则你会陷入中文阅读理解的怪圈,而怀疑你的高中语文老师是不是体育老师客串的!!我举个例子:13页第八段,我整段读了三遍硬是没想明白前面那么多的文字,是干什么用的,就是最后一句话才是核心:告诉你,线程安全性,最正规的定义应该是什么!(情允许我,向上交的几个翻译此书的,所谓的“教授”致敬,在你们的引领下,使我们的意志与忍受力更上了一个台阶,人生更加完美!)

一、多线程开发所要平衡的几个点

看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点

  • 安全性
  • 活跃性
    • 无限循环问题
    • 死锁问题
    • 饥饿问题
    • 活锁问题(这个还没具体的了解到)
  • 性能要求
    • 吞吐量的问题
    • 可伸缩性的问题

二、多线程开发所要关注的开发点

要想平很好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:

  • 原子性
    • 先检查后执行
    • 原子类
    • 加锁机制
  • 可见性
    • 重排
    • 非64位写入问题
    • 对象的发布
    • 对象的封闭
  • 不变性

在一本国人自己写的,介绍线程工具api的书中,看到了这么一句话:外练原子,内练可见。感觉这几点如果在多线程中尤为重要。我在有赞,去年还记得上线多门店的那天凌晨,最后项目启动报一个类加载的错误,一堆人过来看问题,基德大神站在攀哥的后面,最后淡淡的说了句:已经很明显是可见性问题了,加上volatile,不行的话,我把代码吃了!!可以见得,多线程这几个点,在“居家旅行”,生活工作中是多么的常见与重要!不出问题不要紧,只要一出,就会是头痛的大问题,因为你根本不好排查根本原因在这。所以我们需要平时就练好功底,尽量避免多线程问题的出现!而不是一味的用框架啊用框架、摞代码啊摞代码!

三、原子性下面的安全问题

1. 下面代码有什么问题呢?

public class UnsafeConuntingFactorizer implements Servlet{
    private long count = 0;
    private long getCount(){
        return count;
    }
    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp,factor);
    }
}

思考:如何让一个普普通通的类变得线程安全呢?一个类什么叫做有状态,而什么又叫做无状态呢?

2. 解答上面代码

  • 一个请求的方法,实例都是一个,所以每次请求都会访问同一个对象
  • 每个请求,使用一个线程,这就是典型的多线程模型
  • count是一个对象状态属性,被多个线程共享
  • ++count并非一次原子操作(分成:复制count->对复制体修改->使用复制体回写count,三个步奏)
  • 多个线程有可能多次修改count值,而结果却相同

3. 使用原子类解决上面代码问题

public class UnsafeConuntingFactorizer implements Servlet{
    private final AtomicLong count = new AtomicLong(0);
    private long getCount(){
        return count.get();
    }
    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();//使用了新的原子类的原子方法
        encodeIntoResponse(resp,factor);
    }
}

4. 原子类也不是万能的

//在复杂的场景下,使用多个原子类的对象
public class UnsafeConuntingFactorizer implements Servlet{
    private final AtomicReference<BigInteger> lastNumber 
        = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors 
        = new AtomicReference<BigInteger[]>();


    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        if(i.equals(lastNumber.get())){//先判断再处理,并没有进行同步,not safe!
            encodeIntoResponse(resp,lastFactors.get());
        }else{
            BigInteger[] factors = factor(i);
            lastNumer.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

思考:什么叫做复合型操作?

5. 先列举一个我们常见的复合型操作

public class LazyInitRace {
    private ExpensiveObject instace = null;
    public ExpensiveObject getInstace(){
        if(instace == null){
            instace = new ExpensiveObject();
        }
        return instace;
    }
}

看好了,这就是我们深恶痛绝的一段代码!如果这段代码还分析不了的,对不起,出门左转~

6. 提高“先判断再处理”的警觉性

  • 如果没有同步措施,直接对一个状态进行判断,然后设值的,都是不安全的
  • if操作和下面代码快中的代码,远远不是原子的
  • 如果if判断完之后,接下来线程挂起,其他线程进入判断流程,又是同样的状态,同样进入if语句块
  • 当然,只有一个线程执行的程序,请忽略(那还叫能用的程序吗?)

7. 性能的问题来了

//在复杂的场景下,使用多个原子类的对象
public class UnsafeConuntingFactorizer implements Servlet{
    private final AtomicReference<BigInteger> lastNumber 
        = new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors 
        = new AtomicReference<BigInteger[]>();

    //这下子总算同步了!
    public synchronized void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        if(i.equals(lastNumber.get())){//先判断再处理,并没有进行同步,not safe!
            encodeIntoResponse(resp,lastFactors.get());
        }else{
            BigInteger[] factors = factor(i);
            lastNumer.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

思考:有没有种“关公挥大刀,一砍一大片”的感觉?

8. 上诉代码解析

  • 加上了synchronized关键字的确解决了多线程访问,类安全性问题
  • 可是每次都是一个线程进行计算,所有请求变成了串行
  • 请求量低于100/s其实都还能接受,可是再高的话,这就完全有问题的代码了
  • 性能问题,再网络里面,是永痕的心病~

9. 一段针对原子性、性能问题的解决方案

//在复杂的场景下,使用多个原子类的对象
public class CacheFactorizer implements Servlet{
    private BigInteger lastNumber;
    private BigInteger[] lastFactors ;
    private long hits;
    private long cacheHits;

    public synchronized long getHits(){
        return hits;
    }
    public synchronized double getCacheHitRadio(){
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this){
            ++hits;
            if(i.equals(lastNumber)){
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null){
            factors = factor(i);
            synchronized (this){
                lastNumer = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。