0

0

Java 8 并发教程:同步和锁

黄舟

黄舟

发布时间:2017-02-07 10:17:17

|

1647人浏览过

|

来源于php中文网

原创

欢迎阅读我的 java8 并发教程的第二部分。这份指南将会以简单易懂的代码示例来教给你如何在 java8 中进行并发编程。这是一系列教程中的第二部分。在接下来的 15 分钟,你将会学会如何通过同步关键字,锁和信号量来同步访问共享可变变量。

  • 第一部分:线程和执行器

  • 第二部分:同步和锁

  • 第三部分:原子操作和 ConcurrentMap

这篇文章中展示的中心概念也适用于Java的旧版本,然而代码示例适用于Java 8,并严重依赖于 lambda 表达式和新的并发特性。如果你还不熟悉 lambda,我推荐你先阅读我的 Java 8 教程。

立即学习Java免费学习笔记(深入)”;

出于简单的因素,这个教程的代码示例使用了定义在这里的两个辅助函数sleep(seconds) 和 stop(executor)。

同步

在上一章中,我们学到了如何通过执行器服务同时执行代码。当我们编写这种多线程代码时,我们需要特别注意共享可变变量的并发访问。假设我们打算增加某个可被多个线程同时访问的整数。

我们定义了count字段,带有increment()方法来使count加一:

int count = 0;

void increment() {
    count = count + 1;
}

当多个线程并发调用这个方法时,我们就会遇到大麻烦:

ExecutorService executor = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::increment));

stop(executor);

System.out.println(count);  // 9965

我们没有看到count为 10000 的结果,上面代码的实际结果在每次执行时都不同。原因是我们在不同的线程上共享可变变量,并且变量访问没有同步机制,这会产生竞争条件。

增加一个数值需要三个步骤:(1)读取当前值,(2)使这个值加一,(3)将新的值写到变量。如果两个线程同时执行,就有可能出现两个线程同时执行步骤1,于是会读到相同的当前值。这会导致无效的写入,所以实际的结果会偏小。上面的例子中,对count的非同步并发访问丢失了35次增加操作,但是你在自己执行代码时会看到不同的结果。

幸运的是,Java自从很久之前就通过synchronized关键字支持线程同步。我们可以使用synchronized来修复上面在增加count时的竞争条件。

synchronized void incrementSync() {
    count = count + 1;
}

在我们并发调用incrementSync()时,我们得到了count为10000的预期结果。没有再出现任何竞争条件,并且结果在每次代码执行中都很稳定:

ExecutorService executor = Executors.newFixedThreadPool(2);

IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::incrementSync));

stop(executor);

System.out.println(count);  // 10000

synchronized关键字也可用于语句块:

void incrementSync() {
    synchronized (this) {
        count = count + 1;
    }
}

Java在内部使用所谓的“监视器”(monitor),也称为监视器锁(monitor lock)或内在锁( intrinsic lock)来管理同步。监视器绑定在对象上,例如,当使用同步方法时,每个方法都共享相应对象的相同监视器。

所有隐式的监视器都实现了重入(reentrant)特性。重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁(例如,同步方法调用相同对象的另一个同步方法)。

并发 API 支持多种显式的锁,它们由Lock接口规定,用于代替synchronized的隐式锁。锁对细粒度的控制支持多种方法,因此它们比隐式的监视器具有更大的开销。

锁的多个实现在标准 JDK 中提供,它们会在下面的章节中展示。

ReentrantLock

ReentrantLock类是互斥锁,与通过synchronized访问的隐式监视器具有相同行为,但是具有扩展功能。就像它的名称一样,这个锁实现了重入特性,就像隐式监视器一样。

让我们看看使用ReentrantLock之后的上面的例子。

ReentrantLock lock = new ReentrantLock();
int count = 0;

void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

锁可以通过lock()来获取,通过unlock()来释放。把你的代码包装在try-finally代码块中来确保异常情况下的解锁非常重要。这个方法是线程安全的,就像同步副本那样。如果另一个线程已经拿到锁了,再次调用lock()会阻塞当前线程,直到锁被释放。在任意给定的时间内,只有一个线程可以拿到锁。

