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

本贴最后更新于 2624 天前,其中的信息可能已经事过境迁

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

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 {
// 数据数组
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 函数的线程,使之能够取得元素

作者:提辖鲁
来源:CSDN
原文:https://blog.csdn.net/lj402159806/article/details/83051518?utm_source=copy

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1062 引用 • 3456 回帖 • 124 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3206 引用 • 8217 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    127 引用 • 169 回帖
  • Git

    Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    215 引用 • 358 回帖
  • Sym

    Sym 是一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)系统平台。

    下一代的社区系统,为未来而构建

    524 引用 • 4602 回帖 • 731 关注
  • 电影

    这是一个不能说的秘密。

    125 引用 • 610 回帖
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    695 引用 • 538 回帖 • 1 关注
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖 • 2 关注
  • JRebel

    JRebel 是一款 Java 虚拟机插件,它使得 Java 程序员能在不进行重部署的情况下,即时看到代码的改变对一个应用程序带来的影响。

    26 引用 • 78 回帖 • 693 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 633 关注
  • Sillot

    Insights(注意当前设置 master 为默认分支)

    汐洛彖夲肜矩阵(Sillot T☳Converbenk Matrix),致力于服务智慧新彖乄,具有彖乄驱动、极致优雅、开发者友好的特点。其中汐洛绞架(Sillot-Gibbet)基于自思源笔记(siyuan-note),前身是思源笔记汐洛版(更早是思源笔记汐洛分支),是智慧新录乄终端(多端融合,移动端优先)。

    主仓库地址:Hi-Windom/Sillot

    文档地址:sillot.db.sc.cn

    注意事项:

    1. ⚠️ 汐洛仍在早期开发阶段,尚不稳定
    2. ⚠️ 汐洛并非面向普通用户设计,使用前请了解风险
    3. ⚠️ 汐洛绞架基于思源笔记,开发者尽最大努力与思源笔记保持兼容,但无法实现 100% 兼容
    29 引用 • 25 回帖 • 152 关注
  • Facebook

    Facebook 是一个联系朋友的社交工具。大家可以通过它和朋友、同事、同学以及周围的人保持互动交流,分享无限上传的图片,发布链接和视频,更可以增进对朋友的了解。

    4 引用 • 15 回帖 • 443 关注
  • 开源中国

    开源中国是目前中国最大的开源技术社区。传播开源的理念,推广开源项目,为 IT 开发者提供了一个发现、使用、并交流开源技术的平台。目前开源中国社区已收录超过两万款开源软件。

    7 引用 • 86 回帖
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 702 关注
  • 千千插件

    千千块(自定义块 css 和 js)
    可以用 ai 提示词来无限创作思源笔记

    32 引用 • 69 回帖
  • Bootstrap

    Bootstrap 是 Twitter 推出的一个用于前端开发的开源工具包。它由 Twitter 的设计师 Mark Otto 和 Jacob Thornton 合作开发,是一个 CSS / HTML 框架。

    18 引用 • 33 回帖 • 646 关注
  • 博客

    记录并分享人生的经历。

    274 引用 • 2393 回帖 • 1 关注
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    167 引用 • 408 回帖 • 494 关注
  • Typecho

    Typecho 是一款博客程序,它在 GPLv2 许可证下发行,基于 PHP 构建,可以运行在各种平台上,支持多种数据库(MySQL、PostgreSQL、SQLite)。

    12 引用 • 67 回帖 • 436 关注
  • 互联网

    互联网(Internet),又称网际网络,或音译因特网、英特网。互联网始于 1969 年美国的阿帕网,是网络与网络之间所串连成的庞大网络,这些网络以一组通用的协议相连,形成逻辑上的单一巨大国际网络。

    99 引用 • 367 回帖 • 1 关注
  • 又拍云

    又拍云是国内领先的 CDN 服务提供商,国家工信部认证通过的“可信云”,乌云众测平台认证的“安全云”,为移动时代的创业者提供新一代的 CDN 加速服务。

    20 引用 • 37 回帖 • 577 关注
  • 百度

    百度(Nasdaq:BIDU)是全球最大的中文搜索引擎、最大的中文网站。2000 年 1 月由李彦宏创立于北京中关村,致力于向人们提供“简单,可依赖”的信息获取方式。“百度”二字源于中国宋朝词人辛弃疾的《青玉案·元夕》词句“众里寻他千百度”,象征着百度对中文信息检索技术的执著追求。

    63 引用 • 785 回帖 • 46 关注
  • 尊园地产

    昆明尊园房地产经纪有限公司,即:Kunming Zunyuan Property Agency Company Limited(简称“尊园地产”)于 2007 年 6 月开始筹备,2007 年 8 月 18 日正式成立,注册资本 200 万元,公司性质为股份经纪有限公司,主营业务为:代租、代售、代办产权过户、办理银行按揭、担保、抵押、评估等。

    1 引用 • 22 回帖 • 838 关注
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 3 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    950 引用 • 1460 回帖 • 2 关注
  • SQLite

    SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是全世界使用最为广泛的数据库引擎。

    4 引用 • 7 回帖
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    35 引用 • 468 回帖 • 768 关注
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    40 引用 • 157 回帖
  • Gitea

    Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证。

    5 引用 • 16 回帖 • 3 关注