多线程之常用线程安全类型分析

本文介绍了Java中线程安全的数据类型,如线性数据结构的List、Set、Queue及其子类,以及字典结构的Map和Dictionary。重点讨论了ArrayList和LinkedList的线程安全问题,提出了解决方案,如使用Vector、Collections.synchronizedXXX、Arrays.asList和CopyOnWriteArrayList。同时,文章还提到了HashMap、LinkedHashMap和ConcurrentHashMap在多线程环境下的表现和选择。此外,文章还涵盖了ThreadLocal、并行Stream和线程间通信的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

本文一起看下在日常工作中我们经常用到的线程安全的数据类型,以及一些经验总结。

1:常用线程安全数据类型

1.1:jdk集合数据类型

jdk的集合数据类型分为两类,一种是线性数据结构,另外一种是字典结构,分别看下。

1.1.1:线性数据结构

线性数据结构主要包括List,Set,Queue,其中List是一种允许数据重复的顺序数据结构,Set是一种不允许数据重复的数据结构,Queue是一种队列的数据结构,接口如下:

public interface List<E> extends Collection<E> {}
public interface Set<E> extends Collection<E> {}
public interface Queue<E> extends Collection<E> {}

二者都继承了相同的集合类结构Collection,如下:

public interface Collection<E> extends Iterable<E> {}

Collection接口🈶继承了jdk rt.jar包中的java.lang.Iterable接口,所以又是支持迭代的。

  • List
    List的主要子类包括ArrayList,LinkedList,Vector,Stack(Vector子类)
  • Set
    Set主要子类有HashSet,LinkedSet,TreeSet
  • Queue
    接口定义如下:
public interface Queue<E> extends Collection<E> {
    // 向队列添加元素,如果是有空间则插入成功,返回true,否则抛出IllegalStateException
    boolean add(E e);

    // 向队列添加元素,如果是有空间则插入成功,返回true,否则返回false(这里不同于add方法抛出异常)
    boolean offer(E e);

    // 获取并且删除队列头元素,如果是队列为空则抛出NoSuchElementException异常
    E remove();

    // 获取并且删除队列头元素,如果是队列为空则返回null(不同于remote抛出NoSuchElementException异常)
    E poll();

    // 获取但是不删除队列头元素,如果是队列为空则抛出NoSuchElementException异常
    E element();

    // 获取但是不删除队列头元素,如果是队列为空则返回null(不同于element抛出NoSuchElementException异常)
    E peek();
}

双向队列子接口Deque,一种双端都可以入队和出队的数据结构,源码如下:

public interface Deque<E> extends Queue<E> {
    // 在deque头添加元素,如果有空间则返回true,否则抛出IllegalStateException异常
    void addFirst(E e);

    // 从deque尾入队,如果成功则返回true,如果空间不足插入失败则抛出IllegalStateException异常
    void addLast(E e);

    // 在deque头添加元素,如果有空间则返回true,否则返回false(优于addFirst抛出IllegalStateException异常)
    boolean offerFirst(E e);

    // 从deque尾入队,如果成功则返回true,如果空间不足插入失败则返回false(优于addLast抛出IllegalStateException异常)
    boolean offerLast(E e);

    // 从deque中获取并且删除头元素,如果dequeu为空则抛出NoSuchElementException异常
    E removeFirst();

    // 获取并且删除deque尾元素,如果是deque没有元素则抛出NoSuchElementException异常
    E removeLast();

    // 从deque中获取并且删除头元素,如果dequeu为空则返回null(不同于removeFirst抛出NoSuchElementException异常)
    E pollFirst();

    // 获取并且删除deque尾元素,如果是deque没有元素则返回null(不同于removeLast抛出NoSuchElementException异常)
    E pollLast();

    // 获取deque头元素,如果deque为空,则抛出NoSuchElementException异常
    E getFirst();

    // 获取deque尾元素,如果deque为空,则抛出NoSuchElementException异常
    E getLast();

    // 获取deque头元素,如果deque为空,则返回null(不同于getFirst抛出NoSuchElementException异常)
    E peekFirst();

    // 获取deque尾元素,如果deque为空,则返回null(不同于getLast抛出NoSuchElementException异常)
    E peekLast();

    // 从deque中删除首次出现的指定元素,如果存在则删除并返回true
    boolean removeFirstOccurrence(Object o);

    // 从deque中删除最后出现的指定元素,如果存在则删除并返回true
    boolean removeLastOccurrence(Object o);

    // *** 队列方法,在父类Queue中已经定义了,这里为什么要重复定义??? ***

    // 在deque尾添加元素,成功返回true,没有可用空间则抛出IllegalStateException异常
    boolean add(E e);

    // 在deque尾添加元素,成功返回true,没有可用空间则返回false(不同于add抛出IllegalStateException异常)
    boolean offer(E e);

