|
JavaEE
JavaEE——Java线程的几种状态
JavaEE——No.1 线程安全问题
public class Test {
static class Counter {
public int count = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.count == 0) {
}
});
}
}
上述代码, 在第10行上, while()
中的语句, 在读取内存 (LOAD) , 进行比较(CMP).
while循环会转的非常快, 会频繁的进行多次 LOAD 和 CMP.
LOAD 消耗的时间长, 比 CMP 慢 3-4 个数量级 (1w倍).
#
这时编译器开始优化, 既然需要频繁的执行 LOAD, 并且 LOAD 的结果还一样, 干脆就只执行一次 LOAD 就可以了, 后续进行 CMP就不再重新读内存了
这时如果再来一个 t2
, 就会出现一些问题
public class Test {
static class Counter {
public int count = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.count == 0) {
}
System.out.println("t1 执行结束");
});
Thread t2 = new Thread(() -> {
System.out.println("请输入一个整数(int) :");
Scanner scanner = new Scanner(System.in);
counter.count = scanner.nextInt();
});
t1.start();
t2.start();
}
}
我们这时运行代码, 并输入一个整数, 我们发现程序还是没有停止.
我们刚刚输入的1 已经赋值给 counter.count
, 内存已经被修改, 但是对于刚才的修改, 对于 t1 的读内存不会有影响, 因为 t1
已经被优化成不再循环读内存了(读一次就不读了).
这样就会出现, 存可见性问题, 编译器优化惹下的祸事 (编译器优化, 在多线程环境下可能存在误判).
volatile 修饰的变量, 能够保证 “内存可见性”.
此时被修饰的变量, 编译器就不会做出 “不读内存, 只读寄存器” 这样的优化
static class Counter {
volatile public int count = 0;
}
我们这时再次运行代码, 输入一个整数, 我们发现程序停止
了.
代码在
写入
volatile 修饰的变量的时候,
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在
读取
volatile 修饰的变量的时候,
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
# 注意 #
编译器的优化, 并不是一直存在的, 会根据代码的实际情况.
public class Demo12 {
static class Counter {
public int count = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.count == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 执行结束");
});
Thread t2 = new Thread(() -> {
System.out.println("请输入一个整数(int) :");
Scanner scanner = new Scanner(System.in);
counter.count = scanner.nextInt();
});
t1.start();
t2.start();
}
}
这时是不会出现内存可见性问题的
volatile 不保证原子性
volatile
和 synchronized
有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
volatile
禁止了编译器优化, 避免了直接读取 CPU 寄存器 (工作内存 work memory) 中缓存的数据, 而是每次都读内存 (主内存 main memory)
正常程序执行的过程中, 会把主内存的数据, 先加载到工作内存中, 再进行计算处理
编译器优化可能会导致不是每次都真正的读取主内存, 而是直接取工作内存中的缓存数据. (就可能导致内存可见性问题)
volatile 起到的效果, 就是保证每次读取内存都是真的从主内存重新读取.
# 注意 #
上述的工作内存并不是真正的内存, 主内存才是真正的内存. (“工作内存” 是 CPU 寄存器 + 缓存的 抽象的表述)
由于线程, 调度过程是随机的. 很多时候, 我们希望多个线程按照一个预期的顺序来执行.
wait 和 notify 就可以用来调配线程执行顺序
wait 是 Object
的方法, 如果我们想调用 wait 一定要有一个对象(任意类的实例),
wait 这里会有一个异常, 可能会被 interrupt
方法唤醒.
我们来看这样一个代码
public class Test {
public static void main(String[] args) {
Object object = new Object();
System.out.println("wait 之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
}
当线程执行到 wait
, 就会发生阻塞. 直到另一个线程, 调用 notify
把这个 wait
唤醒, 才会继续执行.
wait 本质上做了三件事
- 释放当前锁
- 等待 notify 唤醒
- 被唤醒后, 尝试重新获取锁
此时我们的代码还没有加锁, wait
无法释放锁. 所以我们需要添加一行代码.
public class Test {
public static void main(String[] args) {
Object object = new Object();
System.out.println("wait 之前");
synchronized (object) {//加锁
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("wait 之后");
}
}
那么 wait 为什们要先释放锁呢?
wait 释放锁, 保证其他线程能够正常往下进行. 给其他线程机会, 让其他线程可以拿到锁.
wait 结束等待的条件:
其他线程调用该对象的
notify
方法.wait 等待时间超时 (wait 方法提供一个带有
timeout
参数的版本, 来指定等待时间).其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify 方法是唤醒等待的线程.
方法
notify()
也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程调度器随机挑选出一个呈
wait
状态的线程。(并没有 “先来后到”)在notify()方法后,当前线程不会马上释放该对象锁,要等到执行
notify()
方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
我们来画图理解一下, 加入 t1 执行了 wait
, t2 调用了 notify
.
# 注意事项 #
我们再写一个代码
public class Test {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
while(true) {
synchronized (object) {
System.out.println("wait 之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
while(true) {
synchronized (object) {
System.out.println("notify 之前");
object.notify();
System.out.println("notify 之后");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t2.start();
}
}
我们运行一下, 我们可以看到 通过 wait
和 notify
我们干预了线程的执行顺序.
# 注意 #
|
以上就是今天要讲的内容了,希望对大家有所帮助,如果有问题欢迎评论指出,会积极改正!!