前言:
25年初,这个时候好多小伙伴都在备战期末
我们新年第二天照样日更一篇,今天这篇一定会对小白非常有用的!!!
因为我们会把案例到用代码实现的全过程思路呈现出来!!!
我们一直都是以这样的形式,让新手小白轻松理解复杂晦涩的概念,把Java代码拆解的清清楚楚,每一步都知道他是怎么来的,为什么用这串代码关键字,对比同类型的代码,让大家真正看完以后融会贯通,举一反三,实践应用!!!!
①官方定义 和 大白话拆解对比
②举生活中常见贴合例子、图解辅助理解的形式
③对代码实例中关键部分进行详细拆解、总结
我们上一篇提到,这些概念的拆解:
线程安全问题及解决
- 我们举了一个案例:(火车多窗口售票)
文字案例→代码实现?小编给你详细拆解!!!
- 实现过程:(一般就三步)
- 文字方案中关键部分与Java代码中的功能性代码对应起来:
- 为什么运行结果会是这样呢?(代码的关键部分详细拆解)
大家点一个 赞 或者 关注,我们一起进步!!!
我们接着继续!!!!
5.4 同步机制解决线程安全问题
概述:
- 要解决多线程并发访问一个资源的安全性问题:
- 也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
比如说咱们上一篇中提到的火车多窗口卖票的案例中:
- 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作 结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线 程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在 任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程 只能在外等着(BLOCKED)。
5.4.1 同步机制解决线程安全问题的原理
官方语言:
- 在多线程编程中,同步机制用于确保在任何时刻只有一个线程可以访问临界区(即代码中对共享资源进行操作的部分),以防止多个线程同时修改共享资源而导致数据不一致或破坏。
- Java中的同步机制主要通过内置锁(也称为监视器锁或monitor lock)来实现。每个对象都有一个与之关联的锁,当一个线程执行带有synchronized关键字的方法或代码块时,它会尝试获取该对象的锁。如果成功获得锁,则允许进入临界区执行代码;如果未能获得锁(因为另一个线程已经持有该锁),则当前线程将被阻塞,直到锁变得可用。
- 一旦线程进入临界区,它会在Mark Word中记录自己的线程ID,表明自己正在使用这个锁。其他试图获取同一锁的线程将会检测到锁已经被占用,并因此等待。当拥有锁的线程完成了临界区的操作并退出了同步方法或代码块,它会释放锁,这样其他等待的线程就可以尝试获取锁并继续执行。
大白话拆解:
- 你和朋友们在一个房间里玩电脑游戏,但是只有一台电脑可以用来玩游戏。为了公平起见,你们决定一次只能有一个人玩。
- 所以,你们制定了一个规则:每个人想要玩的时候必须先拿到一把特别的钥匙——这把钥匙就是“锁”。谁拿到了钥匙,谁就能打开电脑开始玩;其他人就必须等,不能碰电脑,直到玩的人结束并把钥匙还回来。
- 这样就保证了任何时候都只有一个人在用电脑,不会出现两个人同时想玩而发生争执的情况。
- 电脑就像是程序里的共享资源,那把特别的钥匙就是“同步锁”,而你们轮流玩游戏的过程就是在模拟多个线程安全地访问共享资源的情景。
举个栗子:
- 一个银行账户类 BankAccount,其中有一个方法 withdraw 用于取款。
- 多个线程代表不同的ATM机尝试从同一个账户中取钱,我们需要确保这些操作是线程安全的,避免并发问题导致账户余额错误。
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// 使用synchronized关键字确保线程安全
public synchronized void withdraw(double amount) {
if (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 for " + Thread.currentThread().getName());
}
}
}
代码解释和总结:
BankAccount 类
- BankAccount 是一个模拟银行账户的类。它有一个属性 balance,表示账户里的余额。这个类允许创建一个新的银行账户,并且提供了一个从账户中取钱的方法 withdraw。
构造函数
- public BankAccount(double initialBalance) 这行是构造函数,当你创建一个新的 BankAccount 对象时会调用它。
BankAccount myAccount = new BankAccount(100.0);
- 这里我们创建了一个新的银行账户 myAccount,并且初始余额为 100.0(假设是100元)。构造函数将这个值赋给 balance 属性。
withdraw 方法
- public synchronized void withdraw(double amount) 是一个方法,用来处理从账户中取款的行为。让我们逐步解析它:
- public:这意味着任何其他类都可以调用这个方法。
- synchronized:这是一个特殊的关键词,确保在同一时间只有一个线程(可以理解为一个任务或者操作)能够执行这个方法。这是为了防止多个线程同时修改 balance 导致的数据混乱。
- void:这表示该方法不会返回任何值。
- withdraw:这是方法的名字,表明它的用途是从账户中取款。
- (double amount):这是方法接收的参数,表示想要取出的金额数量。
方法体
- 在方法体内,首先检查想要取出的金额 amount 是否小于或等于当前余额 balance:
- 如果是的话,程序会打印出哪个线程正在取款以及取款的金额。然后,它会从余额中减去取款金额,并打印出新的余额。
- 如果不是,即账户里没有足够的钱,程序会打印一条消息,说明当前线程无法完成取款,因为余额不足。
5.4.2 同步代码块和同步方法
官方语言:
- 在Java中,synchronized 关键字可以用于代码块或方法,以实现对共享资源的互斥访问,从而解决线程安全问题。当 synchronized 用于一个代码块时,它会指定一个对象作为同步锁,只有持有该锁的线程才能执行这个代码块中的代码。对于同步方法,synchronized 会自动使用当前实例(对于非静态方法)或类本身(对于静态方法)作为同步锁。
- 同步代码块:通过 synchronized(锁对象) 的形式,开发者可以精确地控制哪些代码需要被保护,以及使用哪个对象作为锁。这提供了更细粒度的锁定机制,可以在不影响其他未受保护代码的情况下,确保特定部分的线程安全。
- 同步方法:当 synchronized 修饰一个方法时,它简化了锁的管理,因为每个进入该方法的线程都会尝试获取隐式的锁(实例锁或类锁)。这适用于整个方法体,意味着只要有一个线程在执行该方法,其他试图调用同一方法的线程就必须等待。
大白话拆解:
- 你和你的朋友想要同时使用一台打印机打印文件,但打印机一次只能处理一份文件。为了避免混乱,你们决定用一个规则:谁想打印,就必须先拿到一个特殊的“打印令牌”。拿到了令牌的人可以使用打印机,其他人必须等待,直到前一个人打印完并把令牌传回来。这就像是一个接一个地排队使用打印机,而不是所有人都挤在一起争抢打印机。
- 同步代码块:如果只是打印封面,不需要整个文件都等,那么你可以只对打印封面的部分使用“打印令牌”,这样其他不涉及封面的操作就可以继续进行,不会被阻塞。这就是说,我们只对需要特别小心的部分使用同步,而不限制所有操作。
- 同步方法:如果你有一整套操作都需要确保顺序进行,比如从头到尾打印整个文档,那你可以直接规定,谁拿到“打印令牌”谁就能执行这一整套操作,其他人则必须等待。这就像是把一整套动作打包成一个任务,让每个人依次完成自己的任务。
举个栗子:
同步代码块:
public class BankAccount {
private double balance;
private final Object lock = new Object(); // 专门用于同步的锁对象
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
synchronized (lock) { // 使用自定义锁对象来同步代码块
if (amount > 0) {
System.out.println(Thread.currentThread().getName() + " is depositing " + amount);
balance += amount;
System.out.println("New balance: " + balance);
}
}
}
public void withdraw(double amount) {
synchronized (lock) { // 使用相同的锁对象来同步代码块
if (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 for " + Thread.currentThread().getName());
}
}
}
}
代码解释和总结:
- private final Object lock = new Object();:这里创建了一个特殊的“钥匙”,只有拥有这把“钥匙”的人才能进入存款或取款的操作。这个“钥匙”是专门为同步设计的,不涉及其他功能。
- synchronized (lock):当一个线程想要进行存款或取款时,它必须先拿到这把“钥匙”。如果已经有另一个线程在使用“钥匙”(也就是正在进行存款或取款),那么新的线程就必须等待,直到前一个线程完成了操作并放回了“钥匙”。
- 好处:这种方法只锁定了需要保护的那一小部分代码,而没有锁定整个方法。这意味着如果有其他不需要同步的操作,它们可以继续执行而不必等待。
同步方法:
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public synchronized void deposit(double amount) {
if (amount > 0) {
System.out.println(Thread.currentThread().getName() + " is depositing " + amount);
balance += amount;
System.out.println("New balance: " + balance);
}
}
public synchronized void withdraw(double amount) {
if (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 for " + Thread.currentThread().getName());
}
}
}
代码解释和总结:
- public synchronized void deposit 和 public synchronized void withdraw:这里的方法名前面有一个 synchronized 关键字,这就像是说:“谁想要调用这个方法,就必须先得到一把特别的‘钥匙’,这把‘钥匙’是与整个账户相关的。”所以,只要有一个线程正在存款或取款,其他所有尝试调用这两个方法的线程都必须排队等待,直到当前线程完成操作并释放了“钥匙”。
- 好处:这种方式简单直接,程序员不需要额外考虑哪个代码块需要同步。然而,这也意味着即使有一些不需要同步的操作,也会因为整个方法被锁定而被迫等待,可能会降低效率。
小编也要考试啦
今天就先分享到这里,我们下次再见!!!