多线程带来的的风险-线程安全(重点)
1. 观察线程不安全
package Test.thread;
public class ThreadDemo {
private static class Counter {
private long n = 0;
public void increment() {
n++;
}
public void decrement() {
n--;
}
public long value() {
return n;
}
}
public static void main(String[] args) throws InterruptedException {
final int COUNT = 1000_0000;
Counter counter = new Counter();
Thread thread = new Thread(() -> {
for (int i = 0; i < COUNT; i++) {
counter.increment();
}
}, "t1");
thread.start();
for (int i = 0; i < COUNT; i++) {
counter.decrement();
}
thread.join();
// 期望最终结果应该是 0
System.out.println(counter.value());
}
}
2.线程不安全的原因
- CPU是抢占式执行
- 线程共同操作同一变量
- 内存不可见问题
- 原子性
- 代码顺序性 编译器优化(会使代码顺序变乱,可能导致错误)
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如n++,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
3. 解决线程不安全问题
3.1 线程不安全问题解决方案分析:
- CPU抢占执行的问题(不可控)
- 每个线程操作自己的私有变量(有可能可以)
- 只需要在关键步骤上排队执行【加锁】
3.2 volatile 关键字
- 修饰的共享变量,可以保证可见性,部分保证顺序性
可以解决内存不可见和指令重排序问题,但是不能解决原子性问题。
3.3 加锁
操作锁的流程:
- 尝试获取锁
- 使用锁
- 释放锁
Java语言中加锁的操作有两种:
方法1. synchronized (给某一对象的对象头加锁)
实现分为:
1.synchronized 在JVM层面使用 monitor 来实现,它帮我们实现了加锁和释放锁的过程。
2.synchronized 在操作系统层面,它是依靠motex 互斥锁。
3.针对Java语言来说
①锁存放的位置:对象头
②头信息 monitor:Owner(拥有者),EntryQ,Rcthis,Next(使用次数),HashCode
注意事项:
如果是同一业务的多线程执行,一定要使用同一个锁
synchronized 的3种使用场景:
使用 synchronized 修饰代码块(可以给任意对象加锁)
使用 synchronized 修饰静态方法(可以对当前的类进行加锁)
使用 synchronized 修饰普通方法(对当前类实例进行加锁)
高频面试点:
synchronized 锁升级的过程(JDK 1.6):重量级:用户态 → 内核态(有特别大的性能消耗)
方法2. 手动式Lock方式
Lock(重入锁) 只能用来修饰代码块
注意事项:
1. 使用 Lock 一定要释放锁
2. lock() 操作一定要放在try 外面。
如果放在 try 里面会造成两个问题:
1.如果 try 里面抛出异常了,还没有加锁成功就执行 finally 里面释放锁的操作。因为还没有得到锁就释放锁。
2.如果放在 try 里面,如果没有锁的情况下试图释放锁,这个时候产生的异常就会将业务代码的异常(也就是 try 里面的异常)吞噬掉,增加了代码调试的难度。
如果一定要把 lock 放在 try 里面的话,一定要放在第一行。
在Java 语言中所有锁的默认实现方式都是非公平锁
面试重点:
synchronized 和 Lock 的区别:
- 关键字不同。
- synchronized 是自动进行加锁和释放锁的过程;而 Lock 需要手动的加锁和解锁。
- Lock 是Java层面的锁的实现,synchronized 是JVM层的实现。
- synchronized 和 Lock 适用范围不同,Lock 只能用来修饰代码块,而 synchronized 既可以修饰代码块,又可以修饰静态方法和普通方法。
- synchronized 锁的模式只有非公平锁模式,而 Lock 既可以使用公平锁的模式又可以使用非公平锁的模式。
- Lock 的灵活性更高(tryLock)。