【多线程(五)】volatile关键字、原子性问题、AtomicInteger内存分析与源码分析、悲观锁和乐观锁

news2024/11/19 23:31:59

文章目录

  • 5.原子性
    • 5.1 volatile-问题
    • 2.2 volatile解决
    • 5.3 synchronized 解决
    • 5.4 原子性
    • 5.5 volatile关键字不能保证原子性
    • 5.6 原子性 AtomicInteger
    • 5.7 AtomicInteger-内存解析
    • 5.8 AtomicInteger-源码解析
    • 5.9 悲观锁和乐观锁
  • 小结

5.原子性

5.1 volatile-问题

  • 代码分析
package com.hcx.myvolatile;

public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();

    }
}
package com.hcx.myvolatile;

public class Money {
    public static   int  money = 100000;
}

package com.hcx.myvolatile;

public class MyThread1 extends Thread{
    @Override
    public void run() {
        while(Money.money == 100000){

        }

        System.out.println("结婚基金已经不是十万了");
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money = 90000;
    }
}

  • 运行结果
    在这里插入图片描述

    • 程序一直在线程1的While循环中跑,说明此时线程1认为共享数据money仍为100000,但是线程2在开启以后,抢到CPU的执行权,JVM调用run()方法,已经将共享数据修改为90000,此时线程1无法知道最新的数据。

2.2 volatile解决

以上案例出现的问题:
当线程2修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题。

  • 1.内存是唯一的,每一个线程都有自己的线程栈
  • 2.每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
  • 3.在线程中,每一次使用是从变量的副本中获取
    以上案例出现的原因:
    在开启线程1和线程2后,线程1先抢到CPU的使用权,将堆内存的数据拷贝到自己的变量副本中,然后从副本中获取共享数据,此时线程1中变量副本的值为100000,线程2后抢到CPU的执行权,然后JVM调用run()方法,将共享数据money的值改为90000,然后拷贝到自己的副本中,再从自己的副本中获取共享数据。所以线程1的数据为100000,线程2的数据为90000。造成了上述出现的情况。
    Volatile关键字强制线程每次在使用的时候,都会看一下共享区域最新的值。
    代码实现:使用volatile关键字解决
package com.hcx.myvolatile;

public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();

    }
}
package com.hcx.myvolatile;

public class Money {
    public static volatile   int  money = 100000;
}
package com.hcx.myvolatile;

public class MyThread1 extends Thread{
    @Override
    public void run() {
        while(Money.money == 100000){

        }

        System.out.println("结婚基金已经不是十万了");
    }
}
package com.hcx.myvolatile;

public class MyThread2 extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money = 90000;
    }
}

  • 运行结果
    在这里插入图片描述

5.3 synchronized 解决

  • synchronized解决:

    • 1.线程获得锁
    • 2.清空变量副本
    • 3.拷贝共享变量最新的值到变量副本中
    • 4.执行代码
    • 5.将修改后变量副本中的值赋值给共享数据
    • 6.释放锁
  • 代码实现

package com.hcx.myvolatile2;

import com.hcx.myvolatile.MyThread2;

public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();

    }
}
package com.hcx.myvolatile2;

public class Money {
    public static Object lock = new Object();
    public static   int  money = 100000;
}

package com.hcx.myvolatile2;

public class MyThread1 extends Thread{
    @Override
    public void run() {
        while(true){
            synchronized (com.hcx.myvolatile2.Money.lock){
                if(Money.money!=100000){
                    System.out.println("结婚基金已经不是十万了");
                    break;
                }
            }
        }
    }
}

package com.hcx.myvolatile2;

import com.hcx.myvolatile.Money;

public class MyThread2 extends Thread {
    @Override
    public void run() {
        synchronized (com.hcx.myvolatile2.Money.lock) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Money.money = 90000;
        }
    }
}

5.4 原子性

  • 概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
  • 代码实现
package com.hcx.threadatom;

public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread myAtomThread = new MyAtomThread();
        for (int i = 0;i < 100; i++) {
            new Thread(myAtomThread).start();
        }
    }
}
package com.hcx.threadatom;

