🚩volatile 关键字
首先,volatile 是一个关键字,它具有什么作用?又具有哪些特性呢?请看下文。
🚩volatile 能保证内存可见性
volatile 修饰的变量能够保证 “内存可见性”。
这意味着什么呢?
首先,让我们看一个例子:
假设创建了两个线程 t1 和 t2:
- 线程 t1 包含一个循环,循环条件是 flag == 0。
- 线程 t2 从键盘读入一个整数,并将这个整数赋值给 flag。
预期结果是当用户输入非 0 的值时,线程 t1 结束。
<code>class Counter { public int flag = 0; } public class TestMain { public static void main(String[] args) { Counter counter = new Counter(); Thread thread1 = new Thread(() -> { while (counter.flag == 0) { // 一系列操作 } }); thread1.start(); Thread thread2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("请输入:"); counter.flag = scanner.nextInt(); }); thread2.start(); } }</code>
结果并不会如预期停止。
这是为什么呢?
这与内存有关。这里暂时将内存分为两大类:主内存和工作内存。
- 主内存一般存储数据,而工作内存用于运算。
- 上述代码中,flag == 0 的判断需要经历两步:
- 从主内存读取 flag 的值到工作内存中。
- 在工作内存中进行判断。
由于第一步的执行时间相比第二步太慢了,编译器会对该变量进行优化,只读取一次,以提高运行速度。这就导致了上述的 flag 一直保持为 0,并没有读取新的 flag。
Java 引入了 volatile 关键字来解决这个问题。volatile 修饰的变量不会被编译器优化,每次都会读取,从而保证了内存的可见性。
<code>class Counter { public volatile int flag = 0; }</code>
在写入 volatile 修饰的变量时,会:
- 改变线程工作内存中 volatile 变量副本的值。
- 将改变后的副本的值从工作内存刷新到主内存。
在读取 volatile 修饰的变量时,会:
- 从主内存中读取 volatile 变量的最新值到线程的工作内存中。
- 从工作内存中读取 volatile 变量的副本。
这样就能保证内存的可见性。
🚩volatile 不保证原子性
注意:volatile 和 synchronized 有着本质的区别。synchronized 能够保证原子性,volatile 保证的是内存可见性。
比如下面的例子:
<code>class Count { public volatile int count = 0; void increase() { count++; } }</code>
结果中 count 的值仍然无法保证是 100000,这说明 volatile 不保证原子性。
🎋wait 和 notify
在多线程编程中,有时候需要合理地协调多个线程的执行顺序。这就涉及到 wait() 和 notify() 方法。
🚩wait() 方法
wait 方法使当前执行代码的线程进行等待,释放当前的锁,直到其他线程调用该对象的 notify 方法或者等待时间超时,才会被唤醒。
注意:wait 方法要搭配 synchronized 使用,脱离 synchronized 使用 wait 会直接抛出异常。
<code>public static void main(String[] args) throws InterruptedException { Object object = new Object(); synchronized (object) { System.out.println("等待中"); object.wait(); System.out.println("等待结束"); } }</code>
🚩notify() 方法
notify 方法用于唤醒等待的线程,只会唤醒一个等待的线程。
<code>public static void main(String[] args) { Object locker = new Object(); Thread thread1 = new Thread(() -> { synchronized (locker) { System.out.println("wait 之前"); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("wait 之后"); } }); thread1.start(); Thread thread2 = new Thread(() -> { synchronized (locker) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("notify 之前"); locker.notify(); System.out.println("notify 之后"); } }); thread2.start(); }</code>
🚩notifyAll() 方法
notify 方法只唤醒一个等待的线程,而 notifyAll 方法则唤醒所有等待的线程。
public static void main(String[] args) {
Object locker = new Object();
Thread thread1 = new Thread(() -> {
synchronized (locker) {
System.out.println("thread1:wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread1:wait 之后");
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
synchronized (locker) {
System.out.println("thread2:wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread2:wait 之后");
}
});
thread2.start();
Thread thread3 = new Thread(() -> {
synchronized (locker) {
System.out.println("thread3:wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("thread3:wait 之后");
}
});
thread3.start();
Thread thread4 = new Thread(() -> {
synchronized (locker) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("notifyAll 之前");
locker.notifyAll();
System.out.println("notifyAll 之后");
}
});
thread4.start();
}
这样调用 notifyAll 方法可以同时唤醒所有等待的线程。
深入理解 notify 和 notifyAll
- notify: 唤醒等待队列中的一个线程,其他线程继续等待。
- notifyAll: 一次性唤醒所有等待的线程,它们会竞争锁重新执行。
🌳比较 wait 和 sleep
在理论上,wait 和 sleep 是无法直接比较的。因为它们一个用于线程间通信,一个用于使线程暂停一段时间。它们唯一的相似之处在于都能让线程暂停一段时间。
它们的不同之处在于:
- wait 需要与 synchronized 结合使用,而 sleep 不需要。
- wait 是 Object 类的方法,而 sleep 是 Thread 类的静态方法。
⭕总结
本文深入探讨了 volatile 关键字以及 wait() 和 notify() 方法的使用。volatile 保证了内存可见性但不保证原子性,而 wait() 和 notify() 方法用于线程间的通信和协调。
暂无评论内容