【漫画】JAVA并发编程 如何解决原子性问题

简介: 线程切换导致了并发编程中的原子性问题,那么如何解决原子性问题呢?互斥锁来帮你搞定它!

并发编程BUG源头文章中,我们初识了并发编程的三个bug源头:可见性、原子性、有序性。在如何解决可见性和原子性文章中我们大致了解了可见性和有序性的解决思路,今天轮到最后一个大bug,那就是原子性。

知识回顾

_1

锁模型

_2
_3

JAVA中的锁模型

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。

  • synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!synchronized并不能改变CPU时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待。
  • synchronized一定能保证原子性,因为被 synchronized 修饰某段代码后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一定能保证原子操作
  • synchronized也能够保证可见性和有序性。根据前第二篇文章:Happens-Before 规则之管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。即前一个线程的解锁操作对后一个线程的加锁操作可见。综合 Happens-Before 的传递性原则,我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。- synchronized 关键字可以用来修饰静态方法,非静态方法,也可以用来修饰代码块

理论说完了,来点实际的吧!首先我们用synchronized 修饰非静态方法来改写第一章中原子性问题的那段代码:

    private long count = 0;
    
    // 修饰非静态方法 当修饰非静态方法的时候,锁定的是当前实例对象 this。
    // 当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,如果这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不同方法,但是他们使用同一个对象来调用,那么这些方法的锁就是一样的,就是这个对象,那么会造成阻塞。如果多个线程通过不同的对象来调用方法,那么他们的锁就是不一样的,不会造成阻塞。
    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized2 test = new TestSynchronized2();
        // 创建两个线程,执行 add() 操作
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // 启动两个线程
        th1.start();th2.start();
        // 等待两个线程执行结束
        th1.join();th2.join();
        System.out.println(test.count);
    }

运行一下吧!你会发现永远都可以达到我们想要的效果了~
除了上面代码中修饰非静态方法,还可以修饰静态方法和代码块

    // 修饰静态方法 当修饰静态方法的时候,锁定的是当前类的 Class 对象,即TestSynchronized2.class 。这个范围就比对象锁大。这里就算是不同对象,但是只要是该类的对象,就使用的是同一把锁。
    synchronized static void bar() {
        // 临界区
    }
    // 修饰代码块 java中经典的双重锁检查机制
    private volatile static TestSynchronized2 instance;
    public static TestSynchronized2 getInstance() {
        if (instance == null) {
            synchronized (TestSynchronized2.class) {
                if (instance == null) {
                    instance = new TestSynchronized2();
                }
            }
        }
        return instance;
    }

明确锁和资源的关系

深入分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。受保护资源和锁之间的关联关系是 N:1 的关系。如果一个资源用N个锁,那肯定出问题的,就好像一个厕所坑位,你有10把钥匙,那不是可以10个人同时进了?

现在给出两段错误代码,想一想到底为啥错了吧?

    static long value1 = 0L;

    synchronized long get1() {
        return value1;
    }

    synchronized static void addOne1() {
        value1 += 1;
    }
    long value = 0L;

    long get() {
        synchronized (new Object()) {
            return value;
        }
    }

第一段错误原因:
因为我们说过synchronized修饰普通方法 锁定的是当前实例对象 this 而修饰静态方法 锁定的是当前类的 Class 对象
所以这里有两把锁 分别是 this 和 TestSynchronized3.class
由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。

第二段错误原因:
加锁本质就是在锁对象的对象头中写入当前线程id,但是synchronized (new Object())每次在内存中都是新对象,所以加锁无效。

问:刚刚的例子都是多个锁保护一个资源,这样百分百是不行的。那么一个锁保护多个资源,就一定可以了吗?

答:如果多个资源彼此之间是没有关联的,那可以用一个锁来保护。如果有关联的话,那是不行的。比如说银行转账操作,你给我转账,我账户多100,你账户少100,我不能用我的锁来保护你,就像现实生活中我的锁是不能保护你的财产的。

划重点!要区分多个资源是否有关联!但是一个锁保护多个没关联的资源,未免性能太差了哦,比如我听歌和玩游戏可以同时进行,你非得让我做完一个再做另一个,岂不是要双倍时间。所以即使一个锁可以保护多个没关联的资源,但是一般而已,会各自用不同的锁,能够提升性能。这种锁还有个名字,叫细粒度锁。

问:刚刚说到银行转账的案例,那么假如某天在某银行同时发生这样一个事,柜员小王需要完成A账户给B账户转账100元,柜员小李需要完成B账户给A账户转账100元,请问如何实现呢?

答:其实用两把锁就实现了,转出一把,转入另一把。只有当两者都成功时,才执行转账操作。

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(200); //A的初始账户余额200
        Account b = new Account(300); //B的初始账户余额200
        Thread threadA = new Thread(()->{
            try {
                transfer(a,b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                transfer(b,a,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        threadB.start();
    }

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        synchronized (source) {
            log.info("持有锁{} 等待锁{}",source,target);
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

至此,恭喜你,一波问题解决了,可是遗憾的告诉你:又导致了另一个bug。这段代码是有可能发生死锁的!并发编程中要注意的东西可真是多哟。咱们先把死锁这个名词记住!持续关注【胖滚猪学编程】公众号!在我们后面的文章中找答案!

如何保证原子性

现在我们已经知道互斥锁可以保证原子性,也知道了如何使用synchronized来保证原子性。但synchronized 并不是JAVA中唯一能保证原子性的方案。

如果你粗略的看一下J.U.C(java.util.concurrent包),那么你可以很显眼的发现它俩:
image.png

一个是lock包,一个是atomic包,只要你英语过了四级。。我相信你都可以马上断定,它们可以解决原子性问题。

由于这两个包比较重要,所以会放在后面的模块单独说,持续关注【胖滚猪学编程】公众号吧!

相关文章
|
4天前
|
IDE Java 物联网
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
11 0
|
4天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
5天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
7天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
|
7天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
8天前
|
SQL 安全 Java
Java安全编程:防范网络攻击与漏洞
【4月更文挑战第15天】本文强调了Java安全编程的重要性,包括提高系统安全性、降低维护成本和提升用户体验。针对网络攻击和漏洞,提出了防范措施:使用PreparedStatement防SQL注入,过滤和转义用户输入抵御XSS攻击,添加令牌对抗CSRF,限制文件上传类型和大小以防止恶意文件,避免原生序列化并确保数据完整性。及时更新和修复漏洞是关键。程序员应遵循安全编程规范,保障系统安全。
|
缓存 前端开发 Java
高级java必须清楚的概念:原子性、可见性、有序性
原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的。
109 0
高级java必须清楚的概念:原子性、可见性、有序性

热门文章

最新文章