关于java:Java并发同步容器篇

36次阅读

共计 6553 个字符,预计需要花费 17 分钟才能阅读完成。

作者:汤圆

集体博客:javalover.cc

前言

官人们好啊,我是汤圆,明天给大家带来的是《Java 并发 - 同步容器篇》,心愿有所帮忙,谢谢

文章如果有问题,欢送大家批评指正,在此谢过啦

简介

同步容器次要分两类,一种是 Vector 这样的一般类,一种是通过 Collections 的工厂办法创立的外部类

尽管很多人都对同步容器的性能低有偏见,但它也不是一无是处,在这里咱们插播一条阿里巴巴的开发手册标准:

高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个办法体;能用对象锁,就不要用类锁。

能够看到,只有在高并发才会思考到锁的性能问题,所以在一些小而全的零碎中,同步容器还是有用武之地的(当然也能够思考并发容器,前面章节再探讨)

附言:这不是洗白贴

目录

咱们这里分三步来剖析:

  1. 什么是同步容器
  2. 为什么要有同步容器
  3. 同步容器的优缺点
  4. 同步容器的应用场景

注释

1. 什么是同步容器

定义:就是把容器类同步化,这样咱们在并发中应用容器时,就不必手动同步,因为外部曾经主动同步了

例子:比方 Vector 就是一个同步容器类,它的同步化就是把外部的所有办法都上锁(有的重载办法没上锁,然而最终调用的办法还是有锁的)

源码:Vector.add

// 通过 synchronized 为 add 办法上锁
public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}

同步容器次要分两类:

  1. 一般类:Vector、Stack、HashTable
  2. 外部类:Collections 创立的外部类,比方 Collections.SynchronizedList、Collections.SynchronizedSet 等

那这两种有没有区别呢?

当然是有的,刚开始的时候 (Java1.0) 只有第一种同步容器(Vector 等)

然而因为 Vector 这品种太 局气 了,它就想着把所有的货色都弄过去本人搞(Vector 通过 toArray 转为己有,HashTable 通过 putAll 转为己有);

源码:Vector 构造函数

public Vector(Collection<? extends E> c) {
    // 这里通过 toArray 将传来的汇合 转为己有
  elementData = c.toArray();
  elementCount = elementData.length;
  // c.toArray might (incorrectly) not return Object[] (see 6260652)
  if (elementData.getClass() != Object[].class)
    elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}

所以就有了第二种同步容器类(通过工厂办法创立的外部容器类),它就比拟聪慧了,它只是把原有的容器进行包装(通过 this.list = list 间接指向须要同步的容器),而后部分加锁,这样一来,即生成了线程平安的类,又不必太费劲;

源码:Collections.SynchronizedList 构造函数

SynchronizedList(List<E> list) {super(list);
  // 这里只是指向传来的 list,不转为己有,前面的相干操作还是基于原有的 list 汇合
  this.list = list;
}

他们之间的区别如下:

两种同步容器的区别 一般类 外部类
锁的对象 不可指定,只能 this 可指定,默认 this
锁的范畴 办法体(包含迭代) 代码块(不包含迭代)
适用范围 窄 - 个别容器 广 - 所有容器

这里咱们重点说下锁的对象:

  • 一般类 锁的是以后对象this(锁在办法上,默认 this 对象);
  • 外部类 锁的是 mutex 属性,这个属性默认是 this,然而能够通过 构造函数 (或工厂办法)来 指定锁 的对象

源码:Collections.SynchronizedCollection 构造函数

final Collection<E> c;  // Backing Collection
// 这个就是锁的对象
final Object mutex;     // Object on which to synchronize

SynchronizedCollection(Collection<E> c) {this.c = Objects.requireNonNull(c);
// 初始化为 this
  mutex = this;
}

SynchronizedCollection(Collection<E> c, Object mutex) {this.c = Objects.requireNonNull(c);
  this.mutex = Objects.requireNonNull(mutex);
}

