怎样实现安全的List

2020-11-27 16:54发布

12条回答
我是大脸猫
2楼 · 2020-11-27 18:23

对于JDK中的线程安全集合类,个人用的一般是map和队列相关的用的比较多,线程安全的List很少会接触到。开发的时候突然遇到别人问我说有没有线程安全的List…一脸懵逼,所以决定网上找找看有没有相关的结构,别说,还真找到了:

Collections.synchronizedList与CopyOnWriteArrayList

本人之前写的话都是用一个锁来锁定的,今天就来看下这两种结构是不是会比自己用锁会更好。

 

调试代码如下:

public static void main(String[] args) {
    List l1 = new CopyOnWriteArrayList<>();
    l1.add("1");

    List l2 = new ArrayList<>();
    List l3 = Collections.synchronizedList(l2);
    l3.add("a");
}

 

先看Collections.synchronizedList:

其实就是内部给你synchronize加了个锁…没啥好看的,不过感觉可以简化下你的代码,至少这个里面的方法全部都是加了锁的。

而且锁的是一个变量,所以锁的粒度小。

 

再看CopyOnWriteArrayList:

当数组有变化时重新建立一个新的数组

一看这个add()就觉得在性能方面肯定不太靠谱:

第一个原因是使用的是Lock,之前分析ReentrantLock的时候说过,虽然它比synchronize提供了更丰富的功能,但是由于它是API级别的,JVM不会对这个锁进行优化,而synchronize之前分析过,它的锁是一步步升级上来的。

第二个原因是它每增加一个元素都要copy一遍旧数组中的元素,生成一个新数组。

 

好了…接下来的我就不用看了,我觉得一般情况下,前面那个用的会比较多,后面这个的场景我暂时没碰到。


云鹰的指尖故事
3楼 · 2020-11-27 19:08

怎么实现安全的List, 安全, 其实就是线程安全,  List其实是一个集合接口Collection的子接口,  表示的是 集合元素可以重复,并且有序,  

当然接口List也有自己的实现类, 包括:

  Linked List (双向循环链表)、

   ArrayList(可变数组+操作)、

   还有就是 Vector(底层也是数组实现的),

当然如果说安全 ,那么安全和性能是成反比的,   往往安全些的,效率就会降低点,   

Vector 就是线程安全的  但是效率没有 ArrayList的效率高.

如果想让ArrayList也是线程安全的,我们可以调用ArrayList 相关方法时添加线程锁,当然也可以使用集合的工具类Collections.synchronizedList()这个方法处理下, 就相当于Vector一样安全了


我想吃肉
4楼 · 2020-11-28 08:54

大部分人会脱口而出:用Vector,这样只会让面试官鄙视!除了Vector,你还会别的吗?

你至少还得说得上这种:

java.util.Collections.SynchronizedList

它能把所有 List 接口的实现类转换成线程安全的List,比 Vector 有更好的扩展性和兼容性,SynchronizedList的构造方法如下:

final List list;

SynchronizedList(List list) {
    super(list);
    this.list = list;
}

SynchronizedList的部分方法源码如下:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
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);}
}

很可惜,它所有方法都是带同步对象锁的,和 Vector 一样,它不是性能最优的。即使你能说到这里,面试官还会继续往下追问,比如在读多写少的情况,SynchronizedList这种集合性能非常差,还有没有更合适的方案?

介绍两个并发包里面的并发集合类:

java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet

CopyOnWrite集合类也就这两个,Java 1.5 开始加入,你要能说得上这两个才能让面试官信服。

CopyOnWriteArrayList

CopyOnWrite(简称:COW):即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。

先来看下它的 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();
    }
}

添加元素时,先加锁,再进行复制替换操作,最后再释放锁。

再来看下它的 get 方法源码:

private E get(Object[] a, int index{
    return (E) a[index];
}

public E get(int index{
    return get(getArray(), index);
}

可以看到,获取元素并没有加锁。

这样做的好处是,在高并发情况下,读取元素时就不用加锁,写数据时才加锁,大大提升了读取性能。


哆啦公
5楼 · 2020-11-28 08:59

一:使用synchronized关键字

二:使用Collections.synchronizedList


List集合相信大家都经常用吧,但是可惜的是它不是线程安全的,那么,该如何把list变成一个线程安全的集合呢?下面就来看看吧。

首先我们知道,ArrayList不是一个线程安全的集合,因此在实现多线程开发时,我们不能够使用多个线程同时操作List。如果我们让一个线程向 ArrayList中添加元素,又让另一个线程从其中删除元素,就会出现线程安全问题,抛出ConcurrentModificationException异常。

Vector

我们要知道Vector,这是一个可变长度的数组,它类似于ArrayList,但与ArrayList不同,Vector是绝对线程安全的,它的机制就是会给几乎所有的public方法都加上synchronized关键字。但是,由于加锁会导致性能降低到一定程度,在不需要并发访问同一对象时,像这样强制性的同步机制就显得非常多余了,所以Vector现在已经被抛弃啦。

Collections

但是办法总是想出来的,在Vector和HashTable相继被弃用后,它们被ArrayList及HashMap代替,但ArrayList及HashMap它们不是线程安全的,所以java中就有了Collections这么一个工具类,它能够提供相应的包装方法把ArrayList和HashMap包装成线程安全的集合。


芒果
7楼 · 2020-11-29 09:17

主要有两种方式:

一:使用synchronized关键字,这个大家应该都很熟悉了,不解释了;
二:使用Collections.synchronizedList();使用方法如下:
       假如你创建的代码如下:List> data=new ArrayList>();
       那么为了解决这个线程安全问题你可以这么使用Collections.synchronizedList(),如:
       List> data=Collections.synchronizedList(new ArrayList>());
      其他的都没变,使用的方法也几乎与ArrayList一样,大家可以参考下api文档;
额外说下 ArrayList与LinkedList;这两个都是接口List下的一个实现,用法都一样,但用的场所的有点不同,ArrayList适合于进行大量的随机访问的情况下使用,
LinkedList适合在表中进行插入、删除时使用,二者都是非线程安全,解决方法同上(为了避免线程安全,以上采取的方法,特别是第二种,其实是非常损耗性能的)。

rayfoo.cn
8楼 · 2020-11-29 17:08

使用Vertor即可

小小收藏家
9楼 · 2020-11-29 19:12

Java的List如何实现线程安全?

Collections.synchronizedList(names);效率最高,线程安全

Java的List是我们平时很常用的集合,线程安全对于高并发的场景也十分的重要,那么List如何才能实现线程安全呢 ?


加锁

首先大家会想到用Vector,这里我们就不讨论了,首先讨论的是加锁,例如下面的代码


public class Synchronized{

    private List  names = new LinkedList<>();
    
    public synchronized void addName(String name ){
        names.add("abc");
    }
    public String getName(Integer index){
        Lock lock =new ReentrantLock();
        lock.lock();
        try {
            return names.get(index);
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
        return null;
    }
}

synchronized一加,或者使用lock 可以实现线程安全,但是这样的List要是很多个,代码量会大大增加。

java自带类

在java中我找到自带有两种方法


CopyOnWriteArrayList

CopyOnWrite 写入时复制,它使一个List同步的替代品,通常情况下提供了更好的并发性,并且避免了再迭代时候对容器的加锁和复制。通常更适合用于迭代,在多插入的情况下由于多次的复制性能会一定的下降。


下面是add方法的源代码


    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; // 赋值
            setArray(newElements); // 设置内部的数组
            return true;
        } finally {
            lock.unlock();
        }
    }


Collections.synchronizedList

Collections中有许多这个系列的方法例如


主要是利用了装饰者模式对传入的集合进行调用 Collotions中有内部类SynchronizedList


   static class SynchronizedList
        extends SynchronizedCollection
        implements List {
        private static final long serialVersionUID = -7754090372962971524L;

        final List list;

        SynchronizedList(List list) {
            super(list);
            this.list = list;
        }
        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        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);}
        }
        
    static class SynchronizedCollection implements Collection, Serializable {
        private static final long serialVersionUID = 3053995032091335093L;

        final Collection c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize


这里上面的mutex就是锁的对象 在构建时候可以指定锁的对象 主要使用synchronize关键字实现线程安全

    /**
     * @serial include
     */
    static class SynchronizedList
        extends SynchronizedCollection
        implements List {
        private static final long serialVersionUID = -7754090372962971524L;

        final List list;

        SynchronizedList(List list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }
这里只是列举SynchronizedList ,其他类类似,可以看下源码了解下。

测试
public class Main {
    public static void main(String[] args) {
        List names = new LinkedList<>();
        names.add("sub");
        names.add("jobs");
        // 同步方法1 内部使用lock
        long a = System.currentTimeMillis();
        List strings = new CopyOnWriteArrayList<>(names);
        for (int i = 0; i < 100000; i++) {
            strings.add("param1");
        }
        long b = System.currentTimeMillis();
        // 同步方法2 装饰器模式使用 synchronized
        List synchronizedList = Collections.synchronizedList(names);
        for (int i = 0; i < 100000; i++) {
            synchronizedList.add("param2");
        }
        long c = System.currentTimeMillis();
        System.out.println("CopyOnWriteArrayList time == "+(b-a));
        System.out.println("Collections.synchronizedList time == "+(c-b));
    }
}


两者内部使用的方法都不一样,CopyOnWriteArrayList内部是使用lock进行加锁解锁完成单线程访问,synchronizedList使用的是synchronize

进行了100000次添加后时间对比如下:

相关问题推荐

  • 回答 156

    对于每一位才开始接触JAVA的新手来说,先不要管算法和数据结构,大多数简单的程序不需要用到算法和数据结构,所以当你真正需要时再去学习。编程一段时间以后,你就会知道在哪些地方用到他们。这时知道算法的名字并了解它们的功能,然后动手去实践。当我们在去...

  • 回答 93

    2个都很好就业,更关键的是要学得到东西

  • 回答 12
    已采纳

    获取Map集合中所有的key可以通过map集合的keySet()方法获取例如:    Map map = new HashMap();    map.put(xx,xx); //存放数据    //.... 省略    Set set = map.keySet();    //可以通过迭代器进行测试    Iterator iter = set.iter...

  • 回答 56
    已采纳

    不同年龄,不同掌握程度,学历,找工作城市,面试能力这是一个多方面影响的结果,如果是平均值的话,全国平均薪资14k左右

  • 回答 38

    具体学多久,根据自己的学习力,自律性、解决问题能力来决定若系统性学习,跟着讲师的节奏走,大概半年左右,有专业的讲师把课程进行规划,尽心系统学习,有问题,讲师会帮忙解决,学习的效率很高,避免了自学中出现各种问题解决不了,而耽误很多时间,可能会...

  • 回答 23
    已采纳

    (1)idea启动时会有两个快捷方式,安装完后默认生成在桌面的是32位的idea的快捷方式,如果我们使用这个快捷方式运行大项目,一般都会很卡。解决方法是找到idea的安装目录,然后进入bin文件夹,找到名称为idea64的应用程序,右键他生成桌面快捷方式。以后每次...

  • BIO与NIO、AIO的区别2020-05-19 15:59
    回答 4
    已采纳

    IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。一、BIO     在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要...

  • Java方法的命名规则2021-04-06 19:07
    回答 31

    ava是一种区分字母的大小写的语言,所以我们在定义变量名的时候应该注意区分大小写的使用和一些规范,接下来我们简单的来讲讲Java语言中包、类、变量等的命名规范。(一)Package(包)的命名Package的名字应该都是由一个小写单词组成,例如com、xuetang9、compan...

  • 回答 2

    public class Point {    private int x;    private int y;    public int getX() {        return x;    }    public void setX(int x) {        this.x = x;    }    public int getY() {        return y;    } ...

  • 回答 6

    经典版单例模式public class Singleton {        private static Singleton uniqueInstance;//利用一个静态常量来记录singleton类的唯一实例。     private Singleton() {     }     public static  Singleton getInstance()...

  • 回答 3

    哈希表的长度一般是定长的,在存储数据之前我们应该知道我们存储的数据规模是多大,应该尽可能地避免频繁地让哈希表扩容。但是如果设计的太大,那么就会浪费空间,因为我们跟不用不到那么大的空间来存储我们当前的数据规模;如果设计的太小,那么就会很容易发...

  • 回答 14

    1. DOM(Document Object Model)        DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才...

  • 回答 19

    1)作用不同: throw用于程序员自行产生并抛出异常; throws用于声明在该方法内抛出了异常2) 使用的位置不同: throw位于方法体内部,可以作为单独语句使用; throws必须跟在方法参数列表的后面,不能单独使用。3)内容不同: throw抛出一个异常对象,且只能是...

  • 回答 11

    基本执行过程如下:1)程序首先执行可能发生异常的try语句块。2)如果try语句没有出现异常则执行完后跳至finally语句块执行;3)如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。4)catch语句块可以有多个,分别捕获不同类型...

  • 回答 20

    100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 400 语义有误,当前请求无法被服务器理解。 401 当前请求需要用户验证...

  • 回答 16

    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译...

没有解决我的问题,去提问