1.线程的状态
1.1 观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
2. 多线程带来的的风险-线程安全 (重点)
2.1 观察线程不安全
static class Counter {
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);
}
2.2 线程安全的概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
2.3 线程不安全的原因
1.线程的随机调度
操作系统中, 线程的调度顺序是随机的 (抢占式执行).罪魁祸首,万恶之源,
如果是多个线程执行上述代码,由于线程之间的调度顺序,是“随机"的,就会导致在有些调度顺序下,上述的逻辑就会出现问题
在多线程程序中,最困难的一点:线程的随机调度,使两个线程执行逻辑的先后顺序,存在诸多可能,我们必须要保证在所有可能的情况下,代码都是正确的!
以下是正确的执行顺序
不是按照此顺序,最终结果一定有bug,且最终值小于10w。
2.两个线程,针对同一个变量进行修改
1)一个线程针对一个变量修改.ok
2)两个线程针对不同变量修改.ok
3)两个线程针对一个变量读取.ok
3.修改操作,不是原子的.
此处给定的 count++ 就属于是 非原子 的操作.(先读,再修改)类似的,如果一段逻辑中,需要根据一定的条件来决定是否修改,也是存在类似的问题
假设 count++ 是原子的(比如有一个 cpu 指令,一次完成上述的三步)
4.内存可见性问题.
5.指令重排序
要想解决线程安全问题,就是要从上述方面入手。
1.系统内核里实现的->最初搞多任务操作系统的人,制定了"抢占式执行大的基调.在这个基调下,想做出调整是非常困难的。
2.有些情况下,可以通过调整代码结构,规避上述问题但是也有很多情况,调整不了。
3.通过加锁!!!
通过加锁, 就能解决上述问题.
如何给 java 中的代码加锁呢?
其中最常用的办法, 就是使用 synchronized 关键字!
// 线程安全
public class Demo13 {
// 此处定义一个 int 类型的变量
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(() -> {
// 对 count 变量进行自增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进行自增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不行的. 线程还没自增完, 就开始打印了. 很可能打印出来的 count 就是个 0
t1.join();
t2.join();
// 预期结果应该是 10w
System.out.println("count: " + count);
}
}
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) {
}
}
}
// synchronized 使用方法
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(counter.count);
}
}
3.synchronized 关键字-监视器锁monitor lock
3.1 synchronized 的特性
4.volatile 关键字
1)volatile 能保证内存可见性
volatile 修饰的变量 , 能够保证 " 内存可见性 ".
public class Demo17 {
private 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);
// 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
isQuit = scanner.nextInt();
});
t2.start();
}
}
2) volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
3)synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
5.wait和notify
多线程中一个比较重要的机制.
协调多个线程的执行顺序的
本身多个线程的执行顺序,是随机的(系统随机调度,抢占式执行的)很多时候,是希望能够通过一定的手段,协调的执行顺序的,join 是影响到线程结束的先后顺序相比之下,此处是希望线程不结束,也能够有先后顺序的控制。
- wait 等待,让指定线程进入阻塞状态
- notify 通知,唤醒对应的阻塞状态的线程,
public class Demo18 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 结束!");
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 结束!");
});
t1.start();
t2.start();
System.out.println("主线程结束!");
}
}
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 之后");
}
}
}
wait和notify
//释放锁的前提,是加锁
//wait 会持续的阻塞等待下去,直到其他线程调用 notify 唤醒,
public class Demo20 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("wait 之前");
try {
object.wait(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
System.out.println("进行通知");
object.notify();
}
});
t1.start();
t2.start();
}
}
5.1 notify和notifyAll
notify->一次唤醒一个线程
notifyAll->一次唤醒全部线程 (唤醒的时候,wait 要涉及到一个重新获取锁的过程也是需要串行执行的)
调用 wait 不一定就只有一个线程调用.
N 个线程都可以调用 wait此时,当有多个线程调用的时候,这些线程都会进入阻塞状态
唤醒的时候,也就有两种方式了