public class MyAtomThread implements Runnable {
    private  volatile  int  count = 0;//送冰淇淋的数量
    @Override
    public void run() {
        for (int i = 0;i < 100;i++) {
            //1.从共享数据中读取数据到本线程中
            //2.修改本线程栈中变量副本的值
            //3.会把本线程栈中变量副本的值赋值给共享数据。
            count++;
            System.out.println("已经送了"+count +"个冰淇淋");
        }
    }
}

  • 运行结果
    在这里插入图片描述

  • 思考:100个线程执行100次,最后应该打印结果为10000,为什么运行结果是9999呢?

    • 假如现在有A、B两个线程,堆内存的共享数据此时为100,此时A线程抢夺到CPU的执行权,然后将共享数据100拷贝到自己的变量副本中,然后做count++,也就是将101赋值给了自己的变量副本,这个时候,CPU 的执行权被B线程抢走,现在A线程还没有将自己变量副本的值写回到共享数据中,所以B线程读取的值为共享数据的最新值,还为100,然后B线程执行跟A线程一样的操作,count++后,变量副本的值也为101,所以导致最后的运行结果为9999,而不是10000。
      代码总结:count++ 不是一个原子性操作,他在执行的过程中,有可能被其他线程打断。
  • 综上,我们可以得出volatile关键字

    • 只能保证线程每次在使用共享数据的时候是最新值。
    • 但是不能保证原子性。

5.5 volatile关键字不能保证原子性

  • 解决方案:我们可以给 count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以count++就变成了原子操作。
package com.hcx.threadatom2;

import com.hcx.threadatom.MyAtomThread;

public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread myAtomThread = new MyAtomThread();
        for (int i = 0;i < 100; i++) {
            new Thread(myAtomThread).start();
        }
    }
}
package com.hcx.threadatom2;

public class MyAtomThread implements Runnable {
    private  volatile  int  count = 0;//送冰淇淋的数量
    private Object lock = new Object();
    @Override
    public void run() {
        for (int i = 0;i < 100;i++) {
            //1.从共享数据中读取数据到本线程中
            //2.修改本线程栈中变量副本的值
            //3.会把本线程栈中变量副本的值赋值给共享数据。
            synchronized (lock) {
                count++;
                System.out.println("已经送了"+count +"个冰淇淋");
            }
        }
    }
}
  • 运行结果

在这里插入图片描述

