第7章 JMM 教程
2.2 JMM
2.2.1 什么是JMM
CPU与内存交互存取数据
在讲线程安全概念的时候讲到一个多个线程执行sum++导致线程不安全的例子:

造成线程不安全的原因是多线程在交替执行sum++。

为什么sum++由多条指令组成呢?这和计算机硬件结构有关,下图是CPU与内存交互存取数据的示意图:

CPU负责计算,内存负责存储,CPU计算速度要比CPU从内存中存取数据要快的多,所以硬件工程师在CPU中添加了高速缓存,CPU把数据从内存拿到高速缓存,CPU从高速缓存进行存取的速度要远远大于CPU从内存存取数据的速度,所以数据计算会先进入高速缓存,最后再将计算结果从高速缓存同步到内存。
JMM内存模型介绍
为了适应不同的硬件,Java定义了JMM内存模型,它与计算机硬件结构相似如下:

每个线程有自己的工作内存,工作内存相当于CPU高速缓存,共享内存相当于主内存,共享内存中存储了共享数据(对象的成员变量、类的静态变量等)。如果将JMM和JVM的内存模型对应,共享内存相当于堆内存,工作内存相当于栈内存。
JMM即Java内存模型(Java Memory Model ),它和JVM的内存模型不是一回事,JMM内存模型是描述多线程对共享数据读写的规则,旨在定义共享变量的读写规则保证线程安全。
JMM中定义线程对共享变量的操作有四个基础操作:read、write、lock、unlock。
read:将共享变量从共享内存读到工作内存。
write:将工作内存中的数据写入共享内存。
lock:锁定共享变量,此时共享变量被一个线程独占。
unlock: 释放锁,此时共享变量可被其它线程锁定。
详细的JMM请参考jsr133标准,需要获取jsr133英文版与中文版请关注预见猿份公众号,进入菜单“自学Java”-->"开发工具",查找并下载。
原子性可见性有序性
JMM定义了对共享变量操作的原子性、可见性、有序性:
1、原子性
原子性是指一个操作在执行过程中不会中断不会出现多线程交替执行。
比如对一个int变量的赋值操作:
a=1;
a=2;
多线程执行这两条语句,执行结果要么a=1,要么a=2,不会出现第三个结果,这说明对int变量a的赋值操作具有原子性。
对a变量执行a++操作则不具有原子性,前边学习线程安全知道多线程执行a++存在交替执行的问题,因为a++底层由多条指令组成,不具有原子性的操作会导致线程不安全。
稍后会讲解保证原子性的解决方案。
2、可见性
可见性是指:多线程访问共享变量,一个线程修改了共享变量的值,另一个线程可以立即得到共享变量的最新值,简言之:一个线程修改共享变量对其它线程可见。
因为共享内存与工作内存之间存在数据同步,一个线程修改了共享变量还没有同步到共享内存时被其它线程读取旧值此时程序出现与预期不一致的结果,即线程不安全。
比如:线程1修改共享变量a为1,线程2要在a变为1后立即读取新值,如果a的值修改后对线程2不可见就会造成线程不安全的问题。
稍后会讲解保证可见性的解决方案。
3、有序性
有序性是指一块代码的执行顺序和代码书写顺序是一致的。
什么情况下代码的执行顺序和书写顺序不一致?当出现指令重排序时则可能违反了有序性规则。
比如下边的代码:
int a=1;
int b=2;
int c = a+b;在执行时可以按如下顺序执行:
int b=2;
int a=1;
int c = a+b;这时就发生了指令重排序,指令重排序在CPU和和编译器都可能存在。
多线程程序发生了指令重排序就可能导致程序执行的预期和结果不一致,导致 线程不安全。
稍后介绍保证程序有序性的解决方案。
2.2.2JMM原子性
什么是原子性
原子性是指一个操作在执行过程中不会中断不会出现多线程交替执行。
比如对一个int变量的赋值操作:
a=1;
a=2;
多线程执行这两条语句,执行结果要么a=1,要么a=2,不会出现第三个结果,这说明对int变量a的赋值操作具有原子性。
对a变量执行a++操作则不具有原子性,前边学习线程安全知道多线程执行a++存在交替执行的问题,因为a++底层由多条指令组成,不具有原子性的操作会导致线程不安全。
JMM定义对基本类型变量的读取和赋值操作具有原子性,double、long类型除外。
下边的每条语句都不具有原子性:
int a=a+1;
int a+=2;
int c=a;
volatile修饰double 和long
为什么对double、long类型的赋值操作不具有原子性?
参考JSR133.pdf中关于“double 和 long 的非原子性处理 ”章节,double和long是64位存储,读写操作会在两次进行,一次进行32位,当对一个double类型数据写操作时一个线程可能看到了前32位的数据或后32位的数据,所以对double和long类型的赋值操作不具有原子性,根据规范中描述可以使用volaitle 实现对double和long类型数据读写操作的原子性。
下边代码实现了用volatile修饰一个double类型的成员变量a。
private volatile double a;
public void test(){
a=10L;//写操作
int b = a;//读操作
}由于double共享变量由volatile修饰,所以对它的读取和赋值操作实现了原子性。
同步锁实现原子性
前边学习了synchronized、ReentrantLock同步锁,同步锁可以实现原子性。拿synchronized举例,被synchronized标记的代码是临界区,进入临界区前获取锁,结束临界区则释放锁,执行临界区期间不允许多线程交替执行,所以用synchronized保护的临界区代码具有原子性。
2.2.3 可见性
什么是可见性
可见性是指:多线程访问共享变量,一个线程修改了共享变量的值,另一个线程可以立即得到共享变量的最新值,简言之:一个线程修改共享变量对其它线程可见。
因为共享内存与工作内存之间存在数据同步,一个线程修改了共享变量还没有同步到共享内存时被其它线程读取旧值此时程序出现与预期不一致的结果,即线程不安全。
比如:线程1修改共享变量a为1,线程2要在a变为1后立即读取新值,如果a的值修改后对线程2不可见就会造成线程不安全的问题。
造成可见性问题的原因是由于线程的工作内存与共享内存之间的数据同步存在问题,即在线程工作内存中的变量副本与共享内存中的共享变量之间的同步不及时导致。
如下图:

