Java 多线程-1

一. LockSupport

LockSupport 我们可以把它当成工具类来使用;

  • park():阻塞挂起当前线程;
  • unpark(Thread thread):恢复某个线程的运行;

值得一提的是,LockSupport 的 park() 不会抛出任何异常;

1. 常规使用

常规使用如下:

public class TestLockSupport {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("锁住当前线程t1");
                LockSupport.park();
                System.out.println("解锁了当前线程t1");
            }
        });

        t1.start();
        Thread.sleep(4000);

        System.out.println("主线程解锁t1线程");
        LockSupport.unpark(t1);
        System.out.println(t1.isInterrupted());
    }
}

打印如下:

锁住当前线程t1
主线程解锁t1线程
false
解锁了当前线程t1

2. park()

park() 可以被 LockSupport.unpark() 和 thread.interrupt() 打断;

2.1 LockSupport.unpark()

  • 如果线程已经处于 park() 状态,即已经被阻塞,那么此时调用 unpark() 会使该线程解除阻塞状态;

  • 如果线程尚未被阻塞,再次调用 unpark() 不会影响线程的执行状态,会确保线程下一次调用 park() 时立即返回,线程不会被阻塞;

多次调用 LockSupport.unpark() 有用吗?

LockSupport 的 park() 和 unpark() 是基于一个类似二元信号量的机制,即只有一个许可证可供使用,这个许可证是不可累积的,意味着无论 unpark() 被调用多少次,都只会释放一个许可证;

如果在一个线程尚未调用 park() 之前,多次调用 unpark(),那么只有第一次调用会实际产生效果,为后续可能的 park() 调用提供一个许可证,后续的 unpark() 调用在该线程下一次 park() 之前是没有实际作用的,因为它们不会累积许可证;

2.2 thread.interrupt()

  • 对于被 park() 阻塞住的线程 thread,如果执行 thread.interrupt(),线程会被中断,线程中断标识位 thread.isInterrupt() == true,线程恢复运行;
  • 可以理解为 LockSupport.unpark() 的效果和 thread.interrupt() 差不多,都会使线程 thread 重新运行,区别在于后者会将线程中断标识位 thread.isInterrupt() 置为 true;

示例如下:

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("锁住当前线程t1");
                LockSupport.park();
                System.out.println("解锁了当前线程t1");
            }
        });

        t1.start();
        Thread.sleep(4000);

        System.out.println("主线程解锁t1线程");
        t1.interrupt();
        System.out.println(t1.isInterrupted());
    }

打印如下,可以看到的是,对于被 park() 阻塞住的线程,如果执行 thread.interrupt() 唤醒 thread 线程时,不会抛出异常;

锁住当前线程t1
主线程解锁t1线程
true
解锁了当前线程t1

二. Thread.sleep()

Thread.sleep() 是我们经常使用到的方法,但我们对这个方法的理解可能还不够,主要是因为 Thread.interrupt() 的存在;

1. 简单使用

主要使用如下,我们一般忽略 Thread.sleep() 抛出的 InterruptedException 异常;

public static void main(String[] args) {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        System.out.println("当前线程被中断了");
        e.printStackTrace();
    }
    System.out.println("主线程执行完成");
}

打印如下:

主线程执行完成

2. 执行thread.interrupt()

如果我们想打断正在 sleep() 的线程 t1,可以执行 t1.interrupt();

需要注意的是:

  • 对于 LockSupport.park() 阻塞住的线程,执行 t1.interrupt() 会中断线程,t1 线程会恢复运行,t1.isInterrupted() == true;
  • 对于 t1.sleep() 阻塞住的线程,执行 t1.interrupt() 会中断线程,t1.sleep() 会抛出 InterruptedException 异常,但是线程的中断标志位会重置为 false,t1.isInterrupted() == false;这里比较绕;

我们看一个 Thread.sleep() 被 interrupt() 中断的例子;

public class TestSleep02 {
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("thread working");
                Thread.sleep(5000);
                System.out.println("thread working over");  // 该行不会被执行
            } catch (InterruptedException e) {
                System.out.println("occur interruptException");
                // 当 sleep() 被中断时,会抛出InterruptedException
                System.out.println(Thread.currentThread().isInterrupted());  // false
                // 通常我们也会在这里重新设置中断状态,以便上层代码知道发生了中断
                // Thread.currentThread().interrupt();
            }
        });

        thread.start();

        Thread.sleep(3000);
        System.out.println("开始打断 thread 线程");
        thread.interrupt();
    }
}

打印如下:

thread working
开始打断 thread 线程
occur interruptException
false

三. Thread.interrupt()

通过上面 LockSupport 和 Thread.sleep() 的学习,其实我们对 Thread.interrupt() 已经有了一定的了解,下面我们介绍一下 Thread.interrupt() 和线程的中断标志位;

  1. interrupt() 会将线程的中断标志位 interrupt 置为 true,默认值是 false;

    • 如果线程 t1 处于阻塞状态(即调用了sleep()、wait()、join()),当调用 t1.interrupt() 时会抛出中断异常InterruptedException,并且中断标记立即清除重置为 false;
    • 如果线程 t1 是被 park() 阻塞挂起,当调用 t1.interrupt() 会打断 t1 线程,t1 线程恢复运行,t1 线程的中断标志位为 true,不会被清除重置;
  2. isInterrupted() 会返回该线程的 interrupt 中断状态;

  3. interrupted() 是测试当前线程是否处于中断状态,是的话返回 true,并且调用完毕后立即将该中断标记清除重置为 false;线程不是中断状态的话,直接返回 false;

