大白话拆解—多线程(六)— 同步锁机制 和 synchronized

前言:

25年初,这个时候好多小伙伴都在备战期末

我们新年第二天照样日更一篇,今天这篇一定会对小白非常有用的!!!

因为我们会把案例到用代码实现的全过程思路呈现出来!!!

我们一直都是以这样的形式,让新手小白轻松理解复杂晦涩的概念,把Java代码拆解的清清楚楚,每一步都知道他是怎么来的,为什么用这串代码关键字,对比同类型的代码,让大家真正看完以后融会贯通,举一反三,实践应用!!!!


①官方定义  和  大白话拆解对比

②举生活中常见贴合例子、图解辅助理解的形式

③对代码实例中关键部分进行详细拆解、总结


我们上一篇提到,这些概念的拆解:

5.4 同步机制解决线程安全问题

  •  电脑就像是程序里的共享资源,那把特别的钥匙就是“同步锁”,而你们轮流玩游戏的过程就是在模拟多个线程安全地访问共享资源的情景。

5.4.1 同步机制解决线程安全问题的原理

  •  银行账户类 BankAccount,其中有一个方法 withdraw 用于取款。

5.4.2 同步代码块和同步方法

  • 同样举(银行取款存款)代码例子进行说明
  • 对比同步代码块和同步方法


大家点一个 赞  或者  关注我们一起进步!!!

我们接着继续!!!!


5.4.3 同步锁机制

官方语言:

  • 在多线程环境中,同步锁(Synchronization Lock),也称为互斥锁(Mutex Lock),是一种用来管理对共享资源的访问控制的机制。当一个线程获取了某个资源的锁之后,其他试图访问同一资源的线程将被阻塞,直到当前持有锁的线程释放了该锁。这种机制确保了在同一时间点只有一个线程能够修改或访问特定的共享资源,从而避免了数据竞争和不一致的状态。
  • Java中提供了多种方式来实现同步锁机制,最基础的是通过synchronized关键字,它可以应用于方法或者代码块。当一个线程进入一个synchronized的方法或代码块时,它会自动获得与该对象关联的内部锁(Intrinsic Lock)。其他试图进入同一个对象的synchronized方法或代码块的线程将会等待,直到当前线程退出并释放锁。

大白话拆解:

  • 你和你的朋友都想用同一个浴室,但是这个浴室一次只能有一个人使用。为了公平起见,你们决定谁先进入浴室就给门上锁,这样其他人就知道浴室正在使用中,他们必须在外面等着。当你洗完澡后,你会把锁打开,这时候下一个想要使用浴室的人就可以进去并再次上锁。
  • 这就是同步锁的工作原理,只不过这里的“浴室”是计算机程序中的共享资源,“锁”是用来确保一次只有一个线程可以访问这些资源的工具。

举个栗子:

public class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // 使用synchronized关键字保证同一时间只有一个线程可以执行withdraw方法
    public synchronized void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            System.out.println(Thread.currentThread().getName() + " is withdrawing " + amount);
            balance -= amount;
            System.out.println("New balance: " + balance);
        } else {
            System.out.println("Insufficient funds or invalid amount.");
        }
    }

    // 其他方法...
}

代码解释和总结:

public synchronized void withdraw(double amount) {
    if (amount > 0 && amount <= balance) {
        System.out.println(Thread.currentThread().getName() + " is withdrawing " + amount);
        balance -= amount;
        System.out.println("New balance: " + balance);
    } else {
        System.out.println("Insufficient funds or invalid amount.");
    }
}

if (amount > 0 && amount <= balance):

  • 这一部分是在检查两个条件:一是取出的钱数是否是正数(因为不能取负数的钱),二是账户里是否有足够的余额来完成这次取款。如果这两个条件都满足,那么接下来就可以安全地进行取款操作了。