5.6 原子性 AtomicInteger

  • 概述:Java 从JDK1.5开始提供了java.util.concurrent.atomic包(简Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:
    • AtomicBoolean: 原子更新布尔类型
    • AtomicInteger: 原子更新整型
    • AtomicLong: 原子更新长整型
  • 以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下 :
    在这里插入图片描述
  • 代码实现
package com.hcx.threadatom3;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomIntergerDemo1 {
    //public AtomicInteger(): 初始化一个默认值为0的原子型Integer
    //public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
    public static void main(String[] args) {
        AtomicInteger ac = new AtomicInteger();
        System.out.println(ac);

        AtomicInteger ac2 = new AtomicInteger(10);
        System.out.println(ac2);

    }
}

package com.hcx.threadatom3;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomIntergerDemo2 {
    //  int get():          获取值
    //  int getAndIncrement():      以原子方式将当前值加1,注意,这里返回的是自增前的值。
    //  int incrementAndGet():      以原子方式将当前值加1,注意,这里返回的是自增后的值。
    //  int addAndGet(int data):    以原子方式将参数与对象中的值相加,并返回结果。
    //  int getAndSet(int value):   以原子方式设置为newValue的值,并返回旧值。
    public static void main(String[] args) {
//        AtomicInteger ac1 = new AtomicInteger(10);
//        System.out.println(ac1.get());
//
//        AtomicInteger ac2 = new AtomicInteger(10);
//        int andIncrement = ac2.getAndIncrement();
//        System.out.println(andIncrement);
//        System.out.println(ac2.get());

//        AtomicInteger ac3 = new AtomicInteger(10);
//        int incrementAndGet = ac3.incrementAndGet();
//        System.out.println(incrementAndGet);

//        AtomicInteger ac4 = new AtomicInteger(10);
//        int i = ac4.addAndGet(20);
//        System.out.println(i);
//        System.out.println(ac4.get());

        AtomicInteger ac5 = new AtomicInteger(10);
        int andSet = ac5.getAndSet(20);
        System.out.println(andSet);
        System.out.println(ac5.get());
    }
}

5.7 AtomicInteger-内存解析

AtomicInteger原理:自旋锁+CAS算法
CAS算法
在这里插入图片描述
我们来解释一下,首先还是一个堆内存,堆内存中有共享数据值为100,然后我开启了两个线程,一个线程为A线程,另一个线程为B线程,现在A线程跟B线程想要进行的事情是要将共享数据里边的值进行自增,也就是我们最后要把共享数据的值变成102,共享数据100相当于CAS算法中的内存值,假设A线程先抢到CPU的执行权,它接下来需要将内存值100读到自己的线程栈里边,读过来以后存到自己的变量副本中,此时这个值为旧的预期值,现在线程A需要进行自增, 是不是应该将101覆盖给了变量副本,那么变量副本里边现在变成了101,这个时候变量副本里边的值就是CAS算法中的要修改的值,此时B线程抢到了CPU的执行权,它同样将100读到变量副本当中,那么B线程,也有一个旧的预期值,然后线程B也进行了自增,将101覆盖给了变量副本,此时,B线程中要修改的值也为101,现在两个线程的自增都做完了,现在需要将101写到共享数据里面了,假设A线程先写, 它发现A里边的旧的预期值==内存值,表示这个内存值没有被其他线程操作过,所以就将A线程的101写到共享数据修改成功,我们B线程也需要往共享区域写,它发现旧的预期值!=内存值,表示有可能共享数据已经被其他线程操作过了,所以这个时候它修改失败,它需要将现在最新的内存值101再次读到自己的变量副本中,这个时候线程B的旧的预期值就变成101了,然后进行自增,并将102覆盖给变量副本,102就是要修改的值,然后将102写到堆内存的共享数据中。它发现第二次旧的预期值=内存值,所以修改成功。

5.8 AtomicInteger-源码解析

  • 代码实现
package com.hcx.threadatom4;

import com.hcx.threadatom.MyAtomThread;

public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread myAtomThread = new MyAtomThread();
        for (int i = 0;i < 100; i++) {
            new Thread(myAtomThread).start();
        }
    }
}

package com.hcx.threadatom4;

import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomThread implements Runnable {
//    private  volatile  int  count = 0;//送冰淇淋的数量
//    private Object lock = new Object();
    AtomicInteger ac = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0;i < 100;i++) {
            //1.从共享数据中读取数据到本线程中
            //2.修改本线程栈中变量副本的值
            //3.会把本线程栈中变量副本的值赋值给共享数据。
//            synchronized (lock) {
//                count++;
            int count = ac.incrementAndGet();
            System.out.println("已经送了"+count +"个冰淇淋");
//            }
        }
    }
}
  • 运行结果
    在这里插入图片描述

  • 源码解析
    在这里插入图片描述
    在这里插入图片描述