说明如下:
1、共享内存即堆内存,里边存储了共享变量。
2、每个线程有自己的工作内存区域,也叫线程栈内存。
工作内存存储了每个线程创建的局部变量、方法参数等,线程栈的内容仅当前线程可见。
3、启动线程后会将共享变量从共享内存复制到线程栈中,线程对共享变量的操作是在线程栈中完成。
线程栈内存相当于CPU高速缓存,其作用是为了提高处理速度,不用每次都去主存读数据,等待数据处理完毕再更新到共享内存。
可见性的例子
下边程序实现了开关控制程序的执行,共两个线程,一个线程控制开关,一个线程根据开关状态执行程序,如下图:

代码如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* JMM测试
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
public class JmmTest {
private static Logger logger = Logger.getLogger(JmmTest.class);
//开关
static boolean status=true;
//累加变量
static int sum = 0;
//测试可见性问题
public static void test_visible() {
//线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//线程1使用开关
threadPool.execute(()->{
while(status) {
for (int j = 1; j <= 10; j++) {
sum++;
}
}
logger.debug("结束采集任务");
});
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程2控制开关
threadPool.execute(()->{
logger.debug("关闭开关");
status = false;
});
//关闭线程池
shutdown(threadPool);
}
public static void main(String[] args) {
test_visible();
}
}从代码上看线程1和线程2存在共享资源:sum和status,这里我们主要看对status的访问,一个线程设置开关状态为false,另一个线程调用获取开关状态,两个线程不存在并发访问的代码段,因为线程1和线程2访问了不同的代码块。
通过阅读程序,预期结果是:
1)线程1和线程2分别启动成功
2)线程1判断开关的状态,开关为true则进入循环并执行累加操作,开关为false则退出循环最后结束线程。
3)线程2在2秒后启动并关闭开关
4)线程1判断开关状态为false则结束线程。
运行程序测试:
通过多次运行程序,输出如下:
关闭开关存在问题是:只有线程2输出的“关闭开关”日志信息,线程1没有输出"结束任务"的日志,这说明线程2将开关设置为false,线程1读取到的开关状态仍然为true。
结论:
根据测试发现,当多线程访问同一个共享资源时,存在共享资源对某些线程不可见的问题。
根据程序的内容分析,开关状态为共享变量,开关状态的同步示意图如下:

造成不可见的原因分析如下:
线程2将开关状态设置为false,如果线程2还没有将开关状态同步到堆内存,那么线程1则无法读取到最新值。
线程2将开关状态同步到了堆内存,但是线程1读取的是本地内存的开关状态则仍然没有读取最新值。
synchronized/Lock解决可见性
如何解决可见性问题呢?
可见性问题是由于线程栈中的共享变量副本与堆中的共享变量之间同步不及时导致,JMM提供了一些技术方案可以解决可见性问题。
synchronized/Lock同步锁对临界区的代码加锁控制,一个线程进入临界区会清除共享变量在线程栈中的副本,重新从堆中读取共享变量,当完成临界区的代码执行会将共享变量在线程栈中的副本写回共享内存。
为了保险起见,下边在读取共享变量和写共享变量的代码处都使用synchronized进行标记,代码如下:
//使用synchronized解决可见性问题
public static void test_visibleBySynchronized() {
//线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//锁对象
Object obj = new Object();
//线程1使用开关
threadPool.execute(()->{
while(true) {
synchronized (obj){
if(!status){
break;
}
}
for (int j = 1; j <= 10; j++) {
sum++;
}
}
logger.debug("结束采集任务");
});
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程2控制开关
threadPool.execute(()->{
logger.debug("关闭开关");
synchronized (obj){
status = false;
}
});
//关闭线程池
shutdown(threadPool);
}运行程序,输出 :
11:25:36.179 FINE com.yjoffer.javase.thread.safe.JmmTest[pool-1-thread-2] - 关闭开关
11:25:36.206 FINE com.yjoffer.javase.thread.safe.JmmTest[pool-1-thread-1] - 结束采集任务ReentrantLock也可以解决可见性问题,它的实现原理与synchronized类似,请自行测试。
volatile解决可见性
volatile实现可见性相比同步锁要简单,只需要使用volatile关键字修饰共享变量即可,如下所示:
//开关
volatile static boolean status=true;volatile可以修饰成员变量、静态变量,不可以修饰局部变量,由volatile修饰的变量线程必须从主存中读取最新值 ,它可以解决可见性问题。
运行test_visible()方法,结果 实现了可见性。
volatile的底层是内存屏障,内存屏障是一种CPU指令,在java中内存屏障有两种类型:Load和Store。
Load:在读取标记为volatile的共享变量时会在读取指令前插入Load指令(读屏障),它的作用的是让线程栈中的变量副本失效,必须从共享内存中读取最新值。
Store:在写入标记volatile的共享变量时会在写入指令后插入Store指令(写屏障),它的作用的是将线程栈中的变量副本写入主存中。
synchronized在使用时会创建监视器,相比synchronized解决可见性volatile更轻量,建议解决可见性问题使用volatile。
2.2.4 可见性vs原子性
可见性vs原子性
至此我们学习了JMM中关于可见性和原子性的概念及解决方案:
synchronized可以解决原子性、可见性。
volatile可以解决可见性,可以实现double、long类型数据读写操作的原子性。
synchronized相比volatile解决可见性笨重,推荐使用volatile解决可见性问题。
final可以解决可见性,final标记的变量表示不可变,final变量赋值后自动加入写屏障,保证final的变量的可见性。
Balking模式
Balking模式是设计模式的一种,Balking表示“犹豫不前”,它是指如果没有达到预定条件就终止返回。
下边是一个Balking模式的例子,使用多线程实现此案例,并思考是用synchronized还是volatile。
需求描述:
我们在使用IDEA写代码时除了手动保存以外IDEA还提供自动保存的功能,保存文件这个方法存在多线程调用的情况,一个是程序员手动点击保存按钮,一个是IDEA自动保存。
代码如下:
启动两个线程调用save方法保存文件,save方法中根据changed标记来判断是否退出返回,changed为true表示文件修改,为false表示文件没有修改,当文件没有修改时则直接退出方法,当文件修改则保存文件内容。
预期的结果是:每修改一次文件保存文件动作只执行一次。
package com.yjoffer.javase.thread.safe;
import com.yjoffer.javase.config.Logger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* JMM测试
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
public class JmmTest {
private static Logger logger = Logger.getLogger(JmmTest.class);
/**
* Java文件类
*/
static class JavaFile{
//文件内容
private String content;
//是否修改
private boolean changed;
//修改文件内容
public void change(String content){
this.content = content;
changed = true;
}
//保存文件内容
public void save(){
if(!changed){
return ;
}
logger.debug("保存文件"+content);
changed = false;
}
}
//自动保存案例(Balking模式)
public static void test_balking(){
JavaFile javaFile = new JavaFile();
ExecutorService threadPool = Executors.newCachedThreadPool();
javaFile.change("01");
threadPool.execute(()->{
javaFile.save();
});
threadPool.execute(()->{
javaFile.save();
});
shutdown(threadPool);
}
//等待线程池中的线程完成
private static void shutdown(ExecutorService threadPool) {
//线程池结束
threadPool.shutdown();
while (!threadPool.isTerminated()) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
test_balking();
}运行程序,输出 :
20:31:56.966 FINE com.yjoffer.javase.thread.safe.JmmTest[pool-1-thread-1] - 保存文件01
20:31:56.966 FINE com.yjoffer.javase.thread.safe.JmmTest[pool-1-thread-2] - 保存文件01并没有按照预期执行,两个线程都执行了保存文件动作。
尝试解决1:
可能是由于一个线程执行save方法后将changed修改为false而另一个线程无法读取changed的最新值 导致。
使用volatile修饰changed,如下:
private volatile boolean changed;运行程序结果仍然没有达到预期。
尝试解决2:
出错的关键代码块如下:
//保存文件内容
public void save(){
if(!changed){
return ;
}
logger.debug("保存文件"+content);
changed = false;
}save方法的代码存在线程交替执行的情况,假如线程1执行到"保存文件"处还没有设置changed为false时,此时切换到线程2执行就会存在多次执行“保存文件”动作。
根据分析需要 保证save方法的原子性即可解决,使用synchronzied修饰save方法,代码如下:
//保存文件内容
public synchronized void save(){
if(!changed){
return ;
}
logger.debug("保存文件"+content);
changed = false;
}再次运行程序达到预期,只执行一次“保存文件”动作。
2.2.5 有序性
什么是有序性
有序性是指线程内一块代码的执行顺序和代码书写顺序是一致的。
到目前为止在我们的认知中,代码的执行顺序就是按照代码的书写顺序去执行的,其实不然,编译器和CPU为了提升代码执行性能可能会对代码进行指令重排,比如下边的代码:
int a = 1;
int b = 2;
int c = a + b;第1行和第2行交换顺序执行不会最终影响程序的结果,这里就可能指令重排如下:
int b = 2;
int a = 1;
int c = a + b;但是指令重排决不会是如下的顺序,因为c依赖a和b,如下的代码会报错:
int c = a + b;
int a = 1;
int b = 2;单线程下即使发生了指令重排序最终的结果也是正确的,但在多线程下发生指令重排序,违反有序性规则可能结果是不正确的,导致线程不安全。
所以我们编写程序要做到的不是绝对去避免程序的指令重排序,而是通过局部避免指令重排序或其它方法最终保证程序在多线程下执行结果是正确的。
指令重排序的例子
下边的代码说明这个问题,下边的程序每次启动两个线程,它们操作共享变量,如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* JMM测试
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
public class JmmTest {
//测试有序性
static int a=0;
static int b=0;
static int x=0;
static int y=0;
public static void test_order() throws InterruptedException {
long n = 0;
while(true){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 如果发生指令重排则先执行x=b;再执行a=1
a = 1;
x = b;
}
});
Thread t2= new Thread(new Runnable() {
@Override
public void run() {
// 如果发生指令重排则先执行y = a;再执行b = 1;
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();//等t1和t2运行完再继续执行主线程
/**
* 按代码顺序执行,x和y可能的值为:(x=0,y=1)(x=1,y=1)(x=1,y=0)
*/
n++;
if(x == 0 && y == 0){
//由于发生指令重排出现x=0,y=0
System.out.println("第 "+ n + "次,x="+x +", y="+y);
break;
}
a=0;
b=0;
x=0;
y=0;
}
}
public static void main(String[] args) throws InterruptedException {
test_order();
}
}不考虑指令重排,按代码顺序执行,x和y可能的值为:(x=0,y=1)(x=1,y=1)(x=1,y=0)。
如果考虑指令重排,x=b和y=a会先执行,x和y的值都为0。
通过运行程序,在第271329次时发生x和y等于0的现象,这就说明发生了指令重排。
第 271329次,x=0, y=0注意:本程序发生的时间点不固定,主要理解指令重排的现象。
上边的代码发生了指令重排,违反了有序性,程序结果与我们预期不一致,导致线程不安全。
注意:有序性是指线程内的有序性,不是指多线程交替执行的有序性,下边介绍一些方法去解决线程内的有序性,而不是去解决多线程交替执行的有序性,多线程交替执行的次序由操作系统调度分配CPU时间片来决定。
happens-before原则
happens-before翻译为先行发生原则,它定义了一个操作先于另一个操作发生,这里强调的是一个操作的结果对另一个操作是可见的,即先于另一个操作发生。
定义happens-before原则的目的是在一些场景下保证操作的有序性,最终保证结果的正确性。
1)单线程内程序次序规则,单线程内即使发生指令重排但结果是正确的。
比如单线程执行如下代码:
int a = 1;
int b = 2;
int c = a+b;执行后a=1,b=2,c=3, 不会出现其它结果。
2)同步锁规则,同一个锁对象的unlock(释放锁)先与lock(上锁)操作,解锁之前对变量的写操作对其它线程加锁后是可见的。
比如下边的代码:
Object obj = new Object();
new Thread(()->{
synchronized(obj){
x=1;
}
},"t1").start();
new Thread(()->{
synchronized(obj){
//输出1
System.out.println(x);
}
},"t2").start();t1线程先执行,向x赋值为1,释放锁后x的值 同步至主存,t2线程后执行,输出 x最新的值。
3)volatile变量规则,对一个volatile变量的写操作先与对该变量的读操作,即一个线程先向volatile变量写入操作,另一个线程一定可以读到最新的值。
4)线程启动规则,线程的start()方法先与线程run()中的每个操作,start()方法之前执行的操作对run()中的是可见的。
如下代码:
x = 1;
new Thread(()->{
//输出1
System.out.println(x);
},"t1").start();线程没有启动之前对x赋值为1,线程启动后x对该线程是可见的。
5)线程终止规则,线程run()中的每个操作都先与线程终止操作(如join()、stop()方法),线程中对变量的操作对线程结束后是可见的。
如下代码:
Thread t1 = new Thread(() -> {
x = 1;
}, "t1");
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出 1
System.out.println(x);在t1线程中对x赋值,线程结束后输出 x最新的值。
6)线程中断规则, 线程interrupt()方法先与线程中断事件isInterupted()的发生。
如下代码:
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(x);
break;
}
}
},"t1");
t1.start();
x = 1;
t1.interrupt();7)对象终结规则,一个对象的构造及初始化先与调用对象的finalize()清理方法,对象不用时由垃圾回收器进行回收,此时会调用对象的finalize()方法(不保证一定调用),构造方法先与清理方法。
static class CustomObj{
public CustomObj(){
x = 1;
}
protected void finalize() throws Throwable {
super.finalize();
System.out.println(x);
}
}
public static void test_happensbefore5(){
CustomObj customObj = new CustomObj();
customObj = null;
System.gc();
}
public static void main(String[] args) throws InterruptedException {
test_happensbefore5();
}调用对象customObj设置为null便于垃圾收集,调用System.gc()执行垃圾收集,此时调用对象的finalize()方法。
注意:对系统的清理工作建议放在try/finally中,因为finalize()不一定及时被调用。
8)传递性规则,如果操作A先于操作B,操作B先于操作C,则操作A先于操作C。
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
//输出 1
logger.debug(String.valueOf(x));
new Thread(()->{
//输出 1
logger.debug(String.valueOf(x));
},"t2").start();
break;
}
}
},"t1");
t1.start();
x = 1;
t1.interrupt();在主线程中对x=1赋值对t1可见,t1先于t2线程,所以x=1对t2也可见。
synchronized保证有序吗?
synchronized是不能避免指令重排序,无法保证有序性,但在synchronized的同步块中是单线程执行,符合happens-before原则的第一条,在单线程内指令重排不影响结果的正确性。
volatile保证有序性
被volatile修饰的共享变量,对它的操作禁止指令重排。
volatile保证了有序性,在读指令前插入load屏障防止load屏障之后的代码排序到它之前,这样做是为了保证先读取最新的数据再去使用;
比如下边的代码:
a或b用volatile修饰可以保证下边代码的有序性,比如将a修饰为volatile,int b = a;代码表示要读取a的值,在它之前插入load屏障,保证不将它之后的代码排序到它之前,这样就保证了将a的初始值 1赋值给b而不会将a++ 排在int b=a之前。
a = 1;
//读屏障
int b = a;
a++;在写指令后插入store屏障防止store屏障之前的代码排序到它之后,这样做是为了将写入到变量的数据同步到主存中。
a = 1;
//写屏障
int b = a;
a++;a=1后插入写屏障,保证写屏障之前的指令不会排到它之后,保证写屏障之后的指令读到a的最新值,a的最终值为2。
2.2.6 单例模式
什么是单例模式
单例模式是一种设计模式,单例模式要求在使用一个类的过程中只允许创建一个实例。单例模式在项目中应用很多,比如:系统初始化对象在系统启动时只会创建一个,监控项目中监视器对象只有一个。
一般情况下只要一个类是线程安全的就可以使用单例模式。
单例模式的好处是:
1、在内存中只有一个实例,节省内存。
2、避免对资源的多重占用。
单例模式有饿汉式和懒汉式:
饿汉式:类加载时自动创建实例。
懒汉式:在使用时再创建实例。
饿汉式实现
下边实现饿汉式单例模式,代码如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 饿汉式单例
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
//设置为final避免子类覆盖方法迫害单例特性
public final class Singleton {
//设置为私有构造方法避免在外部创建对象
private Singleton(){
}
//类加载时创建单例对象赋值给静态成员变量
private static final Singleton SINGLETON = new Singleton();
public static Singleton getInstance(){
return SINGLETON;
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Singleton.getInstance());
});
}
}
}运行程序,输出一个单例对象的地址。
懒汉式实现
下边实现懒汉式单例模式,代码如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 懒汉式单例
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
//设置为final避免子类覆盖方法迫害单例特性
public final class SingletonLazy {
//设置为私有构造方法避免在外部创建对象
private SingletonLazy(){
}
private static SingletonLazy SINGLETON = null;
public static synchronized SingletonLazy getInstance(){
if(SINGLETON == null){
SINGLETON = new SingletonLazy();
}
return SINGLETON;
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(SingletonLazy.getInstance());
});
}
}
}运行程序,输出一个单例对象的地址,结果正确。
DCL双重检查锁
懒汉式的程序虽然正确但是性能不高,因为整个getInstance()方法被 synchronized修饰,每次调用此方法都会上锁。
设想:当单例没有创建时可以用synchronized,一旦创建就不需要 使用锁,这样可以大大提高性能。
下边使用Double check Locking来实现,Double check Locking表示双重检查锁,即在代码中两次检查单例对象,实现代码如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 懒汉式单例,双重检查锁方式(临时版本)
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
//设置为final避免子类覆盖方法迫害单例特性
public final class SingletonDclTemp {
//设置为私有构造方法避免在外部创建对象
private SingletonDclTemp(){
}
private static SingletonDclTemp SINGLETON = null;
public static SingletonDclTemp getInstance(){
if(SINGLETON == null){
synchronized (SingletonDclTemp.class){
if(SINGLETON == null){
SINGLETON = new SingletonDclTemp();
}
}
}
return SINGLETON;
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(SingletonDclTemp.getInstance());
});
}
}
}从代码实现可以看出,getInstance()方法两次检查单例是否为空,运行上边的程序看似没有问题,实质存在问题。
首先查看SINGLETON = new SingletonDclTemp();代码由四行字节码指令组成,如下:

NEW:创建 一个实例。
DUP:复制该实例。
INVOKESPECIAL: 执行构造方法。
PUTSTATIC: 将实例赋值给SINGLETON引用变量。
虽然SINGLETON = new SingletonDclTemp();被 synchronized包裹但仍然可能发生指令重排序,上边四条指令INVOKESPECIAL和PUTSTATIC可能会交换顺序,交换后的顺序 如下:
NEW
DUP
PUTSTATIC
INVOKESPECIAL如果像原始代码synchronized将整个getInstance()方法包裹这是没有问题的,但由于synchronized并没有包含所有针对SINGLETON的操作代码,所以假设有两个线程来请求getInstance(),线程A执行完PUTSTATIC后当还没有INVOKESPECIAL时由线程B进入getInstance(),判断if(SINGLETON == null)不满足返回SINGLETON,但是此时SINGLETON并没有初始化导致存在问题。
如何 解决上边的问题呢?
导致上边的问题就是因为指令重排序导致,只需要使用volatile修饰SINGLETON即可,如下代码:
private volatile static SingletonDcl SINGLETON = null;使用volatile修饰SINGLETON可以保证SINGLETON = new SingletonDclTemp();的有序性,保证INVOKESPECIAL在PUTSTATIC之前。
所以建议对共享变量可能存在可见性问题的使用volatile修饰。
完整代码如下:
package com.yjoffer.javase.thread.safe;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 懒汉式单例,双重检查锁方式
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
//设置为final避免子类覆盖方法迫害单例特性
public final class SingletonDcl {
//设置为私有构造方法避免在外部创建对象
private SingletonDcl(){
}
private volatile static SingletonDcl SINGLETON = null;
public static SingletonDcl getInstance(){
if(SINGLETON == null){
synchronized (SingletonDcl.class){
if(SINGLETON == null){
SINGLETON = new SingletonDcl();
}
}
}
return SINGLETON;
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 500; i++) {
threadPool.execute(()->{
System.out.println(SingletonDcl.getInstance());
});
}
}
}2.2.8 ThreadLocal入门
从一个需求开始
学习了JMM我们知道线程的工作内存相当于栈内存,它是每个线程的私有数据,堆中的共享变量被多线程共享会导致线程不安全问题,如果一些数据仅需要在线程内共享时要考虑使用类似线程工作内存的存储方式而不是共享内存,这样可以避免线程不安全的问题。
下边的案例说明了这个需求:
预见猿份使用Java语言开发,提供Java在线学习功能,系统采用接口层、业务层、数据层三层架构,三层的职责如下:
接口层:负责接收处理用户的请求,比如:用户请求学习“Java并发编程教程”,接口层的程序收到用户的请求会请求业务层获取教程的视频和文本内容。
业务层:为接口层提供业务处理支撑,比如:用户请求学习“synchronized”知识点,业务层会根据教程和知识点的信息找到相关的视频、文档等内容提供给用户。
数据层:为业务层提供数据处理及存储支撑,比如:用户请求学习“synchronized”知识点,业务层根据教程和知识点信息从数据层查询,并且最终会在数据层存储用户的学习记录,方便用户根据学习记录继续学习。

