线程的状态
观察线程的所有状态
- NEW: 安排了工作, 还未开始行动
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
- BLOCKED: 这几个都表示排队等着其他事情
- WAITING: 这几个都表示排队等着其他事情
- TIMED_WAITING: 这几个都表示排队等着其他事情
- TERMINATED: 工作完成了.
把小二找来,给他安排任务,没让他行动起来,就是 NEW 状态;
当小二开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并
不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被
服务的状态,是否开始服务,则看调度器的调度;
当小二因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会
等等时,进入BLOCKED 、 WATING 、 TIMED_WAITING 状态;
如果小二已经忙完,为 TERMINATED 状态。
代码重排序
一段代码是这样的:
- 去快递点寄个快递
- 回家放快递
- 去快递点取下个快递
正常按我们编程的逻辑,按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 的工作过程:
-
获得互斥锁
-
从主内存拷贝变量的最新副本到工作的内存
-
执行代码
-
将更改后的共享变量的值刷新到主内存
-
释放互斥锁
所以 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 的对比
- wait 需要搭配 synchronized 使用;sleep 不需要.
- wait 是 Object 的方法 ;sleep 是 Thread 的静态方法.