滚雪球学Java(81):Java多线程编程的关键一环:深入剖析同步与互斥机制
咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java之多线程篇啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~
🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
前言
实际开发过程中,我们就能了解到,在项目开发中,特别是涉及到多线程这块,线程同步和互斥是非常重要和常见的概念。在并发编程中,多个线程同时访问和修改共享资源时,如果没有合适的线程同步和互斥机制,就会出现数据不一致和并发错误的问题。
本文将以Java开发语言为例,详细介绍线程同步和互斥的概念、原理和应用场景。我们将从源代码解析、应用场景案例、优缺点分析等方面来探讨线程同步和互斥的实现方式和效果,以帮助零基础的Java小白理解和应用线程同步和互斥的相关知识。
摘要
线程同步和互斥是多线程编程中的核心概念,通过合适的同步机制可以保证共享资源的正确访问和修改。在本文中,我们将介绍Java中的几种常见的线程同步和互斥机制,包括synchronized
关键字、ReentrantLock
类、Semaphore
类等,如果你还想学习其他的,也可以评论区告知与我,只要我会,我定不负所望,把它以大白话给讲的透透白白的。
此文,我将通过对源代码解析、实际应用场景案例、优缺点分析、案例演示等方式,深入讨论线程同步和互斥的实现原理、效果和适用场景。最后,我们将给出一些类代码方法介绍和测试用例,以帮助读者更好地理解和应用这些同步机制。
概述
在多线程编程中,线程同步和互斥是为了解决多个线程并发访问共享资源时可能出现的问题。共享资源是多个线程共同使用和修改的数据,例如全局变量、静态变量等。当多个线程同时读写共享资源时,就可能导致数据不一致的问题。
线程同步就是为了保证多个线程在访问和修改共享资源时的有序性,避免数据冲突和错误。线程同步的核心概念是互斥,即同一时间只允许一个线程访问共享资源,其他线程需要等待。这样可以避免多个线程同时修改共享资源导致的数据不一致问题。
在Java语音中,我们可以使用synchronized
关键字、ReentrantLock
类、Semaphore
类等方式来实现线程同步和互斥。这些机制都提供了加锁和解锁的操作,保证了同一时间只有一个线程可以访问共享资源。不同的机制适用于不同的场景,你们可以根据具体的需求来选择合适的机制。
源代码解析
synchronized
关键字
synchronized
关键字是Java中内置的线程同步机制,用于修饰方法或代码块。被synchronized
修饰的方法或代码块在同一时间只允许一个线程执行,其他线程需要等待。synchronized
关键字使用示例如下:
public synchronized void synchronizedMethod() {
// 代码块
}
在上述代码中,synchronized
关键字修饰synchronizedMethod()
方法,使得该方法在同一时间只能被一个线程执行。当一个线程进入synchronizedMethod()
方法时,其他线程则需要等待,需等到该方法执行完了,其他线程才能进。
ReentrantLock类
ReentrantLock
类是Java提供的可重入锁实现,可以用于替代synchronized
关键字实现线程同步和互斥。ReentrantLock
类使用示例如下:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 代码块
} finally {
lock.unlock();
}
在上面的代码中,我们首先创建了一个ReentrantLock
对象lock,然后在需要同步的代码块前调用lock()
方法获取锁,在代码块执行完成后调用unlock()
方法释放锁。
Semaphore类
Semaphore
类是Java提供的信号量机制,可以用于控制同时访问某个共享资源的线程数量。Semaphore
类使用示例如下:
Semaphore semaphore = new Semaphore(2);
semaphore.acquire();
try {
// 代码块
} finally {
semaphore.release();
}
在上面的代码中,我们首先创建一个Semaphore
对象,然后在需要同步的代码块前调用acquire()
方法获取信号量,获取成功后才能进入代码块。在代码块执行完成后调用release()
方法释放信号量。
应用场景案例
生产者消费者模型
这里,我要重点介绍一波,生产者消费者模型
是一个典型的线程同步和互斥
的应用场景。在该模型中,生产者线程生成数据并放入共享队列,消费者线程从队列中取出数据进行消费。
在Java中,我们可以使用BlockingQueue
类来实现生产者消费者模型。演示代码如下:
先定义一个Producer 生产者线程类,模拟一个生产者的角色,使生成数据并将其放入一个队列中,以便其他线程(消费者)可以处理这些数据。
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-03 17:56
*/
public class Producer implements Runnable {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
public void run() {
while (true) {
try {
String data = produceData();
queue.put(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private String produceData() {
// 这里是产生数据的逻辑,返回一个String类型的数据
return "Data: " + System.currentTimeMillis();
}
}
再定义一个Consumer消费者线程类,负责生成数据并将其放入一个共享的队列中,而消费者则从队列中取出数据并进行处理。
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-03 18:06
*/
public class Consumer implements Runnable {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
@Override
public void run() {
while (true) {
try {
String data = queue.take(); // 从共享队列中取出数据
consumeData(data); // 处理取出的数据
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void consumeData(String data) {
// 这里是处理数据的逻辑
System.out.println("Consumed: " + data);
}
}
在上面的代码中,我们首先创建了一个容量为10的BlockingQueue对象,生产者线程通过put()方法将数据放入队列,消费者线程通过take()方法从队列中取出数据。BlockingQueue提供了线程安全的操作,内部实现了对共享资源的同步和互斥。
应用场景
Consumer
类可以与 Producer
类一起使用,形成生产者-消费者模式的一个完整解决方案。这种模式在多线程编程中非常常见,用于处理并发任务,如消息队列处理、事件监听、后台任务执行等。通过使用线程安全的队列,可以确保数据在生产者和消费者之间安全地传递,同时避免了直接共享资源可能导致的并发问题。
优缺点分析
synchronized关键字
优点:
- 内置的Java线程同步机制,方便使用和理解。
- 可以修饰方法或代码块,灵活性较高。
缺点:
- 在某些复杂场景下,可能会带来性能问题。
- 无法中断一个正在执行的线程。
ReentrantLock类
优点:
- 提供了比synchronized更丰富的功能,例如可重入、公平锁等。
- 可以替代synchronized关键字。
缺点:
- 使用较复杂,需要手动加锁和解锁。
- 如果忘记释放锁,可能会导致死锁等问题。
Semaphore类
优点:
- 可以控制同时访问共享资源的线程数量。
- 提供了灵活的信号量机制。
缺点:
- 使用较复杂,需要手动获取和释放信号量。
- 可能会导致资源的浪费,例如多个线程同时等待信号量。
类代码方法介绍
synchronized关键字
- synchronized修饰方法:
public synchronized void synchronizedMethod() {
// 代码块
}
- synchronized修饰代码块:
public void synchronizedBlock() {
synchronized (this) {
// 代码块
}
}
ReentrantLock类
- 加锁和解锁:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 代码块
} finally {
lock.unlock();
}
Semaphore类
- 获取和释放信号量:
Semaphore semaphore = new Semaphore(2);
semaphore.acquire();
try {
// 代码块
} finally {
semaphore.release();
}
测试用例
为了验证线程同步和互斥的有效性,我们可以编写测试用例来模拟多线程环境。以下是一个简单的测试用例,用于验证Counter类的线程安全性:核心就是使用 if
语句来检查 Counter
的值是否符合预期。
测试代码
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-03 18:11
*/
public class Test {
public static void main(String[] args) {
Counter counter = new Counter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int expectedCount = 100 * 100; // 100 threads, each incrementing 100 times
if (counter.getCount() == expectedCount) {
System.out.println("Test passed: The count is correct. Expected " + expectedCount + ", got " + counter.getCount());
} else {
System.out.println("Test failed: The count is incorrect. Expected " + expectedCount + ", got " + counter.getCount());
}
}
}
如下是Counter类。
/**
* @Author bug菌
* @Source 公众号:猿圈奇妙屋
* @Date 2024-04-03 18:12
*/
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
我们首先创建了一个 Counter
对象,并初始化了一个线程数组。然后,我们创建了100个线程,每个线程都会调用 increment
方法100次。所有线程启动后,我们等待它们全部完成。最后,我们检查 Counter
的 getCount
方法返回的值是否等于预期值(100个线程,每个线程增加100次,所以预期值是10000)。
如果计数器的值与预期相符,我们打印一条消息表示测试通过。如果不符,我们打印一条消息表示测试失败,并显示实际的计数值。这样的输出可以帮助我们了解 Counter
类是否正确地实现了线程安全。
测试结果展示
可以看到控制台输出结果,100个线程,每个线程增加100次,预期值与执行结果是一致的:10000,所以输出了这句:Test passed: The count is correct. Expected 10000, got 10000
。
用例代码解析
针对如上测试代码,这里我再具体给大家讲解下,希望能够更透彻的帮助大家理解。
这段代码是一个多线程测试用例,用于验证 Counter
类的线程安全性。Counter
类包含一个共享资源 count
,并提供了 increment
方法来增加该资源的值。测试的目的是确保当多个线程并发地调用 increment
方法时,count
的最终值是预期的 100 个线程各自增加 100 次的总和,即 10000。
代码组件
Counter
类:这是一个包含共享资源count
的类。main
方法:这是程序的入口点,它创建了一个Counter
实例和 100 个线程。- 线程创建和启动:使用一个
for
循环创建 100 个线程,每个线程都会循环 100 次调用counter.increment()
。 - 线程等待:使用另一个
for
循环等待所有线程完成执行。thread.join()
方法确保了主线程在继续执行之前等待每个子线程完成。 - 断言检查:使用
if
语句检查count
的最终值是否为预期值。如果是,打印一条通过消息;如果不是,打印一条失败消息。
代码目的
- 验证
Counter
类的线程安全性:确保即使在高并发的情况下,count
的值也能正确地反映所有线程的increment
操作。 - 演示多线程环境下的共享资源管理:通过这个测试用例,可以展示如何在实际应用中管理共享资源,以及如何编写代码来确保线程安全。
注意事项
Counter
类的实现细节未给出:为了使测试有效,Counter
类必须实现适当的同步机制,如synchronized
关键字、ReentrantLock
或其他同步工具。- 线程创建开销:创建大量线程可能会对系统资源造成压力,因此在实际应用中需要权衡线程数量和性能。
- 等待线程完成:
thread.join()
方法用于确保所有线程都有机会完成它们的任务,这是测试线程安全的关键部分。
小结
这段代码是一个简单的多线程测试框架,用于验证共享资源在并发访问下的线程安全性。通过运行这个测试,开发者可以确保 Counter 类在多线程环境中能够正确地管理其内部状态。
全文小结
本文详细介绍了Java中线程同步和互斥的概念、原理和实现方式。通过源代码解析,我们学习了如何使用synchronized关键字、ReentrantLock类和Semaphore类来实现线程同步和互斥。同时,我们通过生产者消费者模型的应用场景案例,了解了线程同步和互斥在实际开发中的应用。我们还分析了这些同步机制的优缺点,并提供了一些类代码方法介绍和测试用例,以帮助读者更好地理解和应用线程同步和互斥。
总结
线程同步和互斥是解决多线程并发问题的重要手段。Java提供了多种同步机制,每种机制都有其适用的场景和特点。开发者需要根据具体的业务需求和性能考虑,选择合适的同步机制。在实际开发中,正确使用线程同步和互斥机制,可以有效地避免并发问题,保证数据的一致性和系统的稳定性。
结尾
希望本文能够帮助Java零基础的开发者理解线程同步和互斥的概念,并能够在实际开发中正确应用这些知识。多线程编程是一个复杂而又充满挑战的领域,掌握线程同步和互斥机制是走向高效并发编程的第一步。在未来的学习中,我们还需要不断深入探索和实践,以不断提高自己的多线程编程能力。
… …
ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看专栏的导读篇《「滚雪球学Java」教程导航帖》,每天学习一小节,日积月累下去,你一定能成为别人眼中的大佬的!功不唐捐,久久为功!
「赠人玫瑰,手留余香」,咱们下期拜拜~~
附录源码
如上涉及所有源码均已上传同步在「Gitee」,提供给同学们一对一参考学习,辅助你更迅速的掌握。
☀️建议/推荐你
无论你是计算机专业的学生,还是对编程感兴趣的跨专业小白,都建议直接入手「滚雪球学Java」专栏;该专栏不仅免费,bug菌还郑重承诺,只要你学习此专栏,均能入门并理解Java SE,以全网最快速掌握Java语言,每章节源码均同步「Gitee」,你真值得拥有;学习就像滚雪球一样,越滚越大,带你指数级提升。
码字不易,如果这篇文章对你有所帮助,帮忙给bugj菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。
同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!
📣关于我
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云2023年度十佳博主,掘金多年度人气作者Top40,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 20w+;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。
- 点赞
- 收藏
- 关注作者
评论(0)