平台支持高并发处理,多用户来访问预见猿份的教程资源平台通过多线程让多用户同时学习,每个用户的请求将分配单独的线程去处理,如下图:

预见猿份为了更好的用户体验需要记录用户的学习历史,后期为用户推荐更好的课程,每一层都需要获取用户的身份信息。
用户登录后用户的身份信息如何在三层之间进行传递?
1、共享变量
如果将用户身份信息放在共享变量中由多线程共享会存在线程不安全的问题,线程1登录成功更新用户身份为小创,线程2登录成功更新用户身份为小恒,此时用户身份错乱。

2、通过方法参数传递
接口层的调用业务层的方法将用户身份传递过去,业务层调用数据层方法将用户身份传递过去。由于代码量大通过方法参数传递会非常繁琐。

伪代码如下:
//接口层方法
public 课程信息 studyAction(){
//构建业务层对象
Service service = new Service();
//调用业务层方法,传入用户身份信息
return service.studyServie(user);
}
//业务层方法
public 课程信息 studyService(User user){
//构建数据层对象
Dao dao = new Dao();
return dao.studyDao(user);
}
...3、线程本地存储
线程本地存储只有线程本身可以访问,线程1登录成功将“小创”用户身份存储到线程1本地,线程2登录成功将“小恒”存储到线程2本地中,每个线程在执行过程中,三层的方法代码都可以访问当前线程的本地数据。

