Java容器与容器安全

Java容器与容器安全

Java容器关系图

idea使用快捷键 ctrl+h 可以查看类之间的继承关系,以上根据类继承接口的关系所绘,其中标绿色的是接口,深蓝色的是线程不安全的实现类,浅蓝色的是线程安全的实现类。

List与List不安全

只看图中的四个List实现类,正常情况下使用ArrayList

List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.forEach(e-> System.out.println(e));

同样的,你也可以这样使用 LinkedList (LinkedList与ArrayList均是List的实现,不同的是ArrayList使用数组实现,支持随机访问,而LinkedList使用链表实现)

 List<String> list = new LinkedList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.forEach(e-> System.out.println(e));

上面的代码均会顺序打印 a,b,c 如下,无论运行100次还是10000次都是如此

a
b
c

在单线程情况下,该程序无论如何都不会出错,但在多线程情况下也如此吗?

我们来编写代码验证一下

List<String> list = new ArrayList<>();
        for(int i = 0;i < 6;i++){
            int finalI = i;
            new Thread(()->{
                    list.add("thread"+ finalI);
                    System.out.println("线程"+finalI+list);
                },"thread"+i
            ).start();
        }

多试几次,会出现程序并发修改异常 ConcurrentModificationException ,为什么会出现这个错误呢,根据打印的错误栈追踪可以看到下列源码

if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

当修改次数与希望的修改次数不一致时则抛异常,即***多线程情况下该线程的期望修改次数与程序实际的修改次数不一致*** ,那么怎么解决这个问题呢

方法一:使用 Vector 类
List<String> list = new Vector<>();//修改容器为 Vector
        for(int i = 0;i < 6;i++){
            int finalI = i;
            new Thread(()->{
                    list.add("thread"+ finalI);
                    System.out.println("线程"+finalI+list);
                },"thread"+i
            ).start();
        }

多次运行也不会出现异常,我们查看 Vector 类的 add 方法

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

发现该方法给 synchronized 加了锁,保证了容器每次只有单个线程操作

方法二:使用 Collections 工具类
//通过 Collections的synchronizedList()方法来获得一个线程安全的List容器
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for(int i = 0;i < 6;i++){
            int finalI = i;
            new Thread(()->{
                    list.add("thread"+ finalI);
                    System.out.println("线程"+finalI+list);
                },"thread"+i
            ).start();
        }

我们查看 synchronizedList() 方法的源码

public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

发现该方法 return 部分是一个三元表达式,会查看传如的类是否支持随机访问,支持则返回一个支持随机访问的容器。

继续查看 add 方法

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

发现同样使用了synchronized 关键字

方法三:使用 CopyOnWriteArrayList

使用加锁固然简单,但会在频繁写入的时候出现访问慢的问题,那么有没有能在写入时候访问对象的呢

List<String> list = new CopyOnWriteArrayList<>();
for(int i = 0;i < 6;i++){
    int finalI = i;
    new Thread(()->{
            list.add("thread"+ finalI);
            System.out.println("线程"+finalI+list);
        },"thread"+i
    ).start();
}

我们查看该容器的 add 方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); //加锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); //复制
        newElements[len] = e; //修改
        setArray(newElements); //写入原件
        return true;
    } finally {
        lock.unlock();
    }
}

通过可重入锁来支持多个线程访问写入,先加锁,然后复制修改并写入原件;虽然该方法支持多个线程访问,但它每次写入都会完全复制一份原件,会消耗大量的空间

Set与Set不安全

		Set<String> set = new HashSet<>();
        //Set<String> set = new LinkedSet<>();
        set.add("a");
        set.add("a");
        set.add("b");
        set.forEach(e -> System.out.println(e));

HashSet,LinkedSet,TreeSet都是线程不安全的Set,因为他们底层实现是HashMap,LinkedMap,TreeMap,通过Map的key的不可重复的特性来实现的Set

同样在多线程的情况下

Set<String> set = new HashSet<>();
for(int i = 0;i < 100;i++){
        new Thread(()->{
            set.add(UUID.randomUUID().toString().substring(0,4));
            System.out.println(set);
        },"thread"+i
    ).start();
}

多次运行可能会出现 ConcurrentModificationException 的异常

解决方法,类比上面的 List 的解决方法

方法一:使用Collections工具类
Set<String> set = Collections.synchronizedSet(new HashSet<>());
for(int i = 0;i < 100;i++){
        new Thread(()->{
            set.add(UUID.randomUUID().toString().substring(0,4));
            System.out.println(set);
        },"thread"+i
    ).start();
}

查看synchronizedSet()方法的源码

public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

发现它会new一个线程安全的Set容器

方法二:使用CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();
for(int i = 0;i < 100;i++){
        new Thread(()->{
            set.add(UUID.randomUUID().toString().substring(0,4));
            System.out.println(set);
        },"thread"+i
    ).start();
}

同样追踪源码,发现与上面的CopyOnWriteArrayList的实现方式一致

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock(); //可重入锁锁定
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);//拷贝复制
        newElements[len] = e; //修改
        setArray(newElements);//写入原数组
        return true;
    } finally {
        lock.unlock();
    }
}

Map与Map不安全

Map是一种 k,v 数据结构,初始化大小为16,扩容因子为0.75,当达到扩容因子设置的阈值(16*0.75=12)时,大小会扩容一倍(即从16扩容至32)

jdk1.7时HashMap的实现是经典的哈希表实现,即数组+链表;从1.8开始,实现变成了数组+链表+红黑树

Map<String,String> map = new HashMap<>();
//Map<String,String> map = new LinkedHashMap<>();
//Map<String,String> map = new TreeMap<>();
map.put("a","aaa");
map.put("b","bbb");
map.put("c","ccc");
System.out.println(map);

使用这几种Map运行上面的这段程序,永远不会出问题

老生常谈,多线程情况下

//HashMap TreeMap LinkedHashMap 均会出现ConcurrentModificationException的错误
Map<String,String> map = new LinkedHashMap<>();
for(int i = 0;i < 100;i++){
        new Thread(()->{
            map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
            System.out.println(map);
        },"thread"+i
    ).start();
}

运行这段代码会出现ConcurrentModificationException的错误

方法一:使用HashTable解决
Map<String,String> map = new Hashtable<>();
for(int i = 0;i < 100;i++){
        new Thread(()->{
            map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
            System.out.println(map);
        },"thread"+i
    ).start();
}

HashTable是一个线程安全的Map

public synchronized V put(K key, V value)

查看源码发现它使用了 synchronized 关键字来解决多线程访问的问题

方法二:使用ConcurrentHashMap解决
Map<String,String> map = new ConcurrentHashMap<>();
for(int i = 0;i < 100;i++){
        new Thread(()->{
            map.put(UUID.randomUUID().toString().substring(0,2),UUID.randomUUID().toString().substring(0,2));
            System.out.println(map);
        },"thread"+i
    ).start();
}

ConcurrentHashMap使用的是分段锁来保证多线程情况下可以并发的执行put操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值