Java并发编程那些事儿(三)——ThreadLocal及原子性与可见性

这是并发编程系列的第三篇文章。上一篇介绍的是线程间通过锁同步的方式实现共享资源的安全访问,这篇讲一下如何通过不加锁的方式实现共享可变资源的访问。

ThreadLocal介绍

上篇文章讲到,如果想在多线程的环境下,实现共享可变资源的安全访问,最好的方式是加锁,也就是同一时刻只有一个线程在使用共享可变资源。如果我们有一种方式可以根除对变量的共享,那么就可以实现不加锁的情况下对变量进行安全访问。

还拿之前抢卫生间坑位的例子举例,如果只有一个卫生间坑位,五个人都想去卫生间的话,那么就需要加锁同步。如果给每个人都提供一个单独的坑位,那么就可以不加锁了,因为没有争抢的场景发生。

Java通过ThreadLocal来实现每个线程都拥有一份自己的共享变量的拷贝。大家可以把ThreadLocal<T>简单的理解成Map<Thread,T>
ThreadLocal提供了getset等方法,get方法总是返回当前线程调用set方法时设置的最新值。如果是第一次调用get方法,将会返回initialValue方法里面的设置的初始值。

ThreadLocal使用场景

ThreadLocal通常用在防止全局变量的共享,或者单例实例的共享。
举个例子,连接数据库的时候,首先要创建一个connection连接对象,但是这个connection对象不一定是线程安全的,如果所有线程方法都使用这个对象,进行数据库的连接,就有可能会出问题。如果使用加锁进行同步,那么性能上会有问题,这个时候就可以通过ThreadLocal来帮忙,让每个线程都持有一份connection对象。这样就可以完美解决问题。

1
2
3
4
5
6
7
8
9
10
11
//设置初始值,通过initialValue()
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection("DB_URL");
}
};

//通过get()方法获得ThreadLocal的值
public static Connection getConnection() {
return connectionThreadLocal.get();
}

各位一定要注意ThreadLocal的使用场景,千万不要乱用。

原子性和可见性

在使用加锁同步的方式来保证共享资源实现安全访问的方案中,锁除了保证资源的原子性以外还对可见性做了保证。

原子性:并发编程里面的原子性,与数据库里面的原子性概念是一致,都是表示操作时不可分割的,必须在不打断的情况下,一次执行完成。

可见性:在单线程的情况下,一个变量被修改之后,当再次需要使用的时候,肯定会读取到正确的值,但是在多线程情况下,一个线程修改变量之后,其他线程并不能保证第一时间读到这个变量。

如果要理解这个问题,需要对JVM的重排序有一定的理解。所谓的重排序就是编译器会对你写的代码进行顺序调整,以达到优化运行效率的目的。

对于可见性问题,可以通过如下代码示例进行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NoVisibility {
//private static volatile boolean ready = false;
private static boolean ready = false;
private static int number = 0;

private static class ReaderThread extends Thread {
public void run() {
while (!ready) ;
System.out.println(number);
}
}

public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
number = 34;
ready = true;
}
}

这段会启动一个读线程,当readytrue时会打印出number的值。然后主线程会修改readynumber的值。如果该段代码是在client模式下运行,你很可能会看到正确的结果34,但是如果是在server模式下运行,那么程序可能进入死循环,因为读线程看不到主线程对ready的修改。

如果是本地开发环境,JVM一般都是client模式,可以在你的IDE里面设置JVM的模式为server模式,运行该段代码。

如果想让读线程及时发现ready变量的修改,可以使用volatile关键字对变量ready进行修饰,可以保证所有线程第一时间看到该变量。

对于原子性,Java提供了atomic包,比如对于上篇文章提到的任务计数器示例,我们可以不使用synchronized,而使用AtomicInteger来达到同样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
public class Task implements Runnable {
//使用AtomicInteger初始化
public static AtomicInteger count = new AtomicInteger(0);
public void increase(){
//如下方法保证原子递增
count.incrementAndGet();
}
@Override
public void run() {
increase();
}
}

AtomicInteger可以保证自增操作是原子性的。

注意
并不是有了原子性及可见性操作,就可以放弃使用锁同步。原子性及可见性并不能保证线程安全,只有在一些特定的场景下才能够达到避免使用锁同步的效果,上面的样例只是为了说明Java提供的Atomvolatile功能,而特意设计的样例场景。如果真实生产中想使用原子性及可见性替代锁同步时,要认真分析。

结束

这篇文章介绍如何通过不使用锁同步的情况,实现正确的并发访问。至此,并发编程里面两种访问共享可变资源的方式就都介绍完了。下一篇会介绍线程间的通信问题。


推荐阅读:
1. Java并发编程那些事儿(一) ——任务与线程
2. Java8的Stream流真香,没体验过的永远不知道
3. Awk这件上古神兵你会用了吗
4. 手把手教你搭建一套ELK日志搜索运维平台

-------------本文结束-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%