什么是并发
并发,实际上是多个程序之间或者同一个程序内不同部分之间能够并行执行的一种能力。
它极大的提升了CPU的吞吐量,特别是对于多核CPU尤为明显。(随着并发量的增加会达到性能上限,并发量过大,致使运行上下文切换过于频繁,甚至有可能会降低性能)
同时,也提升了程序的交互性,比如在App开发过程中,同一个页面需要若干线程同时执行,绘制UI线程,后台线程等等,分工提供高质量的用户交互需求。
进程并发和线程并发
多个程序之间的并行,表现为进程的并行,这一点很普遍,也不会存在什么问题。并行的进程之间,一般不会相互影响,它们是相互独立的,相互不可见,进程的资源比如内存和CPU时间被操作系统管理。在应用层的表现,在电脑上,你可以在听音乐的同时打字。
线程也被称为是轻量级的进程。同一个进程可以拥有多个线程,每个线程有自己的本地内存(调用栈和缓冲内存等)。它的控制,是由进程管理的,并且,重要的是多个线程之间可以共享进程中的变量以达到通信的目的。
线程并发中的问题
根据JVM的内存模型,一个线程访问主内存中的共享变量的步骤是:
- 总是先将该内存变量的内容副本拷贝进自己的本地内存中;
- 对于基础类型的变量,在更改了副本中之后,会将更改后的值同步刷新到主内存;引用类型则不存在这种同步操作。
同时,我们知道: - 对一个变量的基本操作,哪怕是简单的变量自增操作,虽然代码上只有一行,但其实CPU是分几步完成的:得到变量副本,加一,生成临时变量接收结果,然后将结果写会到主内存刷新变量值。##Java中原子操作包括:除了long和double之外的赋值操作(long和double是64位的,在32位机器上将分两次进行操作);对于引用的赋值操作##)##
- JVM在不影响最终执行结果的情况下,会进行优化,完成指令重排,这对同一个线程不会产生什么影响,但是在不同的线程之间就不同了。
因此,最终会产生的问题是: - 如何在高层级上保证操作的原子性,对于加一操作而言,最终目的就是保证这句代码已经完成之后,再允许其他线程访问该变量,否则可能导致的结果是加一操作并没有完成,其他线程读取到了未进行该操作之前的值;
- 当一个线程更改了变量(基础类型变量)的值后,如何保证立刻对其他线程是可见性,即当其他线程再访问该变量的时候,获取到的是最新的该变量的副本;
- 如何消除由于指令重排引发的多线程问题?
总结起来,其实就是三个方面:变量操作的原子性、可见性(对其它线程)、顺序执行。
Java中的同步
每个Java程序默认运行在它自己进程的线程中,Java同时支持多个线程并发执行,这一点是通过Thread对象实现的。Java应用可以通过Thread类实现多线程开发。在使用过程中,为了保证程序的正常运行,它同时提供了一些安全机制,来实现线程操作的原子性、可见性和顺序性。
锁和线程同步
Java提供了锁来控制某段代码在不同线程之间的互斥执行。使用锁的一个最简单的方式,是通过 synchronized 关键字修饰方法或者代码块。
synchronized 关键字保证了:
- 同时只能由一个线程执行代码块,实现多个线程对同一代码块或者方法的互斥访问;(对于自增操作而言,实际上仍然并非是原子的,但是通过互斥访问达到了这样的目的)
- 每个线程在进入同步代码块之后,会看到先前所有对代码块中变量的修改。(保证可见性和顺序)
//同步方法
public synchronized void add() {
}
public void delete() {
//同步代码块
syschronized(this){
}
}
上述在使用 synchronized 时,使用的锁实际上是通过该类实例化出来的对象。当然,也可以使用其他对象作为锁。
private Object lock = new Object();
public void add(){
synchronized(lock){
}
}
对于静态方法,需要使用类对象
class Test{
}
public static void add(){
synchronized(Test.class){
}
}
总结
使用锁进行操作无疑是最安全最可靠的做法。但是它最大的弊端在于对CPU资源的消耗。
Volatile使用
Volatile的特点就是在,任何一个线程读取主内存的变量时,将读取的是该变量最新的值。即满足线程对变量修改后,在其他线程中的可见性。即如果A和B同时读取了变量param的值为1,此时如果在A中设置param为2.
下面一段代码可以证明(代码来源)
public class Test extends Thread {
boolean keepRunning = true;
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
t.start();
Thread.sleep(1000);
t.keepRunning = false;
System.out.println("keepRunning is false");
}
public void run() {
int x = 10;
while (keepRunning)
{
x++;
}
System.out.println("x:"+x);
}
}
-------------------------------------------------------------------------------------------------------
实测结果:
执行 x++ 的while循环一直没有停下来过。
我对于一直处于while死循环也感到有点不解。可以肯定是,这一定和共享变量的可变性有关。查了一些资料,一个可以接受的说法是:
由于对keepRunning变量没有使用任何同步措施,比如volatile和synchronized,编译器认为这个变量不会被多个线程共享,从而进行了循环不变式的优化,优化结果为:
if(keepRunning){
while(true){
x++ ;
}
}
而这种优化实际上跟具体的运行环境有关。也就是说,在有些设备上,不一定会出现这种死循环。
使用volatile是可以保证共享变量的可见性,然而这并不能完全解决线程之间的同步问题。
static volatile int counter = 0;
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
counter++;
};
}.start();
}
预计结果应该是 10000,然而实际上,运行结果每次都不一样,最终的结果很少有到10000的。问题就在于,多个线程同时对 counter 进行了修改,只是保证了 counter 在每个线程中的可见性,但是并不能保证它的同步。
以上,我们开了十个线程同时修改 counter
总结
volatile只能作为一个轻量级的synchronized,而不能完全替代synchronized。volatile适用于那些只会在一个线程中进行更改数据的情况,因为大多数情况下对数据变量的操作 都不是原子的。更加安全的做法是使用synchronized,但是synchronized由于需要使用锁,这将更加消耗CPU资源。
AtomicInteger
这是java.util.concurrent.atomic种的类,其中还包括: AtomicBoolean; AtomicLong;AtomicReference等。具体我们来看一看AtomicInteger,其他的大同小异。
AtomicInteger内部其实有一个volatile的value。
private volatile int value;
而它内部的操作,大都使用了 sun.misc.Unsafe 这个类。这个类被称为魔术类,会绕过Java的安全检查机制,直接操作内存以获取高性能,而对于它的set、get、compareAndSet操作都是原子性的。
对于这个类有多强大,可以看这篇文章,这是个强大到即将在JDK9中不被支持。太强大的东西往往都是一把双刃剑,用不好就是灾难。
总结
AtomicX中借用volatile和Unsafe实现了对共享变量操作的互斥性。是一种很安全的使用方式。注意,单纯的AtomicInteger的set和get方法仅仅是返回上面的value,并没有使用Unsafe操作。
感想
在项目过程中由于使用了多线程操作,涉及到多线程同步的问题,所以对此进行了一次梳理,记录在此。
其实在使用过程中,更多的是使用synchronized,这也是非常安全的一个做法,但是问题就在于性能消耗大,但是对于多线程读写共享变量时,又不得不这么做。
后来查看RxJava的源码实现,发现其使用AtomicX(AtomicInteger、AtomicReference等)实现了一个多线程共享的队列,它借此完全做到了对变量原子性、可见性、顺序性的功能,同时性能消耗比synchronized要低。我觉得对于不需要复杂同步操作时,使用AtomicX是一种很好的选择。
当然,volatile的性能相对于synchronized和AtomicX应该是最高的,它不涉及到锁的操作。但是却只能保证共享变量的可见性,而无法保证原子性。所以,它的应用场景在于那些只有一个线程需要对共享变量做复合赋值操作的情况。