最近偶然间看见一道名为史上最难的java面试题,这个题让了我对线程安全的有了一些新的思考,给你们分享一下这个题吧:java
public class TestSync2 implements Runnable {
int b = 100;
synchronized void m1() throws InterruptedException {
b = 1000;
Thread.sleep(500); //6
System.out.println("b=" + b);
}
synchronized void m2() throws InterruptedException {
Thread.sleep(250); //5
b = 2000;
}
public static void main(String[] args) throws InterruptedException {
TestSync2 tt = new TestSync2();
Thread t = new Thread(tt); //1
t.start(); //2
tt.m2(); //3
System.out.println("main thread b=" + tt.b); //4
}
@Override
public void run() {
try {
m1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
推荐你们先别急着看下面的答案,试着看看这个题的答案是什么?刚开始看这个题的时候,第一反应我擦嘞,这个是哪一个老铁想出的题,如此混乱的代码调用,真是惊为天人。固然这是一道有关于多线程的题,最低级的错误,就是一些人对于.start()和.run不熟悉,直接会认为.start()以后run会占用主线程,因此得出答案等于:面试
main thread b=2000
b=2000
复制代码
比较高级的错误:了解start(),可是忽略了或者不知道synchronized,在那里瞎在想sleep()有什么用,有可能得出下面答案:sql
main thread b=1000
b=2000
复制代码
总而言之问了不少人,大部分第一时间都不能得出正确答案,其实正确答案以下:数据库
main thread b=2000
b=1000
or
main thread b=1000
b=1000
or
b=1000
main thread b=2000
复制代码
有人没测出来b=2000这里给你们看看:编程
解释这个答案以前,这种题其实在面试的时候遇到不少,依稀记得再学C++的时候,考地址,指针,学java的时候又在考i++,++i,"a" == b等于True? 这种题家常便饭,想必你们作这种题都知道靠死记硬背是解决不来的,由于这种变化实在太多了,因此要作这种比较模棱两可的题目,必需要会其意,方得齐解。尤为是多线程,若是你不知道其原理,不只仅在面试中过不了,就算侥幸过了,在工做中如何不能很好的处理线程安全的问题,只能致使你的公司出现损失。安全
这个题涉及了两个点:bash
若是对这几个不熟悉的同窗不要着急下面我都会讲,下面我解释一下整个流程:网络
状况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的地方了。
整个流程以下面所示:
对于上面的题的代码,虽然在咱们实际场景中很难出现,但保不齐有哪位同事写出了相似的,到时候有可能排坑的仍是你本身,因此针对此想聊聊一些线程安全的事。
咱们用《java concurrency in practice》中的一句话来表述:当多个线程访问一个对象时,若是不用考虑这些线程在运行时环境下的调度和交替执行,也不须要进行额外的同步,或者在调用方进行任何其它的协调操做,调用这个对象的行为均可以得到正确的结果,那这个对象就是线程安全的。
从上咱们能够得知:
咱们能够按照java共享对象的安全性,将线程安全分为五个等级:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立:
在java中Immutable(不可变)对象必定是线程安全的,这是由于线程的调度和交替执行不会对对象形成任何改变。一样不可变的还有自定义常量,final及常池中的对象一样都是不可变的。
在java中通常枚举类,String都是常见的不可变类型,一样的枚举类用来实现单例模式是天生自带的线程安全,在String对象中你不管调用replace(),subString()都没法修改他原来的值
咱们来看看Brian Goetz的《Java并发编程实战》对其的定义:当多个线程访问某个类时,无论运行时环境采用何种调度方式或者这些线程将如何交替进行,而且在主调代码中不须要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。
周志明在<<深刻理解java虚拟机>>中讲到,Brian Goetz的绝对线程安全类定义是很是严格的,要实现一个绝对线程安全的类一般须要付出很大的、甚至有时候是不切实际的代价。同时他也列举了Vector的例子,虽然Vectorget和remove都是synchronized修饰的,但仍是展示了Vector其实不是绝对线程安全。简单介绍下这个例子:
public Object getLast(Vector list) {
return list.get(list.size() - 1);
}
public void deleteLast(Vector list) {
list.remove(list.size() - 1);
}
复制代码
若是咱们使用多个线程执行上面的代码,虽然remove和get是同步保证的,可是会出现这个问题有可能已经remove掉了最后一个元素,可是list.size()这个时候已经获取了,其实get的时候就会抛出异常,由于那个元素已经remove。
周志明认为这个定义能够适当弱化,把“调用这个对象的行为”限定为“对对象单独的操做”,这样一来就能够获得相对线程安全的定义。其须要保证对这个对象单独的操做是线程安全的,咱们在调用的时候不须要作额外的操做,可是对于一些特定的顺序连续调用,须要额外的同步手段。咱们能够将上面的Vector的调用修改成:
public synchronized Object getLast(Vector list) {
return list.get(list.size() - 1);
}
public synchronized void deleteLast(Vector list) {
list.remove(list.size() - 1);
}
复制代码
这样咱们做为调用方额外加了同步手段,其Vector就符合咱们的相对安全。
线程兼容是指其对象并非线程安全,可是能够经过调用端正确地使用同步手段,好比咱们能够对ArrayList进行加锁,同样能够达到Vector的效果。
线程对立是指不管调用端是否采起了同步措施,都没法在多线程环境中并发使用的代码。因为Java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是不多出现的,并且一般都是有害的,应当尽可能避免。
对于解决线程安全通常来讲有几个办法:互斥阻塞(悲观,加锁),非阻塞同步(相似乐观锁,CAS),不须要同步(代码写得好,彻底不须要考虑同步)
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程(或是一些,使用信号量的时候)线程使用。
互斥是一种悲观的手段,由于他担忧他访问的时候时刻有人会破坏他的数据,因此他须要经过某种手段进行将这个数据在这个时间段给占为独有,不能让其余人有接触的机会。临界区(CriticalSection)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。在Java中通常用ReentrantLock和synchronized 实现同步。 而实际业务当中,推荐使用synchronized,在第一节的代码其实也是使用的synchronized ,为何推荐使用synchronized 的呢?
若是你在业务中须要等待可中断,等待超时,公平锁等功能的话,那你能够选择这个ReentrantLock。
固然在咱们的Mysql数据库中排他锁其实也是互斥同步的实现,当加上排他锁,其余事务都不能进行访问其数据。
非阻塞同步是一种乐观的手段,在乐观的手段中他会先去尝试操做,若是没有人在竞争,就成功,不然就进行补偿(通常就是死循环重试或者循环屡次以后跳出),在互斥同步最重要的问题就是进行线程阻塞和唤醒所带来的性能问题,而乐观同步策略解决了这一问题。
可是上面就有个问题操做和检测是否有人竞争这两个操做必定得保证原子性,这就须要咱们硬件设备的支持,例如咱们java中的cas操做其实就是操做的硬件底层的指令。
在JDK1.5以后,Java程序中才可使用CAS操做,该操做由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法作了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS之类,没有方法调用的过程,或者能够认为是无条件内联进去了
要保证线程安全,并不必定就要进行同步,二者没有因果关系。同步只是保障共享数据争用时的正确性手段,若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性,所以会有一些代码天生就是现场安全的。 通常分为两类:
public int sum(){
return 1+2;
}
复制代码
例如这种代码就是可重入代码,可是在咱们本身的代码中其实出现得不多
上面写得都比较官方,下面说说从一些真实的经验中总结出来的:
本文从最开始的一道号称史上最难的面试题,引入了咱们工做中最为重要之一的线程安全。但愿你们后续能够好好的阅读周志明的《深刻理解jvm虚拟机》的第13章线程安全和锁优化,相信读完以后必定会有一个新的提高。因为做者本人水平有限,若是有什么错误,还请指正。
最后打个广告,若是你以为这篇文章对你有文章,能够关注个人技术公众号,最近做者收集了不少最新的学习资料视频以及面试资料,关注以后便可领取。
若是你们以为这篇文章对你有帮助,或者你有什么疑问想提供1v1免费vip服务,均可以关注个人公众号,关注便可免费领取海量最新java学习资料视频,以及最新面试资料,你的关注和转发是对我最大的支持,O(∩_∩)O: