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);
}
}
}
NEW:Thread 对象已经有了.start 方法还没调用.
TÉRMINATED: Thread 对象还在,内核中的线程已经没了.
RUNNABLE: 就绪状态 (线程已经在 cpu 上执行了/线程正在排队等待上 cpu 执行)
TIMED WAITING: 阻塞. 由于 sleep 这种固定时间的方式产生的阻塞.
WAITING: 阻塞. 由于 wait 这种不固定时间的方式产生的阻塞
BLOCKED: 阻塞. 由于锁竞争导致的阻塞,
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.线程的随机调度
操作系统中, 线程的调度顺序是随机的 (抢占式执行).罪魁祸首,万恶之源,
count++ 这个操作,本质上,是分成三步进行的~~
站在 cpu 的角度上,count++ 是由 cpu 通过三个指令来实现的~~
1)load 把数据从内存, 读到 cpu 寄存器中,
2)add 把寄存器中的数据进行 +1
3)save 把寄存器中的数据, 保存到内存中
如果是多个线程执行上述代码,由于线程之间的调度顺序,是“随机"的,就会导致在有些调度顺序下,上述的逻辑就会出现问题
在多线程程序中,最困难的一点:线程的随机调度,使两个线程执行逻辑的先后顺序,存在诸多可能,我们必须要保证在所有可能的情况下,代码都是正确的!
以下是正确的执行顺序
不是按照此顺序,最终结果一定有bug,且最终值小于10w。
2.两个线程,针对同一个变量进行修改
1)一个线程针对一个变量修改.ok
2)两个线程针对不同变量修改.ok
3)两个线程针对一个变量读取.ok
3.修改操作,不是原子的.
此处给定的 count++ 就属于是 非原子 的操作.(先读,再修改)类似的,如果一段逻辑中,需要根据一定的条件来决定是否修改,也是存在类似的问题
假设 count++ 是原子的(比如有一个 cpu 指令,一次完成上述的三步)
4.内存可见性问题.
5.指令重排序
要想解决线程安全问题,就是要从上述方面入手。
1.系统内核里实现的->最初搞多任务操作系统的人,制定了"抢占式执行大的基调.在这个基调下,想做出调整是非常困难的。
2.有些情况下,可以通过调整代码结构,规避上述问题但是也有很多情况,调整不了。
3.通过加锁!!!
通过加锁, 就能解决上述问题.
如何给 java 中的代码加锁呢?
其中最常用的办法, 就是使用 synchronized 关键字!
synchronized 在使用的时候,要搭配一个 代码块{}进入{就会 加锁.出了}就会解锁.
在已经加锁的状态中,另一个线程尝试同样加这个锁,就会产生"锁冲突/锁竞争",后一个线程就会阻塞等待一直等到前一个线程解锁为止.
【锁对象到底用哪个对象?无所谓!!!对象是谁,不重要: 重要的是俩线程加锁的对象,是否是同一个对象.】
synchronized (locker) {
count++;
}()中需要表示一个用来加锁的对象这个对象是啥不重要,重要的是通过这个对象来区分两个线程是否在竞争同一个锁.
t2 线程由于锁的竞争, 导致 lock 操作出现阻塞, 阻塞到 t1 线程 unlock 之后t2 的 lock 才算执行完,此时 t2 就处在 blocked 状态下 。
阻塞就避免了下列的 load add save 和第一个线程操作出现穿插形成这种"串行"执行的效果此时线程安全问题就迎刃而解。
// 线程安全
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);
}
}
synchronized public void increase() {
count++;
}public void increase2() {
synchronized (this) {
count++;
}
}二者等价
synchronized public static void increase3() {
}
public static void increase4() {
synchronized (Counter.class) {}
}二者等价
3.synchronized 关键字-监视器锁monitor lock
3.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁synchronized 用的锁是存在 Java 对象头里的。Java 的一个对象,对应的内存空间中,除了你自己定义的一些属性之外,还有一些自带的属性(在对象头中,其中就有属性表示当前对象是否已经加锁)
2) 刷新内存
synchronized 的工作过程:1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
3) 可重入synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;所谓的可重入锁,指的是,一个线程,连续针对一把锁,加锁两次,不会出现死锁.满足这个要求,就是"可重入'不满足, 就是"不可重入"上面把 synchronized 设计成" 可重入锁"就可以有效的解决上述死锁问题(让锁记录一下,是哪个线程给它锁住的后续再加锁的时候,如果加锁线程就是持有锁的线程就直接加锁成功!!!)如何判断是不是最外层?引用计数
锁对象中,不光要记录谁拿到了锁,还要记录,锁被加了几次,
每加锁一次,计数器就 + 1.
每解锁一次,计数器就 -1.
关于死锁
1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了.(synchronized 不会出现.)
2.两个线程, 两把锁.(此时无论是不是可重入锁, 都会死锁).1)t1 获取锁 A, t2 获取锁 B
2)t1 尝试获取 B, t2 尝试获取 Apublic class Demo16 { private static Object locker1 = new Object(); private static Object locker2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (locker1) { // 此处的 sleep 很重要. 要确保 t1 和 t2 都分别拿到一把锁之后, 再进行后续动作. try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker2) { System.out.println("t1 加锁成功!"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker1) { System.out.println("t2 加锁成功!"); } } }); t1.start(); t2.start(); } }
死锁代码中两个 synchronized 是嵌套关系,
不是并列关系,嵌套关系说明,
是在占用一把锁的前提下,获取另一把锁.(并列关系,则是先释放前面的锁,再获取下一把锁,(不会死锁的)
3.N 个线程, M 把锁.(相当于 2 的扩充),此时,是更容易出现死锁的情况了~~
1个经典的描述 N 个线程 M 把锁死锁的模型,哲学家就餐问题
死锁,是属于比较严重的 bug(会直接导致线程卡住,也就无法执行后续工作了)
如何解决/避免死锁呢??
死锁的成因, 涉及到 四个 必要条件,
1.互斥使用.(锁的基本特性).当一个线程持有一把锁之后,另一个线程也想获取到锁, 就要阻塞等待.
2.不可抢占.(锁的基本特性).当锁已经被线程1拿到之后,线程2 只能等线程1 主动释放,不能强行抢过来~3.请求保持.(代码结构).一个线程尝试获取多把锁.(先拿到锁1 之后,再尝试获取锁2,获取的时候,锁1 不会释放).(吃着碗里的, 看着锅里的)
4.循环等待/环路等待. 等待的依赖关系,形成环了(钥匙锁车里了,车钥匙锁家里了)
1和2是锁的基本特性,不能破坏,
只要满足3和4就会出现死锁,
解决死锁,破坏3和4条件即可。
对于 3 来说, 调整代码结构,避免编写"锁嵌套" 逻辑(这个方案不一定好使,有的需求可能就是需要进行这种获取多个锁再操作)
对于 4 来说, 可以约定加锁的顺序, 就可以避免循环等待(针对锁,进行编号.比如约定,加多把锁的时候,先加编号小的锁,后加编号大的锁.)
public class Demo16 { private static Object locker1 = new Object(); private static Object locker2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (locker1) { // 此处的 sleep 很重要. 要确保 t1 和 t2 都分别拿到一把锁之后, 再进行后续动作. try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker2) { System.out.println("t1 加锁成功!"); } } }); Thread t2 = new Thread(() -> { synchronized (locker1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker2) { System.out.println("t2 加锁成功!"); } } }); t1.start(); t2.start(); } }
4.volatile 关键字
1)volatile 能保证内存可见性
计算机运行的程序/代码,经常要访问数据
这些依赖的数据,往往会存储在 内存 中.(定义一个变量,变量就是在内存中.)
cpu 使用这个变量的时候,就会把这个内存中的数据,先读出来, 放到 cpu 的寄存器中再参与运算.(load)
cpu 读取内存的这个操作,其实非常慢!!!(快,慢,都是相对的)cpu 进行大部分操作,都很快.一旦操作到读/写内存,此时速度一下就降下来了
- 读内存 相比于 读硬盘, 快几千倍,上万倍,
- 读寄存器, 相比于读内存,又快了几干倍,上万倍
为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器减少读内存的次数,也就可以提高整体程序的效率.
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();
}
}
1)load 读取内存中 isQuit 的值到寄存器中2)通过 cmp 指令比较寄存器的值是否是 0,决定是否要继续循环
由于这个循环,循环速度飞快.短时间内,就会进行大量的循环也就是进行大量的 load 和 cmp 操作.
此时,编译器/JVM 就发现了,虽然进行了这么多次 load,但是 load 出来的结果都一样的. 并且, load 操作又非常费时间,一次 load 花的时间相当于上万次cmp 了.
所以, 编译器就做了一个大胆的决定~~ 只是第一次循环的时候, 才读了内存后续都不再读内存了,而是直接从寄存器中,取出 isQuit 的值了【编译器优化:编译器的初心是好的,希望能够提高程序的效率.但是提高效率的前提是保证逻辑不变此时由于修改 isQuit 代码是另一个线程的操作, 编译器没有正确的判定所以编译器以为没人修改 isQuit, 就做出了上述优化. 也就进一步引起 bug 了】【这就是内存可见性问题】
volatile 就是解决方案
在多线程环境下,编译器对于是否要进行这样的优化, 判定不一定准,就需要程序猿通过 volatile 关键字,告诉编译器, 你不要优化!!!(优化,是算的快了,但是算的不准了)
编译器,也不是万能的.也会有一些自己短板的地方.此时就需要程序猿进行补充了只需要给isQuit 加上 volatile 关键字修饰,此时编译器自然就会禁止上述优化过程
private static volatile int isQuit = 0;
此时没加 volatile,但是给循环里加了个 sleep此时,t1 线程是可以顺利退出的!!!
加了 sleep 之后, while 循环执行速度就慢了由于次数少了,load 操作的开销,就不大了,因此,优化也就没必要进行了.
没有触发 load 的优化,也就没有触发内存可见性问题了
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("主线程结束!");
}
}
join
等待的过程和"主线程"没有直接联系,哪个线程调用 join, 哪个线程就阻塞
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();
}
}
wait和notify都要放在synchronized
使用 wait notify 也可以避免"线程饿死'
这个等的状态,是阻塞的,啥都不做,不会占据 cpu
5.1 notify和notifyAll
notify->一次唤醒一个线程
notifyAll->一次唤醒全部线程 (唤醒的时候,wait 要涉及到一个重新获取锁的过程也是需要串行执行的)
调用 wait 不一定就只有一个线程调用.
N 个线程都可以调用 wait此时,当有多个线程调用的时候,这些线程都会进入阻塞状态
唤醒的时候,也就有两种方式了