这里要留神一点就是,外部类的迭代器没有同步(Vector 的迭代器有同步),须要手动加锁来同步

源码:Vector.Itr.next 迭代办法(有上锁)

public E next() {synchronized (Vector.this) {checkForComodification();
    int i = cursor;
    if (i >= elementCount)
      throw new NoSuchElementException();
    cursor = i + 1;
    return elementData(lastRet = i);
  }
}

源码:Collections.SynchronizedCollection.iterator 迭代器(没上锁)

public Iterator<E> iterator() {
  // 这里会间接实现类的迭代器(比方 ArrayList,它外面的迭代器必定是没上锁的)return c.iterator(); // Must be manually synched by user!}

2. 为什么要有同步容器

因为一般的容器类(比方 ArrayList)是线程不平安的,如果是在并发中应用,咱们就须要手动对其加锁才会平安,这样的话就很麻烦;

所以就有了同步容器,它来帮咱们主动加锁

上面咱们用代码来比照下

线程不平安的类:ArrayList

public class SyncCollectionDemo {
    
    private List<Integer> listNoSync;

    public SyncCollectionDemo() {this.listNoSync = new ArrayList<>();
    }

    public void addNoSync(int temp){listNoSync.add(temp);
    }

    public static void main(String[] args) throws InterruptedException {SyncCollectionDemo demo = new SyncCollectionDemo();
                // 创立 10 个线程
        for (int i = 0; i < 10; i++) {
                    // 每个线程执行 100 次增加操作
          new Thread(()->{for (int j = 0; j < 1000; j++) {demo.addNoSync(j);
                }
            }).start();}
    }
}

下面的代码看似没问题,感觉就算有问题也应该是插入的程序比拟乱(多线程交替插入)

但实际上运行会发现,可能会报错数组越界,如下所示:

起因有二:

  1. 因为 ArrayList.add 操作没有加锁,导致多个线程能够同时执行 add 操作
  2. add 操作时,如果发现 list 的容量有余,会进行扩容,然而因为多个线程同时扩容,就会呈现扩容有余的问题

源码:ArrayList.grow 扩容

// 扩容办法
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
                // 这里能够看到,每次扩容减少一半的容量
              int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

能够看到,扩容是基于之前的容量进行的,因而如果多个线程同时扩容,那扩容基数就不精确了,后果就会有问题

线程平安的类:Collections.SynchronizedList

/**
 * <p>
 *  同步容器类:为什么要有它
 * </p>
 *
 * @author: JavaLover
 * @time: 2021/5/3
 */
public class SyncCollectionDemo {

    private List<Integer> listSync;

    public SyncCollectionDemo() {
          // 这里包装一个空的 ArrayList
        this.listSync = Collections.synchronizedList(new ArrayList<>());
    }

    public void addSync(int j){// 外部是同步操作: synchronized (mutex) {return c.add(e);}
        listSync.add(j);
    }

    public static void main(String[] args) throws InterruptedException {SyncCollectionDemo demo = new SyncCollectionDemo();

        for (int i = 0; i < 10; i++) {new Thread(()->{for (int j = 0; j < 100; j++) {demo.addSync(j);
                }
            }).start();}

        TimeUnit.SECONDS.sleep(1);
          // 输入 1000
        System.out.println(demo.listSync.size());
    }
}

输入正确,因为当初 ArrayList 被 Collections 包装成了一个线程平安的类

这就是为啥会有同步容器的起因:因为同步容器使得并发编程时,线程更加平安

3. 同步容器的优缺点

一般来说,都是先说长处,再说毛病

然而咱们这次先说长处

长处:

  • 并发编程中,独立操作是线程平安的,比方独自的 add 操作

毛病(是的,长处曾经说完了):

  • 性能差,基本上所有办法都上锁,完满的诠释了“宁肯错杀一千,不可放过一个”
  • 复合操作,还是不平安,比方 putIfAbsent 操作(如果没有则增加)
  • 疾速失败机制,这种机制会报错提醒ConcurrentModificationException,个别呈现在当某个线程在遍历容器时,其余线程恰好批改了这个容器的长度

为啥第三点是毛病呢?

因为它只能作为一个倡议,通知咱们有并发批改异样,然而不能保障每个并发批改都会爆出这个异样

爆出这个异样的前提如下:

源码:Vector.Itr.checkForComodification 查看容器批改次数

final void checkForComodification() {
  // modCount:容器的长度变动次数,expectedModCount:冀望的容器的长度变动次数
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();}

那什么状况下并发批改不会爆出异样呢?有两种:

  1. 遍历没加锁的状况:对于第二种同步容器 (Collections 外部类) 来说,假如线程 A 批改了 modCount 的值,然而没有同步到线程 B,那么线程 B 遍历就不会产生异样(但实际上问题曾经存在了,只是临时没有呈现)
  2. 依赖线程执行程序的状况:对于所有的同步容器来说,假如线程 B 曾经遍历完了容器,此时线程 A 才开始遍历批改,那么也不会产生异样

代码就不贴了,大家感兴趣的能够间接写几个线程遍历试试,多运行几次,应该就能够看到成果(不过第一种状况也是基于实践剖析,理论代码我这边也没跑进去)

依据阿里巴巴的开发标准:不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请应用 Iterator 形式,如果并发操作,须要对 Iterator 对象加锁。

这里解释下,对于 List.remove 和 Iterator.remove 的区别

  • Iterator.remove:会同步批改 expectedModCount=modCount
  • list.remove:只会批改 modCount,因为 expectedModCount 属于 iterator 对象的属性,不属于 list 的属性(然而也能够间接拜访)

源码:ArrayList.remove 移除元素操作

public E remove(int index) {rangeCheck(index);
                // 1. 这里批改了 modCount
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

源码:ArrayList.Itr.remove 迭代器移除元素操作

public void remove() {if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                  // 1. 这里调用下面介绍的 list.romove,批改 modCount
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                  // 2. 这里再同步更新 expectedModCount
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();
            }
        }

因为 同步容器 的这些毛病,于是就有了 并发容器(下期来介绍)

4. 同步容器的应用场景

多用在并发编程,然而并发量又不是很大的场景,比方一些简略的集体博客零碎(具体多少并发量算大,这个也是分很多状况而论的,并不是说每秒解决超过多少个申请,就说是高并发,还要联合吞吐量、零碎响应工夫等多个因素一起思考)

具体点来说的话,有以下几个场景:

  • 写多读少,这个时候同步容器和并发容器的性能差异不大(并发容器能够并发读)
  • 自定义的复合操作,比方 getLast 等操作(putIfAbsent 就算了,因为并发容器有默认提供这个复合操作)
  • 等等

总结

  1. 什么是同步容器:就是把容器类同步化,这样咱们在并发中应用容器时,就不必手动同步,因为外部曾经主动同步了
  2. 为什么要有同步容器:因为一般的容器类(比方 ArrayList)是线程不平安的,如果是在并发中应用,咱们就须要手动对其加锁才会平安,这样的话就很太麻烦;所以就有了同步容器,它来帮咱们主动加锁
  3. 同步容器的优缺点:
长处 毛病
同步容器 独立操作,线程平安 复合操作,还是不平安
性能差
疾速失败机制,只适宜 bug 调试
  1. 同步容器的应用场景

多用在并发量不是很大的场景,比方集体博客、后盾零碎等

具体点来说,有以下几个场景:

  • 写多读少:这个时候同步容器和并发容器差异不是很大
  • 自定义复合操作:比方 getLast 等复合操作,因为同步容器都是单个操作进行上锁的,所以能够很不便地去拼接复合操作(记得内部加锁)
  • 等等

参考内容:

  • 《Java 并发编程实战》
  • 《实战 Java 高并发》

后记

最初,感激大家的观看,谢谢

原创不易,期待官人们的三连哟

Java 并发 - 同步容器篇

正文完
 0