likes
comments
collection
share

【一文通关】Java多线程基础(4)- 线程同步

作者站长头像
站长
· 阅读数 13

线程同步相关

线程同步

Java程序中可以存在多线程,但是在处理多线程问题时必须注意这样一个问题:

两个或多个线程同时访问同一个变量, 并且一些线程需要修改这个变量

程序应对这样的问题做出处理,否则可能发生混乱。

比如, 工资管理负责人正在修改雇员的工资表, 而一些雇员也正在查看工资, 如果允许这样做必然出现混乱(雇员查看工资5000但领取的工资2000,最后把老板打了一顿)

因此, 工资管理员正在修改工资表时(包括他喝杯茶休息一会), 将不允许任何雇员查看, 也就是说他们需要等待。

所谓线程同步就是若干个线程都需要使用一个 synchronized(同步)修饰的方法,即程序中的若干个线程都需要使用一个方法,而这个方法用synchronized 给予了修饰。多个线程调用synchronized方法必须遵守同步机制。

线程同步机制:当一个线程A使用 synchronized方法时,其他线程想使用这个Synchronized 方法时就必须等待,直到线程A使用完该synchronized 方法。

在使用多线程解决许多实际问题时,可能要把某些修改数据的方法用关键字synchronized来修饰,即使用同步机制。

举例

现拥有一个账本。有两个人:会计和出纳,他俩共同拥有一个账本。

会计使用 saveOrTake(int amount)时, 向账本写入存钱记录。

出纳使用 saveOrTake(int amount)方法时, 向账本上写入花钱记录;

因此,当会计正在使用 saveOrTake(int amount)时,出纳被禁止使用,反之也是这样。

会计在账本上存入300万元,每存入100万时会喝茶休息一会儿。

出纳在账本上支出150万元,每支出50万时喝茶休息一会儿。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Bank bank = new Bank();
        bank.accountant.start();
        bank.cashier.start();
    }
}

class Bank implements Runnable {

    Thread accountant, cashier;

    int money = 300;

