栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

3.线程不安全原因-bite

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

3.线程不安全原因-bite

线程安全
  • 线程不安全原因1:线程的抢占式执行
    • 解决原因1的办法
  • 线程不安全原因2:多个线程对同一个变量进行修改操作,3:针对变量的操作不是原子的~
  • 线程不安全原因4:内存的可见性
    • 解决原因4的办法:使用synchronized或volatile关键字
  • 线程不安全原因5:指令重排序
    • 解决原因5的办法:synchronized
  • 对象头

线程不安全原因1:线程的抢占式执行

1.线程是抢占式执行,线程间的调度充满随机性.[线程不安全的万恶之源!]

class Counter {
	public int count;
    public void increase() {
        count++;
    }
}

public class Demo15 {
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        // 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();

        // 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
        System.out.println(counter.count);
    }
}

上面这一段代码的执行结果大概率并不是10 0000 ,只要多个线程操作的是同一个变量,就有问题

count++ 在cpu角度来看是3个指令:

1.load(把内存中的值加载到,cpu寄存器中)

2.add(将寄存器中的值加一)

3.save(将寄存器中的值写回到内存中)

因为抢占式执行,就导致两个线程同时执行这3个指令时,顺序上充满随机性…

情况1:

情况2:

情况3:…

如果类似,情况2出现多次,就会出现线程不安全

极端情况下:
如果所有的操作都是串行的, 此时结果就是10w(可能出现的,但是小概率事件)
如果所有的操作都是交错的,此时结果就是5w(可能出现的,也是小概率事件)

解决原因1的办法

将多条指令打包成一个原子操作
即:
给线程加锁,使用synchronized关键字

class Counter {
	public int count;
   	synchronized public void increase() {
        count++;
    }
}

public class Demo15 {
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        // 必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();

        // 在 main 中打印一下两个线程自增完成之后, 得到的 count 结果~~
        System.out.println(counter.count);
    }
}

给方法直接加上 synchronized关键字.当一个线程进入方法后,就会自动加锁,离开方法后,就会自动解锁.

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BLOCKED状态)
阻塞会一直持续到,占用锁的线程把锁释放为止

线程不安全原因2:多个线程对同一个变量进行修改操作,3:针对变量的操作不是原子的~ 线程不安全原因4:内存的可见性
public class Demo16 {
    private static int isQuit = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("循环结束! t 线程退出!");
        });
        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值: ");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕!");
    }
}

上述代码在运行一段时间后,输入1赋值给isQuit ,但 t 线程并没有结束运行

原因:
t 这个线程在循环读取这个变量,是先从内存中读取后,加载到寄存器,最后打印到控制台,但是从内存中读取,相比于直接从寄存器中读取是非常低效的.

因此在t1中频繁的读取这里的内存的值,就会非常低效!!
而且如果t2线程迟迟不修改, t1线程读到的值又始终是一样的值!!

因此, t1就有了一个大胆的想法:就会不再从内存读数据了,而是直接从寄存器里读(不执行load 了)一旦t1做出了这种大胆的假设, 此时万一t2修改了count值, t1就不能感知到了

这是java编译器进行优化的效果

解决原因4的办法:使用synchronized或volatile关键字

1.使用synchronized关键字:
synchronized 不光能保证指令的原子性,同时也能保证内存可见性,被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设,相当于手动禁用了编译器的优化

使用volatile关键字:
volatile和原子性无关,但是能够保证内存可见性.禁止编译器做出上述优化.编译器每次执行判定相等,都会重新从内存读取isQuit的值.

public class Demo16 {
    private volatile static int isQuit = 0;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("循环结束! t 线程退出!");
        });
        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值: ");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕!");
    }
}
线程不安全原因5:指令重排序

指令重排序,也是编译器优化中的一种操作

咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓,编译器就会智能的调整这里代码的前后顺序从而提高程序的效率,保证逻辑不变的前提,再去调整顺序

如果代码是单线程的程序,编译器的判定一般都是很准,但是如果代码是多线程的,编译器也可能产生误判

解决原因5的办法:synchronized

synchronized:不光能保证原子性,同时还能保证内存可见性,同时还能禁止指令重排序

对象头

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/978756.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号