文章目录
1.线程安全的代码
在前面的博客中提到了Java并发编程中需要注意的线程安全问题,在不同的场景选择使用synchronized和volatile来保证线程之间的原子性、可见性、有序性。那么有没有一种代码,完全不需要做任何的同步操作也一定是线程安全的呢?
答案是肯定有,常见的有下面两种情况:
- 对多个线程访问的共享变量只读不写。
- 使用未逃逸的局部变量。
第一点比较好理解,只要不对共享变量做修改,那所有线程的工作内存中保存的副本就一直不会变,线程就是安全的。
对于第二点,局部变量是在方法体运行时定义的,运行方法时会在虚拟机栈中压入一个栈帧,而栈是属于线程私有的。也就是说,只要局部变量未逃逸,则方法中定义的局部变量就是线程私有的,那肯定就是线程安全的。
那么什么叫逃逸呢?
JVM的逃逸分析对逃逸定义了两种形式,分别是:
- 方法逃逸:作为方法的返回值或作为参数传递给其它方法。
- 线程逃逸:将局部变量赋值给类变量,让其他线程可以访问到。
方法逃逸不会有什么影响,因为都属于是在同一个线程的栈中不断的压入栈帧,其他线程无法访问到。而线程逃逸可能会导致线程安全问题,所以不做同步操作又要保证线程安全的话,就不要将局部变量赋值给类变量。
注:编译器的锁消除优化的判断也是通过逃逸分析来做的,既然变量不会被其它线程访问到,那加上synchronized这样的同步操作只是在浪费性能而已,此时就会判断出可以对消除这个同步锁。
1.1.线程逃逸的例子
验证上述的线程逃逸后,如果不加同步操作,运行的线程将不再安全。
public class ThreadEscapeDemo {
private List<Integer> list = new ArrayList<>();
public void test1() {
List<Integer> localList = new ArrayList<>();
localList.add(1);
this.list = localList;
try {
// 睡1秒,让其他线程对类变量进行操作
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (Integer integer : localList) {
// 如果线程安全,只会打印出1
System.out.print(integer);
}
}
public void test2() {
this.list.add(2);
}
public static void main(String[] args) throws InterruptedException {
ThreadEscapeDemo escapeDemo = new ThreadEscapeDemo();
Thread t1 = new Thread(escapeDemo::test1);
Thread t2 = new Thread(escapeDemo::test2);
t1.start();
// 睡10毫秒,让t1先于t2运行
Thread.sleep(10L);
t2.start();
}
}
打印结果为12,但期望打印出的结果为1。
2.ThreadLocal
2.1.概念
ThreadLocal
是一个维护线程本地变量的工具类,每个线程都可以通过ThreadLocal
来存放自己的本地变量,使自己的本地变量与其它线程隔离,达到线程私有化的效果。
2.2.如何使用ThreadLocal
ThreadLocal
的使用非常的简单,从使用效果上来说,一个ThreadLocal
只能存储一个值,只需要记录3个关键方法就可以了,分别是set()
,get()
和remove()
。
2.2.1.实现线程隔离
下面是一个ThreadLocal
做线程隔离的例子:
public class ThreadLocalDemo {
// 使用ThreadLocal的初始化方法,初始化值为0
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void test() {
for (int i = 0; i < 10; i++) {
Integer value = threadLocal.get();
threadLocal.set(++value);
}
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
// 在使用完ThreadLocal后要remove掉,避免内存泄露
threadLocal.remove();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> new ThreadLocalDemo().test()).start();
}
}
}
打印结果:
Thread-3:10
Thread-1:10
Thread-0:10
Thread-2:10
Thread-4:10
每个线程打印出的结果都是10,而不是50或者其他的什么数字,说明ThreadLocal为每个线程都分配了线程私有的本地变量,各个线程中的变量实现了隔离。
2.3.实现参数传递
我们日常使用的方法间参数传递的方式有两种,要么是通过方法定义的形参来传递,要么是使用公用的成员变量传递,ThreadLocal
的参数传递方式和成员变量的方式类似。
例子如下:
public class ThreadLocalArgsTrans {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void test1() {
threadLocal.set("Hello Thread Local");
}
public void test2() {
System.out.println(threadLocal.get());
}
public static void main(String[] args) {
ThreadLocalArgsTrans t = new ThreadLocalArgsTrans();
t.test1();
t.test2();
}
}
调用test2()后打印出
Hello Thread Local
2.4.PageHelper是如何使用ThreadLocal的
2.4.1.PageHelper的引入和使用
看了两个简单的示例后,可以来看一看PageHelper
是怎么通过ThreadLocal
来实现线程隔离和参数传递的。
首先需要引入mybatis
和pageHelper
的maven依赖,然后写一个分页代码:
public List<User> queryUser() {
Page<User> users = PageHelper.startPage(1, 2);
userMapper.selectAll();
return users;
}
其中selectAll()
里面就写了一个简单的查询所有记录的sql:
select * from `user`;
我们把mybatis
的sql日志打开,看一下执行这个方法会打印出什么日志。
可以看出PageHelper
对打印的sql做出了修改,从代码中的蛛丝马迹去看,只是多加入了PageHelper.startPage()
这行代码,点进去然后在一顿重载方法后找到执行逻辑的方法。
2.4.2.ThreadLocal存放分页信息
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
// 这里省略了部分代码……
setLocalPage(page);
return page;
}
上面的方法中新建了一个Page
对象,并调用了SetLocalPage(page)
的方法,看这个名字可能就和ThreadLocal
有关了,点进去之后可以看到挨着的三个和ThreadLocal
有关的方法:
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
/**
* 设置 Page 参数
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
/**
* 获取 Page 参数
*/
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
/**
* 移除本地变量
*/
public static void clearPage() {
LOCAL_PAGE.remove();
}
// 后面的代码省略……
}
setLocalPage()
方法将page
对象放到了到了ThreadLocal
中。那page
对象是什么时候使用的呢?接着往下看。
2.4.3.PageHelper注册拦截器
我们使用的PageHelper
是通过mybatis
的Interceptor
接口来实现了一个自定义插件,我们可以定义一个实现类实现这个拦截器,然后将这个拦截器放入到mybatis
的拦截器链中,mybaits
框架就会在执行sql之前去回调intercept()
方法,下图就是拦截器的源码。
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* @author Clinton Begin
*/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
如果使用的是pageHelper
的springboot starter,我们就不需要再去自定义拦截器了,在starter的jar包中,已经定义了一个javaconfig类PageHelperAutoConfiguration
,对PageHelper
注册mybatis
的拦截器实现了自动装配:
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
// 这里省略了部分配置代码……
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
// 将pageHelper的拦截器注册到mybaits的拦截器链中
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
2.4.4.通过ThreadLocal获取分页对象
接下来只需要关心PageInterceptor
中的intercept()
方法,分页一定是使用了这个方法,而这个方法中一定会使用到ThreadLocal
,下面的代码中省略了部分和ThreadLocal
无关的代码。
public Object intercept(Invocation invocation) throws Throwable {
try {
// 省略……
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
// 省略……
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
// 省略……
// 返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
// 省略……
}
}
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
// 省略……
}
}
// 装配分页返回对象
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
// 使用完后,remove掉ThreadLocal中的分页参数
dialect.afterAll();
}
}
上面的几个方法,每一个都需要用到Page
对象来完成判断,但是在方法中没有任何一个位置传入了这个参数,那page
对象这个参数来自哪里,想必大家心里已经有了答案了。
上面的每个方法获取Page
对象,都是通过同一个方法来获取的,PageMethod
类中的getLocalPage()
方法,从ThreadLocal
中获取分页参数。
需要注意的是,在方法调用结束后,需要清空ThreadLocal
中的数据,除了避免内存泄露以外,还有个原因是线程池中的线程的可以复用的,如果不清空,其它的连接使用了这个线程,本来不会去做分页查询,也会因为ThreadLocal
中有page
对象而去执行分页查询。
上面的afterAll()
就是在执行这个清理的过程,最终调用到clearPage()
方法。
3.总结
线程安全的代码:
- 每个线程执行的方法中,只会对自身的线程本地变量或局部变量做读写操作的代码。
- 如果有对共享变量的操作,需要设置这个共享变量不可变(final),或者不要对共享变量做写操作。
ThreadLocal的使用方法:
- 一般是在各个线程公用的类中(如工具类),定义ThreadLocal成员变量,线程需要使用到这个ThreadLocal的时候,直接去调用
get()和set()
方法来进行方法间的参数传递。 - 由于线程可能是可复用的,需要是使用完成后调用
remove()
方法,清理下已经不再使用的线程本地变量。
ThreadLocal
的实现原理可以阅读下一篇博客《线程本地变量的实现——ThreadLocal原理详解》。