Java并发容器及其实现原理

不安全集合类

ArrayList

ArrayList是线程不安全类,多线程写入会发生并发修改异常:

/**
 * 集合类不安全问题
 * ArrayList
 */
public class ContainerNotSafeDemo {
    public static void main(String[] args) {
        notSafe();
    }

    /**
     * 故障现象
     * java.util.ConcurrentModificationException
     */
    public static void notSafe() {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, "Thread " + i).start();
        }
    }
}

报错:Exception in thread "Thread 10" java.util.ConcurrentModificationException

ConcurrentModificationException

线程不安全情况之一:

一个 ArrayList ,在添加一个元素的时候,会有两步来完成:

  1. 在 Items[Size] 的位置存放此元素;
  2. 增大 Size 的值。

在多线程情况下,比如有两个线程,线程A先将元素存放在位置0。但是此时CPU调度线程A暂停,线程B得到运行的机会。线程B也向此ArrayList添加元素,因为此时Size仍然等于0 ,所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加Size的值,此时元素实际上只有一个,存放在位置0,而Size却等于2。

线程不安全情况之二:

在多个线程进行add操作时可能会导致elementData数组越界,具体逻辑如下:

  1. 列表大小为9,即size=9;
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断;
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法;
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回;
  5. 线程B也发现需求大小为10,也可以容纳,返回;
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10;
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9,于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException

解决方案:

List<String> list = new Vector<>(); //Vector线程安全
List<String> list = Collections.synchronizedList(new ArrayList<>()); //使用辅助类
List<String> list = new CopyOnWriteArrayList<>(); //写时复制,读写分离

HashMap

HashSet也是线程不安全的集合类,其底层是一个HashMap,存储的值放在HashMap的key里,value存储了一个PRESENT的静态Object对象。

ConcurrentMap

常见的Map包括:

  • HashMap(哈希表、线程不安全)

  • TreeMap(红黑树、排序、线程不安全)

  • LinkedHashMap(保存了记录的插入顺序)

  • Hashtable(线程安全,对HashMap的所有方法加锁)

  • ConcurrentHashMap(线程安全且速度快,替代Hashtable)

  • ConcurrentSkipListMap(支持高并发且排序==>TreeMap)

  • Collections.synchronizedXXX(对所有方法加锁 ≈ Hashtable):

    Map m = Collections.synchronizedMap(new HashMap());

测试一下效率:

方案为:创建100个线程,每个线程向Map中加入10000个数据,共一百万数据,测试速度。

