JAVA并发编程与高并发解决方案 - 并发编程 三
版本 | 作者 | 内容 |
---|---|---|
2018.5.17 | chuIllusions | 线程安全策略 |
线程安全策略
创建后状态不能被修改的对象叫作不可变对象。不可变对象天生就是线程安全的。它们的常量(变量)是在构造函数中创建的,既然它们的状态无法被修改,那么这些常量永远不会被改变——不可变对象永远是线程安全的。
不可变对象需要满足的条件
- 对象创建以后其状态就不能修改
- 对象所有域都是final类型
- 对象是正确创建的(在对象创建期间,this引用没有逸出)
不可变对象
final
final关键字:类、方法、变量
- 修饰类:不能被继承,final类中的成员属性可以根据需要设置为final,但final类中所有的成员方法都被隐式指定为final方法。一般不建议将类设置为final类型。可以参考String类。
- 修饰方法:1)锁定方法不被继承类修改;2)效率
- 修饰变量:1)基本数据类型变量,初始化后便不能进行修改;2)引用类型变量,初始化之后不能再指向别的引用
@Slf4j
@NotThreadSafe
public class ImmutableExample1 {
private final static Integer a = 1;
private final static String b = "2";
//引用类型不允许引用指向改变,但是对象值还是可以进行修改的
private final static Map<Integer, Integer> map = Maps.newHashMap();
static {
map.put(1, 2);
map.put(3, 4);
map.put(5, 6);
}
public static void main(String[] args) {
// a = 2; //编译时报错
// b = "3"; //编译时报错
// map = Maps.newHashMap(); //编译时报错
map.put(1, 3); //容易引发线程安全问题
log.info("{}", map.get(1));
}
//可以修饰参数
private void test(final int a) {
// a = 1;
}
}
Collections
java提供Collections工具类,在类中提供了多种不允许修改的方法
Collections.unmodifiableXXX:Collection、List、Set、Map…
@Slf4j
@ThreadSafe
public class ImmutableExample2 {
private static Map<Integer, Integer> map = Maps.newHashMap();
static {
map.put(1, 2);
map.put(3, 4);
map.put(5, 6);
//处理过后的map是不可以再进行修改的
map = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
//允许操作,但是操作会报错,扔出异常
map.put(1, 3);
log.info("{}", map.get(1));
}
}
public class Collections {
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
return new UnmodifiableMap<>(m);
}
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
@Override
public boolean remove(Object key, Object value) {
throw new UnsupportedOperationException();
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
throw new UnsupportedOperationException();
}
}
}
Guava
谷歌的Guava提供类似Java中的Collections
ImmutableXXX:Collection、List、Set、Map…
pom.xml
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
@ThreadSafe
public class ImmutableExample3 {
private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
private final static List<Integer> lists = ImmutableList.of(1, 2, 3);
private final static ImmutableSet set = ImmutableSet.copyOf(list);
private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4);
private final static ImmutableMap<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder()
.put(1, 2).put(3, 4).put(5, 6).build();
public static void main(String[] args) {
System.out.println(map2.get(3));
}
}
介绍了不可变对象,通过在某些情况下,将不能被修改的类对象,设置为不可变对象,来让对象在多个线程间是线程安全的。归根到底,其实是躲避开了并发的问题。除了不可变对象,还存在一个方法 就是线程封闭
线程封闭
把对象封装到一个线程里,只有这一个线程能看到该对象,那么就算这个对象不是线程安全的,也不会出现任何线程安全的问题,因为它只能在一个线程中被访问,如何实现线程封闭:
- Ad-hoc 线程封闭:程序控制实现,非常脆弱、最糟糕,忽略
- 堆栈封闭:简单的说就是局部变量,无并发问题。多个线程访问同一个方式的时候,方法中的局部变量都会被拷贝一份到线程栈中,方法的局部变量是不被多个线程共享的,因此不会出现线程安全问题,能用局部变量就不推荐使用全局变量,全局变量容易引起并发问题,注意,全局的变量而不是全局的常量。
- ThreadLocal 线程封闭:特别好的封闭方法
ThreadLocal
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
public class ThreadLocal<T> {
}
从类描述上:ThreadLocal提供线程级别的变量.这些变量不同于它们正常下的变量副本,在每一个线程中都有它自己获取方式(通过它的get和set方法),不依赖变量副本的初始化。它的实例通常都是私有的静态的,用于关联线程的上下文。
这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量
总结:ThreadLocal的作用是提供线程内部的局部变量,这种变量只存在线程的生命周期。
声明方式:private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;
类分析
ThreadLocal涉及到的类结构:
(C)ThreadLocal
-> (C)ThreadLocalMap
-> (C)Entry
(C)Thread
-> (f)ThreadLocal.ThreadLocalMap
Thread.java
public class Thread implements Runnable {
/*
* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
其中ThreadLocalMap
类的定义是在ThreadLocal
类中,真正的引用却是在Thread
类中。同时,ThreadLocalMap
中用于存储数据的entry定义:
ThreadLocal.java
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key为ThreadLocal对象,value为存储的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
ThreadLocalMap的key
是ThreadLocal
类的实例对象,value为用户的值
public class ThreadLocal<T> {
//设置值的方法
public void set(T value) {
//1.获取当前线程
Thread t = Thread.currentThread();
//2.从线程中获取该线程的成员属性 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//将值放入Map中
if (map != null)
map.set(this, value);
else
//先创建,在设置值
createMap(t, value);
}
//获取值的方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果没有设置,会调用,设置一个value为null
return setInitialValue();
}
}
工作原理
从上面的源码分析,我们可以得出ThreadLocal
的工作原理如下
- 声明全局的
ThreadLocal
变量,private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>;
- 每个线程中都有属于自己的
ThreadLocalMap
,互不干扰 - 全局只有一个
threadLocal
,当通过set
填充数据时,通过获取当前操作线程的threadLocalMap
,将threadLocal
作为threadLocalMap
中的key
,需要填充的值作为value
当需要从
threadLocal
获取值时,通过获取当前操作线程的threadLocalMap
,并返回key
为threadLocal
对象的value那么就可以理解为:`ThreadLocal`的活动范围是具体的某一个线程,并且是该线程独有的。它不是用来解决共享变量的多线程安全问题。 但是,有一点需要说明的是,如果`ThreadLocal`通过`set`方法放进去的值,这个值是共享对象,那么还是会存在线程安全问题。
多个 ThreadLocal
public class ThreadLocal<T> {
//用于唯一确认一个ThreadLocal对象
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
}
如何保证两个同时实例化的
ThreadLocal
对象有不同的threadLocalHashCode
属性:在ThreadLocal
类中,还包含了一个static修饰的AtomicInteger
([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final 修饰的常量(作为两个相邻nextHashCode
的差值)。由于nextHashCode
是类变量,所以每一次调用ThreadLocal
类都可以保证nextHashCode
被更新到新的值,并且下一次调用ThreadLocal
类这个被更新的值仍然可用,同时AtomicInteger
保证了nextHashCode
自增的原子性。
ThreadLocal
中的ThreadLocalMap
中的key
为ThreadLocal
对象,由于每个实例化的ThreadLocal
对象都是不相同的,所以不会存在key
冲突,所以一个线程存在多个ThreadLocal
对象作为key
是完全没有问题的。也就是说,一个线程中的ThreadLocalMap
可以存在多个key
。
为什么使用ThreadLocal
作为ThreadLocalMap
的key
? 上面的解析已经很明确了。