一、Java内存模型
线程间协作通信可以类比人与人之间的协作的方式,之前网上有个流行语“你妈喊你回家吃饭了”,就以这个生活场景为例,小明在外面玩耍,小明妈妈在家里做饭,做晚饭后准备叫小明回家吃饭,那么就存在两种方式:
- 小明妈妈要去上班了十分紧急这个时候手机又没有电了,于是就在桌子上贴了一张纸条“饭做好了,放在…”小明回家后看到纸条如愿吃到妈妈做的饭菜,那么,如果将小明妈妈和小明作为两个线程,那么这张纸条就是这两个线程间通信的共享变量,通过读写共享变量实现两个线程间协作;
- 妈妈的手机还有电,妈妈在赶去坐公交的路上给小明打了个电话,这种方式就是通知机制来完成协作。同样,可以引申到线程间通信机制。
在并发编程中主要需要解决两个问题:1. 线程之间如何通信;2.线程之间如何完成同步(这里的线程指的是并发执行的活动实体)。通信是指线程之间以何种机制来交换信息,主要有两种:共享内存和消息传递。这里,可以分别类比上面的两个举例。java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信。
共享变量的范围:
Java程序中的所有实例域、静态域和数组元素都是放在堆内存中,所有线程均可访问。而局部变量、方法定义参数和异常处理参数不会在线程之间共享。共享数据会存在线程安全问题。

如图为JMM抽象示意图,线程A和线程B之间要完成通信的话,要经历如下两步:
- 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中
- 线程B从主存中读取最新的共享变量
从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而使共享变量对每个线程都是可见的。
二、synchronized实现原理
具体分类 | 被锁的对象 | 伪代码 |
---|---|---|
实例方法 | 类的实例对象 | //实例方法 public synchronized void method(){ ………. } |
静态方法 | 类对象 | //静态方法 public static synchronized void method(){ ………. } |
实例对象 | 类的实例对象 | //同步代码块 synchronized(this){ ………. } |
class对象 | 类对象 | //同步代码块 synchronized(SynchronizedDemo.class){ ………. } |
任意实例对象Object | 任意实例对象 | //同步代码块 String lock = “”; synchronized(lock){ ………. } |
2.1、对象锁(monitor)机制
现在我们来看看synchronized的底层实现:1
2
3
4
5
6
7
8
9
10public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method();
}
private static void method() {
}
}
上述代码中有一个同步代码块,锁住的是类对象,并且还有一个同步的静态方法,锁住的依旧是类对象。编译以后再SynchronizedDemo.class的目录下使用javap -v SynchronizedDemo.class查看字节码文件,可以发现:

锁对象的重入性
执行同步代码块后首先要先执行monitorenter指令,退出时执行monitorexit指令。代码被同步后,只有线程获取monitor之后才能继续执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
三、volatile实现原理
使用synchronized会产生阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile可以当作一种轻量级的同步机制。从JMM我们可以得知,各个线程会将共享变量从主内存拷贝到工作内存,然后会基于工作内存中的数据及逆行操作处理。线程在工作内存进行操作之后何时将数据写入主内存?这个时机是没有规定的。对于volatile修饰的变量,若发生修改Java虚拟机会立刻将其写入主内存,并告知其他线程该变量已被修改。所以被volatile修饰的变量既不会出现“脏读”,也能保证数据的可见性。
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
上述代码加锁线程A先执行writer方法,然后线程B执行reader方法。

因为flag被volatile修饰,多以当线程A修改了布尔变量flag,其他线程就能立刻感知,不会出现线程安全问题。