JavaEE 初阶篇-线程安全的集合类、多线程环境使用 ArrayList、队列、哈希表(HashMap 、ConCurrentHashMap 、HashTable 的区别)

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍
 

文章目录


        1.0 线程安全的集合类

        对于原来熟悉的集合类,大部分都是不是线程安全的。

        1.2 线程安全的集合类 – Vector

        实现了动态数组的数据结构,可以根据需要动态增长或缩小。

        Vector 是一个线程安全的集合类主要来自以下几个方面:

        1)同步方法:Vector 中的所有方法都使用了 synchronized 关键字进行同步,确保在多线程环境下对集合的操作是线程安全的。这意味着在对 Vector 进行增删改查等操作时,会对整个集合对象进行加锁,从而保证同一时刻只有一个线程能够访问集合。

        2)迭代器同步:Vector 的迭代器是通过同步方法 synchronized 来实现的,这样在迭代过程中,其他线程无法修改集合,避免了并发修改异常。

        1.3 线程安全的集合类 – Stack

        是 Java 中表示堆栈(栈)数据结构的类,它继承自 Vector 类,因此也是线程安全的集合类。所有方法都使用了 synchronized 关键字进行同步,确保在多线程环境下对栈的操作是线程安全的。

        1.4 线程安全的集合类 – HashTable

        HashTable 是基于哈希表的数据结构,使用键值对存储数据,类似于 HashMap。键和值都不允许为 null ,并且 HashTable 的键值对是无序的。

        是一个线程安全的集合类,所有对 HashTable 的操作都是同步的,即线程安全的。在每个公共方法中都使用了 synchronized 关键字,确保多线程环境下的并发访问是安全的。

        2.0 多线程环境使用 ArrayList

        2.1 对 ArrayList 关键方法手动加锁

        自己使用同步机制,自己手动对关键操作方法加上锁。如 synchronized 或者 ReentrantLock 。

        2.2 利用 Collections.synchronized(new ArrayList) 方法创建

创建方式:

List<String> synchronizedList = Collections.synchronizedList(normalList);

代码如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {
    public static void main(String[] args) {
        // 创建一个普通的 ArrayList
        List<String> normalList = new ArrayList<>();

        // 使用 Collections.synchronizedList 方法创建一个线程安全的 List
        List<String> synchronizedList = Collections.synchronizedList(normalList);

        // 在多线程环境下操作 synchronizedList 是线程安全的
        // 可以对 synchronizedList 进行添加、删除、遍历等操作而不用担心线程安全性

        // 示例:在多线程环境下添加元素
        Runnable addTask = () -> {
            synchronizedList.add("Element");
            System.out.println("Added element by thread: " + Thread.currentThread().getName());
        };

        // 创建多个线程来添加元素
        Thread thread1 = new Thread(addTask);
        Thread thread2 = new Thread(addTask);

        thread1.start();
        thread2.start();

        // 等待线程执行完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的 synchronizedList
        System.out.println("Final synchronizedList: " + synchronizedList);
    }
}

        2.3 使用 CopyOnWriteArrayList 容器

        CopyOnWriteArrayList 容器即写时复制的容器。

        当我们往一个容器中添加元素时,不会直接往当前容器中添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后往新的容器里添加元素。

        添加完毕元素之后,再将原容器的引用指向新的容器。

优点:

        1)这样做的好处是我们对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前的容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

        2)在读多写少的场景下,性能很好,不需要加锁竞争。

缺点:

        1)占用内存较多。

        2)新写的数据不能被第一时间读取到。

代码如下:

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();

        // 添加元素
        copyOnWriteList.add("Element 1");
        copyOnWriteList.add("Element 2");

        // 创建多个线程来并发地修改列表
        Runnable addTask = () -> {
            copyOnWriteList.add("New Element");
            System.out.println("Added element by thread: " + Thread.currentThread().getName());
        };

        Thread thread1 = new Thread(addTask);
        Thread thread2 = new Thread(addTask);

        thread1.start();
        thread2.start();

        // 等待线程执行完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的 copyOnWriteList
        System.out.println("Final copyOnWriteList: " + copyOnWriteList);
    }
}

        是一种特定的并发控制技术,它在读操作时不加锁,只有在写操作时才会加锁。

        3.0 多线程环境使用队列

        1)基于数值实现的阻塞队列:ArrayBlockingQueue

        2)基于链表实现的阻塞队列:LinkedBlockingQueue

        3)基于优先级队列实现的阻塞队列:PriorityBlockingQueue

        4)最多只包含一个元素的阻塞队列:TransferQueue

以 LinkedBlockingQueue 举个例子:

import java.util.concurrent.LinkedBlockingQueue;

public class LinkedBlockingQueueExample {
    public static void main(String[] args) {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();

        // 创建生产者线程
        Runnable producer = () -> {
            try {
                for (int i = 0; i < 5; i++) {
                    String element = "Element " + i;
                    queue.put(element);
                    System.out.println("Produced: " + element);
                    Thread.sleep(1000); // 模拟生产过程
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        // 创建消费者线程
        Runnable consumer = () -> {
            try {
                while (true) {
                    String element = queue.take();
                    System.out.println("Consumed: " + element);
                    Thread.sleep(1500); // 模拟消费过程
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        // 启动生产者和消费者线程
        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);

        producerThread.start();
        consumerThread.start();
    }
}

        LinkedBlockingQueue 实例 queue,然后分别创建了生产者线程和消费者线程。生产者线程不断向队列中生产元素,消费者线程则从队列中消费元素,实现了生产者和消费者之间的协作。

        4.0 多线程环境使用哈希表

        HashMap 本身不是线程安全的。

        在多线程环境下使用哈希表可用使用:HashTable 、ConcurrentHashMap

        4.1 HashTable 类

        现在虽然已经有了 HashTable 类实现了线程安全了,只是简单的把关键方法加上了 synchronized 关键字而已。一个 HashTable 只有一把锁,两个线程访问 HashTable 中的任意数据都会出现锁竞争。

        1)如果多线程访问同一个 HashTable 就会直接造成锁冲突。

        2)size 属性也是通过 synchronized 来控制同步,也是比较慢的。

        3)一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会非常低。

        4.2 ConcurrentHashMap 类

        相比于 HashTable 做出了一系列的改进和优化。

        ConcurrentHashMap 每个哈希桶都有一把锁,只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突。但是这个概率很低,所以锁冲突也很小,锁竞争小。

        1)读操作没有加锁,但是使用了 Volatile 保证从内存读取结果。只对写操作进行加锁,加锁的方式仍然是 synchronized ,但是不是锁整个对象,而是“锁桶”,每一个链表的头节点作为锁对象,大大降低了锁冲突的概率。由于哈希表是由数组与链表或者红黑树组成的,数组的长度很长,因此相对链表的长度来说,链表的长度就很短了,所以在多线程中,对数组中的某一个链表大概率是不会冲突的,因此即使每一个链表都上锁了,这个锁也大概率是偏向锁,大概率是没有加锁和解锁的开销。

        2)充分利用 CAS 特性,比如 size 属性通过 CAS 来更新,避免出现重量级锁的情况。

        3)优化了扩容方式:化整为零

        发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去。扩容期间,新老数组同时存在。后续每一个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程,每个操作负责搬运一小部分元素。这个期间,插入只往新数组加,查询需要同时新旧数组。

        搬完最后一个元素再把旧数组删掉。

版权声明:本文为博主作者:小扳原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/Tingfeng__/article/details/137754271

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2024年4月16日
下一篇 2024年4月16日

相关推荐