目录
一 线程安全的概念
线程安全的概念是指在多线程环境下运行代码的结果符合我们的预期,即与单线程环境下运行结果相同,则这个程序是安全的。
二 观察线程不安全现象
有些代码单个环境下执行是正确的,但在多线程环境下就出现了问题。比如下面这个例子:分别使用两个线程对变量进行5k次的自增,预期结果应该是1w。
public class demo21 {
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 < 5000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
结果分析:
但实际上这个结果和1w相差很大,这种情况就是典型的线程安全问题。
如果代码改成这样,t1在运行的时候t2不会启动。
三 线程不安全的原因
😶🌫️1.线程的调度顺序是随机的(抢占式执行)
最初操作系统中制订了"抢占式执行",很难做出调整。
😶🌫️2.多个线程修改同一个共享变量
有些情况下可以通过调整代码结构规避,但很多情况下调整不了
注意:
多个线程针对一个变量,线程安全
多个线程修改不同变量,线程安全
一个线程修改一个变量,线程安全
😶🌫️3.内存可见性问题
😶🌫️4.代码重排序问题
😶🌫️5.原子性问题
什么是原子性:
代码的原子性是指一个操作不可中断的,要么全部执行,要么全部不执行的情况。比如阿红去医院做乳腺超声检查,进去很久没有出来,下一个患者等急了,一撩帘子就进来了,这时候就打断了阿红的隐私,这就不具备原子性了。如果给房间加一把锁,其他人就进不来了,这样就保证了代码的原子性了。
四 解决线程不安全现象:加锁
1.synchronized关键字
1.1 synchronized使用示例
😶🌫️修饰代码块:
明确指出锁哪个对象
public class demo21 {
static class Counter{
public int count = 0;
void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
//括号里需要表示一个要加锁的对象
//这个对象是什么不重要,重要的是通过这个对象来区分两个线程在竞争同一把锁
synchronized (locker){
counter.increase();
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (locker){
counter.increase();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
😶🌫️修饰一个实例方法
static class Counter{
public int count = 0;
//这两个方法是等价的
synchronized public void increase(){
count++;
}
public void increase2(){
synchronized (this){
count++;
}
}
}
😶🌫️修饰一个静态方法:
相当于针对类对象加锁
static class Counter{
public int count = 0;
//这两个方法是等价的
synchronized public static void increase3(){
}
public static void increase4(){
//Counter.class是类对象
synchronized ((Counter.class)){
}
}
}
😶🌫️注意:两个线程中的对象要是同一个
1.2 synchronized的特性
互斥性:同一时间只有一个线程能获取到锁,其他线程只能阻塞等待。
可重入性:synchronized是可重入锁,不会出现死锁现象。
😶🌫️关于死锁:
一个线程针没有释放锁,又尝试加锁,如果不是可重入锁,就是死锁
😶🌫️死锁的成因(满足3 ,4就会形成死锁):
1.互斥使用(锁的基本特性):当一个线程获取到一把锁另一个线程也想获取到锁时,就要阻塞等待。
2.不可抢占(锁的基本特性):当锁被线程1拿到以后,线程2 只能等线程1主动释放锁。
3.请求保持(代码结构):一个线程尝试获取到多把锁。(吃着碗里看着锅里)
4.循环等待:等待成依赖关系。(哲学家吃面)
//死锁 public class demo23 { private static Object locker1 = new Object(); private static Object locker2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(()->{ //locker1 和 locker2是嵌套关系 synchronized (locker1){ try { //此处的sleep很重要,确保t1和t2都分别拿到了一把锁之后再进行后面的动作。 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(); } }
😶🌫️破解死锁:
1.改变代码结构
public static void main(String[] args) { Thread t1 = new Thread(()->{ synchronized (locker1){ try { //此处的sleep很重要,确保t1和t2都分别拿到了一把锁之后再进行后面的动作。 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加锁成功!"); } });
2.约定加锁顺序
public static void main(String[] args) { Thread t1 = new Thread(()->{ synchronized (locker1){ 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加锁成功!"); } } });
2.volatile关键字
volatile能保证内存可见性
😶🌫️内存可见性:指当一个线程对共享变量做出修改后,其他线程能够立即看到这个修改后的值。
public class demo26 {
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 sc = new Scanner(System.in);
//用户输入的值不为0,t1的线程就会结束
isQuit = sc.nextInt();
});
t2.start();
}
}
这段代码因为内存可见性问题,t2修改了isQuit之后,t1却看不见。导致输入了不为零的数以后,线程仍没有结束。这是因为代码优化错误,解决方案就是:volatile
//加上volatile:告诉编译器不要优化
private volatile static int isQuit = 0;
3.总结
synchronized保证原子性,volatile保证内存可见性。