Java多线程之线程不安全的原因

抢占式执行

  • 抢占式调度:
    抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

  • JVM的实现:
    JVM规范中规定每个线程都有优先级,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

  • java使用的线程调度式抢占式调度
    Java中线程会按优先级分配CPU时间片运行

抢占式执行也是我们线程不安全的罪魁祸首,因为两个线程执行的时候是抢占式执行,我们无法预知到两个线程执行的具体过程,也就无法预知运行的结果

原子性

原子性就是一些操作或者一组操作是不可分割的,在执行结束前,其他操作不能执行。
我们举个例子,n++,看起来这是一个原子性的指令,但是其实它不是,要给n自加,首先要从内存中读取数据,然后在给n自加,然后把操作完成的数据再放回到内存中。
假如我们让两个线程都进行n++操作
在这里插入图片描述
假如上边两个线程,线程一先执行,当线程一执行到第二步的时候,线程二开始执行第一步,这时候线程二读取的n还是0,因为此时线程一还没有来的及将改完的数据写回到内存中。这就是操作不是原子性带来的问题,如果n++整个操作都是原子性的,即读改写是一步操作,那就不会出现这个问题。

修改同一个数据

修改同一个数据即多个线程在执行的时候,如果只是读取数据,或者每个线程修改每个线程自己的数据,就不会触发线程安全问题。但是如果涉及到多个线程修改同一个数据,就会触发我们的线程安全问题。

内存可见性

内存可见性 : 一个线程对共享变量值的修改,能够及时地被其他线程看到。
我们的每个线程在工作的时候都会有一个工作内存,我们先看一下简易的内存模型
在这里插入图片描述
为了程序的执行效率,每个线程都有一个自己的工作内存,然后一个线程读取数据都是先从主存中读取到工作内存中,使用的时候直接从工作内存中读取,这样做就相当于加了一个缓冲区,可以加快程序的读取速度。但是这样也会引发我们的线程的安全问题。还是以n++那个例子,假设我们线程执行很快,快到线程一虽然已经将修改完的n写回到内存了,但是线程二工作内存中的n值还没有来的及刷新,修改完后n的值还是1,所以还是会造成我们的线程安全问题。

代码优化(重排序)

代码优化就是JVM会对我们写的代码在执行的时候会优化指令,就比如一个线程正在重复读取一个变量的值,JVM就会对其优化,cpu不会在去重复的从工作内存中读取,他会直接从cpu的寄存器里面直接读取刚刚用过的数据,假如此时另一个线程对这个变量进行了修改,那第一个线程就接受不到改变后的值,也会造成我们的线程安全问题。

public class Test {
    static  int  n = 0;
    static Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("循环开始");
            while (n == 0){

            }
            System.out.println("循环结束");
        }
    };

    static Thread t2 = new Thread(){
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            n = scanner.nextInt();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

在这里插入图片描述
这就是不管你输入什么,循环都不会结束,就是因为我们的JVM对我们的代码进行了优化。

解决方案

解决我们线程安全的问题,我们可以使用synchornized加锁,和volatile。
synchornized就是给一段代码或者一个方法加上锁,让同一时间只能有一个线程来执行这段代码,,另外synchornized在加锁和释放锁的时候会刷新工作内存,这就解决了我们的操作原子性和内存可见性。
volatile可以让线程每次读取更新数据的时候都直接从主内存读取更新。还可以禁止代码进行指令重排序。也解决了内存可见性和指令重排序问题。
我们先写代码来解决第一个n++的问题

public class SynchronizedThread {
    static int count = 0;
    static class Test{
        synchronized public void func(){
            count++;
        }

    }

    public static void main(String[] args) {
        Test t = new Test();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println("线程1开始运行");
                for (int i = 0; i < 5000; i++) {
                    t.func();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public  void run() {
                System.out.println("线程2开始运行");
                for (int i = 0; i < 5000; i++) {
                    t.func();
                }
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

当我们加上了synchornized关键字之后,每次打印的count值都是10000.这就解决了我们的线程安全问题,不加volatile是因为这没有涉及到指令重排序,如果为了保证安全,加上也可以。不过在这里一个synchornized就可以解决问题了。

现在我们解决指令重排序那个Test类例子,只需要简单的加一个volatile关键字就好

public class Test {
    static volatile int  n = 0;  //加volatile关键字
    static Thread t1 = new Thread(){
        @Override
        public void run() {
            System.out.println("循环开始");
            while (n == 0){

            }
            System.out.println("循环结束");
        }
    };

    static Thread t2 = new Thread(){
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            n = scanner.nextInt();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

在这里插入图片描述
当我们加了volatile关键字之后,当一个线程对变量修改后,另一个线程就能立马获取到。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值