当前位置: 首页 > news >正文

做网站刷流量挣钱吗大学生实训网站建设心得

做网站刷流量挣钱吗,大学生实训网站建设心得,智慧团建网站登陆平台,品牌推广语目录 1.内存可见性问题 1.1 一段非线程安全的代码 1.2 分析原因 2.volatile 2.1 处理机制 2.2 不保证原子性 2.3 (Java内存模型)JMM 3.指令重排序问题 4.小结 在前面学习线程安全问题中,我们说内存可见性问题和指令重排序问题会造成…

目录

1.内存可见性问题

1.1 一段非线程安全的代码

1.2 分析原因

2.volatile

2.1 处理机制

2.2 不保证原子性

2.3 (Java内存模型)JMM

3.指令重排序问题

4.小结


在前面学习线程安全问题中,我们说内存可见性问题和指令重排序问题会造成线程安全问题,这两个问题是怎么造成线程安全问题的呢?这是我们这期要探寻的答案。


1.内存可见性问题

1.1 一段非线程安全的代码

public class Main {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("线程1开始");while (flag == 0) {//.........}System.out.println("线程1结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入flag的值:");flag = scanner.nextInt();});t1.start();t2.start();}
}

代码解读:在线程t1中,有一个永远为真的 while 循环,现在希望通过在线程t2中修改 flag 达到值来结束线程t1。对于多线程来讲,这两个线程同时启动,那么我们通过线程t2对 flag 进行修改,应该是可以结束 t1 这个线程的。但是,当我们运行后发现,线程t1并不会结束,而是继续死循环:

很明显,这个代码存在了bug,存在线程安全问题。像这种一个线程读取,另一个线程修改,当修改线程对数据进行修改时,读取线程并没有读取到的问题,就叫“内存可见问题”

1.2 分析原因

在前面的学习,我们说遇到线程安全问题,可以给期望运行的逻辑代码加锁,但是在这里,如果我们尝试加锁,那是不行的。可以简单地分析:我们加锁的目的就是让先抢到锁的线程先执行,其它线程阻塞等待,就这个代码而言,如果是线程t2先抢到锁,自然线程t1是可以正常结束,而如果是线程t1先抢到锁,一直进入 while 循环中,而循环条件永远满足,自然就不会释放锁,会一直永远循环,即便是想让t2线程先执行,对t1线程用sleep,这种情况就属于t2先抢到锁,但这种做法又会影响程序的效率,我们使用多线程的目的就是希望得到更高的效率,这种做法就不再符合我们的初衷。这种概率的可能不是我们想要的,打败妖怪,只需要从很多种可能中找到一种可能就可以击败,但对多线程来说,任何一种可能都不能放过。

在计算机中,程序运行是快速运转的,可以达到每秒几十亿次,这取决于CPU,不过就现代计算机而言,也不会相差多少。我们人类的几秒钟,在计算机看来就是“沧海桑田”。因此,在Java中,JVM优化了我们书写的代码。是的,没有听错,是优化。优化了怎么还出问题呢?是如何进行优化的呢?

在上面的代码中,JVM会对我们的代码,在保证逻辑不变的前提下进行修改,使得程序效率更高,这就是所谓的优化。但是,虽然是优化,确保保证了代码的逻辑不变,而在多线程中,这种编译器的判断就引来了比较严重的失误。

比如:有两个快递员负责一个驿站工作,有一天王先生买了一个包裹,王先生就问快递员1“我的包裹到了吗?”,快递员1说“没有。”,并且在自己的备忘录里记录王先生的包裹flag == 0。几天后呢,是快递员2把王先生的包裹送上货架,快递员1没来得及更新自己的备忘录。王先生又打电话问快递员1“我的包裹到了吗?”,快递员1马上查看自己备忘录,还是 flag == 0,就是“您的快递还没到”。本来快递实际已经送到,但是王先生迟迟没有收到快递,陷入了等待。

所以,对于计算机来说,每秒可以达到几十亿次,而执行这么多次(执行的次数取决于我们在多久之后输入,如果十几秒才输入,那就更不得了了),flag 一直等于0,于是JVM就认为,既然都是一样的结果,干脆把它看成一个固定值,所以线程t2去修改的时候,线程t1得到的值依旧不会发生改变。

在这里,JMM(Java内存模型)是这么描述的每个线程有自己的工作内存,同时这些线程共享一个主内存,当一个线程循环进行读取变量操作的时候,就把主内存的数据拷贝到该线程的工作内存中,后续另一个线程进行修改操作时,也是修改自己工作内存的数据,再拷贝到主内存中,由于第一个线程仍然在等待自己的工作内存,因此感受不到主内存的变化。

所以,为了解决这个问题,引入了关键字 volatile

2.volatile

2.1 处理机制

volatile 是 Java 提供的一种轻量级同步机制,用于确保多线程环境下变量的可见性和有序性,用于修饰变量,

在计算机中,每个线程有自己的一个工作内存(理解为每个人上班都有一个工位),这些线程不论是读操作还是写操作,都要从主内存中获取数据。所以,上述的问题就是:线程t1一直决定flag不会变,就干脆把这个flag“据为己有”,每次需要读的时候就读自己的工作内存数据,而不会从主内存中获取数据。使用关键字 volatile ,就可以确保这些线程在读写写时,都会从主内存获取一个最新的数据,然后再读或者写,如果是写,还会把写之后的数据更新到主内存。

所以,对上述代码的 flag 使用volatile 修饰,来看是否符合预期结果:

public class Main {private volatile static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {System.out.println("线程1开始");while (flag == 0) {//.........}System.out.println("线程1结束");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("请输入flag的值:");flag = scanner.nextInt();});t1.start();t2.start();}
}

输出结果:

当输入一个非零的 flag 值时,程序运行结束,说明符合预期结果。

flag 被 volatile 修饰后,使得 flag 是可见的,当线程想要对 flag 进行读 / 写时,都会从主内存中获取最新数据。

volatile机制:

  1. 对于普通变量,线程针对写操作时,修改可能会先写入自己的工作内存中,而不会立即同步到主内存中,针对读操作时可能从线程工作内存中读取旧值
  2. 对于volatile变量,线程针对写操作时会立即同步到主内存中,读的操作会从主内存中读取最新值

实现原理:JMM

  • 写一个volatile变量时,JMM把该线程对应的工作内存中的共享变量刷新到主内存中
  • 读一个volatile变量时,JMM把该线程对应的工作内存中的数据设为无效,从主内存读取最新值

2.2 不保证原子性

在前期解决线程安全问题时,我们用到关键字 synchronized ,这是针对原子性问题。那么,volatile 关键字是否也可以保证原子行呢?

先来回顾代码:

public class Main {private volatile static int count = 0;public static void main(String[] args) throws InterruptedException {//线程1Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//线程2Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});//开启线程thread1.start();thread2.start();//等待线程thread1.join();thread2.join();//两个线程各自增 5000 次,期望输出 count = 10000System.out.println("count = " + count);}
}

这段代码两个线程,各自增5000次,期望结果 count = 10000。在定义时,count 是被关键字 volatile 修饰的。来看结果:

显然这个结果,是不符合预期结果的,因此 volatile 不保证原子性

2.3 (Java内存模型)JMM

JMM 是 Java 虚拟机规范中定义的一种模型,目的是屏蔽掉各种硬件和操作系统的内存访问异常,以实现让Java程序可以在任何平台下都能达到一致的并发效果。

这个工作内存其实并不难理解,就是CPU的寄存器。

核心概念:

  • 线程之间共享的变量存放在主内存中
  • 每一个线程都有自己的工作内存
  • 当线程要修改一个共享变量的时候,会先从修改工作内存的副本(拷贝),再同步到主内存
  • 当线程要读取一个共享变量的时候,会从主内存中把这个共享变量拷贝到工作内存中,再从工作内存读取数据

因为每个线程都有自己的工作内存,这些工作内存中的内容相当于一个共享变量的副本,因此在修改一个线程的工作内存中的值,另一个线程可能来不及发生变化。

 导致线程不安全的过程

(1)初始情况,两个线程的工作内存中的数据一样(线程1和线程2都从主内存拷贝数据 a = 5)

(2)如果线程1发生修改,但是可能没有及时同步到主内存中,对应的线程2得到的又是主内存中的数据

    经过这两拨操作,就导致了线程出现安全问题。文章开头的代码就是因为这种导致,所以使用关键字 volatile 修饰变量后,线程1修改操作时及时地让更新的内容同步到主内存中,线程2读取时也会从主内存从读取最新值。

    3.指令重排序问题

    关于指令重排序问题,先来理解这个术语。拆开来看:指令就相当于一个要执行的任务,比如生活中我们要去拿快递就是一个指令,重排序就是调整指令的执行顺序。

    假设你要去超时买牙膏、酱油、零食、洗衣粉,这几样东西在超市布局如下:

    现在让你最短时间内就进这个超市买完你的需求品,任何出超市,你肯定不会按清单牙膏、酱油、零食、洗衣粉购买,而是先买零食、洗衣粉、牙膏,最好买酱油就出超市,这就是指令重排序。

    但在计算机中,线程指令不能重排序,如果指令重排序就会造成线程安全问题

    分析:

    指令重排序是指编译器在逻辑不变的情况下,调整代码的执行顺序,以达到提升性能的效果但在多线程环境下,以读操作和写操作为例,指令重排序问题是在读操作时可能会把读之前的操作放在读之后,写操作时可能把写之后的操作放在写之前。

    来看代码:

    public class Main {private static int x = 0;private static int y = 0;private static int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {for (int i = 0; ; i++) {x = y = a = b = 0;//每次循环后都初始化为0Thread t1 = new Thread(() -> {a = 1;  // 操作1x = b;   // 操作2});Thread t2 = new Thread(() -> {b = 1;  // 操作3y = a;   // 操作4});t1.start();t2.start();t1.join();t2.join();if (x == 0 && y == 0) {System.out.printf("第%d次循环后出现重排序 (x=%d, y=%d)\n", i, x, y);break;}}}
    }

    先看结束条件:x == 0 && y == 0,再看前面的代码,在给 x 和 y 赋值时,都是先执行了 a = 1, b = 1,这样看来,结束的条件应该不会成立。程序是一直死循环,还是会结束?来看运行结果:

    结果现实第 4898 次循环后程序就退出了(当然这个循环每次结果会不一样,因为多线程的调度是随机的、不可抢占的)。为什么会出现 x = 0, y = 0?因为在某时候的执行顺序可能是:

    线程1:x = b (读到b=0)
    线程2:y = a (读到a=0)
    线程1:a = 1
    线程2:b = 1
    

    这种情况下,a 和 b 还没来得及修改,所以这是程序退出的原因。

    指令重排序问题怎么解决呢?

    在文章前面就已经给出答案,也就是使用关键字 volatile,对变量用 volatile 修饰即可。

    关键字 volatile 的两个功能就是解决这两个问题:内存可见性问题和指令重排序问题。

    两个问题的处理是差不多的,它们的解决方案都是依赖JMM指定的内存屏障规则,内存可见性问题是确保主内存访问,指令重新排序问题确保指令有序执行。

    4.小结

    本期主要介绍线程安全——内存可见性和指令重排序问题的解决方法,关键字 volatile 主要用于解决内存可见性和指令重排序问题,但不保证原子性。处理机制是

    1. 对于普通变量,线程针对写操作时,修改可能会先写入自己的工作内存中,而不会立即同步到主内存中,针对读操作时可能从线程工作内存中读取旧值
    2. 对于volatile变量,线程针对写操作时会立即同步到主内存中,读的操作会从主内存中读取最新值

    在前面学习线程状态的转换时,我们看到等待状态有个方法:wait()。wait()、join()、sleep() 它们有什么区别呢?欲知后事如何,且听下回分解!

    http://www.dtcms.com/a/480018.html

    相关文章:

  • 哪些网站可以做招商广告crm管理系统定制
  • 东莞网站seo公司平台网站开发简报
  • 云南建设工程网站发稿媒体平台
  • 网站设计需求表西北电力建设甘肃工程公司网站
  • windows10如何修改mac地址
  • ◆comfyUI教程◆第2章05节 comfyui的控制约束-controlnet基础与应用
  • 许昌做网站九零后网站关键词百度自然排名优化
  • 百度注册网站简单网页
  • 美容营销型网站中国建筑网官网查询阮国方
  • 天津网站制作免费高清logo在线
  • 网站系统重要性做设计找图片的网站有哪些
  • 台州企业网站的建设厦门公司注册名称查询系统
  • 东阳畅销自适应网站建设婚庆策划公司招聘
  • 扁平式网站seo 内链哪里有网络课程平台网站_就是帮老师建设一个教学的网站
  • 简易制作网站wordpress 随机浏览量
  • 网站后台添加文章后怎么不显示百度推广一个月费用
  • 计算机类本科毕业设计论文大纲设计及论文撰写指南
  • 网站建设学习培训建设网站招标
  • qq群推广网站运营的工资一般是多少
  • Go语言Slice的一道骚题
  • 做网站如何语音泉州哪个公司网站做的好
  • 校园电商平台网站建设自己怎么个人网站
  • 宣城网站seo诊断河北新增9个中风险地区
  • 懒人手机网站模板电子商务开发公司
  • 什么是偏自相关函数PACF
  • 网站建设费应计入什么科目手机礼品网站模板
  • 潍坊快速网站排名上海外贸财经大学
  • 杭州网站设计开发做网站软件是什么下载
  • 模板网站建设公司wordpress 干净主题
  • JavaSPI机制