王子,公主请阅
- 1. Thread类的常见方法
- 2. 线程的状态
- 3. 多线程带来的的风险-线程安全 (重点)
- 4. synchronized关键字 - 监视锁monitor lock
- 5. volatile 关键字
- 6. wait和notify
1. Thread类的常见方法
1. 等待线程 - join()
有时,我们需要等待⼀个线程完成它的工作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个方法明确等待线程的结束。
//join 实现线程等待效果
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("t 线程工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
//让主线程来等待t线程执行结束.
//一旦调用join, 主线程就会触发阻塞, 此时t线程可以趁机完成后续的工作
//一直阻塞到 t 执行结束完毕了, join 才会解除阻塞, 才能继续执行.
System.out.println("join 等待开始.");
t.join(); //主线程等待 t 线程结束
System.out.println("join 等待结束.");
}
}
1.2 获取当前线程引用
public class test6 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
1.3 休眠当前线程
因为线程的调度是不可控的,所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class test6 {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3*1000);
System.out.println(System.currentTimeMillis());
}
}
2. 线程的状态
2.1 线程的所有状态
线程的状态是⼀个枚举类型 Thread.State
public class test6 {
public static void main(String[] args) {
for(Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
观察 1: 关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换
public class test6 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
}
},"李四");
System.out.println(t.getName() + ": " + t.getState());
t.start();
while(t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());
}
System.out.println(t.getName() + ": " + t.getState());
}
}
观察 2: 关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
public class test7 {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized(object) {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized(object) {
System.out.println("嘻嘻嘻");
}
},"t2");
t1.start();
t2.start();
}
}
使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
public class test7 {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized(object) {
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized(object) {
System.out.println("嘻嘻嘻");
}
},"t2");
t1.start();
t2.start();
}
}
3. 多线程带来的的风险-线程安全 (重点)
3.1 观察线程安全
public class test7 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
//如果没有join方法肯定不行, 线程还没自增完就开始打印了, 很可能打印出来的count是 0;
t1.join();
t2.join();
System.out.println("count: " + count);
}
}
3.2 线程安全的概念
3.3 线程不安全的原因
3.3.1 操作系统中,线程的调度顺序是随机的
这是线程安全问题的罪魁祸首, 随机调度使一个程序在多线程环境下,执行顺序存在很多的变数. 程序员必须保证在任意执行顺序下, 代码都能正常执行.
3.3.2 修改数据共享
多个线程修改同一个变量, 上面的线程不安全的代码中, 涉及到多个线程针对 count 变量进行修改.此时这个 count 是一个多个线程都能访问到的 “共享数据”
3.3.3 修改操作不是原子性的
什么是原子性呢?
我们把一段代码想象成一个房间, 每个线程就是要进入房间里的人. 如果没有任何的保护机制, 当A进入房间后, 还没有出来; B是不是也可以进入房间,打断A在房间里的隐私. 这个就是不具备原子性的.
有时也把这个现象叫做同步互斥.
那么其实解决这个问题的关键就是给房间里加上一把锁, A进入房间后,把门锁上自然就不会被打断了. 这样就保证了代码的原子性.
一条 java 语句不一定是原子的,也不一定只是一条指令
比如: 上述代码中的count++, 其实是由CPU中的三条指令来实现的.
//线程安全问题.
public class Demo13 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// 不调整代码结构, 进行写加锁 也能解决线程安全问题.
//() 中 需要针对同一个对象.
synchronized(locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized(locker) {
count++;
}
}
});
t1.start();
t2.start();
//没有调用join方法肯定是不行的.
//两线程都调用join()方法, 确保 主线程main 等待 t1,t2线程执行结束,再结束.
t1.join();
t2.join();
// 预期结果10W
System.out.println("count: " + count );
}
}
synchronized除了用于修饰代码块还可用于修饰一个实例方法或者静态方法
class Counter {
public int count;
//第一种简化写法: 本质上就是第二种写法.
synchronized public void increase() {
count++;
}
//第二种写法:
public void increase2() {
synchronized(this) {
count++;
}
}
//第三种写法
//修饰静态方法相当于给类对象加锁.
synchronized public static void increase3() {
}
//第四种写法
public static void increase4() {
synchronized (Counter.class/*类对象*/) {
}
}
}
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = "+ counter.count);
}
}
3.3.4 内存可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到. 系统编译器为了提高效率做出一些优化导致线程不安全.(详见 5. volatile关键字)
3.3.5 指令重排序
什么是代码重排序?
⼀段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
4. synchronized关键字 - 监视锁monitor lock
4.1 synchronized的特性
4.1.1 可重入
理解 “死锁”
一个线程没有释放锁, 然后⼜尝试再次加锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁
for (int i = 0; i < 50000; i++) {
synchronized(object) {
synchronized(object) {
count++;
}
}
}
在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (出了最后一个 “}” 才能被别的线程获取到)
4.1.2 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
理解 “阻塞等待”.
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试
进行加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程,再来获取到这个锁.
注意:
• 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
• 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.
4.1.3 多个线程,N把锁.即哲学家就餐问题 (重点)
五个哲学家(线程)坐在桌前, 桌上只有一碗面条和五只筷子(锁), 五个哲学家都想吃面条:
如果五个哲学家都想吃面条,他们拿起左边的筷子, 此时会发现右边没有筷子,于是五个哲学家阻塞等待, 出现死锁, 因为等待的过程中哲学家都不会放下左手的筷子.
4.1.4 死锁的成因(重点)
4.2 synchronized 使用示例
synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也必要搭配⼀个具体的对象来使用
4.2.1 修饰代码块
public class test8 {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
public class test8 {
/* private Object locker = new Object();*/
public void method() {
synchronized (this) {
}
}
}
4.2.2 synchronized 修饰普通方法
public class test8 {
public synchronized void methond() {
}
}
4.2.3 修饰静态方法
public class test8 {
public synchronized static void methond() {
}
}
4.3 Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
5. volatile 关键字
5.1 volatile能保证内存可见性
计算机运行的程序/代码,经常要访问数据,这些数据往往会存储在内存中, (比如: 定义一个变量,变量就是在内存中.)
cpu 使用这个变量的时候,就会把这个内存中的数据,先读出来,放到 cpu 的寄存器中再参与运算.(load)
CPU 进行大部分操作,都很快,一旦操作到读/写内存,此时速度一下就降下来了
public class test9 {
private /*volatile*/ static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(isQuit == 0) {
//...
}
System.out.println("t1结束");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("输入isQuit的值:");
Scanner scanner = new Scanner(System.in);
isQuit = scanner.nextInt();
});
t2.start();
}
}
上述代码的预期结果是输入非0值,t1线程就要退出.
但是当输入非0值时,此时t1线程并未结束.
很明显上述代码存在线程安全问题, 这本质上是由编译器的错误优化引起的.
5.2 volatile不保证原子性
public class tes10 {
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
如上代码, 删掉synchronized ,利用volatile修饰count, count 达不到预期结果. 说明volatile 不能保证原子性.
6. wait和notify
篮球场上的每个运动员都是独立的 “执行流” , 可以认为是一个 “线程”.而完成⼀个具体的进攻得分动作, 则需要多个运动员相互配合, 按照⼀定的顺序执行⼀定的动作, 线程1 先 “传球” , 线程2 才能 “扣篮”.
完成这个协调工作, 主要涉及到三个方法
• wait() / wait(long timeout): 让当前线程进入等待状态.
• notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.
6.1 wait()方法
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足⼀定条件时被唤醒, 重新尝试获取这个锁.
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("wait之前");
//把 wait 要放到 synchronized 里面来调用. 保证确实是拿到锁了的.
object.wait();
System.out.println("wait之后");
}
}
}
6.2 notify()方法
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到"原则)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class test10 {
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) {
throw new RuntimeException(e);
}
}
}
}
}
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("notif 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(new WaitTask(object));
Thread t2 = new Thread(new NotifyTask(object));
t1.start();
Thread.sleep(1000);
t2.start();
}
}
6.3 notifyAll()方法
6.4 wait 和 sleep的区别(面试题)
相同点:
就是都可以让线程放弃执行一段时间
不同点:
1. wait 需要搭配 synchronized 使用. sleep 不需要.
2. wait 是 Object 的方法 sleep 是 Thread 的静态方法
🐎🐎🐎期待与你的下一次相遇!!!