Java多线程:同步集合与同步锁

同步集合

同步集合在多线程开发中扮演非常重要的角色,本文介绍些常用但被忽略的同步集合。

CopyOnWriteArrayList

Copy-On-Write是一种用于程序设计中的优化策略,基本思路是多个线程共享同一个列表,当某个线程想要修改这个列表的元素时会把列表中的元素Copy一份,然后进行修改,修改完后再讲新的元素设置给这个列表,是一种延时懒惰策略。好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加、移除任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。使用Copy-On-Write机制实现的并发容器有两个分别是:CopyOnWriteArrayList和CopyOnWriteArraySet。

下面来分析下CopyOnWriteArrayList的核心源码,首先看下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();}}

可以看到在添加的时候进行了加锁操作,否则多线程写的时候会Copy出N个副本出来。复制一份之后将新的元素设置到元素数组的len位置,然后再把最新的元素设置给该列表。

get方法:

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

读不需要加锁,如果读的时候多个线程正在向容器内添加数据,还是会读到旧数据,因为写的时候不会锁住旧的元素数组。

这种写时拷贝的原理优点是读写分离,并发场景下操作效率会提高,缺点是写操作时占用的内存空间翻了一倍,因此是以空间换时间。

ConcurrentHashMap

HashTable是HashMap的线程安全实现,但是HashTable使用synchronized来保证线程安全,这就会导致它的效率非常低下,因为当线程1使用put添加元素,线程2不但不能使用put添加元素,同时也不能使用get获取元素,竞争越激烈效率越低。

因此替代HashTable的ConcurrentHashMap就出现了,ConcurrentHashMap的优点在于容器里有多把锁,每一把锁用于锁容器其中一部分数据,当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。它的原理是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。有些方法需要跨段,如size()和containsValue(),他们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完毕后又按顺序释放所有段的锁。

BlockingQueue

阻塞队列是生产者-消费者的一个实现,当队列满了时,再次调用put函数添加元素,那么调用线程将会阻塞,直到队列不再是填满状态。避免了手动判断以及同步操作。

函数名 作用
add(e) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则抛异常
offer(e) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则返回false
offer(e,time,unit) 把元素e添加到BlockingQueue里,如果BlockingQueue可以容纳,则返回true,否则在等待指定的时间之后继续尝试添加,如果失败则返回false
put(e) 把元素e添加到BlockingQueue里,如果BlockingQueue不能容纳,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续添加
take() 取走BlockingQueue里排在队首的对象,若BlockingQueue为空,则进入等待状态直到BlockingQueue有新的对象被加入为止
poll(time,unit) 取出并移除队列中的队首元素,如果设定的阻塞时间内还没有获得数据,那么返回null
element() 获取队首元素,如果队列为空,那么抛出NoSuchElementException异常
peek() 获取队首元素,如果队列为空,那么返回null
remove() 获取并移除队首元素,如果队列为空,那么抛出NoSuchElementException异常

BlockingQueue多种常用实现:

  • ArrayBlockingQueue 数组实现的、线程安全的、有界的阻塞队列
    按FIFO(先进先出)原则对元素进行排序,元素从尾部插入到队列,从头部开始返回。
  • LinkedBlockingQueue 单向链表实现的队列
    按FIFO(先进先出)原则对元素进行排序,元素从尾部插入到队列,从头部开始返回。吞吐量高于ArrayBlockingQueue,但是在大多数并发应用程序中其可预知的性能要低,功能类似的有ConcurrentLinkedQueue
  • LinkedBlockingDeque 双向链表实现的双向并发阻塞队列
    同时支持FIFO和FILO,即可以从队列的头和尾同时操作(插入/删除),支持线程安全。可以指定队列容量(默认容量大小等于Integer.MAX_VALUE)

同步锁

线程安全就是必须通过各种锁机制来进行同步,防止某个对象或者值在多个线程中被修改导致的不一致问题。为了保证数据的一致性,需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或者数据,修改完毕之后再将最新数据同步到主存中,使其他线程能够得到最新数据。

synchronized

Java中最常用的同步机制就是synchronized关键字,它是一种基于语言的粗略锁,能够作用于对象、函数、类。每个对象都只有一个锁,谁拿到锁就得到了访问权限。