ThreadLocal是什么
ThreadLocal是一个方便访问线程本地数据的工具类,线程本地存储类似线程栈的特点,注意它不是线程栈。数据存储在线程本地的好处是不被多线程共享,保证线程安全。
ThreadLocal是什么?有什么用?应用场景 有哪些?
ThreadLocal是维护线程本地数据的工具类,使用ThreadLocal可以方便去操作线程的本地数据。
ThreadLocal可以将数据在多线程之间隔离,可在线程内共享数据,应用场景 如下:
1)会话管理
将用户会话用ThreadLocal管理,每个线程的用户会话不同,并在线程内存共享同一个会话。
2)事务管理
将数据库事务用ThreadLocal管理,每个线程的事务不同,并在线程内共享同一个事务。
ThreadLocal和synchronized有关系吗?
ThreadLocal和synchronized是两个不同的东西,其目的不一样,synchronized用于同步访问共享变量的同步锁用来实现线程安全,ThreadLocal是存储访问线程本地数据的工具类,线程的本地数据只能在线程内部共享不能跨线程共享。
ThreadLocal入门程序
使用ThreadLocal可以非常方便的实现上边需求的第三种方案,TheradLocal的使用方法非常简单,查询它的API非常少,如下:
T get()
返回当前线程的此线程局部变量的副本中的值。
protected T initialValue()
返回此线程局部变量的当前线程的“初始值”。
void remove()
删除此线程局部变量的当前线程的值。
void set(T value)
将当前线程的此线程局部变量的副本设置为指定的值。
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
创建线程局部变量。首先看set和get两个方法,set方法是向线程栈中放数据,get方法是从线程栈中取数据。
下边是测试程序:
1、启动两个线程
2、每个线程在本地放入自己的身份信息
3、线程去执行业务方法、数据存储方法时从自己的线程本地取出身份信息
代码如下:
package com.yjoffer.javase.thread.safe;
import com.yjoffer.javase.config.Logger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 测试ThreadLocal
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
public class ThreadLocalTest {
private static Logger logger = Logger.getLogger(ThreadLocalTest.class);
//线程本地工具类
static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
static class User{
private String name;
public User(String name){
this.name = name;
}
public String getName(){
return name;
}
}
//业务处理类
static class UserService{
public void study(){
//取出当前线程本地中的用户身份
User user = userThreadLocal.get();
logger.debug(user.getName()+"开始学习");
UserDao userDao = new UserDao();
userDao.saveHistory();
}
}
//数据存储类
static class UserDao{
public void saveHistory(){
//取出当前线程本地中的用户身份
User user = userThreadLocal.get();
logger.debug("存储"+user.getName()+"学习历史记录");
}
}
//入门程序
public static void test_first(){
ExecutorService threadPool = Executors.newCachedThreadPool();
//线程1
threadPool.execute(()->{
//向当前线程本地存储数据
userThreadLocal.set(new User("小创"));
UserService userService = new UserService();
userService.study();
});
//线程2
threadPool.execute(()->{
//向当前线程本地存储数据
userThreadLocal.set(new User("小恒"));
UserService userService = new UserService();
userService.study();
});
}
public static void main(String[] args) {
test_first();
}
}运行程序:
15:25:51.896 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-1] - 小创开始学习
15:25:51.896 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-2] - 小恒开始学习
15:25:51.965 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-2] - 存储小恒学习历史记录
15:25:51.965 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-1] - 存储小创学习历史记录ThreadLocal原理
ThreadLocal将数据存储在堆中,ThreadLoal工具类作了数据隔离,每个线程只能访问自己的本地数据。上边入门程序执行后线程的本地数据存储如下图所示:

