GuangchaoSun's Blog

Java线程内存模型-工作内存和主内存

Java线程内存模型

  • 工作内存:即java线程的本地内存,是单独给某个线程分配的,存储局部变量等,同时也会复制主内存的共享变量到本地内存作为副本,目的是为了减少和主内存的通信频率,提高效率。
  • 主内存:存储类成员变量等,所有的线程共享主内存,每个线程都有自己的工作内存

线程的working memory是cpu寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不是从内存读取数据,它的数据读取优先级是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自于内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完成后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及三个特性:原子性、有序性、可见性

每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存。

可见性:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量的副本值,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。

在没有正确同步的情况下,如果多个线程同时访问了同一个变量,该程序就存在隐患,有三种方式可以修复它:

  1. 不要跨线程共享变量
  2. 使状态变量为不可变的
  3. 在任何访问状态变量的时候使用同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Task implements Runnable{
boolean running = true;
// volatile boolean running = true;
int i=0;
@Override
public void run() {
// TODO Auto-generated method stub
while(running)
i++;
}
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
Thread t = new Thread(task);
t.start();
Thread.sleep(10);
task.running = false;
Thread.sleep(100);
System.out.println(task.i);
System.out.println("程序停止");
}
}

在上面的程序中,如果running变量没有加volatile,程序会一直运行,陷入死锁。加上volatile便可以正常执行到结束。

总结:

  • 对volatile变量的写会立即刷新到主存
  • 读volatile变量是会到主存中读值

也可以用下面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Task2 implements Runnable{
boolean running = true;
int i=0;
@Override
public void run() {
// TODO Auto-generated method stub
while(this.isRunning())
i++;
}
public synchronized boolean isRunning() {
return running;
}
public synchronized void setRunning(boolean running) {
this.running = running;
}
public static void main(String[] args) throws InterruptedException {
Task2 task2 = new Task2();
Thread t = new Thread(task2);
t.start();
Thread.sleep(20);
task2.setRunning(false);
Thread.sleep(10000);
System.out.println(task2.i);
System.out.println("程序停止执行");
}
}

总结:虽然running没有volatile关键字修饰,但是读和写running都是同步方法

  • 进入同步方法,访问共享变量会去主内存访问
  • 退出同步方法,本地内存对共享变量的修改会被更新到主内存。

volatile变量的“原子性”

对一个volatile变量的写操作,只有所有步骤完成,才能被其他线程读取到。
多个线程对volatile变量的写操作本质上是有先后顺序的。也就是说并发写没有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class VolatileDemo implements Runnable{
volatile int i = 0;
@Override
public void run() {
// TODO Auto-generated method stub
for(int j=0; j < 10000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
VolatileDemo task = new VolatileDemo();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(task.i);
}
}

注意:

  • volatile对类似于i++这种操作不会是原子操作
  • volatile并不会有锁的特性

双重检验的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

总结:
这里volatile修饰符是必须的,如果没有的话,可能发生的情况:当instance为null时,线程A进入同步代码块,进行Singleton的实例化操作,其中包含几个步骤,在实例化到一半的时候线程B调用getInstance方法,instance不为null,返回一个未完全实例化的Singleton。如果加上volatile,Singleton实例化的几个操作就会变成原子的,只有实例化完成了才会被其他线程所看到。

两个instance == null校验的作用:

  • 第一个:我觉得其主要作用就是在instance不为null的时候,直接返回instance,而不需要进入synchronized,提高了多线程环境下的运行效率。
  • 第二个:这个就是进行判断进入同步代码块的那个线程的instance是否为null。

reference: