JAVA并发编程与高并发解决方案 - 并发编程 三 之 线程安全策略

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的keyThreadLocal类的实例对象,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,并返回keythreadLocal对象的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

如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final 修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。

ThreadLocal中的ThreadLocalMap中的keyThreadLocal对象,由于每个实例化的ThreadLocal对象都是不相同的,所以不会存在key冲突,所以一个线程存在多个ThreadLocal对象作为key是完全没有问题的。也就是说,一个线程中的ThreadLocalMap可以存在多个key

​ 为什么使用ThreadLocal作为ThreadLocalMapkey? 上面的解析已经很明确了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值