Java线程安全问题


什么是线程安全问题

线程安全概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线
程安全的。

线程不安全的案例

多个线程对同一个变量进行修改操作时,可能会有线程安全问题。
我们创建两个线程同时对同一个变量count自增5k次,那么这个变量最终的结果应该是1w

class Counter {
    public int count = 0;
    public void add() {
        count++;
    }
}
public class ThreadDemo11 {
    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.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        // 启动两个线程
        t1.start();
        t2.start();
        // 等待
        t1.join();
        t2.join();
        // 等两个线程都结束后打印
        System.out.println(counter.count);
    }
}

执行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从执行结果可以看出,这个值是不符合我们的预期,这就是线程安全问题。
这个值是随机的,而且小于等于10000。

线程不安全的原因

  • 操作系统随机调度线程,抢占式执行。线程中的代码执行到任意一行,都随时可能被切换出去。
  • 多个线程修改同一个变量。上述案例两个线程对 Counter.count 这个变量同时修改。
  • 修改操作,不是原子的。上述案例对 count++ 这个操作可以细分为 三个cpu指令。
  1. load 从内存中读取变量的值到cpu寄存器中。
  2. add 在寄存器中对这个值加一。
  3. save 将cpu寄存器中的值保存至内存中。

再加上线程抢占式执行的原因,当线程1进行 loadadd 后,还没来得及save,线程2 就 load 了,那么此时两个线程 load 到的值是一样的,这两个线程执行完这次自增操作后,count的值只加了1。所以这就是上述案例中,最终结果会小于等于10000的原因了。

  • 内存可见性问题

案例:
t1线程包含一个循环,循环结束条件是 flag == 0
t2线程从键盘读入一个整数,并给flag赋值。
预期结果是输入一个非0的数,然后t1线程结束。

public class ThreadDemo12 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                // 空着
            }
            System.out.println("t线程结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入一个整数:");
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

执行结果:
在这里插入图片描述

这个执行结果与我们的预期不符。
这是编译器优化导致的,t1线程中频繁读取内存flag 的值,一开始读内存flag 的值,flag 一直不变,编译器就进行优化,t1线程直接用寄存器flag 的值(从寄存器读取比从内存读取快几个数量级),这时候t2线程对 flag 进行修改,t1线程也感知不到。

  • 指令重排序问题

这也是编译器优化导致的,一个操作有多条cpu指令,由于编译器优化改变执行cpu指令的顺序,而出现的问题。
编译器优化在单线程环境下的准确性可以保证,但是在多线程环境下就没那么容易了。

如何解决线程安全问题

synchronized 关键字

synchronized 的特性

一、 互斥

一个线程进入 synchronized 代码块对一个对象加锁,其他线程如果也想对这个对象加锁就只能阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

二、内存刷新

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

这说明 synchronized 也能解决内存可见性问题

三、可重入

一个线程进入 synchronized 对某个对象加锁,这个线程还没释放锁就又进入一个 synchronized
对同一个对象加锁,这个时候不会互斥。synchronized 代码块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

代码示例:

static class Counter {
	public int count = 0;
	synchronized void add() {
		count++;
	}
	synchronized void add2() {
		add();
	}
}

synchronized 如何使用

  1. 修饰普通方法

锁的是 Test 的对象

class Test {
    synchronized public void method() {
    	
    }
}
  1. 修饰静态方法

锁的是 Test 类的对象

class Test {
    synchronized public static void method() {
    	// 锁的是
    }
}
  1. 修饰代码块,指定加锁对象
class Test {
    public void method() {
        synchronized (this) {
            // this 与修饰普通方法等价
        }
    }
}
class Test {
    public void method() {
        synchronized (Test.class) {
        	// Test.class 与修饰静态方法等价
        }
    }
}

synchronized 解决问题

在第一个案例中,由于 count++ 这个操作不是原子的,而引起的线程不安全问题。
我们可以通过 synchronized 关键字对这个操作加锁来解决这个问题。

class Counter {
    public int count = 0;
    public void add() {
        synchronized (this) {
       		// this 当前对象
            count++;
        }
    }
}
public class ThreadDemo11 {
    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.add();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        // 启动两个线程
        t1.start();
        t2.start();
        // 等待
        t1.join();
        t2.join();
        // 等两个线程都结束后打印
        System.out.println(counter.count);
    }
}

执行结果:
在这里插入图片描述

synchronized 解决内存可见性问题

public class ThreadDemo12 {
    public static int flag = 0;
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (locker) {
                    if (flag != 0) {
                        break;
                    }
                }
            }
            System.out.println("t线程结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入一个整数:");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

执行结果:
在这里插入图片描述

volatile关键字

volatile 修饰的变量,可以保证 内存可见性
前面在内存可见性说的问题,直接读取寄存器的值,导致数据不一致的情况。
volatile 修饰变量后,可以强制读写内存,速度变慢了,但是准确性提高了。

代码示例:

public class ThreadDemo12 {
	// 加上volatile
    volatile public static int flg = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flg == 0) {
                // 空着
            }
            System.out.println("t线程结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入一个整数:");
            flg = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

执行结果:
在这里插入图片描述
volatile 不能保证原子性。
volatile 可以禁止指令重排序。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值