多线程背景知识介绍
1 多线程基础概念
**进程与线程 **
进程 进程程序(任务)的执行过程(动态性),持有资源(共享内存,共享文件)和线程,是资源和线程的载体。
线程 进程的最小执行单元,比如将一个班级视为进程,则班里的每一个学生就是线程,所有学生共享黑板,教师等。
线程的交互:互斥与同步,公共资源有限,需要竞争。
2 可见性
什么是可见性?
可见性:一个线程对共享变量的修改,能及时的被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
工作内存:java内存模型抽象出的概念。
###Java内存模型(JMM) java内存模型描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。可这么理解:
(1) 所有的变量都存储在住内存中;
(2)每个线程都有自己独立的工作内存,里边保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
两条规定: (1) 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
(2) 不同线程无法直接访问其他线程工作内存中的变量,线程之间传递变量值需要通过主内存完成。
共享变量可见性实现原理
线程1对共享变量的修改要被线程2看到,需要下边两个步骤:
(1) 把线程1工作内存中修改后的共享变量刷新到主内存中;
(2) 把主内存中更新过的共享变量更新到工作内存2中。
3 synchronized实现可见性
JMM关于synchronized的两条规定:
(1) 线程解锁前,必须把共享变量最新值刷新到主内存中;
(2) 线程加锁时,将清空工作内存中共享变量的值,使用共享变量时,重新从主内存中读取最新值(加锁解锁需要是同一把锁)
线程解锁前对共享变量的修改在下次加锁时对其他线程可见
线程执行互斥代码的过程:
(1)获得互斥锁;
(2) 清空工作内存;
(3) 从主内存拷贝共享变量副本到工作内存;
(4) 执行代码;
(5)将修改后的共享变量刷新到主内存;
(6) 释放互斥锁。
补充2个知识点:重排序
重排序
代码的执行顺序与实际书写顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化,主要有3种:
(1)编译器优化的重排序;(编译器优化)
(2)指令集并行重排序(处理器优化);
(3) 内存系统的重排序(处理器优化)。
as-if-serial
as-if-serial:无论如何重排序,程序执行结果应该与代码书写顺序执行结果一致(java编译器,运行时和处理器都会保证java在单线程下遵循as-if-serial语义)。
```
int num1=1;
int num2=2;
int sum=num1+num2;
```
单线程:第1,2行的顺序可以重排,但第3行不能,重排序不会给单线程带来内存可见性的问题;
多线程中程序交错执行时,重排序可能会造成内存可见性问题。
下面的代码演示内存可见性问题:
```
public class SynchronizedDemo {
//共享变量
private boolean ready = false;
private int result = 0;
private int number = 1;
//写操作
public void write(){
ready = true; //1.1
number = 2; //1.2
}
//读操作
public void read(){
if(ready){ //2.1
result = number*3; //2.2
}
System.out.println("result的值为:" + result);
}
//内部线程类
private class ReadWriteThread extends Thread {
//根据构造方法中传入的flag参数,确定线程执行读操作还是写操作
private boolean flag;
public ReadWriteThread(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){
//构造方法中传入true,执行写操作
write();
}else{
//构造方法中传入false,执行读操作
read();
}
}
}
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
//启动线程执行写操作
synDemo .new ReadWriteThread(true).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//启动线程执行读操作
synDemo.new ReadWriteThread(false).start();
}
}
```
可能的结果 (1) 1.1->2.1->2.2->1.2 result:3;
(2) 1.1->2.1->1.2->2.2 result:6;
(3) 1.2->2.1->2.2->1.1 result:0.
可见性分析
导致共享变量在线程间不可见的原因: synchronized解决方案
(1)线程的交叉执行; 原子性
(2) 重排序结合线程交叉执行; 原子性
(3) 共享变量更新后没有在工作内存与主内存间及时更新。 可见性
安全的代码
```
//写操作
public synchronized void write(){
ready = true; //1.1
number = 2; //1.2
}
//读操作
public synchronized void read(){
if(ready){ //2.1
result = number*3; //2.2
}
System.out.println("result的值为:" + result);
}
```
synchronized既可以保证原子性,也可以保证可见性。
4 volatile实现可见性
volatile如何实现内存可见性:
深入来说,通过加入内存屏障和禁止重排序优化来实现的:
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,强制刷新到主内存;
对volatile变量执行读操作时,会在读操作前加入一条load指令,强制从主内存读取。
java中volatile相关的指令共有8个,store和load是其中两个,具体可自行百度。
但是,volatile不能保证原子性
以num++为例,这行代码并非原子操作,而是分为3步:
(1) 读取num;
(2) num+1;
(3) 写入num。
加入有A ,B两个线程同时对num进行num++操作:volatile int num=5;
(1)线程A读取num; (2)线程B读取num; (3)线程B对num+1; (4)线程B写入最新的num值6; (5)线程A中的num仍然为5,执行+1操作后为6,写入主内存6
结果为6,期望值为7.这就是volatile不能保证原子性造成的。
解决方法:
保证num自增操作的原子性:
(1) 使用synchronized关键字;
(2) 使用ReentrantLock;
(3) 使用AtomicInteger。
volatile适用场合
要在多线程中安全的使用volatile变量,必须同时满足:
(1) 对变量的操作不依赖其当前值:
不满足:count++;count=count+1;
满足:Boolean变量,温度值等。
(2) 该变量没有包含在具有其他变量的不变式中:
不满足:不变式,low<up.
volatile不需要加锁,不会阻塞线程,效率高于synchronized,但只能保证内存可见性,无法保证原子性。
### synchronized和volatile的比较
volatile比synchronized更轻量级;
volatile没有synchronized使用广泛。