盘点java中线程安全的组件

前言

在java中有很多已经为我们提供好的线程安全的组件,在并发中使用这些组件并不用担心线程安全的问题,使用起来很方便,在本篇本章中我主要总结了八种线程的组件,分别是StringBuffer,HashTableConcurrentHashMapVector ,CopyOnWriteArrayList ,BlockingQueue,Collection.synchronList 和所有的原子类

下面本将介绍其的使用方法以及其为什么是安全的。

StringBuffer

StringBuffer是java中一个线程安全的可变字符序列,可以用来进行字符串的拼接操作。提到StringBuffer就不得不提StringBuilder,二者功能类似,不过StringBuilder速度会更快,StringBuffer速度会慢一些因为里面加了synchronized锁来保证安全。但要注意这二者都比直接使用String进行字符串拼接要快得多。

一些基本的使用:

    public static void main(String[] args) {
        StringBuffer stringBuffer = new StringBuffer();
        //拼接字符串"abc"
        stringBuffer.append("abc");
        //将前两个个字符换为"asd"
        stringBuffer.replace(0,2,"asd");
        //删除第一个字符
        stringBuffer.deleteCharAt(0);
        //获取当前字符串的长度
        System.out.println(stringBuffer.length()); ;
    }

为什么是安全的,StringBuffer之所以是安全的就是因为其中加入了锁,从源码中我们可以看到

    @Override
    public synchronized int length() {
        return count;
    }

    @Override
    public synchronized int capacity() {
        return value.length;
    }

    @Override
    public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);
    }
    @Override
    public synchronized void trimToSize() {
        super.trimToSize();
    }

    ......

StringBuffer中各种方法都加了synchronized锁。

HashTable

Hashtable 和HashMap一样,也是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable 的函数都是同步的,同样是加了synchronized锁,因此它是线程安全的。

它的key、value都不可以为null。并且Hashtable中的映射不是有序的。

Hashtable的使用方法和HsahMap类似,都是对散列表的一些操作

    public static void main(String[] args) {
        Hashtable<Integer,String> hashtable = new Hashtable<>();
        hashtable.put(1,"aaa");
        hashtable.put(2,"bbb");
        hashtable.get(1);
        hashtable.clear();
    }

从源码中我们同样可以看到加了锁,变为了同步的。


public synchronized V get(Object key) {
       ……
    }


public synchronized V put(K key, V value) {
       ……
    }

public synchronized V remove(Object key) {
     ……
    }

但是这种方案虽然实现了线程安全,但相当于给整个哈希表都加了一把锁,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用,推荐使用ConcurrentHashMap

ConcurrentHashMap

上面提到了Hashtable和hashmap类似也是散列表,其实想要使用线程安全的散列表还可以用ConcurrentHashMap ,它是HashMap的线程安全版本,内部也是使用了数组+链表+红黑树。相比于同样线程安全的HashTable来说,效率等各方面都有极大地提高

其使用就不再多说,下面来看其为什么是线程安全的。

在jdk1.7中,ConcurrentHashMap的底层实现是用的数组加链表的方式,并且是采用分段锁的方式来保证线程安全。不过在jdk1.8改变了数据结构中是用数组+链表+红黑树的方式来实现,并且也放弃了原有分段锁的这种方法,采用了cas+synchronized的方式来实现线程安全。

在1.8中一旦因为哈希冲突产生的链表长度超过8就会将节点自动转化为红黑树,

  • 首先ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,并且这样锁得范围小,极大提高了性能。
  • 其次使用无锁的 CAS 操作来管理其内部结构的一部分,特别是在进行某些关键的更新操作时。CAS 操作允许一个线程在修改变量之前检查变量没有被其他线程改变。这种技术有助于在没有全面加锁的情况下保证数据的一致性。
  • 最后,为了保证多线程下得可见性,一些关键字段还加了volatile修饰。使用 volatile变量意味着写入这些变量的值会立即被更新到主内存中,而读取操作会从主内存中进行,这保证了内存的可见性。

下面来看一个例子,例如ConcurrentHashMap中put方法。其大致分为以下几步。

  • 首先会根据key算出其哈希值,随后判断是否需要初始化
  • 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入
  • 判断是否需要扩容
  • 如果以上的情况都不满足则利用synchronized锁进行写入数据
  • 最后判断是否需要转化为红黑树
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //根据key算出哈希值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; 
        //判断是否需要初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                    new Node<K,V>(hash, key, value, null)))
                break;                   
        }
        //如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //如果都不满足,则利用 synchronized 锁写入数据
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
        }
        addCount(1L, binCount);
        return null;
    }

其他方法中也是进行了一些类似的操作,利用cas和synchronized锁来保证安全。

Vector

Vector 是 Java 中的一个线程安全的动态数组(动态数组也称为可变数组),它实现了 List 接口,可以动态地增加或减少元素的大小。与 ArrayList 类似,但 Vector 是同步的,即它的所有操作都是线程安全的。

基本使用:


    public static void main(String[] args) {
        Vector<Integer> vector = new Vector<>();
        // 添加元素
        vector.add(1);
        vector.add(2);
        vector.add(3);
        // 获取元素
        System.out.println("Element at index 1: " + vector.get(1));
        // 删除元素
        vector.remove(0);
        // 遍历元素
        for (Integer element : vector) {
            System.out.println("Element: " + element);
        }
    }

与上面几个很像,Vector 的方法也都使用了 synchronized 关键字进行同步化,例如 add()get()remove() 等方法,这意味着每次只允许一个线程访问该方法,并且会对整个方法体进行加锁,防止其他线程同时访问。

例如:

   public synchronized void setSize(int newSize) {
      ....
    }
    public synchronized int capacity() {
        ....
    }

    public synchronized int size() {
      ....
    }
    public synchronized boolean isEmpty() {
     ......
    }

CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 集合框架中的一个并发集合类,它实现了 List 接口,并提供了一种线程安全的并发访问机制。与 VectorArrayList 不同的是,CopyOnWriteArrayList 的线程安全机制不是通过同步化来实现的,而是通过复制(copy-on-write)机制来保证线程安全性。

当需要进行写操作时(添加、修改、删除元素),CopyOnWriteArrayList 会先复制当前的数组,然后在副本上进行操作,最后将修改后的副本替换原来的数组。这样做的好处是,读操作可以在不受影响的情况下继续进行,不需要加锁或同步。

CopyOnWriteArrayList 适用于读操作频繁、写操作相对较少的场景。由于写操作会触发数组的复制,因此写操作的开销比较大,不适合在写操作频繁的情况下使用。

但是由于写操作会触发数组的复制,因此 CopyOnWriteArrayList 不支持修改操作,如 set() 方法。如果尝试修改集合中的元素,会抛出 UnsupportedOperationException 异常。

基本使用:


    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        // 添加元素
        list.add(1);
        list.add(2);
        list.add(3);
        // 遍历元素
        for (Integer element : list) {
            System.out.println("Element: " + element);
        }
    }

BlockingQueue

BlockingQueue(阻塞队列)它表示一种线程安全的队列,支持在队列为空或队列已满时阻塞线程,直到条件满足为止。BlockingQueue 接口提供了一系列的方法,用于向队列中添加元素、从队列中获取元素以及查询队列的状态信息。

基本使用:利用BlockingQueue 实现生产者消费者模式

public class ProducerConsumerExample {
    private static final int CAPACITY = 5;
    private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(CAPACITY);

    static class Producer implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    Integer value = queue.take();
                    System.out.println("Consumed: " + value);
                    Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        Thread producer = new Thread(new Producer());
        Thread consumer = new Thread(new Consumer());
        
        producer.start();
        consumer.start();
    }
}

为什么它是安全的呢?

首先,BlockingQueue 的实现类在队列操作中通常使用了原子性的操作,如 CAS(Compare and Swap)等,确保了多线程环境下的线程安全性。这意味着队列的添加(入队)和获取(出队)操作都是原子性的,不会发生竞态条件。

并且应用了内部锁ReentrantLock来保证线程安全。例如源码中offferFirst方法:

    public boolean offerFirst(E e) {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return linkFirst(node);
        } finally {
            lock.unlock();
        }
    }

原子类

Java中提供了一些原子类,原子类包装了一个变量,并且提供了一系列对变量进行原子性操作的方法。我们在多线程环境下对这些原子类进行操作时,不需要加锁,大大简化了并发编程的开发。

目前Java中提供的原子类大部分底层使用了CAS锁(CompareAndSet自旋锁),如AtomicInteger、AtomicLong等;也有使用了分段锁+CAS锁的原子类,如LongAdder等。

Collections工具类

Java提供一组包装方法,将一个普通的基础容器包装成一个线程安全的同步容器。

例如通过Collections.synchronizedSortedSet 包装方法能将一个普通的SortedSet容器包装成一个线程安全的SortedSet同步容器。

synchronizedList()方法将基础List包装成线程安全的列表容器

synchronizedMap()方法将基础Map容器包装成线程安全的容器

synchronizedCollection()方法将基础Collection容器包装成线程安全的Collection容器

其实现方式都是通过加上synchronized锁来实现,因此也具有性能不好的缺点,因此也不推荐使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值