    // 从deque获取并且删除首个元素,如果deque为空则抛出NoSuchElementException
    E remove();

    // 从deque获取并且删除首个元素,如果deque为空则返回null(不同于remove抛出NoSuchElementException)
    E poll();

    // 从deque头获取元素,但是不删除,如果是deque为空则抛出NoSuchElementException异常
    E element();

    // 从deque头获取元素,但是不删除,如果是deque为空则返回null(不同于element抛出NoSuchElementException异常)
    E peek();


    // *** 栈相关方法定义 ***

    // 入栈,这里就是将元素添加到deque的头,如果没有可用空间则抛出IllegalArgumentException,该方法同addFirst,但是push方法更加能够对应栈的入栈操作
    void push(E e);

    // 出栈,即获取deque头的元素,如果deque为空则抛出NoSuchElementException异常,该方法同removeFirst()
    E pop();


    // *** 集合方法,看来Deque是一个线性数据结构大杂烩,定义了常见线性数据结构的各种操作 ***

    // 删除首次出现的指定元素,同#removeFirstOccurrence(Object)方法
    boolean remove(Object o);

    // 判断deque中指定元素是否存在
    boolean contains(Object o);

    // 获取deque元素的个数
    public int size();

    // 获取deque对应的迭代器,按照head(first)->tail(last)的顺序获取元素
    Iterator<E> iterator();

    // 获取deque对应的倒叙迭代器,按照tail(last)->head(first)的顺序获取元素
    Iterator<E> descendingIterator();
}

其实LinkedList实现了Deque接口,所以我们不仅可以将LinkedList当做Collection来使用,也可以把其当做Stack,Queue,Deque来使用,如下当做队列Queue使用:

// linkedlist当做queue使用
private static void useAsQueue() {
    Queue<String> queueList = new LinkedList<>();
    // 入队
    queueList.offer("xxxx");
    queueList.offer("yyyy");
    // 出队
    System.out.println(queueList.poll());
    System.out.println(queueList.poll()); // 空了
    System.out.println(queueList.poll());
}

运行结果:
xxxx
yyyy
null

当做Stack使用:

private static void userAsStack() {
    // Deque中定义了stack相关的方法,所以引用使用java.util.Deque
    Deque<String> stack = new LinkedList<>();
    stack.push("aaaa");
    stack.push("bbbb");
    System.out.println(stack.pop());
    System.out.println(stack.pop());
}

运行结果:
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ gogogo ---
bbbb
aaaa

1.1.2:字典结构

字典结构相关有两个顶层类是Map(是个接口),Dictionary(是个抽象类),如下:

public interface Map<K,V> {}
public abstract class Dictionary<K,V> {}

分别看下。

  • Map
    主要子类HashMap,LinkedHashMap,TreeMap。
  • Dictionary
    主要子类HashTable,Properties,其中Properties是HashTable的子类,另外Properties有个需要小心,即如果是value是int时,可以插入数据,但是当get时会返回null,如下测试:
public static void main(String[] args) {
//        useAsQueue();
//        userAsStack();
    Properties p = new Properties();
    p.put("name", "jack");
    p.put("age", 90);
    System.out.println(p.getProperty("name"));
    System.out.println(p.getProperty("age"));
}

运行结果:
jack
null

如果不小心掉到了properties的这个坑里还真是不好发现😭😭😭。

1.2:List分析

不管是ArrayList还是LinkedList,都不是线程安全的数据结构,存在线程安全问题:

1:写写冲突
    当多个线程并发写时可能会出现数据覆盖的问题,比如线程A和线程B都修改位置2的元素,最终到底是谁设置成功就不一定了,特别是对于+1场景会导致少1
2:读写冲突
    当读时写可能会产生不可预期的后果,当出现读的过程中发现元素个数发生变化的情况,将会抛出ConcurrentModificationException异常。

那么,如何实现List的线程安全呢?分别来看下。

1.2.1:使用Vector

Vector通过synchronized关键字对方法上对象锁,实现了线程安全,即串行执行,效率堪忧。该方法也是List接口的子类,方法签名如下:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

1.2.2:使用Collections.synchronizedXXX方法

在这里插入图片描述

使用Collections.synchronizedXXX方法转换为线程安全的集合对象,其实就是对其进行简单包装,方法加上synchronized关键字,所以其本质上通Vector,以synchronizedList为例:

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

返回的是SynchronizedList包装类,该类方法如下:

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

public int indexOf(Object o) {
    synchronized (mutex) {return list.indexOf(o);}
}

可以看到都是使用synchronized关键字将具体集合方法的调用放在了同步代码块中。

1.2.3:使用Arrays.asList

