多线程-指令重排序、synchronized、volatile关键字

线程的状态

观察线程的所有状态

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了.

[外链图片转存中...(img-ERwdA3rA-1733738604891)]

把小二找来,给他安排任务,没让他行动起来,就是 NEW 状态;

当小二开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并
不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被
服务的状态,是否开始服务,则看调度器的调度;

当小二因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会
等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态;

如果小二已经忙完,为 TERMINATED 状态。

代码重排序

一段代码是这样的:

  1. 去快递点寄个快递
  2. 回家放快递
  3. 去快递点取下个快递

正常按我们编程的逻辑,按1->2->3的方式执行。
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次快递点。这种叫做指令重排序。

编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 
但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在
编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

synchronized 关键字

synchronized 的特性

1.互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁

  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

阻塞等待

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

synchronized的底层是使用操作系统的mutex lock实现的.

2.刷新内存

synchronized 的工作过程:

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

3.可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

把自己锁死

一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();
不可重入锁

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经开躺了, 啥也不干, 也就无法进行解锁操作. 这时候就会 死锁.

这样的锁称为 不可重入锁

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题

synchronized 使用

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具
体的对象来使用.

1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
    }
}

2) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
    public synchronized static void method() {
    }
}

3) 修饰代码块 明确指定锁哪个对象.

锁当前对象

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

锁类对象

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.

volatile 关键字

volatile 能保证内存可见性

volatile 修饰的变量, 能够保证 “内存可见性”.

在这里插入图片描述

代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值

  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中

  • 从工作内存中读取volatile变量的副本

volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

synchronized 也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性.

wait 和 notify

  • wait() / wait(long timeout): 让当前线程进入等待状态.

  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

wait, notify, notifyAll 都是 Object 类的方法.

wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)

  • 释放当前的锁

  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.

  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).

  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

例:

使用notify()方法唤醒线程

  • 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.

  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify

  • 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.

static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;}
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

static class WaitTask implements Runnable {
    ~~~
}
static class NotifyTask implements Runnable {
    ~~~
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t3 = new Thread(new WaitTask(locker));
    Thread t4 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    t3.start();
    t4.start();
    Thread.sleep(1000);
    t2.start();
}

调用 notify 只能唤醒一个线程

把 notify 替换成 notifyAll

public void run() {
    synchronized (locker) {
        System.out.println("notify 开始");
        locker.notifyAll();
        System.out.println("notify 结束");
    }
}

调用 notifyAll 能同时唤醒 3 个 wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行

wait sleep 的对比

  1. wait 需要搭配 synchronized 使用;sleep 不需要.
  2. wait 是 Object 的方法 ;sleep 是 Thread 的静态方法.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值