源码先锋

源码先锋

多线程!你只要看这一篇就够了

admin 32 11

码个蛋(codeegg)第749次推文

作者:蓝师傅_Android

原文:

多线程并发问题,基本是面试必问的。

大部分同学应该都知道Synchronized,Lock,部分同学能说到volatile、并发包,优秀的同学则能在前面的基础上,说出Synchronized、volatile的原理,以及并发包中常用的数据结构,例如ConcurrentHashMap的原理。

这篇文章将总结多线程并发的各种处理方式,希望对大家有所帮助。

一、多线程为什么会有并发问题

为什么多线程同时访问(读写)同个变量,会有并发问题?

Java内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。

线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。

线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

二、Java内存模型(JMM)

Java内存模型(JMM)作用于工作内存(本地内存)和主存之间数据同步过程,它规定了如何做数据同步以及什么时候做数据同步,如下图。

三、并发三要素

原子性:在一个操作中,CPU不可以在中途暂停然后再调度,即不被中断操作,要么执行完成,要么就不执行。

可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性:程序执行的顺序按照代码的先后顺序执行。

四、怎么做,才能解决并发问题?(重点)

下面结合不同场景分析解决并发问题的处理方式。

一、volatile

1.1volatile特性

保证可见性,不保证原子性。

禁止指令重排序指令重排序是指编译器和处理器为了优化程序性能对指令进行排序的一种手段,需要遵守一定规则:

1.2使用场景

对于一个变量,只有一个线程执行写操作,其它线程都是读操作,这时候可以用volatile修饰这个变量。

1.3单例双重锁为什么要用到volatile?

publicclassTestInstance{

privatestaticvolatileTestInstancemInstance;

publicstaticTestInstancegetInstance{//1
if(mInstance==){//2
synchronized(){//3
if(mInstance==){//4
mInstance=newTestInstance;//5
}
}
}
returnmInstance;
}

假如没有用volatile,并发情况下会出现问题,线程A执行到注释5newTestInstance的时候,分为如下几个几步操作:

分配内存

初始化对象

mInstance指向内存

这时候如果发生指令重排,执行顺序是132,执行到第3的时候,线程B刚好进来了,并且执行到注释2,这时候判断mInstance不为空,直接使用一个未初始化的对象。所以使用volatile关键字来禁止指令重排序。

1.4volatile原理

在JVM底层volatile是采用内存屏障来实现的,内存屏障会提供3个功能:

1.5volatile的局限性

volatile只能保证可见性,不能保证原子性写操作对其它线程可见,但是不能解决多个线程同时写的问题。

二、Synchronized

2.1Synchronized使用场景

多个线程同时写一个变量。

例如售票,余票是100张,窗口A和窗口B同时各卖出一张票,假如余票变量用volatile修饰,是有问题的。

A窗口获取余票是100,B窗口获取余票也是100,A卖出一张变成99,刷新回主内存,同时B卖出一张变成99,也刷新回主内存,会导致最终主内存余票是99而不是98。

前面说到volatile的局限性,就是多个线程同时写的情况,这种情况一般可以使用Synchronized。

Synchronized可以保证同一时刻,只有一个线程可执行某个方法或某个代码块。

2.2Synchronized原理

publicclassSynchronizedTest{

publicstaticvoidmain(String[]args){
synchronized(){
("123");
}
method;
}

privatestaticvoidmethod{
}
}

将这段代码先用javac命令编译,再命令查看字节码,部分字节码如下

publicstaticvoidmain([]);
descriptor:([Ljava/lang/String;)V
flags:ACC_PUBLIC,ACC_STATIC
Code:
stack=2,locals=3,args_size=1
0:ldc3//Fieldjava/lang/:Ljava/io/PrintStream;
8:ldc5//Methodjava/io/:(Ljava/lang/String;)V
13:aload_1
14:monitorexit
15:goto23
18:astore_2
19:aload_1
20:monitorexit
21:aload_2
22:athrow
23:invokestaticgetAndAddInt
publicfinalintgetAndAddInt(Objectvar1,longvar2,intvar4){
intvar5;
do{
var5=(var1,var2);
}while(!(var1,var2,var5,var5+var4));
returnvar5;
}

通过compareAndSwapInt保证原子性。

五、总结

面试中问到多线程并发问题,可以这么答:

当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量

当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。

ReentranLock可以通过代码释放锁,可以设置锁超时。

高并发下,Synchronized、ReentranLock效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

面试的时候按照上面总结的这个思路回答基本就ok了。既然说到并发包,那么除了ConcurrentHashMap,其它一些常用的数据结构的原理也需要去了解下,例如HashMap、HashTable、TreeMap原理,ArrayList、LinkedList对比,这些都是老生常谈的,自己去看源码或者一些博客。

关于多线程并发就先总结到这里,如果是应付面试的话按照这篇文章的思路来准备应该是没太大问题的。