balance -= amount;:

  • 如果上面的条件都通过了,程序会从账户余额中减去取款金额。这一步非常重要,因为它直接改变了账户的状态。如果我们不使用同步锁机制,多个线程可能会同时尝试改变余额,导致计算错误。

System.out.println(...):

  • 这两行代码是用来打印信息的,告诉我们哪个线程正在进行取款操作,以及取款后的最新余额是多少。这有助于我们跟踪每个线程的行为,特别是在调试多线程程序时非常有用。

synchronized 的锁是什么

官方语言

  • 在Java编程语言中,synchronized 是一个关键字,用于指示一种特殊的锁机制,它确保在同一时间只有一个线程能够执行被 synchronized 修饰的方法或代码块。当一个线程进入 synchronized 方法或者代码块时,它会自动获取与该对象关联的内部锁(也称为监视器锁)。其他试图进入同一个对象的 synchronized 方法或代码块的线程将会被阻塞,直到第一个线程完成并释放锁。
  • Synchronized 关键字可以用来修饰实例方法、静态方法以及代码块。对于实例方法,锁是当前实例对象;对于静态方法,锁是该类的 Class 对象;对于同步代码块,锁是括号内指定的对象。

大白话拆解:

  • 你和你的朋友们都想要使用一个公共的笔记本写东西,但是这个笔记本一次只能由一个人写。为了保证大家不会同时写而弄乱内容,你们定下了一个规则:谁要是想写,就必须先拿一个小木棍作为“通行证”。只有拿到这个小木棍的人才能开始写,等他写完了,就要把小木棍放回原处,这样其他人就可以拿起来写了。
  • 在计算机里,多个程序(我们叫它们“线程”)有时候也需要像你们一样共享一些资源,比如读取或修改同一个数据。为了不让这些线程同时改变数据导致混乱,程序员们使用了 synchronized 这个工具。它就像那个小木棍一样,确保一次只有一个线程可以访问特定的部分代码(我们称之为“同步代码段”),其他的线程必须等待,直到当前的线程完成了它的操作并释放了“通行证”。
  • 所以,synchronized 就是用来协调多个线程之间对共享资源访问的一种方式,以避免数据被错误地修改

举个栗子:

public class Counter {

    private int count = 0;

    // synchronized 方法
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter myCounter = new Counter();

        // 创建两个线程,它们都会尝试增加计数器的值
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                myCounter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                myCounter.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待两个线程都完成
        thread1.join();
        thread2.join();

        // 输出最终的计数值
        System.out.println("Final count is: " + myCounter.getCount());
    }
}
例子中的  synchronized:
  • 一个名为 Counter 的类,它包含一个整型变量 count 和一个 increment 方法,该方法用于将 count 的值加一。
  • increment 方法被 synchronized 关键字修饰,这意味着如果多个线程试图同时调用这个方法,Java 会确保这些调用不会重叠执行。

synchronized 方法的工作原理:
  • 当线程 A 调用了 increment 方法时,它首先需要获取与当前 Counter 实例关联的内部锁。一旦获取到锁,线程 A 就可以安全地执行 increment 方法中的代码,即对 count 进行递增操作。在此期间,任何其他想要调用 increment 方法的线程(比如线程 B)都必须等待,直到线程 A 完成操作并释放锁。之后,线程 B 才能获取锁并执行自己的 increment 操作。
synchronized 方法工作原理的大白话拆解:
  • 你和你的朋友都在一个房间里,房间里有一台游戏机。但是,这台游戏机一次只能由一个人玩。为了决定谁先玩,你们定下了一个规则:谁想要玩游戏,就必须先拿到房间里的遥控器。只有拿到遥控器的人才能开始玩,等他玩完了,就要把遥控器放回原处,这样其他人就可以拿起来玩了。
  • 在编程里,Counter 类就像是这个房间,而 increment 方法就是那个游戏。synchronized 关键字就像遥控器一样,确保同一时间只有一个线程(玩家)可以进入 increment 方法(玩游戏)。
  • 当一个线程(比如说线程 A)想要调用 increment 方法时,它必须首先获得与 Counter 实例相关的锁(也就是“通行证”或“遥控器”)。一旦线程 A 获得了锁,它就可以安全地执行 increment 方法中的代码,即增加 count 的值。在这个时候,其他任何想调用 increment 方法的线程(比如线程 B)都必须等待,直到线程 A 完成并释放锁。之后,线程 B 才能获取锁并执行自己的 increment 操作。

