【Java|多线程与高并发】线程安全问题以及synchronized使用实例

简介: Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。

1. 前言

Java多线程环境下,多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。


线程安全一直都是一个令人头疼的问题.为了解决这个问题,Java为我们提供了很多方式.


1.synchronized关键字、ReentrantLock类等。

2.使用线程安全的数据结构,例如ConcurrentHashMap、ConcurrentLinkedQueue等,避免共享资源

3.使用volatile关键字保证内存可见性等方法。

本文主要介绍synchronized关键字



2. 线程安全问题演示

多线程环境下可能会产生的问题

先看下面这个Test类:


class Test{
    private int count = 0;
    public void add(){
        count++;
    }
    public int getCount() {
        return count;
    }
}

count是一个普通的成员变量,提供了一个add()方法,让count进行自增

同时提供了一个get方法获取到count的值.


再来看下面这段代码:

public class Demo8 {
    private static Test test = new Test();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(test.getCount());
    }
}

创建了一个Test类的实例,同时创建两个线程.

让这两个线程各调用50000次Test实例的add()方法,最后输出count的值.


按理来说应该是count的值应该是100000.


运行结果:


可以看到运行结果不是100000,而是比100000小. 而且两次运行的结果还不一样.

这就是一个典型的线程安全问题


3.线程安全问题的原因

上述问题产生的原因:

这里的原因主要是 count++操作,count++是三条指令在CPU上执行的.

count++可以分为3个步骤:

937c40ff63d54f519d037426538fee52.png


1.由于当前是两个线程同时修改同一份变量

2.且修改操作不是原子的

3.加上线程之间调度的不确定性

上述三条原因导致了这两个线程执行count++操作时,执行的顺序会有多种情况.

例如:


bc5c435ab5ea468b83cfe84bd3e60951.png

ps: 这只是其中的一种情况


如果是这种情况会导致什么呢?


看这张图:


adec852d739c4ac9887430866de5ee24.png

t2线程已经修改过count的值了,但是由于t1并没有获取到count最新的值,因此t1只是在原来读到的count值得基础上进行修改.


因此虽然是执行了两次count++操作,但是真正执行完的效果只有一次count++.这也就是为什么count的值小于100000的原因了.


如果想要让执行完的结果为100000,那么就需要使用synchronized关键字进行加锁了


4.synchronized关键字

synchronized关键字是用于实现线程同步的机制,可以保证在同一时刻只有一个线程可以访问被synchronized包围的代码块或方法,从而避免了多个线程同时访问共享资源时可能出现的数据竞争和不一致的情况。


以上是一些synchronized关键字的介绍,相比大家也不喜欢看这些定义.


接下来就以我自己的方式为大家讲解:


上述问题的解决:

使用synchronized对count++操作进行加锁,count++之后再进行解锁


此时一个线程在count++时进行加锁后,另外一个线程想进行修改,是修改不了的.这个线程只能阻塞等待


上面提到了使用synchronized进行加锁,什么是加锁呢?

例如下面这张图:

653b5e6322b24416b5c495368a350240.png


之前疫情期间,相比大家核酸都没少做吧.

做核酸一次只能给一个人做,如有当前已经有人在做核酸了,那么下一个人只能等待.只有等前面那个人做完,才能够去做.


这里的做核酸就相当于 count++, 去做核酸就相当于是加锁, 在有人做核酸时,其它人只能等待(阻塞等待).做完离开,就相当于解锁(释放锁)


那么如果使用synchronized进行加锁解决上面的问题呢? .


可以对add方法进行加锁.


d7076c71675149289d20d4db8b88a399.png

执行add方法时会加锁,执行完add方法会解锁.


修改之后的运行结果就是100000了.

b44ee8cc9a204610bb66547184cfcc64.png



synchronized关键字可以应用于方法(上述示例)和代码块两种情况:


修饰方法:将synchronized关键字放在方法声明前,表示该方法是同步方法,只有一个线程可以访问该方法。例如:

public synchronized void method() {
    // 同步代码块
}

修饰代码块:将synchronized关键字放在代码块前,表示该代码块是同步代码块,只有一个线程可以执行该代码块。例如:

public void method() {
    synchronized (锁对象) {
        // 同步代码块
    }
}

在Java中任何对象都可以作为"锁对象".这里的锁对象十分关键


两个(多个)线程针对同一个对象进行加锁,才会有锁竞争. 如果不对同一个对象进行加锁,每个线程各执行各的,就不会产生锁竞争.也就不会阻塞等待.