锁对细粒度的控制支持多种方法,就像下面的例子那样:

executor.submit(() -> {
    lock.lock();
    try {
        sleep(1);
    } finally {
        lock.unlock();
    }
});

executor.submit(() -> {
    System.out.println("Locked: " + lock.isLocked());
    System.out.println("Held by me: " + lock.isHeldByCurrentThread());
    boolean locked = lock.tryLock();
    System.out.println("Lock acquired: " + locked);
});

stop(executor);

在第一个任务拿到锁的一秒之后,第二个任务获得了锁的当前状态的不同信息。

Locked: true
Held by me: false
Lock acquired: false

tryLock()方法是lock()方法的替代,它尝试拿锁而不阻塞当前线程。在访问任何共享可变变量之前,必须使用布尔值结果来检查锁是否已经被获取。

WHEE
WHEE

WHEE是一款AI绘画与图片生成器,提供一站式AI视觉创作服务。WHEE不仅会画也会修图,各种AI修图功能一应俱全。

下载

ReadWriteLock

ReadWriteLock接口规定了锁的另一种类型,包含用于读写访问的一对锁。读写锁的理念是,只要没有任何线程写入变量,并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更加频繁。

ExecutorService executor = Executors.newFixedThreadPool(2);
Map map = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();

executor.submit(() -> {
    lock.writeLock().lock();
    try {
        sleep(1);
        map.put("foo", "bar");
    } finally {
        lock.writeLock().unlock();
    }
});

上面的例子在暂停一秒之后,首先获取写锁来向映射添加新的值。在这个任务完成之前,两个其它的任务被启动,尝试读取映射中的元素,并暂停一秒:

Runnable readTask = () -> {
    lock.readLock().lock();
    try {
        System.out.println(map.get("foo"));
        sleep(1);
    } finally {
        lock.readLock().unlock();
    }
};

executor.submit(readTask);
executor.submit(readTask);

stop(executor);

当你执行这一代码示例时,你会注意到两个读任务需要等待写任务完成。在释放了写锁之后,两个读任务会同时执行,并同时打印结果。它们不需要相互等待完成,因为读锁可以安全同步获取,只要没有其它线程获取了写锁。

StampedLock

Java 8 自带了一种新的锁,叫做StampedLock,它同样支持读写锁,就像上面的例子那样。与ReadWriteLock不同的是,StampedLock的锁方法会返回表示为long的标记。你可以使用这些标记来释放锁,或者检查锁是否有效。此外,StampedLock支持另一种叫做乐观锁(optimistic locking)的模式。

让我们使用StampedLock代替ReadWriteLock重写上面的例子:

ExecutorService executor = Executors.newFixedThreadPool(2);
Map map = new HashMap<>();
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.writeLock();
    try {
        sleep(1);
        map.put("foo", "bar");
    } finally {
        lock.unlockWrite(stamp);
    }
});

Runnable readTask = () -> {
    long stamp = lock.readLock();
    try {
        System.out.println(map.get("foo"));
        sleep(1);
    } finally {
        lock.unlockRead(stamp);
    }
};

executor.submit(readTask);
executor.submit(readTask);

stop(executor);

通过readLock() 或 writeLock()来获取读锁或写锁会返回一个标记,它可以在稍后用于在finally块中解锁。要记住StampedLock并没有实现重入特性。每次调用加锁都会返回一个新的标记,并且在没有可用的锁时阻塞,即使相同线程已经拿锁了。所以你需要额外注意不要出现死锁。

就像前面的ReadWriteLock例子那样,两个读任务都需要等待写锁释放。之后两个读任务同时向控制台打印信息,因为多个读操作不会相互阻塞,只要没有线程拿到写锁。

下面的例子展示了乐观锁:

ExecutorService executor = Executors.newFixedThreadPool(2);
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.tryOptimisticRead();
    try {
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
        sleep(1);
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
        sleep(2);
        System.out.println("Optimistic Lock Valid: " + lock.validate(stamp));
    } finally {
        lock.unlock(stamp);
    }
});

