疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

Java并发编程实战


 

 
一、多线程开发所要平衡的几个点
 
看了很多次的目录,外加看了第一部分,发现,要想做好多线程的开发,无非就是平衡好以下的几点
 
安全性
活跃性
无限循环问题
死锁问题
饥饿问题
活锁问题(这个还没具体的了解到)
性能要求
吞吐量的问题
可伸缩性的问题
二、多线程开发所要关注的开发点
 
要想平衡好以上几点,书中循序渐进的将多线程开发最应该修炼的几个点,娓娓道来:
 
原子性
先检查后执行
原子类
加锁机制
可见性
重排
非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);
    }
}
在修改状态值的时候,才进行加锁,平时对状态值的读操作可以不用加锁,当然,最耗时的计算过程,也是要同步的,这种情况下,才会进一步提高性能。
四、可见性下面的对象共享
 
可见性这个话题,在多线程的环境下,是相当棘手的。可能多年之后,你成为万人心中的老鸟,也同样会对这个问题,怅然若失!我自己总结了几点,可见性问题的难处:
 
道理简单,真实场景代码错中复杂,你根本不知道是可见性导致的
小概率事件,往往可能只有百分之一,甚至千分之一的出事概率,容易被我们“得过且过”
容易直接扔到一个synchronized加锁块里面,进行“大刀”式的处理,而忽略了高效性
可见性+原子性的综合考虑
针对这些问题,我们只能先从基本功抓起,然后在日积月累的开发工作中,多多分析程序运行的场景,多多尝试,才能大有裨益。
 
插曲:昨天看了《恋爱回旋》这部日本电影,其中有个场景让我记忆深刻:女主是小时候被魔鬼母亲常年训练的乒乓球少年运动员,后来总总原因,放弃了乒乓球,当起了OL,这一别就是15年。当再次碰到男主的时候,男主向女主发起乒乓球挑战,以为女主是个菜逼,然后赌一些必须要让女主完成的事情。(女主本人也是觉得乒乓球对自己是一种心理的负担,并且放弃这么久了,所以没啥子自信)没想到,女主一拿球拍,在接发球的那一刹那。。。。。大家应该都懂了。我当时就在影院中说出声来:基本功太重要了。
1. 可见性的发生的必要条件
 
可见性,无非就是再多线程环境下,对共享变量的读写导致的。可能一个线程修改了共享变量的值,而另一个线程读取的还是老的值,差不多就是这么大白话的解释了下来。其中发生的必要条件有:
 
多线程环境访问同一个共享变量
服务器模式下启动程序
共享变量并没有做什么处理,代码块也没有同步
当然,要分析为什么会有可见性的问题,要结合JVM虚拟机内存模型分析。以后会在《深入理解Java虚拟机》的学习中,做详细的分析,敬请期待。
 
2. 不多说上代码
 
public class NoVisibility{
    private static boolean ready;
    private static int number;
 
    private static class ReaderThread extends Thread{
        public void run(){
            while(!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }
 
    public static void main(String[] args){
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
思考:上面的打印number的值,有可能会有几种结果呢?什么情况下出现的这些个结果
3. 上诉代码分析
 
number最终打印结果有可能出现42、0,或者根本就不会打印
42:这种情况是运行正确的结果
0:这种情况发生了指令重排(五星级的问题)
不会打印:主线程对ReaderThread线程出现了共享变量不可见
4. “愚钝”的聊聊指令重排
 
之所以说是“愚钝”,原因是重排问题,是一个很底层很考验计算机基础能力的一个问题,小弟不才,当年分析计算机组成原理与指令结构的时候,枯燥极致,都睡过去了。现在回头,才知道其重要性。现阶段,对重排的分析,我只能举例个简单的例子,进行说明,更进一步的分析,同样是要结合JVM的机制(六大Happens-before)来分析,以后再做进一步,详尽的分析。下面就是那个简单的例子:
 
//简单例子
public class OrderConfuse{
    public static void main(String[] args){
        int a = 1;
        int b = 2;
        int c = a+b;
        int d = c+a;
        System.out.println(c);
        System.out.println(d);
    }
}
上面程序是正确的,也能正确输出
对a和b的赋值操作,并非先赋值a再赋值b的
原因是JVM底层会对指令进行优化,保证程序的快速执行,其实就是一种效率优化
变量c会用到a和b变量,所以a和b的操作必须要发生在c之前(happens-before)
有可能b进行了赋值,而a还是初始化的状态,就是值为0
所以结合前面的代码段:
 
public class NoVisibility{
    ......
    public static void main(String[] args){
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
number和ready之后,并没有使用它们的变量了
number和ready会被进行指令重排
结果就是:ready已经赋值变成了true,可是number还是0
这就是为啥会为零的原因所在!
 
5. 针对可见性,还是要上JVM的内存模型进行简单分析
 
每个线程都会有自己的线程虚拟机栈
栈上面存储原始类型和对象类型的引用
每次启动一个线程,都会在对共享数据进行一次复制,复制到每个线程的虚拟机栈中
上面的number是在主线程中,同时在ReaderThread线程的虚拟机栈中有一个副本
各个虚拟机栈最终要进行同步,才能保持一致
所以每次修改一个共享变量(原始类型)其实是在本地线程空间里面修改
number在主线程里面修改了,可是在ReaderThread线程里面并没有修改,因为两个线程访问的空间并不一样,一个线程对另一个线程空间并不可见。
6. volatile关键字横空出世
 
volatile关键字的作用,主要有一下几点:
 
能把对变量的修改,马上同步到主存中
各个线程立马更新自己线程栈中的变量值
防止指令重排
无法保证原子性
对于最底层如何做到这些个点的,具体还可以分析,例如什么内存屏障、状态过期等等,完全可以聊一个专题,今天再次先不聊,同样放到《深入理解JVM虚拟机》的学习中来详尽分析。所以,上面程序可以改成下面这个样子:
 
public class Visibility{
    private static volatile boolean ready;//注意这个类型
    private static volatile int number;//注意这个类型
 
    private static class ReaderThread extends Thread{
        public void run(){
            while(!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }
 
    public static void main(String[] args){
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
7. synchronized关键字同样可以保证可见性
 
public class Visibility{
    private static boolean ready;
    private static int number;
 
    private static Object lock = new Object();
 
    private static class ReaderThread extends Thread{
        public void run(){
            while(!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }
 
    public static void main(String[] args){
        new ReaderThread().start();
        synchronized(lock){//这里进行了加锁
            number = 42;
            ready = true;
        }
    }
}
加锁可以同时保证可见性与原子性
加锁同样可以防止指令重排,内部代码都会照顺序执行
8. volatile不是万能的
 
public class VisibleNotAtomic{
    private static volatile int number = 1;
 
    private static class ReadThread extends Thread{
        public void run(){
            if(number == 2){
                System.out.println("correct!");
            }else{
                System.out.println("error!");
            }
        }
    }
    public static void main(String[] args){
        number++;
    }
}
number是对主线程和ReadThread线程都可见的
可是number++不是原子操作
加加到了一半,主线程挂起,ReadThread线程运行,number的值还是1,输出error
五、第一部分总结
 
我们主要讲了线程的原子性和可见性,结合代码,不知不觉就讲了一堆,而且感觉还可以在讲~~多线程的话题真的是太恐怖了!未来的可预见性的规划如下:
 
对象的安全发布
对象的不变性
对象的合理加锁
生产者消费者模型
构建高效可伸缩的缓存