    Bank() {
        this.accountant = new Thread(this);
        this.cashier = new Thread(this);
    }

    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        if (thread == accountant) {
            saveOrTake(300);
        } else if (thread == cashier) {
            saveOrTake(150);
        }
    }

    public synchronized void saveOrTake(int money) {
        if (Thread.currentThread() == accountant) {
            for (int i = 0; i < money / 100; i++) {
                this.money += 100;
                System.out.println("账户增加了100万, 现在有:" + this.money + "万");
                try {
                    System.out.println("喝茶");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        if (Thread.currentThread() == cashier) {
            for (int i = 0; i < money / 50; i++) {
                this.money -= 50;
                System.out.println("账户减少了50万, 现在有:" + this.money + "万");
                try {
                    System.out.println("喝茶");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

输出: 账户增加了100万, 现在有:400万 喝茶 账户增加了100万, 现在有:500万 喝茶 账户增加了100万, 现在有:600万 喝茶 账户减少了50万, 现在有:550万 喝茶 账户减少了50万, 现在有:500万 喝茶 账户减少了50万, 现在有:450万 喝茶

代码分析

account与cashier线程都使用的是同一种方法saveOrTake(), 这个方法在使用时会被上锁,一次只能一个线程使用,所以当account使用该方法时,cacshier必须要等待。

协调同步

当一个线程使用同步方法时, 其他线程想使用该同步方法必须等待,直到当前线程使用完该方法。

但是实际总会有特殊情况, 当你拿20元去买五元的门票,但是售票员没有零钱,于是你必须放开买票的方法, 你站在旁边等,当售票员得到足够的零钱才来找你。

一个线程使用的同步方法使用到某个变量,而此变量有需要其他线程修改后才能符合线程的需要,那么可以使用wait()和notify()方法。

wait()和notify()

wait()和notify()是Java中用于线程间通信的两个方法,它们通常用于实现线程同步。

wait()方法使当前线程进入等待状态,直到另一个线程调用notify()或notifyAll()方法唤醒它。

wait()方法可以被用于控制线程的执行顺序或等待某些条件的发生。

wait()方法可以在两种情况下被调用:

  1. 当前线程需要等待某个条件的发生,并且没有其他方式可以知道条件是否满足。在这种情况下,线程进入等待状态,并且释放它所持有的任何锁
  2. 当前线程需要等待另一个线程完成某个操作。在这种情况下,线程进入等待状态,并且持有它所持有的锁,以防止其他线程修改它正在等待的数据。

notify()方法则是唤醒一个正在等待的线程,使其从等待状态返回到可运行状态。如果有多个线程在等待,notify()只会唤醒其中一个线程,选择哪个线程被唤醒是不确定的。如果需要唤醒所有等待的线程,可以使用notifyAll()方法。

使用wait()和notify()方法需要注意以下几点:

  1. 必须在同步块中调用wait()和notify()方法,否则会抛出IllegalMonitorStateException异常。
  2. 需要在while循环中调用wait()方法以确保线程在等待时或者被唤醒时会再次检查条件。
  3. 调用wait()方法后,线程会释放它所持有的锁。因此,在调用wait()方法前需要确保线程已经获取所需的锁。
  4. 在调用notify()或notifyAll()方法之前,必须确保已经获取了相应的锁。
  5. notify()和notifyAll()方法只会唤醒正在等待的线程,而不会停止正在运行的线程。如果需要停止线程,需要使用其他手段,如使用volatile变量或使用interrupt()方法。

实际例子

小明带了20买门票,门票5元一张, 但是售票员只有两张五元的,因此,小明去等待直到零钱足够,直到零钱足够。

听到呼唤,小红带着5元也来买门票了

public class Main {
    public static void main(String[] args) throws InterruptedException {
        TicketHouse house = new TicketHouse();
        // ming线程
        Thread ming = new Thread(house);
        ming.setName("ming");
        // hong线程
        Thread hong = new Thread(house);
        hong.setName("hong");
        // 开始买票
        ming.start();
        hong.start();
    }
}

class TicketHouse implements Runnable {

    int fiveMount = 2;
    int twentyMount = 0;


    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        if(name.equals("ming")){
            saleTicket(20);
        } else if (name.equals("hong")) {
            saleTicket(5);
        }
    }

    public synchronized void saleTicket(int money) {
        // money为5, 直接购票成功
        if(money == 5){
            fiveMount+=1;
            System.out.println(Thread.currentThread().getName() + "购票成功");
        } else if (money == 20) {
            // money为20, 需要判断零钱是否足够
            twentyMount+=1;
            // 使用while,以确保线程在等待时或者被唤醒时会再次检查条件
            while (fiveMount < 3){
                System.out.println("零钱不够," + Thread.currentThread().getName()+ "在旁边等");
                try {
                    wait(); // 线程被卡在这个位置
                    System.out.println(Thread.currentThread().getName() + "被激活了");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            fiveMount -= 3;
            System.out.println(Thread.currentThread().getName() + "买票成功,付钱20,找零35元");
        }
        // 这一步每次有人来买票都会运行
        // 激活所有等待该方法的线程
        notifyAll();
    }
}

输出: 零钱不够,ming在旁边等 hong购票成功 ming被激活了 ming买票成功,付钱20,找零3张5元

提示

  • 记得把wait()方法放在while中, 唤醒时可以对条件进行判断,防止逻辑错误。

  • 选择哪个唤醒方法

    在Java中,使用notify()方法唤醒等待线程时,并不能确定具体唤醒哪个线程。这是因为在多线程环境下,存在竞争关系,多个线程可能同时等待同一个锁对象的唤醒信号。在唤醒信号到来时,只有一个线程能够获取锁对象并从wait()方法返回,而其他线程还会继续等待信号。

    具体来说,当一个线程调用notify()方法时,它会随机唤醒一个正在等待该锁对象的线程。这意味着唤醒的线程可能是任何一个在等待该锁对象的线程,而不能确定具体是哪一个线程。因此,在编写多线程程序时,需要特别注意唤醒信号的使用,以避免出现死锁或竞争等问题。

    为了避免唤醒错误的线程,可以采用以下方法:

    1. 使用notifyAll()方法,唤醒所有等待该锁对象的线程,让它们竞争锁对象。
    2. 在等待锁对象前,使用一个标志位来记录等待线程的状态,这样在唤醒线程时可以根据状态来判断唤醒哪个线程。
    3. 使用Lock和Condition接口实现线程同步,其中Condition接口提供了更灵活的等待和唤醒方法,可以更方便地控制线程的执行顺序和唤醒顺序。

    总之,唤醒哪个线程并不是确定的,需要在编写多线程程序时谨慎使用,以避免出现问题。