五、Java多线程之不安全类和Collection中的基本同步容器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_21918021/article/details/88735717

一、线程不安全的类

如果一个类的对象同时可以被多个线程访问,并且你不做特殊的同步或并发处理,那么它就很容易表现出线程不安全的现象。比如抛出异常、逻辑处理错误…
下面列举一下常见的线程不安全的类及对应的线程安全类:

1、StringBuilder 与 StringBuffer

StringBuilder是线程不安全的,而StringBuffer是线程安全的。分析源码:StringBuffer的方法使用了synchronized关键字修饰。

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

2、SimpleDateFormat 与 jodatime插件

SimpleDateFormat 类在处理时间的时候,如下写法是线程不安全的:

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

//线程调用方法
private static void update() {
    try {
        simpleDateFormat.parse("20180208");
    } catch (Exception e) {
        log.error("parse exception", e);
    }
}

但是我们可以变换其为线程安全的写法:在每次转换的时候使用线程封闭,新建对象,这样在每个线程中执行的都是不同的对象。

private static void update() {
    try {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        simpleDateFormat.parse("20180208");
    } catch (Exception e) {
        log.error("parse exception", e);
    }
}

另外我们也可以使用jodatime插件来转换时间:其可以保证线程安全性
Joda 类具有不可变性,因此它们的实例无法被修改。(不可变类的一个优点就是它们是线程安全的)

private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

private static void update(int i) {
    log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
}

分析源码:(不可变性)

public class DateTimeFormatter {
    //均使用final声明
    private final InternalPrinter iPrinter;
    private final InternalParser iParser;
    private final Locale iLocale;
    private final boolean iOffsetParsed;
    private final Chronology iChrono;
    private final DateTimeZone iZone;
    private final Integer iPivotYear;
    private final int iDefaultYear;
    ...
    private InternalParser requireParser() {
        InternalParser var1 = this.iParser;
        if (var1 == null) {
            throw new UnsupportedOperationException("Parsing not supported");
        } else {
            return var1;
        }
    }
    public DateTime parseDateTime(String var1) {
        InternalParser var2 = this.requireParser();
        Chronology var3 = this.selectChronology((Chronology)null);
        DateTimeParserBucket var4 = new DateTimeParserBucket(0L, var3, this.iLocale, 
                                                            this.iPivotYear, this.iDefaultYear);
        ...
    }

3、ArrayList,HashSet,HashMap 等Collection类

详情可见我的另一篇文档集合框架分析(二)
像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:

3.1、ArrayList的基本属性

在声明时使用了transient 关键字,此关键字意为在采用Java默认的序列化机制的时候,被该关键字修饰的属性不会被序列化。而ArrayList实现了序列化接口,自己定义了序列化方法(在此不描述)。

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

2、初始化

/**
 * 创建一个容量为initialCapacity的空的(size==0)对象数组
 */
 public ArrayList(int initialCapacity) {
    super();//即父类protected AbstractList() {}
    if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
    this.elementData = new Object[initialCapacity];
}

/**
 * 默认初始化一个容量为10的对象数组
 */
 public ArrayList() {
    this(10);
 }

3、添加方法(重点)

//每次添加时将数组下标加1,然后再赋值
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
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);
}

4、总结: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。这就是“线程不安全”了。

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

2、同步容器

同步容器分两类,一种是Java提供好的类,另一类是Collections类中的相关同步方法。
注意:即使是同步容器,在特定情况下,也不会线程安全,比如满足先检查后执行,那么这两步就不是原子性操作,就有可能出现问题。

2.1、ArrayList的线程安全类:Vector,Stack

Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized修饰过的方法。
Stack它的方法也是使用synchronized修饰了,继承了Vector,实际上就是栈
使用举例(Vector):

//定义
private static List<Integer> list = new Vector<>();
//多线程调用方法
private static void update(int i) {
   list.add(i);
}

源码分析:使用了synchronized修饰

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

但是Vector也不是完全的线程安全的,比如:

错误[1]:删除与获取并发操作

public class VectorExample {

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {

        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            Thread thread1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };
            thread1.start();
            thread2.start();
        }
    }
}

运行结果:报错java.lang.ArrayIndexOutOfBoundsException: Array index out of range
原因分析:同时发生获取与删除的操作。当两个线程在同一时间都判断了vector的size,假设都判断为9,而下一刻线程1执行了remove操作,随后线程2才去get,所以就出现了错误。synchronized关键字可以保证同一时间只有一个线程执行该方法,但是多个线程同时分别执行remove、add、get操作的时候就无法控制了。

错误[2]:使用foreach\iterator遍历Vector的时候进行增删操作

package com.mmall.concurrency.example.syncContainer;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

public class VectorExample3 {

    // java.util.ConcurrentModificationException
    private static void test1(Vector<Integer> v1) { // foreach
        for(Integer i : v1) {
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // java.util.ConcurrentModificationException
    private static void test2(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // success
    private static void test3(Vector<Integer> v1) { // for
        for (int i = 0; i < v1.size(); i++) {
            if (v1.get(i).equals(3)) {
                v1.remove(i);
            }
        }
    }

    // success
    private static void test4(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                iterator.remove();
            }
        }
    }

    //  存在问题  acd 没删除掉
    private static void testList(List<String> list) { // for
        for(int i=0;i<list.size();i++){
            if(list.get(i).contains("a")){
                list.remove(i);
            }
        }
    }

    //success
    private static void testList2(List<String> list) { // for
        for(int i=0;i<list.size();i++){
            if(list.get(i).contains("a")){
                list.remove(i);
                i--;
            }
        }
    }

    public static void main(String[] args) {

        Vector<Integer> vector = new Vector<>();
        vector.add(1);
        vector.add(2);
        vector.add(3);
        vector.add(4);
        vector.add(5);
        vector.add(6);
        test4(vector);
        System.out.println(vector);

        List<String> list=new ArrayList<String>();
        list.add("asdf");
        list.add("bbb");
        list.add("aaa");
        list.add("acd");
        list.add("da");

        List<String> list2 = new ArrayList<String>();
        list2.addAll(list);
        testList(list);
        System.out.println(list);

        testList2(list2);
        System.out.println(list2);
    }
}

在这里插入图片描述
解决办法:在使用iterator进行增删操作的时候,加上Lock或者synchronized同步措施或者并发容器。
或者使用iterator自带的删除,例如test4

3.2、HashMap的线程安全类:HashTable

使用举例:

//定义
private static Map<Integer, Integer> map = new Hashtable<>();
//多线程调用方法
private static void update(int i) {
    map.put(i, i);
}

源码分析:

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

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

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;
}

3.3、Collections类中的相关同步方法

Collections类中提供了一系列的线程安全方法用于处理ArrayList等线程不安全的Collection类

在这里插入图片描述
使用方法:

//定义
private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
//多线程调用方法
private static void update(int i) {
    list.add(i);
}

源码分析:
内部操作的方法使用了synchronized修饰符,性能比JUC中的并发容器性能查,不推荐使用。

 static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {

        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

 }

猜你喜欢

转载自blog.csdn.net/qq_21918021/article/details/88735717