为什么需要 synchronized:
  • 在多线程环境中,如果不使用 synchronized,可能会发生竞态条件(race condition)。例如,在我们的代码中,两个线程可能会几乎同时读取 count 的值,然后各自将其加一,最后再写回新的值。然而,由于这两个线程的操作是并发进行的,有可能会发生这样的情况:线程 A 读取了 count 的值(假设为 0),紧接着线程 B 也读取了 count 的值(仍然是 0),然后两个线程分别将 count 加一,并将结果(都是 1)写回到 count 中。这样即使两个线程都执行了一次 increment,count 的值却只增加了 1 而不是 2。这就是所谓的“丢失更新”问题。
  • 通过使用 synchronized,我们可以防止这种情况的发生,因为每次只有一个线程能够进入 increment 方法,从而保证了 count 的值正确地递增。

为什么需要 synchronized大白话拆解:
  • 你和你的朋友都想往同一个银行账户存钱。你们每个人都有一个存折,上面记录着当前账户的余额。现在,你想存入100元,同时你的朋友也想存入100元。如果你们两个几乎同时查看存折上的余额(都是500元),然后各自加100元,并更新存折。这时,你们可能会遇到一个问题:两个人都看到余额是500元,所以你们分别将余额设置为600元。结果,即使你们两个人都存了100元,最终的余额却只是600元,而不是预期的700元。这就是所谓的“丢失更新”问题,因为第二个存款操作覆盖了第一个存款的结果。
  • 在编程中,如果我们不使用 synchronized,类似的事情也会发生。多个线程可能会同时读取 count 的值,对其进行递增,然后写回新的值。然而,由于这些操作几乎是同时发生的,它们可能会相互覆盖,导致最终的结果不是我们期望的那样。通过使用 synchronized,我们可以避免这种情况的发生,因为每次只有一个线程能够进入 increment 方法,从而保证了 count 的值正确地递增。

synchronized 的性能考虑
  • 虽然 synchronized 可以帮助我们避免竞态条件,但它也会带来一定的性能开销,因为它引入了线程间的阻塞和等待。在高并发的情况下,这可能导致程序的响应时间变长。因此,在设计多线程程序时,我们应该尽量减少锁定的范围和持续时间,只在确实需要同步的地方使用 synchronized。

性能考虑大白话拆解:
  • 虽然 synchronized 可以帮助我们避免竞态条件,但它也有它的缺点。继续用之前的游戏机的例子来说,如果有太多人想要玩这个游戏,每个人都必须排队等着拿遥控器。这意味着有人可能要等很久才能轮到自己。同样,在编程中,如果有很多线程试图访问同一个同步方法或代码块,它们就不得不排队等候。这可能导致程序运行速度变慢,尤其是在有很多线程竞争的情况下。
  • 因此,在实际编程中,我们应该尽量减少锁定的范围和持续时间。例如,如果你只需要保护一小段代码,那么就只在这小段代码上使用 synchronized,而不是整个方法。此外,我们也可以寻找其他的解决方案,如使用原子变量(AtomicInteger 等)、无锁算法或者并发集合,来提高程序的性能和响应速度。选择哪种方案取决于具体的应用场景和需求。



我们今天就分享到这里,下次再见!!!

6.2 死锁

诱发死锁的原因:

解决死锁:

6.3 JDK5.0 新特性:Lock(锁)

synchronized 与 Lock 的对比

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值