public class ConcurrentMapSpeedTest {
    public static void main(String[] args) {
        Map<String, String> map = new ConcurrentHashMap<>();
        //Map<String, String> map = new ConcurrentSkipListMap<>();

        //Map<String, String> map = new Hashtable<>();
        //Map<String, String> map = new HashMap<>(); 
        Random r = new Random();
        Thread[] ths = new Thread[100];
        CountDownLatch latch = new CountDownLatch(ths.length);
        long start = System.currentTimeMillis();
        for(int i=0; i<ths.length; i++) {
            ths[i] = new Thread(()->{
                for(int j=0; j<10000; j++) map.put("a" + r.nextInt(100000), "a" + r.nextInt(100000));
                latch.countDown();
            });
        }

        Arrays.asList(ths).forEach(t->t.start());
        try {
            // 等100个线程全部执行完时,latch变为0,门闩打开,主线程继续执行
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

测试结果:

  1. Hashtable:758ms
  2. Collections.synchronizedMap(map):746ms
  3. ConcurrentHashMap:512ms

Hashtable与synchronizedMap的任何操作都要锁定整个对象,而ConcurrentHashMap是采用分段锁

CopyOnWriteList

写时复制容器 copy on write,在多线程下写的速度很慢但读的速度很快,适合写少读多的环境。

List<String> lists = new CopyOnWriteArrayList<>();

经测试写入100000个数据需要6秒,写入极慢。

原理:写入时加锁并逐个写入一个新的数组,读时不加锁从原来的数组读取。

写入操作的源码:

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

CopyOnWriteArraySet底层也是CopyOnWriteArrayList

SynchronizedList

List<String> strs = new ArrayList<>();
// 返回加锁的List
List<String> strsSync = Collections.synchronizedList(strs);

源码就是对任何一个方法加锁:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4KzvO8Ih-1587347973862)(http://picture.tjtulong.top/SychronizedList.JPG)]

另一个线程安全的List为Vector

SynchronizedList和Vector最主要的区别

  1. 一个使用了同步代码块,一个使用了同步方法
  2. 扩容机制不同,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%;
  3. SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类;
  4. 使用SynchronizedList的时候,进行遍历时要手动进行同步处理;
  5. SynchronizedList可以指定锁定的对象,Vector锁定的是this。

ConcurrentQueue

在高并发的条件下共有两种Queue,一种是内部加锁的队列ConcurrentQueue,另一种是阻塞式队列BlockingQueue

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class T04_ConcurrentQueue {
	public static void main(String[] args) {
        // 单向队列
		Queue<String> strs = new ConcurrentLinkedQueue<>();
		
		for(int i=0; i<10; i++) {
            //add加不进去抛异常
            //offer加不进去返回false
			strs.offer("a" + i);  
		}
		
		System.out.println(strs);
		
		System.out.println(strs.size());
		
		System.out.println(strs.poll());
		System.out.println(strs.size());
		
		System.out.println(strs.peek());
		System.out.println(strs.size());
		
		//双端队列Deque
        Deque<String> strs = new ConcurrentLinkedDeque<>();
	}
}

原理:并非使用锁,而是使用CAS

入队:如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾点。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    // 入队前,创建一个入队节点
    Node<E> n = new Node<E>(e);
    retry:
    // 死循环,入队不成功反复入队。
    for (;;) {
        // 创建一个指向tail节点的引用
        Node<E> t = tail;
        // p用来表示队列的尾节点,默认情况下等于tail节点。
        Node<E> p = t;
        for (int hops = 0; ; hops++) {
            // 获得p节点的下一个节点。
            Node<E> next = succ(p);
            // next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点
            if (next != null) {
                // 循环了两次及其以上,并且当前节点还是不等于尾节点
                if (hops > HOPS && t != tail)
                    continue retry;
                p = next;
            }
            // 如果p是尾节点,则设置p节点的next节点为入队节点。
            else if (p.casNext(null, n)) {
                /*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
                更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/
                if (hops >= HOPS)
                    casTail(t, n); // 更新tail节点,允许失败
                return true;
            }
            // p有next节点,表示p的next节点是尾节点,则重新设置p节点
            else {
                p = succ(p);
            }
        }
    }
}

doug lea使用hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将tail节点更新成尾节点,而是当tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长,使用CAS更新tail节点的次数就会越少,但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长,因为循环体需要多循环一次来定位出尾节点,但是这样仍然能提高入队的效率,因为从本质上来看它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作,所以入队效率会有所提升。

阻塞队列

当阻塞队列是空的,从队列中获取元素的操作会被阻塞;当阻塞队列是满的,向队列中添加元素会被阻塞。

阻塞式队列:

  • ArrayBlockingQueue:由数据结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
  • SychronousQueue:不存储元素的阻塞队列。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

BlockingQueue核心方法:

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用

LinkedBlockingQueue

核心在于两个方法:put和take,均为阻塞方法,当没有数据时会阻塞,可以参考生产者和消费者的代码。

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class LinkedBlockingQueueDemo {

    static BlockingQueue<String> strs = new LinkedBlockingQueue<>();

    static Random r = new Random();

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    strs.put("a" + i); //如果满了,就会等待
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "p1").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (;;) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " take -" + strs.take()); //如果空了,就会等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "c" + i).start();
        }
    }
}

ArrayBlockingQueue

容量有限,构造方法必须传入容量

ArrayBlockingQueue(int capacity)
Creates an ArrayBlockingQueue with the given (fixed) capacity and default access policy.
public class T06_ArrayBlockingQueue {

    static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);

    static Random r = new Random();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            strs.put("a" + i);
        }

        strs.put("aaa"); //满了就会等待,程序阻塞
        strs.add("aaa"); //添加不进去报错
        strs.offer("aaa"); //添加不进去返回false
        strs.offer("aaa", 1, TimeUnit.SECONDS); //1s添加不进去返回false

        System.out.println(strs);
    }
}

通过查看JDK源码可以发现ArrayBlockingQueue使用了Condition来实现:

    private final Condition notFull;
    private final Condition notEmpty;
    
	public ArrayBlockingQueue(int capacity, boolean fair) {
        // 省略其他代码
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();
    }
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    } 

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    } 

    private void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
    }

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。也就是说**只有在延迟期到时才能够从队列中取元素。**需要实现compareTogetDelay方法。

参考:https://www.jianshu.com/p/6c3d5c7a386d

使用场景:

  • 缓存系统设计:使用DelayQueue保存缓存元素的有效期,用一个线程循环查询DelayQueue,一旦从DelayQueue中取出元素,就表示有元素到期,如订单超时取消。
  • 定时任务调度:使用DelayQueue保存当天要执行的任务和执行的时间,一旦从DelayQueue中获取到任务,就开始执行,比如Timer,就是基于DelayQueue实现的。
public class DelayQueueDemo {
    static BlockingQueue<MyTask> tasks = new DelayQueue<>();

    static Random r = new Random();

    static class MyTask implements Delayed {
        long runningTime;

        MyTask(long rt) {
            this.runningTime = rt;
        }

        @Override
        public int compareTo(Delayed o) {
            if(this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
                return -1;
            else if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
                return 1;
            else
                return 0;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public String toString() {
            return "" + runningTime;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long now = System.currentTimeMillis();
        MyTask t1 = new MyTask(now + 1000);
        MyTask t2 = new MyTask(now + 2000);
        MyTask t3 = new MyTask(now + 1500);
        MyTask t4 = new MyTask(now + 2500);
        MyTask t5 = new MyTask(now + 500);

        tasks.put(t1);
        tasks.put(t2);
        tasks.put(t3);
        tasks.put(t4);
        tasks.put(t5);

        System.out.println(tasks);

        for(int i=0; i<5; i++) {
            System.out.println(tasks.take());
        }
    }
}

[1584411853962, 1584411854462, 1584411854962, 1584411855962, 1584411855462]
1584411853962
1584411854462
1584411854962
1584411855462
1584411855962

延时阻塞队列的实现方法(参考JDK1.8中DelayQueue源码):

    /**
     * Retrieves and removes the head of this queue, waiting if necessary
     * until an element with an expired delay is available on this queue.
     *
     * @return the head of this queue
     * @throws InterruptedException {@inheritDoc}
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;//非公平锁
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null)
                    available.await();
                else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    first = null; // don't retain ref while waiting
                    if (leader != null)
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

代码中的变量leader是一个等待获取队列头部元素的线程。如果leader不等于空,表示已经有线程在等待获取队列的头元素。所以,使用await()方法让当前线程等待信号。如果leader等于空,则把当前线程设置成leader,并使用awaitNanos()方法让当前线程等待接收信号或等待delay时间。

TransferQueue

TransferQueue继承了BlockingQueueBlockingQueue又继承了Queue)并扩展了一些新方法。

BlockingQueue是Java 5中加入的接口,它是指这样的一个队列:当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。

TransferQueue则更进一步,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递。

public class TransferQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();

        strs.transfer("aaa"); //会因为没有消费者而永远阻塞
        //strs.put("aaa"); //与BlockingQueue一样继续执行

        new Thread(() -> {
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

LinkedTransferQueue是JDK1.7才添加的阻塞队列,基于链表实现的FIFO无界阻塞队列,是ConcurrentLinkedQueue(循环CAS+volatile 实现的wait-free并发算法)SynchronousQueue(公平模式下转交元素)LinkedBlockingQueue(阻塞Queue的基本方法)的超集。而且LinkedTransferQueue更好用,因为它不仅仅综合了这几个类的功能,同时也提供了更高效的实现

其核心方法为xfer,可参考:https://www.jianshu.com/p/808da4a75f22

根据不同的方法决定不同的匹配与插入策略。

和SynchronousQueue相比,LinkedTransferQueue多了一个可以存储的队列,与LinkedBlockingQueue相比,LinkedTransferQueue多了直接传递元素,少了用锁来同步。

SynchronousQueue

SynchronousQueue是TransferQueue的一个特例。它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程

主要:不能add

public class SynchronusQueueDemo { //容量为0
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> strs = new SynchronousQueue<>();

        new Thread(()->{
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        strs.put("aaa"); //阻塞等待消费者消费
        //strs.add("aaa");报错
        System.out.println(strs.size());
    }
}

原理参考:https://zhuanlan.zhihu.com/p/29227508

猜你喜欢

转载自blog.csdn.net/TJtulong/article/details/105628077