保证并发安全性的方式有三:
不共享、不可变、同步
前两种方式相对第三种要简单一些。
这一篇不说语言特性和API提供的相关同步机制,主要记录一下关于共享的一些思考。
共享(shared),可以简单地认为多个线程可以同时访问某个对象。
如果仅仅在单线程内进行访问则不存在同步的问题。
保证数据的单线程访问称为线程封闭(thread confinement)。
线程封闭有三种方式:
·Ad-hoc线程封闭
·栈封闭
·ThreadLocal
Ad-hoc线程封闭:
通过程序实现来进行线程封闭,也就是说我们无法利用语言特性将对象封闭到特定的线程上,这一点导致这种方式显得不那么可靠。
举个例子,假设我们保证只有一个线程可以对某个共享的对象进行写入操作,那么这个对象的"读取-修改-写入"(比如自增操作)在任何情况下都不会出现竟态条件。
如果我们为这个对象加上volatile修饰则可以保证该对象的可见性,任何线程都可以读取该对象,但只有一个线程可以对其进行写入。
这样,仅仅通过线程封闭+volatile修饰就适当地保证了其安全性,相比直接使用synchoronized修饰,虽然更适合,但实现起来稍微复杂。
而对于线程封闭方式的选择,这种方式是最不被推荐的。
栈封闭:
这个方式理解起来比较简单,封闭在执行线程是局部变量本身固有的特性,封闭在执行线程的栈里,其他线程无法访问是理所当然的。
对于基本类型的局部变量,我们不用考虑任何事情,因为Java语言特性本身就保证了任何方法都无法获得基本类型的引用。
而对于引用类型的局部变量,我们需要稍微注意一些问题来保证其栈封闭。
参考下面的装载方舟的代码,现在我们要保护animals,则需要保证该方法的参数、调用的外来方法、返回值都不会引用到animals:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0 ; Animal candidate = null ; animals = new TreeSet<Animal>( new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load( new AnimalPair(candidate, a)); ++numPairs; candidate = null ; } } return numPairs; } |
先说说loadTheArk的参数candidates,我们将它的元素进行筛选后装载到了方舟中,方法结束后无法通过该参数影响方舟中的动物夫妇。
其次是外来方法,我们使用了"种类性别比较器"对animals进行排序,但它是一个concrete,不会有不确定的行为对animals的状态产生影响。
最后是返回值,显然我们是想报告装载了多少对动物夫妇,返回类型是个基本类型,无法引用animals。
好了,这就是个成功的栈封闭。
ThreadLocal:
给人一种亲切感,这几乎是很常见的方式,而且也是最规范的方式。
我们通常用ThreadLocal保证可变的单例变量和全局变量不被多线程共享。
先让我们想想单线程场景中使用Connection对象连接数据库,鉴于Connection对象的初始化开销,整个应用中会维护一个全局的Connection对象。
如果我们想将这个应用改为多线程的,鉴于Connection对象本身不是线程安全的,我们需要对其进行线程封闭,此时我们可以使用ThreadLocal:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class ConnectionDispenser { static String DB_URL = "jdbc:mysql://localhost/mydatabase" ; private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { throw new RuntimeException( "Unable to acquire Connection, e" ); } }; }; public Connection getConnection() { return connectionHolder.get(); } } |
不仅是Connection这种场景,如果我们的很多操作频繁地用到某个对象,而我们又需要考虑它的线程封闭又需要考虑它的初始化开销,ThreadLocal几乎是最好的选择。
虽然这看起来有点像一个全局的Map<Thread,T>,事实上也可以这样理解,但其实现并不是这样你懂的。
当然,这种方式很方便,但这并不代表ThreadLocal可以滥用, 比如仅仅是考虑到应用的并发安全性就把全局变量一律变成ThreadLocal。
而这种做法会导致全局变量难以抽象,并降低其可重用性,而且也增加了耦合。