public class SynchronizedDemo {// 只对SynchronizedDemo当前对象生效public synchronized void syncMethod() {}public void syncThis() {// 只对SynchronizedDemo当前对象生效synchronized (this) {}}// 对SynchronizedDemo所有对象生效public void syncClassMethod() {synchronized (SynchronizedDemo.class) {}}// 对SynchronizedDemo所有对象生效public synchronized static void syncStaticMethod() {}
}

上面例子分别演示了同步方法、同步块、同步class对象、同步静态方法。前两种锁的是对象,作用是防止其他线程同时访问同一个对象中的synchronized代码块或者函数。后两种锁的是class对象,作用是防止其他线程同时访问所有对象中的synchronized锁的代码块,因为Class锁对类的所有对象实例起作用。

ReentrantLock与Condition

Java5之前协调共享对象访问时,只有synchronized和volatile,Java6增加了ReentrantLock,与synchronized相比,实现了相同的语义,但具有更高的灵活性,并可以提供轮训锁和定时锁,同时可以提供公平锁或非公平锁。

函数 作用
lock() 获取锁
tryLock() 尝试获取锁
tryLock(long timeout,TimeUnit unit) 尝试获取锁,如果到了指定的时间还获取不到,那么超时
unLock() 释放锁
newCondition() 获取锁的Condition