5.9 悲观锁和乐观锁

  • synchronized和CAS的区别
    • 相同点:在多线程情况下,都可以保证共享数据的安全性
    • 不同点
      • synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都可能修改。所以在每次操作共享数据之前,都会上锁(悲观锁
      • cas 是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。如果别人修改过,那么我再次获取现在最新的值。如果别人没有修改过,那么我现在直接修改共享数据的值。(乐观锁

小结

本篇文章介绍了 volatile 关键字的作用,通过一个用 volatile 关键字解决不了的送冰淇淋问题引出原子性的概念,介绍了JDK1.5使用原子的方式更新整数类型-AtomicInteger,并对AtomicInteger进行了内存分析源码分析,最后还介绍了 synchronizedCAS的相同点与不同点,引出了悲观锁乐观锁的概念。
更新不易,希望大家多多支持!!在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/72116.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

五道LeetCode《中等难度》的单链表题

五道单链表中等难度题型1. 剑指 Offer II 021. 删除链表的倒数第 n 个结点第一种解法&#xff08;单指针&#xff09;&#xff1a;第二种解法(栈)&#xff1a;第三种解法&#xff08;双指针&#xff09;&#xff1a;2. 删除排序链表中的重复元素 II&#xff08;重点&#xff09…

利用VGG16网络模块进行迁移学习实现图像识别

​ ImageNet虽然带有”Net“&#xff0c;但他不是一种深度神经网络模型&#xff0c;它是个数据集&#xff0c;斯坦福大学教授李飞飞带头建立&#xff0c;是目前图像分类、检测、定位的最常用数据集之一。该数据集含大量数据1500万图片&#xff0c;2.2万类别&#xff0c;真彩图&…

PPT免费放送|Zabbix峰会结束了?还有件儿事!

精彩的Zabbix峰会成功举办&#xff0c;这并不意味着学习交流结束&#xff0c;还有件儿事——17份PPT免费获取&#xff0c;网盘见文末。干货满满细细品味。也欢迎你留言评价&#xff01; 值得一提的是&#xff1a;峰会中有理有据说明&#xff1a;Zabbix支持信创。开源免费的Zab…

IBDP学生如何申请中国香港的大学?

作为世界上最具竞争力的城市之一&#xff0c;香港拥有一些亚洲乃至世界上最好的大学。当然&#xff0c;这也使得香港成为内地学生以及国际留学生最喜爱的留学目的地之一。中国香港的教育在很大程度上是模仿英国的教育体系&#xff0c;但本科课程通常是英国和美国体系的混合体。…

Android 中的广播机制

一、Android广播概念&#xff1a; 在Android中&#xff0c;有一些操作完成以后&#xff0c;会发送广播&#xff0c;Android系统内部产生这些事件后广播这些事件&#xff0c;至于广播接收对象是否关心这些事件&#xff0c;以及它们如何处理这些事件&#xff0c;都由广播接收对象…

(附源码)ssm日语学习系统 毕业设计 271621

基于ssm日语学习系统 摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对日语学习等问题&#…

MySQL——表的内容增删查改

文章目录表的增删改查一、Create1、单行全列插入2、多行数据指定列插入3、插入否则更新4、替换二、Retrieve&#x1f60a;(重点)2.1 select 列2.1.1 全列查询2.1.2 指定列查询2.1.3 查询字段为表达式2.1.4 为查询结果指定别名2.1.5 结果去重2.2 where查询2.3 结果排序2.4 筛选分…

【VC7升级VC8】将vCenter Server 7.X 升级为 vCenter Server 8 (下)—— 升级步骤说明

目录前文说明3. 第一阶段升级&#xff08;1&#xff09;点击【升级】&#xff08;2&#xff09;升级介绍&#xff08;3&#xff09;最终中用户许可协议&#xff08;4&#xff09;连接到源设备&#xff08;5&#xff09;VC7与ESXi 证书警告&#xff08;6&#xff09;vCenter Ser…

【Vue】各种loader的基本配置与使用

✍️ 作者简介: 前端新手学习中。 &#x1f482; 作者主页: 作者主页查看更多前端教学 &#x1f393; 专栏分享&#xff1a;css重难点教学 Node.js教学 从头开始学习 ajax学习 目录webpack中的loader  loader概述  打包处理css文件  打包处理less文件  打包处理图片  …

LWIP框架

目录 协议栈分层思想 1. 网络接口层 2. 网络层 3. 传输层 4. 应用层 进程模型 单进程模型 协议栈编程接口 1、Raw/Callback API 2、Netconn API 3、Socket API 协议栈分层思想 TCP/IP协议完整的包含了一系列构成互联网基础的网络协议&#xff0c;TCP/IP协议的开发出…

HTTP Digest Authentication 使用心得

简介 浏览器弹出这个原生的对话框&#xff0c;想必大家都不陌生&#xff0c;就是 HTTP Baisc 认证的机制。 这是浏览器自带的&#xff0c;遵循 RFC2617/7617 协议。但必须指出的是&#xff0c;遇到这界面&#xff0c;不一定是 Basic Authentication&#xff0c;也可能是 Dige…

墨门云终端行为趋势报表,泄密风险提前预警

事件响应滞后&#xff0c;事后再补救&#xff0c;为时晚矣&#xff0c;据IBM的数据泄露成本报告显示&#xff0c;加强风险监测可更快发现数据泄露行为&#xff0c;有效降低企业的数据泄露成本&#xff0c;可见建立完善的风险预警响应机制&#xff0c;可以避免更大的损失&#x…

5G无线技术基础自学系列 | NSA组网场景下移动性管理

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 NSA组网场景下移动性管理涉及的相关概念…

js操作二进制数据

使用ArrayBuffer对象保存二进制数据&#xff0c;使用TypedArray和DataView 视图来读写数据。 ArrayBuffer代码内存中的一段数据 const buff new ArrayBuffer(4)这样就创建了一个4(byte)字节的长度的内存判断&#xff0c;初始值都为0 注&#xff1a;一般中文占2个字节&#xff…

葡聚糖修饰Hrps共价三聚肽|葡聚糖修饰CdSe量子点

葡聚糖修饰Hrps共价三聚肽|葡聚糖修饰CdSe量子点 葡聚糖修饰Hrps共价三聚肽 中文名称&#xff1a;葡聚糖修饰Hrps共价三聚肽 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体 包装&#xff1a;瓶装/袋装 溶解性&am…

爆火的 ChatGPT 会让客服岗位消失吗?

近日&#xff0c;由 OpenAI 推出的 ChatGPT 在全球互联网爆火。具体有多火呢&#xff1f;根据 OpenAI 的 CEO Sam Altman 的说法&#xff1a;上周三才上线的 ChatGPT&#xff0c;短短几天&#xff0c;用户数已突破 100 万大关。 那么&#xff0c;ChatGPT 是什么呢&#xff1f;…

无线充电智能车的制作

本文素材来源于宁夏大学 作者&#xff1a;白二曹、王瑞、穆琴、王童兵 指导老师&#xff1a;康彩 一、项目简介 1.功能介绍 无线充电智能车由无线充电、自动控制、红外遥控、网页显示四部分组成。 &#xff08;1&#xff09;流程描述 用户端浏览器访问http://127.0.0.1页面…

Cy5.5 Tyramide,Cyanine5.5 Tyramide,花青素Cy5.5 酪酰胺化学试剂供应

一&#xff1a;产品描述 1、名称 英文&#xff1a;Cy5.5 Tyramide&#xff0c;Cyanine5.5 Tyramide 中文&#xff1a;花青素Cy5.5 酪酰胺 2、CAS编号&#xff1a;N/A 3、所属分类&#xff1a;Cyanine 4、分子量&#xff1a;738.4 5、分子式&#xff1a;C48H52CIN3O2 6、…

如何用DOS命令设置ip地址及DNS

用DOS命令设置ip地址及DNS 设置/修改IP地址&#xff0c;子网掩码&#xff0c;网关的格式&#xff1a; netsh interface ip set address "本地连接" static 10.25.35.35 255.255.255.0 10.25.35.7 auto[more] 命令的意思是将“本地连接” ip地址设置成 10.25.35.3…

SD NAND 的 SDIO在STM32上的应用详解(下篇)

七.SDIO外设结构体 其实前面关于SDIO寄存器的讲解已经比较详细了&#xff0c;这里再借助于关于SDIO结构体再进行总结一遍。 标准库函数对 SDIO 外设建立了三个初始化结构体&#xff0c;分别为 SDIO 初始化结构体SDIO_InitTypeDef、SDIO 命令初始化结构体 SDIO_CmdInitTypeDef…