需要注意一点,对于正常运行的线程来说,interrupt() 的作用只是将中断标志位置为 true,它并不是真正的中断线程

1. interrupted()

对于 thread.interrupted(),它的作用是:测试当前线程是否处于中断状态,是的话返回 true,并且调用完毕后立即将该中断标记清除重置为 false;线程不是中断状态的话,直接返回 false;

我们举个例子:

public class TestInterrupt {

    public static void main(String[] args) {
        System.out.println("主线程开始");
        Thread.currentThread().interrupt();
        System.out.println("主线程是否处于中断状态:"+Thread.interrupted());    //true
        System.out.println("主线程是否处于中断状态:"+Thread.interrupted());    //false
        System.out.println("主线程是否处于中断状态:"+Thread.interrupted());    //false
        System.out.println("主线程结束");
    }
}

打印如下:

主线程开始
主线程是否处于中断状态:true
主线程是否处于中断状态:false
主线程是否处于中断状态:false
主线程结束

四. Monitor

monitor 是通过 C 来实现的,每个 Java 对象都有它关联的 Monitor 对象,结构如下:

ObjectMonitor() {
    _count        = 0;     // 用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0;     // 锁的重入次数
    _owner        = NULL;  // 指向持有 ObjectMonitor 对象的线程 
    _WaitSet      = NULL;  // 处于 wait 状态的线程,会被加入到 _WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
}
  1. 首先,所有并发获取锁的线程会被放入 EntryList 里;
  2. 当一个线程获取到对象 A 的锁时,将 A 对象关联的 monitor 对象里的 owner 指向当前线程,将 monitor 里的 count 加1;
  3. 这时如果对象 A 调用 wait(),则将 monitor 中 count 减 1,将 owner 置为 null;并将该线程放入 waitSet 里,等待唤醒;

获取 Monitor 过程如下图所示:

  • 刚开始 Monitor 中 Owner 为 null;

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner;

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED;

  • Thread-2 执行完同步代码块的内容,会释放锁,唤醒 EntryList 中等待的线程来竞争锁;

  • WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足执行了 this.wait(),进入 WAITING 状态的线程;这些线程只有当被 notify() 时才会重新进入到 EntryList BLOCKED 竞争获取锁,获取锁成功的话从 wait() 处开始执行;

在这里插入图片描述

1. wait()和notify()

我们看下 Object.wait()、Object.notify()、Object.notifyAll() 的作用:

  • Object.wait():当线程调用 wait() 时,它会释放对象的锁,并且放弃CPU,将自己置于该对象的等待队列中,等待被唤醒。
  • Object.notify():当另一个线程拥有该对象的锁并调用 notify() 时,它会从等待队列中随机选择一个线程并唤醒它,使其有机会重新获取锁并继续执行;一旦 notify() 被调用,等待队列中的其他线程仍然处于等待状态,除非再次调用 notify() 或 notifyAll()。
  • Object.notifyAll():与 notify() 不同,notifyAll() 会唤醒所有在该对象上等待的线程;一旦被唤醒,这些线程将竞争对象的锁,成功获取锁的线程将继续执行,而未获取锁的线程可能再次进入等待状态,直到锁被释放;

值得注意的是,wait()、notify() 和 notifyAll() 都必须在 synchronized 代码块或方法中调用,且调用的对象必须是synchronized 块或方法的锁对象。否则,将抛出 IllegalMonitorStateException 异常,因为这些方法的语义依赖于线程已经持有了对象的锁

等待和阻塞不是一个概念,等待的线程是之前已经获取过锁的线程,当线程被唤醒后重新获取到锁时会从 obj.wait() 处运行代码;阻塞的线程是当前对象 obj 已经被其他线程持有了锁,于是只能阻塞挂起,直到锁被释放之后才会去争抢锁;

2. wait()和sleep()的区别

  1. sleep() 是 Thread 的静态方法,wait() 是 Object 的 方法;
  2. sleep() 可以在任意地方使用,wait() 和 notify() 只能在 synchronized 同步代码块中使用;
  3. sleep() 线程不会释放锁,它也不需要占有锁,wait() 线程会释放锁;
  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,您的问题是关于Java多线程的存钱取钱问题。这是一个经典的多线程同步问题,可以通过使用synchronized关键字来实现线程安全。下面是一个简单的示例代码: ```java public class BankAccount { private int balance; public BankAccount(int balance) { this.balance = balance; } public synchronized void deposit(int amount) { balance += amount; System.out.println("Deposit successful. Balance: " + balance); } public synchronized void withdraw(int amount) { if (balance < amount) { System.out.println("Withdraw failed. Insufficient balance."); return; } balance -= amount; System.out.println("Withdraw successful. Balance: " + balance); } } public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(1000); // Create two threads to simulate deposit and withdraw Thread depositThread = new Thread(() -> { for (int i = 0; i < 5; i++) { account.deposit(100); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread withdrawThread = new Thread(() -> { for (int i = 0; i < 5; i++) { account.withdraw(200); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); depositThread.start(); withdrawThread.start(); } } ``` 在这个示例代码中,我们创建了一个银行账户类BankAccount,并在其中实现了deposit和withdraw方法,并使用synchronized关键字来保证线程安全。 在main方法中,我们创建了两个线程来模拟存款和取款操作,每个线程执行5次操作。我们使用Thread.sleep方法来模拟每个操作之间的间隔,以便更好地观察多线程操作的结果。 当多个线程同时访问BankAccount对象的deposit和withdraw方法时,synchronized关键字可以确保每个方法只能被一个线程访问,从而避免了竞争条件和数据不一致的问题。 希望这个示例代码能够回答您的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值