Java并发(6)--线程安全策略:不可变对象、ThreadLocal、常见的线程安全与线程不安全类、同步容器

为避免在并发环境下的线程不安全问题,可以将对象确保为不可变对象,或者也可以采用线程封闭技术。

文章目录

    • 一. 不可变对象实现线程安全
    • 二. 线程封闭实现线程安全
      • 1. 堆栈封闭:局部变量,无并发问题
      • 2. ThreadLocal
    • 三. 常见线程不安全类
      • 1. StringBuilder 与 StringBuffer
      • 2. ArrayList,HashSet,HashMap 等Collection类
    • 四. 同步容器
      • 1. ArrayList的线程安全类:Vector,Stack
      • 2. HashMap的线程安全类:HashTable
      • 3. Collections类中的相关同步方法

一. 不可变对象实现线程安全

在 Java 语言中,对各种操作共享数据按照安全强度可以分为5类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。

不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障。
如果共享数据是一个基本数据类型,在定义时用final 修饰就是不可变的
如果是对象,就需要保证对象的行为不会对其状态产生任何影响(如 String 对象,调用它的方法都不会影响它原来的值。)保证不受影响的一种方式就是将对象中带有状态的变量声明为 final 。如:String 、Integer、Long、Double、BigInteger、BigDecimal等。

  1. 不可变对象需要满足的条件
    对象创建以后其状态就不能修改
    对象所有域都是final类型
    对象是正确创建的,(在对象创建期间,this引用没有逸出。)
  2. final关键字:类、方法、变量
    修饰类:不能被继承
    修饰方法:1、锁定方法不会被继承类修改;2、效率
    修饰变量:修饰基本数据类型就是意味着不可变了,不能被修改。如果是修饰引用类型变量的话,就说明这个引用不能再指向另外的对象(但是这个引用类型指向的对象是尅修改的。)。
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));
    }

	//用final 修饰 传参时,传参也不可修改
    private void test(final int a) {
//        a = 1;
    }
}


  1. Collections.unmodifiableList 的不可变性
    返回一个只读的对象,这个只读的对象和原先的对象完全不一样了。
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对象
        map = Collections.unmodifiableMap(map);
    }

    public static void main(String[] args) {
		//下面这行代码编译时不会报错,但是运行时会报错
        map.put(1, 3);
        log.info("{}", map.get(1));
    }

}

二. 线程封闭实现线程安全

线程封闭通俗讲就是将对象封装到一个线程里,使得这个对象即时本身不是线程安全的,也不会出现线程安全问题。

1. 堆栈封闭:局部变量,无并发问题

栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量(方法中的本地变量)。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

注:上面的将局部变量都会被拷贝一部分到线程栈中,涉及到Java 内存模型中的 主内存与工作内存

public class Snippet {
    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
    
        // animals被封闭在方法中,不要使它们逸出!
        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;
    }
}



2. ThreadLocal

使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
Java并发编程–理解ThreadLocal

package chapter04;

import java.util.concurrent.TimeUnit;

public class Profiler {
    // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
                                                                protected Long initialValue() {
                                                                    return System.currentTimeMillis();
                                                                }
    public static final void begin() {
        //通过 set()方法设置MAP的值
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end() {
    	//get() 方法获取原先设置的值
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }

    public static void main(String[] args) throws Exception {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost: " + Profiler.end() + " mills");
    }
}

Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。


三. 常见线程不安全类

如果一个类的对象同时被多个线程访问,如果不做特殊的同步或并发处理,很容易表现出线程不安全的现象,比如抛出异常、逻辑处理错误等,这种类我们就称为线程不安全的类

1. StringBuilder 与 StringBuffer

探秘Java中的String、StringBuilder以及StringBuffer
1、如果要操作少量的数据用String
2、单线程操作字符串缓冲区下操作大量数据StringBuilder
3、多线程操作字符串缓冲区下操作大量数据StringBuffer 。StringBuffer的方法使用了synchronized关键字修饰。

@Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

2. ArrayList,HashSet,HashMap 等Collection类

以下内容引自慕课网实战·高并发探索(八):线程不安全类、同步容器
像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:

  1. ArrayList的基本属性:
//对象数组:ArrayList的底层数据结构 
private transient Object[] elementData; 
//elementData中已存放的元素的个数 
private int size; 
//默认数组容量 
private static final int DEFAULT_CAPACITY = 10;

  1. 初始化时
// ArrayList带容量大小的构造函数。
     public ArrayList(int initialCapacity) {
         super();
        if (initialCapacity < 0)
             throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        // 新建一个数组
         this.elementData = new Object[initialCapacity];
    }

 // ArrayList构造函数。默认容量是10。
    public ArrayList() {
       this(10);
   }

  1. 添加对象方法(重点)
//每次添加时将数组扩容1,然后再赋值
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
// 确定ArrarList的容量。
// 若ArrayList的容量不足以容纳当前的全部元素,则设置 新的容量
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
  1. 总结:ArrayList每次对内容进行插入操作的时候,都会做扩容处理,这是ArrayList的优点(无容量的限制),同时也是缺点,线程不安全。(以下例子取材于鱼笑笑博客)
    一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:

    在 Items[Size] 的位置存放此元素;
    增大 Size 的值。

在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。


那么如何将其处理为线程安全的?或者说对应的线程安全类有哪些呢?接下来就涉及到我们同步容器

四. 同步容器

1. ArrayList的线程安全类:Vector,Stack

Java 集合系列06之 Vector详细介绍(源码解析)和使用示例
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized修饰过的方法。
Stack它的方法也是使用synchronized修饰了,继承了Vector,实际上就是栈

//Vector 的add()方法
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

但是vector 也不是绝对线程安全的,在某些情况下还是需要同步手段。详见jvm(13)-线程安全与锁优化


2. HashMap的线程安全类:HashTable

源码分析:

Entry对象唯一表示一个键值对,有四个属性:
-K key 键对象
-V value 值对象
-int hash 键对象的hash值
-Entry entry 指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部

备注:HashMap 和 HashTable 的实现都涉及到数据结构中的散列表。散列表的基本原理与实现

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}

分析:
保证安全性:使用了synchronized修饰
不允许空值(在代码中特殊做了判断)
HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对。

3. Collections类中的相关同步方法

Collections类中提供了一系列的线程安全方法用于处理ArrayList等线程不安全的Collection类 。内部操作的方法使用了synchronized修饰符

同步容器对应的类一般只是相对线程安全的,在并发环境下仍可能存在问题,此时就需要并发容器JUC


参考:

  1. Java并发编程–理解ThreadLocal(源码分析))
  2. SpringBoot | 第七章:过滤器、监听器、拦截器(会使用)
  3. 全面理解Java内存模型(JMM)及volatile关键字(详细阅读)
  4. Java并发编程–线程封闭(Ad-hoc封闭 栈封闭 ThreadLocal)(了解,概念)
  5. 探秘Java中的String、StringBuilder以及StringBuffer(透彻,源码讲解)
  6. Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例(详细)
  7. jvm(13)-线程安全与锁优化:(vector的安全性问题)
  8. Java 集合系列06之 Vector详细介绍(源码解析)和使用示例
  9. 散列表的基本原理与实现(基于算法第4版)
  10. 慕课网实战·高并发探索(八):线程不安全类、同步容器

你可能感兴趣的:(并发)