因此使用synchronized的加锁的时候,要考虑清楚对哪段代码进行加锁,锁的代码不同,对执行的效果会有很大的影响.


锁的代码越多,锁的粒度越大/越粗,所得代码越少,锁的粒度越小/越细


synchronized关键字的使用需要注意以下几点:


synchronized关键字只能保证同一个对象的同步代码块或同步方法是互斥的,不同对象的同步代码块或同步方法是不互斥的。

synchronized关键字的使用会降低程序的执行效率,因为它会导致线程的上下文切换和锁的竞争。

synchronized关键字只能保证互斥的访问,不能保证线程安全,需要结合其他机制来保证线程安全,例如使用volatile关键字、Atomic类、Lock接口等。

5. 总结

synchronized关键字是Java中实现线程同步的重要机制之一,虽然它的使用会带来一定的性能开销,但是在多线程并发访问共享资源时,使用synchronized关键字可以有效地避免数据竞争和不一致的情况,保证程序的正确性和稳定性。


5918be83d71d4993a422b7e9be823adf.gif

感谢你的观看!希望这篇文章能帮到你!

专栏: 《从零开始的Java学习之旅》在不断更新中,欢迎订阅!

“愿与君共勉,携手共进!”

8fbf2a7f2d0e4db782e58035677a303d.png

相关文章
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
191 0
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
2月前
|
存储 Java 关系型数据库
Java 项目实战基于面向对象思想的汽车租赁系统开发实例 汽车租赁系统 Java 面向对象项目实战
本文介绍基于Java面向对象编程的汽车租赁系统技术方案与应用实例,涵盖系统功能需求分析、类设计、数据库设计及具体代码实现,帮助开发者掌握Java在实际项目中的应用。
100 0
|
5月前
|
缓存 监控 Cloud Native
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
本文深入解析了Java Solon v3.2.0框架的实战应用,聚焦高并发与低内存消耗场景。通过响应式编程、云原生支持、内存优化等特性,结合API网关、数据库操作及分布式缓存实例,展示其在秒杀系统中的性能优势。文章还提供了Docker部署、监控方案及实际效果数据,助力开发者构建高效稳定的应用系统。代码示例详尽,适合希望提升系统性能的Java开发者参考。
252 4
Java Solon v3.2.0 高并发与低内存实战指南之解决方案优化
|
4月前
|
安全 Java 测试技术
Java 大学期末实操项目在线图书管理系统开发实例及关键技术解析实操项目
本项目基于Spring Boot 3.0与Java 17,实现在线图书管理系统,涵盖CRUD操作、RESTful API、安全认证及单元测试,助力学生掌握现代Java开发核心技能。
198 0
|
4月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
4月前
|
缓存 NoSQL Java
Java 项目实操高并发电商系统核心模块实现从基础到进阶的长尾技术要点详解 Java 项目实操
本项目实战实现高并发电商系统核心模块,涵盖商品、订单与库存服务。采用Spring Boot 3、Redis 7、RabbitMQ等最新技术栈,通过秒杀场景解决库存超卖、限流熔断及分布式事务难题。结合多级缓存优化查询性能,提升系统稳定性与吞吐能力,适用于Java微服务开发进阶学习。
152 0
|
5月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
344 5
|
5月前
|
存储 算法 Java
【Java实例-智慧牌局】Java实现赌桌上的21点
游戏规则:游戏开始时,玩家和庄家各获得两张牌,玩家可以看到自己手中的两张牌以及庄家的一张明牌。玩家需要根据手中的牌面总和,选择“要牌”(Hit)以获取更多牌,或“停牌”(Stand)停止要牌。如果玩家的牌面总和超过21点,即为爆牌,玩家立即输掉游戏。若玩家选择停牌,庄家则开始行动,其策略是当牌面总和小于17点时必须继续要牌。若庄家牌面总和超过21点,则庄家爆牌,玩家获胜。若双方均未爆牌,最终比较牌面总和,更接近21点的一方获胜;若牌面总和相同,则游戏以平局结束。
83 0
|
5月前
|
Java 开发者
【Java实例-英雄对战】Java战斗之旅,既分胜负也决生死
游戏规则:在“英雄对战”中,玩家和敌人轮流选择行动,目标是在对方生命值归零前将其击败。游戏开始时,玩家和敌人都有100生命值。每回合,玩家可以选择“攻击”,“追击”,“闪避反击”这三种行动之一。
71 0

热门文章

最新文章