该方法返回的是Arrays内部类ArrayList,而非java.util.ArrayList,方法仅仅实现了读取操作和set操作,因此如果是调用add,remove等方法将会调用到AbstractList,会抛出UnsupporedOperationException,如下:

// java.util.Arrays.ArrayList
private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {}

测试代码:
List<String> list = Arrays.asList("aa", "bb");
list.add("ccc");

运行结果:
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.AbstractList.add(AbstractList.java:148)
	at java.util.AbstractList.add(AbstractList.java:108)
	at dongshi.daddy.Huohuo.main(Huohuo.java:17)

从调用栈也可以看出该异常是AbstractList抛出的,其实就是如下方法:

// java.util.AbstractList#add(int, E)
public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

1.2.4:使用Collections.unmodifiableXxxx

在这里插入图片描述

Collections.unmodifiableXxxx方法会返回一个包装类,该类只允许查看数据,不允许更新数据,也不允许排序,因为排序本质上也是一种修改数据的操作,以unmodifiableList为例如下:

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
            new UnmodifiableRandomAccessList<>(list) :
            new UnmodifiableList<>(list));
}

返回的包装类是UnmodifiableList,该类针对修改相关的操作统一抛出UnsupportedOperationException,如下:

public E set(int index, E element) {
    throw new UnsupportedOperationException();
}
public void add(int index, E element) {
    throw new UnsupportedOperationException();
}
public E remove(int index) {
    throw new UnsupportedOperationException();
}
...
public boolean addAll(int index, Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public void replaceAll(UnaryOperator<E> operator) {
    throw new UnsupportedOperationException();
}
@Override
public void sort(Comparator<? super E> c) {
    throw new UnsupportedOperationException();
}

1.2.5:使用CopyOnWriteArrayList

在这里插入图片描述

使用COW,即写时复制方式实现线程安全的集合类,在读时不锁,写时写锁,本事上是一种读写分离思想的运用,读取数据是最终一致的,看下修改方法:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 重入锁上锁
    lock.lock();
    try {
        // 获取当前的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 拷贝原数组并将长度+1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 添加新元素
        newElements[len] = e;
        // 这里设置到private transient volatile Object[] array;注意其是volatile的,根据“对volatile变量的写操作 先行发生于 对volatile的读操作“,后续线程将直接能够读到这里设置的最新值
        setArray(newElements);
        return true;
    } finally {
        //  解锁
        lock.unlock();
    }
}

迭代器的实现也是使用快照,如下:

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        // 这里虽然是引用传递,但是因为如果是原集合被修改,通过COW会创建新数组,所以不会改变这里的值
        // ,即不会破坏这里的快照
        cursor = initialCursor;
        snapshot = elements;
    }
}

适合场景:读锁写少

1.3:Map分析

1.3.1:HashMap

初始容量16,扩容*2,负载因子0.75,jdk8引入红黑树解决hash冲突,当冲突链长度到8,数组长度到64后,升级冲突链表为红黑树。非线程安全,可能如下线程安全问题:

1:读写冲突
2:扩容数据读取导致死循环(严重)
3:keys无序问题

其中2是存在扩容节点重分布导致出现两个节点互为next的情况,进而导致CPU,耗尽CPU资源。3keys方法获取键是通过如下方式循环获取的:

在这里插入图片描述

因此在扩容,节点重分布的过程中导致错误。

1.3.2:LinkedHashMap

继承自HashMap,如下:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{}

在HashMap基础上增加了链表结构维护数据插入的先后顺序,但也有线程安全问题。

1.3.3:ConcurrentHashMap

juc 中提供的HashMap的线程安全版本,在jdk7中使用分段的方式来实现线程安全,如下:

在这里插入图片描述

在这里插入图片描述

jdk7中虽然通过Segment使加锁的概率降低,但是还是有锁,在jdk8中使用基于cas的乐观锁技术进行改版,如下:

在这里插入图片描述

2:其它知识点

2.1:ThreadLocal

在一个线程内传递变量的机制,每个线程独立,通过set方法设置变量,通过get方法获取变量。

在这里插入图片描述

2.2:并行stream

通过添加.parallel以并行的方式来执行集合操作,底层使用的是JUC提供的多线程实现相关功能,如下:

在这里插入图片描述

2.3:加锁需要考虑的问题

1:锁的粒度如何控制
2:使用公平锁还是非公平锁
3:是否需要考虑自旋的情况
4:是否需要考虑重入
5:当前场景适合是用什么锁,适合如何加锁(脱离场景讨论问题都是耍流氓)
6:加锁后是否会严重影响程序性能

2.4:线程间通信

1:Thrad.join
2:Object,wait/notify/notifyAll
3:JUC工具类
    Semaphore,CountDownLatch,CyclicBarrier

写在后面

参考文章列表

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值