作者博客
http://www.jianshu.com/p/0d9b66827271
公众号
CoorChice
文章目录
前言
在日常开发中,线程常常被用作为提升程序效率的重要手段。在CoorChice的这篇文章中,CoorChice介绍了线程的基本运作。【你知道Thread线程是如何运作的吗?】
本篇,CoorChice将从多线程的角度来进一步介绍线程的相关知识。首先,我们需要了解一些基本知识。
想了解更多和Java内存相关的知识,可以看看CoorChice的这几篇文章:
【Android内存基础——Java内存管理机制http://www.jianshu.com/p/54241ca3da5c】;
【Android内存基础——内存抖动:http://www.jianshu.com/p/69e6f894c698】;
【Android内存基础——内存泄漏:http://www.jianshu.com/p/b4325fecdcda】;
主内存和工作内存
1
多线程场景下会出现的问题
共享变量和非共享变量
共享变量: 如果一个变量在多条线程的工作内存中都有拷贝,那么就认定它是一个共享变量。++事实上,类的成员变量、静态变量都是共享变量。++ 如上所术,共享变量对进程中的所有线程都是可见的。我们经常遇到的并发问题通常就是由它引起的。
非共享变量: 就是线程中的私有变量。这些变量对其它线程来说是不可见。当线程退出时,它们会被回收的。非共享变量的值需要通过通讯手段才能传递到其它线程,这个后面再提。
其它
原子操作: 就是不可分割的,连续不断的操作。比如后面将要提到的Read操作。
可见性: 一个线程对共享变量值的修改,能够被其它线程即时看到,就称该共享变量具有可见性。
由共享变量引发的问题
现在,筒靴们已经知道了共享变量对进程中的所有线程都是可见的。并且当一个线程需要使用它时,需要先拷贝一份到自己的工作内存中,然后再工作内存中操作这个copy的对象。下面这张图展示线程中操作共享变量的过程。
图中展示了线程对共享变量的读取/写入操作。可以看到,++它们分别由两个原子操作构成。++注意,CoorChice这句话的意思是,通常意义上的读取一个变量或者写入一个变量的操作都不是原子操作,而是分两步完成的。
读取
read: 将主内存中的变量值读取到线程的工作内存中。
load: 将read到的值赋给新建的拷贝变量。
写入
store: 将线程的工作内存中的,共享变量的拷贝变量的值传到主内存中。
write: 将store后的值赋给主内存中共享变量。
你看,不论是读取还是写入,由于都需要两步完成,所以就很可能发生中途被中断的情况。比如下面这段代码每次执行的结果都有可能不一样。
你看,不论是读取还是写入,由于都需要两步完成,所以就很可能发生中途被中断的情况。比如下面这段代码每次执行的结果都有可能不一样。
第一次运行结果:
第二次运行结果:
这个例子之所以会得到这种结果,是因为当一个线程执行时,另一个线程插入执行。关键插入的地方可能有:
这样的结果我们肯定是不能接受的,事实上如果操作的是非基本类型变量,那么你的程序可能会脆弱不堪,随时面临着崩溃。我们希望程序能够高效且正确的运行,就需要解决多线程场景下的通讯(信息或数据传递)和同步(有序执行)的问题。
2
处理并发操作中的通讯和同步
目前,我们大致有两套解决多线程问题的模型。
下面,我们了解下Java中的同步手段。
synchronized
synchronized关键字相信大家都不陌生,我们常常把它加到方法或代码块上用于同步:
或者这样来同步代码块:
在进行同步时,需要时刻注意,你需要把同步加在真正需要同步的地方,而不是大段的进行同步,那样会有效降低程序效率的!记住:同步粒度尽可能的小!
Lock
与sycnhronized相比,Lock相当于是手动实现同步。在Java中,实现了一个ReentrantLock来帮助我们实现同步。使用起来也比较简单,我们只需要在需要同步的代码块前段加锁,末端释放锁即可。看个例子吧。
同样是上面那个例子,这次看看运行结果吧。
使用Lock实现同步需要注意在发生异常的地方及时释放锁,否则将会导致其它等待获取锁的线程一直阻塞下去!此外,如果使用mLock.tryLock()获取锁可以根据返回值判断是否成功获取到了锁。
final有同步作用吗?
答案是肯定的,但是它只能保证某些情况下的同步。它们是什么情况呢?就是对于不可变对象而言的。不可变对象(成员变量由基本类型或final修饰,或其它不可变对象组成的对象)意味着在安全发布后,我们不能再修改它,所以对于所有可以见到它的线程而言,它是相同的。
对于可变对象(就是非不可变对象喽,例如普通的List、Map等),即使使用了final进行修饰,在并发场景下,你仍然需要进行显示的同步。因为可变对象的内容是可以被修改的。看个例子,筒靴们可能会理解得更清晰。
运行结果比较长,我仅截取一部分能说明问题的:
看,已经发生错乱了!所以fianl并不能保证不可变对象的同步。
volatile有同步作用吗?
++volatile的主要作用是保证被修饰变量的可见性。++ 这意味着,++被volatile修饰的变量的读/写操作类似于是原子性的++,即read和load,stroe和write过程变得连续而不可被中断。所以,某种意义上说,volatile是有同步作用的,但是范围非常小,通常不能满足我们的需求。
此外,volatile能够在一定程度上保证程序的有序性。JVM在编译时会对程序进行指令重排,但这不会影响执行结果。如果一个变量被volatile修饰,那么发生在它读/写操作之前的程序指令,一定不会被重排到它的读/写操作之后。比如:
上面代码中,int a = 3像一道屏障一样,使得int b = 1和int c = 2一定发生在int d = 4和int e = 5之前。
它们自带同步属性
在java.util.concurrent包下,Java为我们提供了不少常用对象的线程安全版,比如AtomicXXX系列、ConcurrentXXX系列、CopyOnWriteXXX系列等。一般情况下,你可以放心的使用它们,而不用担心多线程场景下的各种麻烦问题!
使用多线程吧!
现在,筒靴们应该能够合理的使用多线程来提高程序效率了吧。
本篇主要介绍了关于多线程场景下一些需要注意的点,筒靴们在进行并发操作时需要根据这些特点谨慎的处理线程间的通讯和同步。
参考链接
Java Volatile Keyword:http://tutorials.jenkov.com/java-concurrency/volatile.html;
Java内存模型(一):http://www.cloudchou.com/softdesign/post-631.html;
Java 多线程-可见性问题:https://mritd.me/2016/03/20/Java-%E5%A4%9A%E7%BA%BF%E7%A8%8B-%E5%8F%AF%E8%A7%81%E6%80%A7%E9%97%AE%E9%A2%98/;
Java多线程干货系列—(四)volatile关键字| 掘金技术征文:https://juejin.im/post/590f451c44d904007beaba1b;
关注微信公众号「码个蛋」,每天更新优质文章