zoukankan      html  css  js  c++  java
  • 线程安全思考

    一道号称“史上最难”的java面试题引发的
     
     
    1.史上最难的题
     
    最近偶然间看见一道名为史上最难的java面试题,这个题让了我对线程安全的有了一些新的思考,给大家分享一下这个题吧:
     
     
    1. public class TestSync2 implements Runnable {
     
    1.    int b = 100;        
     
    1.    synchronized void m1() throws InterruptedException {
     
    1.        b = 1000;
     
    1.        Thread.sleep(500); //6
     
    1.        System.out.println("b=" + b);
     
    1.    }
     
    1.    synchronized void m2() throws InterruptedException {
     
    1.        Thread.sleep(250); //5
     
    1.        b = 2000;
     
    1.    }
     
    1.    public static void main(String[] args) throws InterruptedException {
     
    1.        TestSync2 tt = new TestSync2();
     
    1.        Thread t = new Thread(tt);  //1
     
    1.        t.start(); //2
     
    1.        tt.m2(); //3
     
    1.        System.out.println("main thread b=" + tt.b); //4
     
    1.    }
     
    1.    @Override
     
    1.    public void run() {
     
    1.        try {
     
    1.            m1();
     
    1.        } catch (InterruptedException e) {
     
    1.            e.printStackTrace();
     
    1.        }
     
    1.    }
     
    1. }
     
     
    推荐大家先别急着看下面的答案,试着看看这个题的答案是什么?刚开始看这个题的时候,第一反应我擦嘞,这个是哪个老铁想出的题,如此混乱的代码调用,真是惊为天人。当然这是一道有关于多线程的题,最低级的错误,就是一些人对于.start()和.run不熟悉,直接会认为.start()之后run会占用主线程,所以得出答案等于:
     
     
    1. main thread b=2000
     
    1. b=2000
     
     
    比较高级的错误:了解start(),但是忽略了或者不知道synchronized,在那里瞎在想sleep()有什么用,有可能得出下面答案:
     
     
    1. main thread b=1000
     
    1. b=2000
     
     
    总而言之问了很多人,大部分第一时间都不能得出正确答案,其实正确答案如下:
     
     
    1. main thread b=2000
     
    1. b=1000
     
    1. or
     
    1. main thread b=1000
     
    1. b=1000
     
     
    解释这个答案之前,这种题其实在面试的时候遇到很多,依稀记得再学C++的时候,考地址,指针,学java的时候又在考i++,++i,"a" == b等于True? 这种题屡见不鲜,想必大家做这种题都知道靠死记硬背是解决不来的,因为这种变化实在太多了,所以要做这种比较模棱两可的题目,必须要会其意,方得齐解。尤其是多线程,如果你不知道其原理,不仅仅在面试中过不了,就算侥幸过了,在工作中如何不能很好的处理线程安全的问题,只能导致你的公司出现损失。
     
    这个题涉及了两个点:
    • synchronized
    • 线程的几个状态:new,runnable(thread.start()),running,blocking(Thread.Sleep())
    如果对这几个不熟悉的同学不要着急下面我都会讲,下面我解释一下整个流程:
    1. 新建一个线程t, 此时线程t为new状态。
    2. 调用t.start(),将线程至于runnable状态。
    3. 这里有个争议点到点是t线程先执行还是tt.m2先执行呢,我们知道此时线程t还是runnable状态,此时还没有被cpu调度,但是我们的tt.m2()是我们本地的方法代码,此时一定是tt.m2()先执行。
    4. 执行tt.m2()进入synchronized同步代码块,开始执行代码,这里的sleep()没啥用就是混淆大家视野的,此时b=2000。
    5. 在执行tt.m2()的时候。有两个情况:
    情况A:有可能t线程已经在执行了,但是由于m2先进入了同步代码块,这个时候t进入阻塞状态,然后主线程也将会执行输出,这个时候又有一个争议到底是谁先执行?是t先执行还是主线程,这里有小伙伴就会把第3点拿出来说,肯定是先输出啊,t线程不是阻塞的吗,调度到CPU肯定来不及啊?很多人忽略了一点,synchronized其实是在1.6之后做了很多优化的,其中就有一个自旋锁,就能保证不需要让出CPU,有可能刚好这部分时间和主线程输出重合,并且在他之前就有可能发生,b先等于1000,这个时候主线程输出其实就会有两种情况。2000 或者 1000。
     
    情况B:有可能t还没执行,tt.m2()一执行完,他刚好就执行,这个时候还是有两种情况。b=2000或者1000
     
    6.在t线程中不论哪种情况,最后肯定会输出1000,因为此时没有修改1000的地方了。
     
    整个流程如下面所示:
    2.线程安全
     
    对于上面的题的代码,虽然在我们实际场景中很难出现,但保不齐有哪位同事写出了类似的,到时候有可能排坑的还是你自己,所以针对此想聊聊一些线程安全的事。
     
    2.1何为线程安全
     
    我们用《java concurrency in practice》中的一句话来表述:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
     
    从上我们可以得知:
    1. 在什么样的环境:多个线程的环境下。
    2. 在什么样的操作:多个线程调度和交替执行。
    3. 发生什么样的情况: 可以获得正确结果。
    4. 谁 : 线程安全是用来描述对象是否是线程安全。
    2.2线程安全性
     
    我们可以按照java共享对象的安全性,将线程安全分为五个等级:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立:
     
    2.2.1不可变
     
    在java中Immutable(不可变)对象一定是线程安全的,这是因为线程的调度和交替执行不会对对象造成任何改变。同样不可变的还有自定义常量,final及常池中的对象同样都是不可变的。
     
    在java中一般枚举类,String都是常见的不可变类型,同样的枚举类用来实现单例模式是天生自带的线程安全,在String对象中你无论调用replace(),subString()都无法修改他原来的值
     
    2.2.2绝对线程安全
     
    我们来看看Brian Goetz的《Java并发编程实战》对其的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。
     
    周志明在<<深入理解java虚拟机>>中讲到,Brian Goetz的绝对线程安全类定义是非常严格的,要实现一个绝对线程安全的类通常需要付出很大的、甚至有时候是不切实际的代价。同时他也列举了Vector的例子,虽然Vectorget和remove都是synchronized修饰的,但还是展现了Vector其实不是绝对线程安全。简单介绍下这个例子:
     
     
    1. public  Object getLast(Vector list) {
     
    1.    return list.get(list.size() - 1);
     
    1. }
     
    1. public  void deleteLast(Vector list) {
     
    1.    list.remove(list.size() - 1);
     
    1. }
     
     
    如果我们使用多个线程执行上面的代码,虽然remove和get是同步保证的,但是会出现这个问题有可能已经remove掉了最后一个元素,但是list.size()这个时候已经获取了,其实get的时候就会抛出异常,因为那个元素已经remove。
     
    2.2.3相对安全
     
    周志明认为这个定义可以适当弱化,把“调用这个对象的行为”限定为“对对象单独的操作”,这样一来就可以得到相对线程安全的定义。其需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的操作,但是对于一些特定的顺序连续调用,需要额外的同步手段。我们可以将上面的Vector的调用修改为:
     
     
    1. public synchronized Object getLast(Vector list) {
     
    1.    return list.get(list.size() - 1);
     
    1. }
     
    1. public synchronized void deleteLast(Vector list) {
     
    1.    list.remove(list.size() - 1);
     
    1. }
     
     
    这样我们作为调用方额外加了同步手段,其Vector就符合我们的相对安全。
     

  • 相关阅读:
    设计模式——创建型设计模式总结(简单工厂、普通工厂、抽象工厂、建造者、原型和单例)
    网易游戏2011招聘笔试题+答案解析
    华为2011上机笔试题2+参考程序
    趋势科技2011校招笔试题+答案解析
    腾讯2012实习生笔试题2+答案解析
    浙商银行2011笔试题+答案解析
    Linux学习笔记(三)——权限管理
    PowerShell在多个文件中检索关键字
    Linux学习笔记(一)——入门
    PowerShell函数调用问题
  • 原文地址:https://www.cnblogs.com/williamjie/p/9440807.html
Copyright © 2011-2022 走看看