executor.submit(() -> {
    long stamp = lock.writeLock();
    try {
        System.out.println("Write Lock acquired");
        sleep(2);
    } finally {
        lock.unlock(stamp);
        System.out.println("Write done");
    }
});

stop(executor);

乐观的读锁通过调用tryOptimisticRead()获取,它总是返回一个标记而不阻塞当前线程,无论锁是否真正可用。如果已经有写锁被拿到,返回的标记等于0。你需要总是通过lock.validate(stamp)检查标记是否有效。

执行上面的代码会产生以下输出:

Optimistic Lock Valid: true
Write Lock acquired
Optimistic Lock Valid: false
Write done
Optimistic Lock Valid: false

乐观锁在刚刚拿到锁之后是有效的。和普通的读锁不同的是,乐观锁不阻止其他线程同时获取写锁。在第一个线程暂停一秒之后,第二个线程拿到写锁而无需等待乐观的读锁被释放。此时,乐观的读锁就不再有效了。甚至当写锁释放时,乐观的读锁还处于无效状态。

所以在使用乐观锁时,你需要每次在访问任何共享可变变量之后都要检查锁,来确保读锁仍然有效。

有时,将读锁转换为写锁而不用再次解锁和加锁十分实用。StampedLock为这种目的提供了tryConvertToWriteLock()方法,就像下面那样:

ExecutorService executor = Executors.newFixedThreadPool(2);
StampedLock lock = new StampedLock();

executor.submit(() -> {
    long stamp = lock.readLock();
    try {
        if (count == 0) {
            stamp = lock.tryConvertToWriteLock(stamp);
            if (stamp == 0L) {
                System.out.println("Could not convert to write lock");
                stamp = lock.writeLock();
            }
            count = 23;
        }
        System.out.println(count);
    } finally {
        lock.unlock(stamp);
    }
});

stop(executor);

第一个任务获取读锁,并向控制台打印count字段的当前值。但是如果当前值是零,我们希望将其赋值为23。我们首先需要将读锁转换为写锁,来避免打破其它线程潜在的并发访问。tryConvertToWriteLock()的调用不会阻塞,但是可能会返回为零的标记,表示当前没有可用的写锁。这种情况下,我们调用writeLock()来阻塞当前线程,直到有可用的写锁。

信号量

除了锁之外,并发 API 也支持计数的信号量。不过锁通常用于变量或资源的互斥访问,信号量可以维护整体的准入许可。这在一些不同场景下,例如你需要限制你程序某个部分的并发访问总数时非常实用。

下面是一个例子,演示了如何限制对通过sleep(5)模拟的长时间运行任务的访问:

ExecutorService executor = Executors.newFixedThreadPool(10);

Semaphore semaphore = new Semaphore(5);

Runnable longRunningTask = () -> {
    boolean permit = false;
    try {
        permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);
        if (permit) {
            System.out.println("Semaphore acquired");
            sleep(5);
        } else {
            System.out.println("Could not acquire semaphore");
        }
    } catch (InterruptedException e) {
        throw new IllegalStateException(e);
    } finally {
        if (permit) {
            semaphore.release();
        }
    }
}

IntStream.range(0, 10)
    .forEach(i -> executor.submit(longRunningTask));

stop(executor);

执行器可能同时运行 10 个任务,但是我们使用了大小为5的信号量,所以将并发访问限制为5。使用try-finally代码块在异常情况中合理释放信号量十分重要。

执行上述代码产生如下结果:

Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore

信号量限制对通过sleep(5)模拟的长时间运行任务的访问,最大5个线程。每个随后的tryAcquire()调用在经过最大为一秒的等待超时之后,会向控制台打印不能获取信号量的结果。

这就是我的系列并发教程的第二部分。以后会放出更多的部分,所以敬请等待吧。像以前一样,你可以在Github上找到这篇文档的所有示例代码,所以请随意fork这个仓库,并自己尝试它。

我希望你能喜欢这篇文章。如果你还有任何问题,在下面的评论中向我反馈。你也可以在 Twitter 上关注我来获取更多开发相关的信息。