lock、tryLock与unlock一般成对出现,用法如下:

    Lock lock = new ReentrantLock();public int doSth() {lock.lock();try {// do some thing} finally {lock.unlock();}}

需要注意的是必须在finally块中释放lock,否则如果代码抛出异常就永远释放不了锁。而使用synchronized锁,JVM将确保锁会自动释放,并且当JVM使用synchronized管理锁定请求和释放时,JVM在生成线程转储时能够包括锁定信息,这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。而Lock类只是普通的类,JVM不知道具体哪个线程拥有Lock对象,这也是Lock没有完全替代掉synchronized的原因。

ReentrantLock中还有一个重要函数newCondition(),用于获取Lock上的Condition,Condition是用于实现线程间的通讯,解决Object.wait()、notify()、nofityAll()难以使用的问题。

常用方法如下:

函数 作用
await() 线程等待
await(int time,TimeUnit unit) 线程等待特定时间,超过时间则为超时
signal() 随机唤醒某个等待线程
signalAll() 唤醒所有等待中的线程

下面通过ReentrantLock与Condition实现一个简单的阻塞队列,实现代码如下:

public class MyArrayBlockingQueue<T> {// 数据数组private final T[] items;// 锁private final Lock lock = new ReentrantLock();// 队满的条件private Condition notFull = lock.newCondition();// 队空条件private Condition notEmpty = lock.newCondition();// 头部索引private int head;// 尾部索引private int tail;// 数据的个数private int count;public MyArrayBlockingQueue(int maxSize) {items = (T[]) new Object[maxSize];}public MyArrayBlockingQueue() {this(10);}public void put(T t) {lock.lock();try {while (count == getCapacity()) {System.out.println("数据已满,等待");notFull.await();}items[tail] = t;if (++tail == getCapacity()) {tail = 0;}++count;notEmpty.signalAll(); // 唤醒等待数据的线程} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public T take() {lock.lock();try {while (count == 0) {System.out.println("还没有数据,请等待");notEmpty.await();}T ret = items[head];items[head] = null;if (++head == getCapacity()) {head = 0;}--count;notFull.signalAll(); // 唤醒添加数据的线程return ret;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}return null;}public int getCapacity() {return items.length;}public int size() {lock.lock();try {return count;} finally {lock.unlock();}}public static void main(String[] args) {MyArrayBlockingQueue<Integer> aQueue = new MyArrayBlockingQueue<Integer>();aQueue.put(3);aQueue.put(24);for (int i = 0; i < 5; i++) {System.out.println(aQueue.take());}}}

上面代码模拟了一个有界队列阻塞队列,阻塞条件分别使用notfull与notEmpty,当调用put函数时集合元素已满那么会调用notFull.await()堵塞调用线程,直到其他线程调用了take()方法,由于take会在队列中取出一个元素后调用notFull.signalAll()唤醒等待线程,使得put可以继续。同理take函数是当元素数量为0时调用notEmpty.await()进行等待,当其他线程调用put方法执行notEmpty.signalAll()才唤醒take函数的线程,使之能够取得元素。

Semaphore

Semaphore是一个计数信号量,本质上是一个"共享锁"。信号量维护了一个信号量许可集,线程可以通过调用acquire()来获取信号量的许可。当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。

Semaphore的使用示例:

        final ExecutorService executorService = Executors.newFixedThreadPool(3);final Semaphore semaphore = new Semaphore(3);for (int i = 0; i < 5; i++) {executorService.submit(new Runnable() {@Overridepublic void run() {try {semaphore.acquire();System.out.println(" 剩余许可 : "+ semaphore.availablePermits());Thread.sleep(2000);semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}});}

可以看到在创建Semaphore时创建了3个信号量许可集,而消费线程却是5个,因此前3个线程获取了许可之后,信号量的许可就为0。此时后面的线程再调用acquire()就会阻塞,知道前3个线程执行完释放许可后剩余2个线程才能获取许可并继续执行。

CyclicBarrier

CyclicBarrier是一个同步辅助类,允许一组线程相互等待,直到到达某个公共屏障点。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。

CyclicBarrier使用示例:

public class CyclicBarrierDemo {private static int BARRIER_SIZE = 5;private static int THREAD_SIZE = 10;private static CyclicBarrier mCyclicBarrier;public static void main(String[] args) {mCyclicBarrier = new CyclicBarrier(BARRIER_SIZE, new Runnable() {@Overridepublic void run() {System.out.println(" ---> 满足条件,执行特定操作。 参与者: " + mCyclicBarrier.getParties());}});// 新建5个任务for (int i = 0; i < THREAD_SIZE; i++) {new WorkerThread().start();}}static class WorkerThread extends Thread {@Overridepublic void run() {try {System.out.println(Thread.currentThread().getName() + " 等待 CyclicBarrier.");// 将mCyclicBarrier的参与者数量加1mCyclicBarrier.await();// mCyclicBarrier的参与者数量等于5时,才继续往后执行System.out.println(Thread.currentThread().getName() + " 继续执行.");} catch (BrokenBarrierException | InterruptedException e) {e.printStackTrace();}}}
}

可以看到创建CyclicBarrier时第一个参数是5表示公共屏障点是5,第二个参数是一个runnable,表示达到屏障点时运行该runnable,因此该示例执行结果就是5个线程执行CyclicBarrier的await()函数后CyclicBarrier到达屏障点后执行runnable,剩下5个线程继续执行CyclicBarrier的await()函数后再次执行runnable。CyclicBarrier实际上可用于多个线程等待,直到某个条件被满足后执行一段逻辑。

CountDownLatch

CountDownLatch也是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到条件被满足。

与CyclicBarrier相似点在于,都需要设置一个公共屏障计数点。

与CyclicBarrier区别在于:

  1. CountDownLatch允许1个或N个线程调用await()阻塞,等待其他线程调用countDown()到达屏障点后释放阻塞,而CyclicBarrier则是允许N个线程调用await阻塞,直到阻塞线程数量到达屏障点后释放所有阻塞的线程。
  2. CountDownLatch的计数器无法重置,CyclicBarrier的计数器可以被重置后使用,因此被称为是循环的barrier。

CountDownLatch示例代码如下:

public class CountDownLatchDemo {private static int LATCH_SIZE = 5;public static void main(String[] args) {try {CountDownLatch latch = new CountDownLatch(LATCH_SIZE);// 新建5个任务for (int i = 0; i < LATCH_SIZE; i++) {new InnerThread(latch).start();}System.out.println("主线程等待.");// "主线程"等待线程池中5个任务的完成latch.await();System.out.println("主线程继续执行");} catch (InterruptedException e) {e.printStackTrace();}}static class InnerThread extends Thread {CountDownLatch mLatch;public InnerThread(CountDownLatch latch) {mLatch = latch;}@Overridepublic void run() {try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName() + " 执行操作.");// 将CountDownLatch的数值减1mLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}}
}

可以看到创建了一个屏障点数量为5的CountDownLatch对象,启动5个子线程后调用CountDownLatch的await()函数使主线程进入等待状态。子线程最后会执行CountDownLatch的countDown()函数,当5个子线程都调用countDown()之后主线程就会被唤醒。

demo代码:https://github.com/GavinAndre/JTMMultiThread

Java多线程:同步集合与同步锁相关推荐

  1. Java 多线程(三):锁(一)

    Java 多线程(三):锁(一) 作者:Grey 原文地址: 博客园:Java 多线程(三):锁(一) CSDN:Java 多线程(三):锁(一) CAS 比较与交换的意思 举个例子,内存有个值是 3 ...

  2. java多线程之线程的同步与锁定(转)

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. publicc ...

  3. java 多线程同步_浅谈Java多线程(状态、同步等)

    Java多线程是Java程序员必须掌握的基本的知识点,这块知识点比较复杂,知识点也比较多,今天我们一一来聊下Java多线程,系统的整理下这部分内容. 一.Java中线程创建的三种方式: 1.通过继承T ...

  4. 【Java多线程】了解线程的锁池和等待池概念

    文章目录 一.内置锁 二.线程状态 线程的5种状态 线程状态图 线程释放锁的情况 线程阻塞和线程等待的区别 sleep.join.yield.wait区别 yield不释放锁案例 sleep不释放锁案 ...

  5. Java多线程系列——深入重入锁ReentrantLock

    简述 ReentrantLock 是一个可重入的互斥(/独占)锁,又称为"独占锁". ReentrantLock通过自定义队列同步器(AQS-AbstractQueuedSychr ...

  6. java多线程11.非阻塞同步机制

    关于非阻塞算法CAS. 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B.当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不 ...

  7. Java多线程知识点整理(Lock锁)

    2019独角兽企业重金招聘Python工程师标准>>> 1.Lock的使用 private Lock lock = new ReentrantLock();public void t ...

  8. Java 多线程和线程同步总结

    转载:JAVA多线程实现和线程同步总结 1.JAVA多线程实现方式 JAVA多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable ...

  9. java 多线程 张孝祥_多线程11_张孝祥 java5的线程锁技术

    本例子因为两个线程公用同线程中,使用同一个对象,实现了他们公用一把锁,实现了同一个方法的互斥. package locks; /** *会被打乱的效果 */ public class LockTest ...

  10. JAVA Java多线程与并发库

    Java多线程与并发库 同步方式 import javax.xml.stream.events.StartDocument;public class TestSynchronized {public ...

最新文章

  1. python爬虫获取的网页数据为什么要加[0-Python爬虫实战1-解决需要爬取网页N秒后的内容的需求...
  2. PHP5各个版本的新功能和新特性总结(转载 http://www.jb51.net/article/48150.htm)
  3. linux 修改docker配置文件,dockerfile动态修改服务配置文件(示例代码)
  4. KALI Linux中GURB安装失败如何处理
  5. 格式怎么转换_爱奇艺下载的视频怎么转换成常见的mp4格式?
  6. JAVA-WEB开发环境和搭建
  7. 力扣904,水果成篮(JavaScript)
  8. VOICEBOX: Speech Processing Toolbox for MATLAB
  9. 阿里云服务器如何进行网站域名解析?
  10. AI智能电话机器人源码搭建的原理与功能
  11. 群晖-VideoStation-(TMDB刮削器)api申请
  12. matlab最大剩余法,有限元法、有限差分法和有限体积法的区别(转载)
  13. 2022年伊始,IT圈还有这些事是你不知道的?
  14. android ding铃声,Ring a Ding Dong
  15. android高仿ios键盘,iOS仿工商银行app自定义键盘
  16. 2B青年答疑:什么是2B青年?如何脱离2B青年行列?
  17. laravel-admin引用wangEditor编辑器 使用二:上传视频/音频(2)
  18. vim下Project插件用法, 管理项目
  19. HTC手机鉴别终极宝典
  20. 分治法——查找最大最小元素(Python)

热门文章

  1. SCI论文图片拼接Tips
  2. 【动手撸深度学习】不吹不黑一份代码即可进Kaggle排行榜!
  3. 机器视觉可以应用到哪些领域,你都知道吗?
  4. NideShop:基于Node.js+MySQL开发的高仿某易严选开源B2C商城(微信小程序客户端)
  5. Python使用Reportlab处理PDF数据 - 字体
  6. 医院信息化集成平台建设
  7. 教你如何制作网页木马
  8. python爬携程上出境游数据_Python爬虫 —— 携程机票数据
  9. LabVIEW以太网仪器控制
  10. 思科交换机常用命令汇总