Java多线程安全
1. 什么是线程安全
在解释什么是线程安全之前,我们先来看一个线程不安全的场景:
public class Test {
private static long n = 0;
private static long count = 1_000_000_000L;
static class Add extends Thread{
@Override
public void run() {
for(int i = 0; i < count; i++){
n++;
}
}
}
static class Sub extends Thread{
@Override
public void run() {
for(int i = 0; i < count; i++){
n--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread add = new Add();
Thread sub = new Sub();
add.start();
sub.start();
add.join();
sub.join();
System.out.println(n);
}
}
我们预想的结果应该是0,但是并不是这样。实际结果则是一个随机值。
那么什么是线程安全呢?
- 线程安全:通俗的来讲就是可以100%运行正确的就是线程安全。
2. 为什么会出现线程不安全?
- 出现了数据共享:
再上一个场景中线程add和线程sub共享了变量n和count。 - 对共享数据进行了修改。
同样上一个场景中线程add和线程sub都修改了变量n。
3.线程不安全的三种情况
- 破环了原子性
比如刚才我们看到的 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了即原子性遭到了破环,结果就可能是错误的,第一个场景不安全就是因为破环了原子性。
- 内存不可见
- 主内存-工作内存
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是内存呢可见性问题。
- 场景演示
public class 内存不可见场景 {
private static boolean running =true;
static class Test extends Thread{
@Override
public void run() {
int n = 0;
while (running){
n++;
}
System.out.println(n);
}
}
public static void main(String[] args) {
Thread t3 = new Test();
t3.start();
Scanner scanner = new Scanner(System.in);
System.out.println("随便输入什么结束线程");
scanner.nextLine();
System.out.println(running);
running = false;
System.out.println(running);
System.out.println(t3.getState());
}
}
当获取输入后将running的状态改为false,按理来说线程将终止,但线程的状态依然是RUNNABLE,这时就出现了内存不可见。虽然主内存中改变了running状态,但t3的工作内存并不知道。
- 代码重排序
线程的有序性是指:在线程内部,所有的操作都是有序执行的,而在线程之间,因为工作内存和主内存同步的延迟,操作是乱序执行的,发生了代码重排序。
比如创建一个新的对象的执行过程是:(1)new在堆上开辟空间(2)调用构造方法(3)给引用赋值。在单线程的情况下代码重排序的前提是没有副作用,但在多线程情况下并不能保证。
4.线程安全机制
synchronized
- 语法
- 作为方法修饰符存在
- 作为语句块出现
public class SynchronizedDemo {
public synchronized static void fun(){
};
public synchronized void start(){
};
public void method(){
Object o = new Object();
synchronized(o){
//引用不为空,否则抛出NullPointerException异常
}
}
}
- 作用
- 主要保证原子性:互斥,抢的是同一对象的锁(monitor lock )
- 一定程度保证内存可见性:当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中;当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
对刚开始的案例进行改造,再来观察效果。
public class Test {
private static long n = 0;
private static long count = 1_000_000_000L;
static class Add extends Thread{
private Object lock;
Add(Object lock) {
this.lock = lock;
}
@Override
public synchronized void run() {
for(int i = 0; i < count; i++){
synchronized (lock) {
n++;
}
}
}
}
static class Sub extends Thread{
private Object lock;
Sub(Object lock) {
this.lock = lock;
}
@Override
public synchronized void run() {
for(int i = 0; i < count; i++){
synchronized (lock) {
n--;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Test lock = new Test();
Thread add = new Add(lock);
Thread sub = new Sub(lock);
add.start();
sub.start();
add.join();
sub.join();
System.out.println(n);
}
}
输出和我们预想的值一样。
volatile
- 语法
- 修饰属性/静态属性
- 作用
- 主要保证内存可见性
- 限制重排序:对象的初始化
public class 内存不可见场景 {
private volatile static boolean running =true;
static class Test extends Thread{
@Override
public void run() {
int n = 0;
while (running){
n++;
}
System.out.println(n);
}
}
public static void main(String[] args) throws InterruptedException {
Thread t3 = new Test();
t3.start();
Scanner scanner = new Scanner(System.in);
System.out.println("随便输入什么结束线程");
scanner.nextLine();
System.out.println(running);
running = false;
System.out.println(running);
while (true) {
System.out.println(t3.getState());
TimeUnit.SECONDS.sleep(1);
}
}
}
可以看到t3线程接受到了running状态的改变,线程终止。