以上就是Java 8 并发教程:同步和锁的内容,更多相关内容请关注PHP中文网(www.php.cn)!

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
2026春节习俗大全
2026春节习俗大全

本专题整合了2026春节习俗大全,阅读专题下面的文章了解更多详细内容。

68

2026.02.11

Yandex网页版官方入口使用指南_国际版与俄罗斯版访问方法解析
Yandex网页版官方入口使用指南_国际版与俄罗斯版访问方法解析

本专题全面整理了Yandex搜索引擎的官方入口信息,涵盖国际版与俄罗斯版官网访问方式、网页版直达入口及免登录使用说明,帮助用户快速、安全地进入Yandex官网,高效使用其搜索与相关服务。

200

2026.02.11

虫虫漫画网页版入口与免费阅读指南_正版漫画全集在线查看方法
虫虫漫画网页版入口与免费阅读指南_正版漫画全集在线查看方法

本专题系统整理了虫虫漫画官网及网页版最新入口,涵盖免登录观看、正版漫画全集在线阅读方式,并汇总稳定可用的访问渠道,帮助用户快速找到虫虫漫画官方页面,轻松在线阅读各类热门漫画内容。

40

2026.02.11

Docker容器化部署与DevOps实践
Docker容器化部署与DevOps实践

本专题面向后端与运维开发者,系统讲解 Docker 容器化技术在实际项目中的应用。内容涵盖 Docker 镜像构建、容器运行机制、Docker Compose 多服务编排,以及在 DevOps 流程中的持续集成与持续部署实践。通过真实场景演示,帮助开发者实现应用的快速部署、环境一致性与运维自动化。

4

2026.02.11

Rust异步编程与Tokio运行时实战
Rust异步编程与Tokio运行时实战

本专题聚焦 Rust 语言的异步编程模型,深入讲解 async/await 机制与 Tokio 运行时的核心原理。内容包括异步任务调度、Future 执行模型、并发安全、网络 IO 编程以及高并发场景下的性能优化。通过实战示例,帮助开发者使用 Rust 构建高性能、低延迟的后端服务与网络应用。

1

2026.02.11

Spring Boot企业级开发与MyBatis Plus实战
Spring Boot企业级开发与MyBatis Plus实战

本专题面向 Java 后端开发者,系统讲解如何基于 Spring Boot 与 MyBatis Plus 构建高效、规范的企业级应用。内容涵盖项目架构设计、数据访问层封装、通用 CRUD 实现、分页与条件查询、代码生成器以及常见性能优化方案。通过完整实战案例,帮助开发者提升后端开发效率,减少重复代码,快速交付稳定可维护的业务系统。

6

2026.02.11

包子漫画网页版入口与全集阅读指南_正版免费漫画快速访问方法
包子漫画网页版入口与全集阅读指南_正版免费漫画快速访问方法

本专题汇总了包子漫画官网和网页版入口,提供最新章节抢先看方法、正版免费阅读指南,以及稳定访问方式,帮助用户快速直达包子漫画页面,无广告畅享全集漫画内容。

159

2026.02.10

MC.JS网页版快速畅玩指南_MC.JS官网在线入口及免安装体验方法
MC.JS网页版快速畅玩指南_MC.JS官网在线入口及免安装体验方法

本专题汇总了MC.JS官网入口和网页版快速畅玩方法,提供免安装访问、不同版本(1.8.8、1.12.8)在线体验指南,以及正版网页端操作说明,帮助玩家轻松进入MC.JS世界,实现即时畅玩与高效体验。

89

2026.02.10

谷歌邮箱网页版登录与注册全指南_Gmail账号快速访问与安全操作教程
谷歌邮箱网页版登录与注册全指南_Gmail账号快速访问与安全操作教程

本专题汇总了谷歌邮箱网页版的最新登录入口和注册方法,详细提供官方账号快速访问方式、网页版操作教程及安全登录技巧,帮助用户轻松管理Gmail邮箱账户,实现高效、安全的邮箱使用体验。

78

2026.02.10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3.5万人学习

C# 教程
C# 教程

共94课时 | 9.2万人学习

Java 教程
Java 教程

共578课时 | 64万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号