【并发编程笔记】 ---- 分析CopyOnWriteArrayList及BlockingQueue(最后并发容器总结)

目录

1. CopyOnWriteArrayList
2. BlockingQueue
3. 并发容器总结

1. CopyOnWriteArrayList

1.1 诞生历史和原因
Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑

问题:为什么在迭代时无法编辑?(先看代码演示)

		Vector<String> vector = new Vector<>();
		vector.add("1");
		vector.add("2");
		vector.add("3");
		Iterator<String> iterator1 = vector.iterator();
		while(iterator1.hasNext()) {
			String next = iterator1.next();
			System.out.println(next);
			vector.add("2");
		}

		List<String> list = Collections.synchronizedList(new ArrayList<String>());
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
		Iterator<String> iterator = list.iterator();
		while(iterator.hasNext()) {
			String next = iterator.next();
			System.out.println(next);
			// 对list进行编辑,结果会报错
			list.add("2");
		}

在这里插入图片描述
在这里插入图片描述
由错误可知,两者均在iterator.next()时候发生了错误,为什么呢?怎么迭代时候会发生错误?因为在每次迭代后,我都对vector或者list进行了添加操作,从而引起了下一次打印输出错误~,我们跟踪一下iterator.next()源码

1. String next = iterator.next();
2. public E next() {
            checkForComodification(); // 每次进行迭代的时候都会进行检查是否有被修改
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
3. final void checkForComodification() {
            if (modCount != expectedModCount) // 一旦发现被修改,就抛出异常
                throw new ConcurrentModificationException();
        }

从源码知道,modCount 在每次增加、修改等操作的时候会进行+1操作,而expectedModCount是ArrayList一旦创建就有了的,一开始值为modCount, 所以我们在进行迭代的时候,如果发现集合被修改,modCount就+1,此时两者不相等,就抛出异常了,那有没有什么方法解决呢?就是接下来要讲的CopyOnWriteArrayList

1.2 CopyOnWriteArrayList实现原理

  • CopyOnWrite的含义
    只有容器中内容被修改的时候,就会copy出一个容器,然后在这个新的容器里改,之后将原容器的引用指向这个新的容器;好处就是对这个容器进行并发读写操作的时候,不需要额外加锁
  • 创建新副本、读写分离
  • "不可变"原理
  • 迭代的时候
    如果数组原内容被修改过了,但是迭代器是不知道的,迭代器依然使用的是旧数组,而且也不会报错
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
		list.add("1");
		list.add("2");
		list.add("3");
		list.add("4");
		list.add("5");
		Iterator<String> iterator = list.iterator();
		while(iterator.hasNext()) {
			System.out.println("list is" + list);
			String next = iterator.next();
			System.out.println(next);
			if(next.equals("2")) {
				list.remove("5");
			}
			if(next.equals("3")) {
				list.add("3 found");
			}
		}

在这里插入图片描述
1.3 CopyOnWriteArrayList的缺点

  • 数据一致性问题: CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果希望写入的数据,马上能读到,请不要使用CopyOnWrite容器
  • 内存占用问题: 因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存

1.4 CopyOnWriteArrayList源码分析

  • get
    CopyOnWriteArrayList.java

    private transient volatile Object[] array;
    
    final Object[] getArray() {
        return array;
    }
    
    public E get(int index) {
            return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
            return (E) a[index];
    }
    

    由源码可知get操作并没有上锁

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

    由add源码可知,会使用独占锁进行锁定(防止多线程同时add时候数组被覆盖),以及每次添加都会复制一个新的数组,将新元素添加到尾部

1.5 CopyOnWriteArrayList适合场景及读写规则
适合场景:

  • 读操作可以尽可能地快,而写即使慢一些也没有关系
  • 读多写少: 比如黑名单,每日更新,一个网站的敏感词汇存储在黑名单中,并不需要及时更新; 监听器:迭代操作远多于修改操作

读写规则:

  • 原来的读写锁:读读共享、其他互斥(写写互斥、读写互斥、写读互斥)
  • 读写锁的升级:读取是完全不用加锁的 ,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待

2. BlockingQueue

2.1 为什么要使用队列

  • 用队列可以在线程间传递数据:生产者消费者模式、银行转账
  • 考虑锁等线程安全问题的重任从"你"转移到了"队列"上

2.2 并发队列简介

  • Queue

    用来保存一组等待处理的数据,有很多种实现,比如ConcurrentLinkedQueue(非阻塞队列)、LinkedBlockingQueue(阻塞队列)等

  • BlockingQueue

    扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,获取的操作会一直阻塞,直到里面有数据,如果队列为满,插入的操作会一直阻塞,直到队列有可用空间

2.3 各并发队列关系图
在这里插入图片描述
2.4 阻塞队列

2.4.1 什么是阻塞队列

  • 阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能
  • 通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的
  • 阻塞功能:最有特点的两个带有阻塞功能的方法是:
    • take()方法:获取并移除队列的头节点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
    • put()方法:插入数据,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间

2.4.2 BlockingQueue主要方法

  • put,take: 这两个方法都会阻塞
  • add,remove,element: 为空为满进行操作会抛出异
  • offer,poll,peek: 返回布尔或者null值

2.4.3 ArrayBlockingQueue

  • 有界

  • 指定容量

  • 公平:如果想保证公平的话,那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能消耗

  • 使用案例: 有10个面试者,一共只有1个面试官,大厅只有3个位置供面试者休息,每个人的面试时间为1秒,模拟所有人面试的场景

    public class ArrayBlockingQueueDemo {
    
    	public static void main(String[] args) {
    		ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
    
    		Interviewer r1 = new Interviewer(queue);
    		Consumer r2 = new Consumer(queue);
    		new Thread(r1).start();
    		new Thread(r2).start();
    	}
    }
    
    class Interviewer implements Runnable{
    	BlockingQueue<String> queue;
    
    	public Interviewer(BlockingQueue queue) {
    		this.queue = queue;
    	}
    
    	@Override
    	public void run() {
    		System.out.println("10个候选人都来了");
    		for (int i = 0; i < 10; i++) {
    			String candidate = "Candidate" + i;
    
    			try {
    				queue.put(candidate);
    				System.out.println("安排好了" + candidate);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    
    		}
    		try {
    			queue.put("stop");
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    class Consumer implements Runnable{
    	BlockingQueue<String> queue;
    
    	public Consumer(BlockingQueue queue) {
    		this.queue = queue;
    	}
    
    
    	@Override
    	public void run() {
    		try {
    			Thread.sleep(1000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		String msg;
    		try {
    			while(!(msg = queue.take()).equals("stop")) {
    				System.out.println(msg + "到了");
    			}
    			System.out.println("所有候选人都结束了");
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }
    
  • 对put和take源码分析

    put源码

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 获得锁
        lock.lockInterruptibly();
        try {
            // 若队列已满,循环等待被通知,再次检查队列是否未满
            while (count == items.length)
                // notFull表示未满,进行入队操作,由于已经满了,所以调用await方法,使线程阻塞
                notFull.await();
            // 入队,此操作会唤醒出队操作,即 notEmpty.signal()
            enqueue(e);
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    
    1. 若队列已满,调用 notFull 的 #await() 方法,等待被通知。
    2. 被通知后,再次检查队列是否未满。若未满,继续向下执行,否则继续等待被通知

    take源码

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 获得锁
        lock.lockInterruptibly();
        try {
            // 若队列已空,循环等待被通知,再次检查队列是否非空
            while (count == 0)
                // notEmpty表示非空,执行出队操作,由于此次为空,所以让出队操作阻塞,调用await方法,等待被唤醒
                notEmpty.await();
            // 出列,此操作会唤醒入列操作,即notFull.signal();
            return dequeue();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    

2.4.4 LinkedBlockingQueue

  • 无界

  • 容量Integer.MAX_VALUE

  • 内部结构: Node、两把锁。分析put方法

     /** Lock held by take, poll, etc */
        private final ReentrantLock takeLock = new ReentrantLock();
    
        /** Wait queue for waiting takes */
        private final Condition notEmpty = takeLock.newCondition();
    
        /** Lock held by put, offer, etc */
        private final ReentrantLock putLock = new ReentrantLock();
    
        /** Wait queue for waiting puts */
        private final Condition notFull = putLock.newCondition();
        public void put(E e) throws InterruptedException {
            if (e == null) throw new NullPointerException();
            // Note: convention in all put/take/etc is to preset local var
            // holding count negative to indicate failure unless set.
            int c = -1;
            Node<E> node = new Node<E>(e);
            final ReentrantLock putLock = this.putLock;
            final AtomicInteger count = this.count;
            putLock.lockInterruptibly();
            try {
                while (count.get() == capacity) {
                    notFull.await();
                }
                enqueue(node);
                // 原子操作,返回的是旧值
                c = count.getAndIncrement();
                // 未满则唤醒插入
                if (c + 1 < capacity)
                    notFull.signal();
            } finally {
                putLock.unlock();
            }
            if (c == 0)
                signalNotEmpty();
    	 }
    

从上面的属性我们知道,每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加的链表队列中,其中head和last分别指向队列的头结点和尾结点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。
如果不指定队列的容量大小,也就是使用默认的Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出,这点在使用前希望慎重考虑。
另外,LinkedBlockingQueue对每一个lock锁都提供了一个Condition用来挂起和唤醒其他线程。

2.4.5 PriorityBlockingQueue

  • 支持优先级
  • 自然顺序(而不是先进先出)
  • 无界队列
  • PriorityQueue的线程安全版本

2.4.6 SynchronousQueue

  • 它的容量为0
  • 注意:容量不是1而是0,因为SynchronousQueue不需要去持有元素,它所做的就是直接传递(direct handoff)

注意点

  • SynchronousQueue没有peek函数,因为peek的含义是取出头结点,但是SynchronousQueue的容量是0,所以连头节点都没有,也就是没有peek方法,同理,没有iterate相关方法
  • 是一个极好的用来直接传递的并发数据结构
  • SynchronousQueue是线程池**Excutors.newCachedThreadPool()**使用的阻塞队列

2.4.7 DelayQueue

  • 无边界
  • 延迟队列,根据延迟时间顺序
  • 元素需要实现Delayed接口,规定排序规则

2.5 非阻塞队列

  • JUC中的非阻塞队列只有ConcurrentLinkedQueue这一种,是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些
  • 看源码的offer方法的CAS思想,内有p.casNext方法,用了UNSAFE.compareAndSwapObject

2.6 如何选择适合自己的队列

  • 边界
  • 空间
  • 吞吐量

3. 并发容器总结

  • java.util.concurrent包提供的容器,分为三类: Concurrent*、CopyOnWrite*、Blocking*
  • Concurrent*的特点是大部分通过CAS实现并发
  • CopyOnWrite * 则是通过复制一份原数据来实现的
  • BlockingQueue通过AQS实现的

在这里插入图片描述

  • 参考: 慕课网悟空老师课程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值