集合类不安全和写入时复制集合

ArrayList为什么是线程不安全

Arraylist是通过一个Object的数组elementData来实现的,而size是用来保存当前数组已经添加了多少元素。

add方法

add方法主要是三个步骤

1、当对元素进行添加是,通过ensureCapacityInternal方法来判断容器(数组)的容量是否足够,size+1如果超过了数组的容量,则进行扩容操作

2、将元素添加到数组中

3、size++

所以这里如果是多线程的环境下,便会产生问题

1.空值和值覆盖的问题(这里假设两条线程A线程,B线程)

【1】size为0,也就是数组一开始是0

【2】线程A开始添元素,检测到size+1为1,不需要进行扩容,进行元素添加elementData[0]=A,那么就将A放在索引为1的位置上,此时B线程进来,检测到size还是等于0,不需要扩容,那么也将值B放在elementData下标为0的位置上

【3】此时A线程执行size++,将size变成1,B线程执行size++,将size变成2

这样在AB线程执行完之后elementData[0]=B(A线程的值被覆盖),但是elementData[1]=null,size = 2,那么下次再次进入add方法的时候size的值就是2

2.数组下标越界(ArrayIndexOutOfBoundsException)

【1】列表的大小为9,也就是size= 9

【2】线程A 进入add方法,所以获得的size值为9,进入ensureCapacityInternal方法发现并不需要扩容

【3】线程B此时也进入add方法,它获得的size也是9,进入ensureCapacityInternal方法发现也并不需要扩容

【4】线程A开始进行设置值操作, elementData[9] = e 操作,并且size++。此时size变为10。

【5】线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

package com.qjx.unsafe;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

public class ArrayListTest {
    static List<Integer> list = new ArrayList<>(10);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            protoTest();

        }, "A").start();

        new Thread(() -> {
            protoTest();
        }, "B").start();
        Thread.sleep(1 * 1000);
        System.out.println("数组的总长度为:" + list.size());
    }

    public static void signalTest() {
        List<String> list = Arrays.asList("a", "b", "c", "d");
        list.forEach(System.out::println);
    }

    public static void protoTest() {
        for (int i = 0; i < 1000000; i++) {
            list.add(i);
        }
    }

}

解决集合类不安全的三种方法

1.使用Vector

package com.qjx.unsafe;

import java.sql.Time;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

/*
解决集合不安全的方法
 */
public class SolveList {
   static List<Integer> list = new Vector<>();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            test01();
        },"A").start();
        new Thread(()->{
            test01();
        },"B").start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(list.size());
    }

    //第一种方法使用
    public static void test01(){

        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }
    }
}

2.使用Collections.synchronizedList()

package com.qjx.unsafe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/*
 解决集合类不安全的第二种方法
 */
public class SolveListCollections {
   static List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());

    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            test01();
        },"A").start();

        new Thread(()->{
            test01();
        },"B").start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(list.size());
    }

    public static void test01(){
        for (int i = 0; i <1000 ; i++) {
            list.add(i);
        }
    }

}

3.使用CopyOnWriteArrayList

package com.qjx.unsafe;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

public class SolveListCopyOnWriteList {
    static List<Integer> list = new CopyOnWriteArrayList<Integer>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            test01();
        },"A").start();

        new Thread(()->{
            test01();
        },"B").start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(list.size());
    }

    public static void test01(){
        for (int i = 0; i <1000 ; i++) {
            list.add(i);
        }
    }
}

写入时复制(CopyOnWrite)思想
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array


CopyOnWriteArrayList为什么并发安全且性能比Vector好

我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况

线程不安全的类HashSet

package com.qjx.unsafe;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

public class UnSafeSet {
    static Set<String> set =new HashSet<>();
    public static void main(String[] args) {

        for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();

        }
    }
}

解决的方法

1.Collections.synchronizedSet(new HashSet<>())

package com.qjx.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

public class UnSafeSet {
    //解决set不安全的第一种方法
    static Set<String> set = Collections.synchronizedSet(new HashSet<>());
    //static Set<String> set =new HashSet<>();
    public static void main(String[] args) {

        for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();

        }
    }
}

2.使用CopyOnWriteArraySet<>()

package com.qjx.unsafe;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

public class UnSafeSet {
    //解决set不安全的第一种方法
    //static Set<String> set = Collections.synchronizedSet(new HashSet<>());
    //static Set<String> set =new HashSet<>();

    //解决set不安全的第二种方法
    private static Set<String> set = new CopyOnWriteArraySet<>();
    public static void main(String[] args) {

        for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();

        }
    }
}

HashSet底层是通过HashMap实现的,那么hashmap也是线程不安全的

Set<String> set = new HashSet<>();
// 点进去
public HashSet() {
    map = new HashMap<>();
} 


// add方法 就是map的put方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
} 

//private static final Object PRESENT = new Object(); 不变得值

线程不安全的类HashMap

package com.qjx.unsafe;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class UnSafeHashMap {
    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>(16,0.75f);
        for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();

        }
    }
}

HashMap为什么是线程不安全的?

假设这里有两条线程同时判断到hash桶上的第一个元素是为null,因为都是为null,所以两条线程都执行了插入,这里的话插入快的线程的值就会被其他线程的值所覆盖。

解决的方法ConcurrentHashMap

package com.qjx.unsafe;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class UnSafeHashMap {
    public static void main(String[] args) {
        //Map<String,String> map = new HashMap<>(16,0.75f);
        Map<String,String> map = new ConcurrentHashMap<>();
        for (int i = 0; i <30 ; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();

        }
    }
}

在1.7之前的话使用的segment数组+entry数组+分段锁的形式出现,首先会先通过hash算法计算出segment数组的下标,每个segment有自己的锁,所以我们在将元素加入到entry数组的时候就是线程安全的,这样就比HashTable单纯的使用synchroized效率高

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值