在Thread类中有一个ThreadLocalMap类型的属性:threadLocals,源代码如下(jdk1.8):

线程的本地数据就存在此属性中。
ThreadLocalMap是ThreadLocal的内部类,它实现了类似Map的结构,线程本地数据可能为多个,所以采用数组存储每个数据,如下图:

每个本地数据为Entry对象,并存储在ThradLocalMap的数组中,存储前根据ThreadLocal对象哈希值确定Entry在数组中的位置,如果该位置为空则存储,如果已存在数据则判断已有的key和ThreadLocal对象是否为同一个,如果是则覆盖原数据,否则就找下一个为空的位置进行存储。
下边参考set方法的源代码:
public void set(T value) {
//取出当前线程
Thread t = Thread.currentThread();
//获取当前线程下的ThreadLocalMap即本地数据
ThreadLocalMap map = getMap(t);
//如果ThreadLocalMap不为空则设置本地数据,key为threadLocal对象,value为本地数据
if (map != null)
map.set(this, value);
else//否则创建一个ThreadLocalMap用于存储当前线程的本地数据
createMap(t, value);
}getMap方法如下:
ThreadLocalMap getMap(Thread t) {
//返回thread对象中的threadLocals属性
return t.threadLocals;
}createMap的方法如下:
void createMap(Thread t, T firstValue) {
//第一个构造参数为threadLocal,第二个为本地数据
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
...
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//key为ThreadLocal对象,value为本地数据
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}ThreadLocalMap的set方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
//table即存放本地数据的数组
Entry[] tab = table;
int len = tab.length;
//根据key的哈希值计算数组的下标
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果位置上的key与传入的threadLocal相等则将旧数据替换为新数据
if (k == key) {
e.value = value;
return;
}
//如果位置上的key为空则替换一个新的Entry对象
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果数组中不存在传入的threadlocal则在新位置存储新的Entry对象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}get方法即可得到线程的本地数据,源码如下:
public T get() {
//当前线程
Thread t = Thread.currentThread();
//取出当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据ThreadLocal对象从数组 中取出本地数据即Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//返回本地数据
T result = (T)e.value;
return result;
}
}
//如果当前没有ThreadLocalMap则初始一个,此方法在set方法之前会被调用且只被调用一次
return setInitialValue();
}ThreadLocalMap的getEntry方法如下:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果位置上有Entry就直接 返回
if (e != null && e.get() == key)
return e;
else//没有位置上没有Entry就继续找数组上的其它位置
return getEntryAfterMiss(key, i, e);
}既然线程本地支持多个数据存储,如果除了用户身份信息还要存储用户的权限信息如何存储呢?
根据set方法的执行流程,再创建用户权限的ThreadLocal,在线程中调用它的set方法即可将用户的权限存储到当前线程的本地。如下图:

测试代码如下:
package com.yjoffer.javase.thread.safe;
import com.yjoffer.javase.config.Logger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 测试ThreadLocal
* @author 预见猿份(www.yjoffer.com)
* @version 1.0
**/
public class ThreadLocalTest {
private static Logger logger = Logger.getLogger(ThreadLocalTest.class);
//线程本地工具类
static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
//用户权限
static ThreadLocal<String> permissThreadLocal = new ThreadLocal<>();
static class User{
private String name;
public User(String name){
this.name = name;
}
public String getName(){
return name;
}
}
//业务处理类
static class UserService{
public void study(){
//取出当前线程本地中的用户身份
User user = userThreadLocal.get();
String permiss = permissThreadLocal.get();
logger.debug(user.getName()+"开始学习");
logger.debug(user.getName()+"的权限:"+permiss);
UserDao userDao = new UserDao();
userDao.saveHistory();
}
}
//数据存储类
static class UserDao{
public void saveHistory(){
//取出当前线程本地中的用户身份
User user = userThreadLocal.get();
logger.debug("存储"+user.getName()+"学习历史记录");
}
}
//入门程序
public static void test_first(){
ExecutorService threadPool = Executors.newCachedThreadPool();
//线程1
threadPool.execute(()->{
//向当前线程本地存储数据
userThreadLocal.set(new User("小创"));
permissThreadLocal.set("AB");
UserService userService = new UserService();
userService.study();
});
//线程2
threadPool.execute(()->{
//向当前线程本地存储数据
userThreadLocal.set(new User("小恒"));
permissThreadLocal.set("ABC");
UserService userService = new UserService();
userService.study();
});
}
public static void main(String[] args) {
test_first();
}
}运行程序输出 :
15:23:05.218 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-2] - 小恒开始学习
15:23:05.218 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-1] - 小创开始学习
15:23:05.241 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-2] - 小恒的权限:ABC
15:23:05.241 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-1] - 小创的权限:AB
15:23:05.242 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-2] - 存储小恒学习历史记录
15:23:05.242 FINE com.yjoffer.javase.thread.safe.ThreadLocalTest[pool-1-thread-1] - 存储小创学习历史记录注意:使用ThreadLocal的目的就是线程内部仅共享本地数据,而不去访问多线程的共享变量,所以调用set方法设置的本地数据是每个线程独立的,切忌将一个多线程可以访问到的共享变量或对象引用设置为本地数据,小心产生线程不安全的问题。
下图是我们要去避免的:

所以,线程本地数据拥有自己独立的对象,下图是正确的作法:

初始化
当没有调用set方法设置本地数据前去调用了get方法获取本地数据,此时会调用setInitialValue方法初始化ThreadLocalMap,setInitialValue方法如下:
private T setInitialValue() {
//调用初始化方法,此方法由子类重写实现,可通过内部类或Lambda定义
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}ThreadLocal中的initialValue方法默认返回NULL,定义initialValue的目的是为了在没有执行set方法时去执行get方法获取一个初始的本地数据。下边的代码测试了没有定义initialValue时直接调用get方法取出的本地数据为空:
public static void test_initialValue(){
ExecutorService threadPool = Executors.newCachedThreadPool();
//线程3
threadPool.execute(()->{
//获取本地数据
String permiss = permissThreadLocal.get();
System.out.println(permiss);
});
}
public static void main(String[] args) {
test_initialValue();
}运行程序,输出 :
null由于本地数据为空调用get方法获取为null。
下边定义initialValue方法,initialValue方法需要在ThreadLocal子类中定义:
//用户权限
static ThreadLocal<String> permissThreadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {//用户的初始权限为ABC
return "ABC";
}
};再次测试test_initialValue方法,在没有设置本地权限时执行initialValue方法初始化权限数据为ABC。
ABCThreadLocal类中还有一个withInitial方法,它是使用Lambda定义initialValue方法,下边使用withInitial实现默认权限数据为ABC,如下:
static ThreadLocal<String> permissThreadLocal = ThreadLocal.withInitial(()->"ABC");父子线程共享数据
在Thread类中还有一个inheritableThreadLocals属性,见名知意它表示可继承的ThreadLocal,使用inheritableThreadLocals在父线程存储本地数据可以在子线程中读取,inheritableThreadLocals实现了多线程共享数据,但必须是在父子线程之间。
查看源代码,在thread对象的初始化方法中可以看到:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
//如果父线程的inheritableThreadLocals不为空则将其作为子线程的threadLocal。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...下边的测试代码在父线程中设置本地数据为"www.yjoffer.com",在子线程中读取该threadLocal。
//测试inheritableThreadLocals
public static void test_inheritableThreadLocals(){
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("www.yjoffer.com");
//启动子线程
new Thread(()->{
String string = threadLocal.get();
System.out.println(string);
}).start();
}输出 :
www.yjoffer.com2.2.10 ThreadLocal内存泄露问题
内存泄露问题
引用百度百科:内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
Java语言使用垃圾回收器(GC)回收不使用的内存,针对ThreadLocal存储的本地数据是否有内存泄露问题下边进行分析。
线程本地数据以Entry类型存储在ThreadLocalMap下的数组中,Entry类型继承弱引用类型WeakReference,源代码如下:
static class Entry extends WeakReference<ThreadLocal<?>>{...}这里要知道强引用 和弱引用,强引用方式就是将对象赋值给所属类型的引用变量,如:
String str = new String("www.yjoffer.com");、Object obj = new Object();,str和obj就是强引用,它引用了堆中的对象,当强引用不存在时GC才会回收它曾经引用 的对象,比如:显示设置引用为NULL、或者方法执行完成清空栈内存后强引用 不存在,但是具体什么时候回收由GC算法决定。
WeakReference引用为弱引用,不管内存是否充足GC都会优先回收弱引用对象,测试如下:
//强引用
String str = new String("www.yjoffer.com");
WeakReference<String> weakReference2 = new WeakReference<String>(new String("预见猿份"));
//输出 预见猿份
System.out.println(weakReference2.get());
//运行GC
System.gc();
//输出 null
System.out.println(weakReference2.get());
//输出 www.yjoffer.com
System.out.println(str);了解了弱引用再看Entry类的源代码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}在Entry的构造方法中super(k)表示将key作为弱引用,所以key不会存在内存泄露问题。
但是value为强引用,按理说线程销毁后Entry对象的key和value都会被回收,但是如果线程从不销毁就会导致Value不会回收,比如:线程池的核心线程就从不销毁。
处理方案就是显示将value从本地数据中删除 ,在本地数据不再使用时调用ThreadLocal的remove()方法即可,比如下边的代码,在线程执行最后调用remove()方法销毁本地数据:
//线程1
threadPool.execute(()->{
//向当前线程本地存储数据
userThreadLocal.set(new User("小创"));
permissThreadLocal.set("AB");
UserService userService = new UserService();
userService.study();
//显示销毁
userThreadLocal.remove();
});小结
ThreadLocal存在内存泄露问题吗?
Entry元素的key为弱引用不存在内存泄露,key对应的value存在内存泄露问题,解决方法是在不使用ThreadLocal数据时显示调用remove()方法销毁本地数据。
