JavaEE-多线程-Volatile关键字


我们在考虑线程安全的时候,会遇到这样一类问题.
首先观察以下代码

package thread;

public class Test17 {
    static boolean flag = true;
    static int count = 0;
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i=0;i<10000;i++){
                if (i==9999) flag = false;
            }
        });
        Thread thread2 = new Thread(() -> {
           while (flag){
                count++;
           }
            System.out.println("满10000啦,循环次数为:"+count);
        });
        thread1.start();
        thread2.start();
    }
}

这里我写了两个线程,线程1对flag这个静态成员变量进行修改,但不过要等到循环结束的时候再对flag进行修改.线程2则是根据flag的值定义循环条件,每一次循环就让count变量自增一次.这么看下来,意思就是,一个线程写,一个线程读.运行下来我们看结果
image.png
不难发现循环次数和我们预期的10000次并不一样,甚至说是大相径庭
那么这又是为甚麽呢?
因为这里线程在执行读写操作的时候,系统发生了优化
因为这里thread1在循环的前9999次并没有对flag进行修改,那么线程2的while在进行读flag变量操作的时候,发现这个flag老大半天没有改动,所以它就自动优化了,不读了,这个值就是之前它读到的值(要问为甚麽这样子就算优化,因为读内存的操作也是很消耗系统资源的).所以我们看到线程2循环的次数很少,是寄存器偷懒少读了,进行了优化.
那么如果我们想避免这种情况的话,就要对那个读的变量加上volatile关键字,这样可以使该变量每次变化都被读取到.
image.png
循环次数直接彪到1万3,可以看出这个关键字的作用了
虽然这样会导致程序运行变得缓慢
但是我们宁愿算慢也不愿意算错,否则再快也是徒劳

特性

  1. 保证可见性,不保证原子性
    1. 当写一个volatile变量的时候,JVM会把该线程本地内存中的变量强制刷新到主内存中去,
    2. 这个操作会导致其他线程中的volatile变量缓存无效
  2. 禁止指令重排序

重排序是指编译器和处理器为了优化程序新跟那个而对指令序列进行排序的一种手段.重排序需要遵守一定规则:

  1. 重排序操作不会对存在数据依赖关系的操作进行重排序

比如:a=1;b=a;这样的指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序

  1. 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

 a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

 b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

 即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

volatile不合适的情况

不适合符合操作

package thread;

public class Test18 {
    public volatile int a = 0;
    
    public void increase() {
        a++;
    }

    public static void main(String[] args) {
        final Test18 test18 = new Test18();
        for (int i=0;i<10;i++){
            new Thread(){
                public void run(){
                    for (int j=0;j<1000;j++){
                        test18.increase();
                    }
                }
            }.start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(test18.a);
    }
}

例如上述代码中,a++不是一个原子性操作,它由读取,加,赋值三步构成,所以结果并不能达到30000
解决方法:

  1. (1)采用synchronized

image.png

  1. 采用Lock

image.png

  1. 采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的

image.png

原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
JavaEE-图书管理系统通常需要包含以下数据库表: 1. 用户表(user):包括用户ID、用户名、密码、邮箱等字段。 2. 图书表(book):包括图书ID、图书名称、作者、出版社、ISBN号等字段。 3. 借阅记录表(borrow_record):包括借阅记录ID、用户ID、图书ID、借阅时间、归还时间等字段。 4. 管理员表(admin):包括管理员ID、管理员名称、密码等字段。 以下是JavaEE-图书管理系统数据库代码示例: ```sql -- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(50) NOT NULL, `email` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `author` varchar(50) NOT NULL, `publisher` varchar(50) NOT NULL, `isbn` varchar(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录表 CREATE TABLE `borrow_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `book_id` int(11) NOT NULL, `borrow_time` datetime NOT NULL, `return_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_user_id_idx` (`user_id`), KEY `fk_book_id_idx` (`book_id`), CONSTRAINT `fk_book_id` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 管理员表 CREATE TABLE `admin` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` -- 相关问题--: 1. 如何创建数据库表? 2. 如何在JavaEE中连接数据库? 3. 数据库中的什么是外键?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gremmie2003

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值