线程安全问题
运行下面代码
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run(){
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run(){
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t2.start();
t1.join();
t2.join();
//两个线程各自自增 50000 次,预期结果应该是 100000
System.out.println(counter.count);
}
}
运行效果
这样的代码就是线程不安全的代码。
这就是多线程并发编程所涉及到最重要最复杂的问题。
- 线程不安全:多线程并发执行某个代码时,产生了逻辑上的错误,就是“线程不安全”。
- 线程安全:多线程并发执行某个代码,没有逻辑上的错误,就是“线程安全”。
线程不安全的原因是什么?
- 线程是抢占式执行的(线程不安全的万恶之源)。【抢占式:线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制。线程之间谁先执行,谁后执行,谁执行到哪里从 CPU 上下来,这样的过程都是用户无法控制也无法感知的】
- 自增操作不是原子的。【每次 ++ 都能拆分成三个步骤:1、把内存中的数据读取到 CPU 中(load);2、在 CPU 中把数据 +1(increase);3、把计算结束的数据写回到内存中(save)。当 CPU 执行到上面三个步骤中的任何一步的时候,都可能会被调度器调度走,执行其他的线程】
- 多个线程尝试修改同一个变量,如果有多个线程,一个线程读取数据,一个线程修改数据,此时也是可能导致线程不安全【如果是一个线程修改一个变量,线程安全;如果多个线程尝试读取同一个变量,线程安全;如果多个线程尝试修改不同的变量,线程安全】
- 内存可见性导致的线程安全问题
- 指令重排序(Java 的编译器在编译代码时,会针对指令进行优化,调整这领的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率)
如果两个线程是串行执行的,运行后计算结果是正确的,并发执行的效果,线程1 进行 ++ 了一半的时候,线程 2 也在公式进行 ++
如何解决线程不安全问题?
- 抢占式执行【这个没办法解决,操作系统内核实现】
- 自增操作非原子【可以给自增操作加上锁,适用范围最广】
- 多个线程同时修改同一个变量【这个要看具体的需求】
锁
什么是锁?
与生活中的锁类似
锁的特点:
互斥的。同一时刻只有一个线程能获取到锁。其他线程如果也尝试获取锁,就会发生阻塞等待,一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁
锁的基本操作:
- 加锁(获取锁)lock
- 解锁(释放锁)unlock
Java 中使用锁要借助 synchronized 关键字,用法可以灵活的指定某个对象来加锁,而不仅仅是把锁加到某个方法上,如果把 synchronized 关键字写到方法内部,英文原意为同步,在这里代表互斥
在刚刚的代码中,自增方法前加上 synchronized 关键字
此时的运行结果就是正确的 100000
锁是如何解决线程安全问题的?
线程1 释放锁之后,线程2 才可能获取到锁
此时线程1 哪怕被执行了一半被调度走了也没关系,其他的线程想尝试 ++ 操作也不会对线程1 的修改产生任何负面影响,这样的话线程1 的自增操作就能被一鼓作气的执行完,中间不会受到干扰,也就相当于保证了原子性
synchronized 的几种常见用法:
- 加到普通方法前:表示锁 this
- 加到静态方法前:表示锁当前类对象
- 加到某个代码块之前,显示指定给某个对象加锁
使用锁时要注意:
- 使用的时候一定要注意按照正确的方式来使用,否则就容易出现各种各样的问题
-
一旦使用锁,这个代码基本上就和“高性能”无缘了,锁的等待时间可能会很久
因为锁的等待时间是不可控的,可能会等很久,也有可能会出现死锁的问题,一旦程序出现死锁的问题,程序就凉凉~了
看下面对锁的使用
当前这个代码中,两个线程尝试获取同一把锁
import java.util.Scanner;
public class ThreadDemo1 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(){
@Override
public void run(){
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
synchronized (locker) {
//如果用户不输入,锁救护一直阻塞在 nextInt 中
int num = sc.nextInt();
System.out.println("num = " + num);
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run(){
while (true){
synchronized (locker){
System.out.println("线程2获取到锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t2.start();
}
}
执行效果
一旦线程1 获取到锁,并且没有被释放的话,线程2 就会一直在锁这里被阻塞等待
在 jconsole 中查看一下,线程1 就被阻塞在了 nextInt 方法,等待用户输入
再看看线程2 的调用栈,阻塞在了我代码的 32 行,并且进入了 BLOCKED 状态
在锁这里被阻塞
当我输入了一个整数后,线程1 的锁就被释放了,线程2 就可以继续运行了
当两个线程对两个对象来加锁后,两个线程就不再互相竞争了
package com.Test0821;
import java.util.Scanner;
public class ThreadDemo1 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(){
@Override
public void run(){
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
synchronized (locker1) {
//如果用户不输入,锁救护一直阻塞在 nextInt 中
int num = sc.nextInt();
System.out.println("num = " + num);
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run(){
while (true){
synchronized (locker2){
System.out.println("线程2获取到锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t2.start();
}
}
运行效果
线程1 没有释放锁,线程2 也可以正常获取到锁,因为两个线程获取的不是同一个对象的锁