共享模型之无锁

news2024/11/30 7:35:02

目录

无锁实现线程安全

无锁与synchronized效率对比

原子整数

原子引用类型

ABA问题

原子数组

字段更新器

原子累加器LongAdder

LongAdder源码分析

Unsafe

cas修改对象属性值


案例

对于银行取钱来说,需要保证线程安全,一个1w的账户由1k个线程取10块,来保证取完后为0

import java.util.ArrayList;

public class demo7 {
    public static void main(String[] args) {
        Amount unsafeAmount = new UnsafeAmount(10000);
        Amount.demo(unsafeAmount);
    }
}

class UnsafeAmount implements Amount {
    private Integer money;

    public UnsafeAmount(Integer money) {
        this.money = money;
    }

    @Override
    public Integer getMoney() {
        return money;
    }

    @Override
    public void withdraw(Integer money) {
        this.money -= money;
    }
}

interface Amount {
    public Integer getMoney();

    public void withdraw(Integer money);

    static void demo(Amount amount) {
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                amount.withdraw(10);
            }));
        }

        long start = System.nanoTime();
        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(amount.getMoney() + " 时间:" + (end - start) / 100000);
    }
}

170 时间:444

可以通过对withdraw()加锁的方式来保证线程安全,也可以使用不加锁的方式保证线程安全

无锁实现线程安全

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class demo7 {
    public static void main(String[] args) {
        Amount safeAmount = new SafeAmount(10000);
        Amount.demo(safeAmount);

        Amount amountCas = new AmountCas(10000);
        Amount.demo(amountCas);
    }
}

//使用不加锁的方式实现线程安全
class AmountCas implements Amount {

    public AtomicInteger atomicInteger;

    public AmountCas(Integer money) {
        this.atomicInteger = new AtomicInteger(money);
    }

    @Override
    public Integer getMoney() {
        return atomicInteger.get();
    }

    @Override
    public void withdraw(Integer money) {
        while (true) {
            int prev = atomicInteger.get();
            int next = prev - money;
            if (atomicInteger.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}


class SafeAmount implements Amount {
    Integer money;

    public SafeAmount(Integer money) {
        this.money = money;
    }

    @Override
    public Integer getMoney() {
        return money;
    }

    @Override
    public void withdraw(Integer money) {
        synchronized (this) {
            this.money -= money;
        }
    }
}

interface Amount {
    public Integer getMoney();

    public void withdraw(Integer money);

    static void demo(Amount amount) {
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                amount.withdraw(10);
            }));
        }

        long start = System.nanoTime();
        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(amount.getMoney() + " 时间:" + (end - start) / 100000);
    }
}

0 时间:485

0 时间:390

可以看出来,使用AtomicInteger比使用synchronized更加快速。能够实现线程安全主要依靠compareAndSet()方法。首先它是一个原子操作不用担心线程上下文切换问题,它会对比两个参数中的前者与atomicInteger中的值是否相同,如果相同,返回flase,如果不同,则会修改atomicInteger中的值为后者。

CAS必须使用volatile才可以实现,因为我们需要知道最新的数据,来保证compareAndSet()的准确性。

无锁与synchronized效率对比

无锁之所以比synchronized更快,是因为无锁状态下是死循环,而synchronized是线程上下文切换,相应的,无锁状态下更依赖CPU的性能。没有分到CPU时间片时,线程会进入可运行状态,也会进入线程上下文切换,但比synchronized切换次数更少。

原子整数

是JUC包下的方法。包括了AtomicBoolean、Atomicinteger、AtomicLong。

都是通过CAS的方式实现线程安全。

AtomicInteger atomic = new AtomicInteger();
atomic.incrementAndGet();//自增并且返回
atomic.getAndIncrement();//取值并自增

atomic.getAndAdd(value);//取值并增加value

atomic.updateAndGet(value -> value*2);//lambda表达式自定义选择运算方式并返回。

原子引用类型

AtomicReference、AtomicStampedReference

对于银行取钱案例也可以使用AtomicReference来保证线程安全。

ABA问题

public class demo8 {
    static AtomicReference<String> str = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        String prev = str.get();
        Thread.sleep(1000);
        System.out.println(str.compareAndSet(prev,"C"));
    }
}

将A改为C是没有问题的,但是如果其他线程将A修改为B后再修改回来呢?是否还是可以将A修改为C呢?

public class demo8 {
    static AtomicReference<String> str = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        String prev = str.get();
        ABA();
        Thread.sleep(1000);

        System.out.println(str.compareAndSet(prev,"C"));
    }

    public static void ABA(){
        new Thread(()->{
            String prev = str.get();
            str.compareAndSet(prev,"B");
        }).start();

        new Thread(()->{
            String prev = str.get();
            str.compareAndSet(prev,"A");
        }).start();

    }
}

true

答案是也可以修改,因此我们可以知道,主线程是无法感知其他线程对str变量的修改,只要执行compareAndSet()方法时,prev的值与当前的值相同就可以完成修改。

如果要实现其他线程对其修改值后,我们需要主线修改失败,则需要使用AtomicStampedRefernece类,除了要制定属性值外还要指定版本号

public class demo8 {
    static AtomicStampedReference<String> str = new AtomicStampedReference<>("A",0);
    public static void main(String[] args) throws InterruptedException {
        String prev = str.getReference();//获取值
        int stamp = str.getStamp();//获取版本号
        System.out.println("初始版本号:"+stamp);
        ABA();
        Thread.sleep(1000);

        System.out.println(str.compareAndSet(prev,"C",stamp,stamp+1));
    }

    public static void ABA(){
        new Thread(()->{
            String prev = str.getReference();
            int stamp = str.getStamp();
            System.out.println("修改A为B的版本号:"+stamp);
            str.compareAndSet(prev,"B",stamp,stamp+1);
        }).start();

        new Thread(()->{
            String prev = str.getReference();
            int stamp = str.getStamp();
            System.out.println("修改B为A的版本号:"+stamp);
            str.compareAndSet(prev,"A",stamp,stamp+1);
        }).start();

    }
}

初始版本号:0

修改A为B的版本号:0

修改B为A的版本号:1

false

AtomicStampedReference是追踪更改过几次,如果是只需要知道更改过没有,可以使用AtomicMarkableReference来实现

public class demo9 {
    static AtomicMarkableReference<Bag> atomic = new AtomicMarkableReference(new Bag("装满了垃圾"),true);
    public static void main(String[] args) throws InterruptedException {
        //如果装满了垃圾就更换垃圾袋,如果没有就不更换
        Bag prev = atomic.getReference();

        new Thread(()->{
            System.out.println("修改垃圾袋属性");
            prev.setMsg("空垃圾袋");
            atomic.compareAndSet(prev,prev,true,false);
        }).start();
        Thread.sleep(1000);
        boolean result = atomic.compareAndSet(prev, new Bag("空垃圾袋"), true, false);
        System.out.println("是否更换了垃圾袋:"+result);
    }
}

修改垃圾袋属性

是否更换了垃圾袋:false

原子数组

并非针对数组对象,而是针对数组内的数据变化

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

下面案例是创建一个长度为10的数组,需要十个线程向数组中每个位置进行2000字自增。

public class demo10 {
    public static void main(String[] args) {
        demo(
                ()->new int[10],
                (array)->array.length,
                (array,index)->array[index]++,
                (array)-> System.out.println(Arrays.toString(array))
        );

    }

    /**
    * Supplier 提供者 使用方法 ()->new Class		无参数一个结果。作用是提供一个对象
    * Function 函数   使用方法 (array)->array.方法	一个参数一个结果
    * Biconsumer 消费者 使用方法 (参数一,参数二)->具体操作	两个参数无结果
    * Consumer 消费者 使用方法 (参数一)->具体操作		一个参数无结果
    **/
    private static <T> void demo(Supplier<T> arraySupplier,
                                 Function<T, Integer> function,
                                 BiConsumer<T, Integer> consumer,
                                 Consumer<T> printConsumer) {
        ArrayList<Thread> threads = new ArrayList<>();
        T t = arraySupplier.get();
        Integer len = function.apply(t);
        for (int i = 0; i < len; i++) {
            threads.add(new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    consumer.accept(t,j%len);
                }
            }));
        }
        threads.forEach(Thread::start);
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(t);
    }
}

[7800, 7859, 7738, 7749, 7864, 7802, 7858, 7845, 7831, 7886]

很显然,并没有完成原本设想内容。因此我们可以使用AtomicIntegerArray对数组内容进行操作。

public class demo10 {
    public static void main(String[] args) {
        demo(
                ()->new AtomicIntegerArray(10),
                (array)->array.length(),
                (array,index)->array.getAndIncrement(index),
                (array)-> System.out.println(array)
        );

    }

    private static <T> void demo(Supplier<T> arraySupplier,
                                 Function<T, Integer> function,
                                 BiConsumer<T, Integer> consumer,
                                 Consumer<T> printConsumer) {
        ArrayList<Thread> threads = new ArrayList<>();
        T t = arraySupplier.get();
        Integer len = function.apply(t);
        for (int i = 0; i < len; i++) {
            threads.add(new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    consumer.accept(t,j%len);
                }
            }));
        }
        threads.forEach(Thread::start);
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(t);
    }
}	

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

针对对象中的属性修改

AtomicIntegerFieldUpdate、AtomicLongFieldUpdate、AtomicReferenceFieldUpdate

public class demo11 {
    public static void main(String[] args) {
        Student student = new Student();
        //三个参数分别是字段所在的类,被修改字段的类类型,以及被修改字段名称
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater
                .newUpdater(Student.class, String.class, "name");

        System.out.println(updater.compareAndSet(student, null, "张三"));
        System.out.println(student);
    }

}
class Student{
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

注意:被修改的字段要搭配volatile使用。

原子累加器LongAdder

比AtomicInterger中的累加效率更好

下面的案例是4个线程每个线程自增10000次,分别用AtomicInteger与LongAddder实现,观察运行时间。

public class demo12 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
                demo(
                        () -> new AtomicInteger(0),
                        (atomic) -> atomic.getAndIncrement()
                );
        }

        for (int i = 0; i < 5; i++) {
                demo(
                        ()->new LongAdder(),
                        (adder)->adder.increment()
                );
        }
    }

    private static <T> void demo(Supplier<T> supplier,
                                 Consumer<T> consumer) {
        T t = supplier.get();
        long start = System.nanoTime();
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    consumer.accept(t);
                }
            }));
        }

        threads.forEach(Thread::start);
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(t+" 时间:"+(end-start)/100_000);
    }
}

40000 时间:27

40000 时间:15

40000 时间:19

40000 时间:19

40000 时间:13

40000 时间:29

40000 时间:17

40000 时间:2

40000 时间:2

40000 时间:7

可以看出来LongAdder的耗时比AtomicInteger更短

LongAdder源码分析

LongAdder之所以比AtomicInteger更快,是因为LongAdder可以开辟新的单元进行自增操作,大概就是AtomicInteger在while(true)中对一个value进行自增,但是LongAdder可以存在多个value进行自增,最后将多个value综合相加就是自增结果,减少了重试次数。

LongAdder中存在几个属性值

单元格表。非空时,大小为2的幂。

transient volatile Cell[] cells;

基本值,主要在没有争用时使用,但也用作表初始化竞争期间的回退。通过CAS更新。

transient volatile long base;

调整大小和或创建单元格时使用的自旋锁(通过CAS锁定)。为1时表示加锁

transient volatile int cellsBusy;

首先进行单元格判断,是否开辟了新的单元格(如果没有竞争即只有一个线程时,是不会开辟单元格的)如果为null则进行caseBase操作(实际上就是compareAndSet)。如果交换失败,说明是多线程操作。第二个if体内主要判断当前线程是否创建了cell如果创建了就对cell中的value值进行自加操作,如果没有创建则进入longAccumulate()方法进行具体判断

for(;;)内的三个if块对应逻辑分别如下图所示。

for(;;)中三个if分别代表着已经创建有cell、没有创建cell并且没有添加cas锁,对其进行加锁、加锁失败对base进行自加操作。

Unsafe

Unsafe提供非常底层的内存、线程操作。Unsafe对象不能通过创建获取,只能通过反射获取

public class test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //通过反射拿到unsafe对象
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        System.out.println(unsafe);
    }
}

cas修改对象属性值

public class demo13 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        //获取对象字段偏移量
        long id = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

        Teacher teacher = new Teacher();

        unsafe.compareAndSwapInt(teacher,id,0,1);
        unsafe.compareAndSwapObject(teacher,name,null,"张三");

        System.out.println(teacher);
    }
}


class Teacher{
    volatile int id;
    volatile String name;

    @Override
    public String toString() {
        return "Teacher{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Teacher{id=1, name='张三'}

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

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

相关文章

GEE 22:基于GEE实现物种分布模型(更新中。。。。。。)

物种分布模型 1. 数据点准备1.1 数据加载1.2 去除指定距离内的重复点1.3 定义研究区范围1.4 选择预测因子 1. 数据点准备 1.1 数据加载 首先需要将CSV文件导入到GEE平台中&#xff0c;同样也可以导入shp格式文件。 // 1.Loading and cleaning your species data *************…

【KubeSphere】基于AWS在 Linux 上以 All-in-One 模式安装 KubeSphere

文章目录 一、实验配置说明二、实验准备工作1.确认系统版本2. 修改网络DNS3. 关闭SELINUX4. 关闭防火墙 三、实验依赖项安装四、下载 KubeKey五、一键化安装部署六、验证安装结果七、登录KubeSphere管理控制台八、参考链接 一、实验配置说明 本实验基于AWS启动一台新实例&…

@Async注解的坑,小心

背景 前段时间&#xff0c;一个同事小姐姐跟我说她的项目起不来了&#xff0c;让我帮忙看一下&#xff0c;本着助人为乐的精神&#xff0c;这个忙肯定要去帮。 于是&#xff0c;我在她的控制台发现了如下的异常信息&#xff1a; Exception in thread "main" org.s…

信创之国产浪潮电脑+统信UOS操作系统体验7:VSCode任务tasks.json的问题匹配器problemMatcher详解

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、引言 最近在国产操作系统上使用Visual Studio Code的任务配置&#xff0c;发现tasks下的问题匹配器problemMatcher公开资料很少或很简单&#xff0c;直接在某度上通过problemMatcher搜索基本上没有精确…

从代码执行,看单片机内存的分配

1、单片机执行指令过程详解 单片机执行程序的过程&#xff0c;实际上就是执行我们所编制程序的过程。即逐条指令的过程。计算机每执行一条指令都可分为三个阶段进行&#xff0c;即取指令--分析指令--执行指令。 取指令的任务是&#xff1a;根据程序计数器PC中的值从程序存储器读…

粒子群算法Particle Swarm Optimization (PSO)的定义,应用优点和缺点的总结!!

文章目录 前言一、粒子群算法的定义二、粒子群算法的应用三、粒子群算法的优点四、粒子群算法的缺点&#xff1a;粒子群算法的总结 前言 粒子群算法是一种基于群体协作的随机搜索算法&#xff0c;通过模拟鸟群觅食行为而发展起来。该算法最初是由Eberhart博士和Kennedy博士于1…

由于找不到msvcp120.dll无法继续执行代码是什么原因怎么修复

今天我想和大家分享的是关于“msvcp120.dll丢失的解决方法”。或许有些同学在平时使用电脑的过程中会遇到这个问题&#xff0c;但是并不知道该如何解决。那么&#xff0c;接下来我将从三个方面为大家介绍&#xff1a;msvcp120.dll丢失的原因、msvcp120.dll是什么以及msvcp120.d…

小程序如何进行版本升级

小程序版本升级是非常重要的&#xff0c;它可以帮助商家及时更新功能、修复bug&#xff0c;提升用户体验&#xff0c;增加小程序的竞争力。那么&#xff0c;商家怎么进行小程序版本升级呢&#xff1f;下面具体介绍。 在小程序管理员后台->版本设置处&#xff0c;会显示是否…

基于springboot+vue的学生宿舍管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

NEFU离散数学实验PBL

1.青蛙的约会 Description 两只青蛙在网上相识了&#xff0c;它们聊得很开心&#xff0c;于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上&#xff0c;于是它们约定各自朝西跳&#xff0c;直到碰面为止。可是它们出发之前忘记了一件很重要的事情&#xff0c;既…

案例029:基于微信小程序的阅读网站设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

[leetCode]257. 二叉树的所有路径(两种方法)

257. 二叉树的所有路径 题目描述&#xff1a; 给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例&#xff1a; 输入&#xff1a;root [1,2,3,null,5]输出&#xff1a;["1-&g…

超实用!Spring Boot 常用注解详解与应用场景

目录 一、Web MVC 开发时&#xff0c;对于三层的类注解 1.1 Controller 1.2 Service 1.3 Repository 1.4 Component 二、依赖注入的注解 2.1 Autowired 2.2 Resource 2.3 Resource 与 Autowired 的区别 2.3.1 实例讲解 2.4 Value 2.5 Data 三、Web 常用的注解 3.1…

Echarts 设备状态 甘特图

在做工厂智能化生产看板时&#xff0c;绝对会有设备状态看板&#xff0c;展示设备当天或者当前状态&#xff0c;设备状态数据一般是有mes 系统设备管理模块对设备信息进行采集&#xff0c;一般包括过站数据&#xff0c;设备当前状态&#xff0c;是否在线是否故障、检修、待生产…

解决Vue编程式导航路由跳转不显示目标路径问题

我们配置一个编程式导航的路由跳转&#xff0c;跳转到 /search 页面&#xff0c;并且携带categoryName和categoryId两个query参数。 this.$router.push({path: "/search",query: {categoryName: dataset.categoryname,categoryId: dataset.categoryid} }) 如果我们…

清华提出 SoRA,参数量只有 LoRA 的 70%,表现更好!

现在有很多关于大型语言模型&#xff08;LLM&#xff09;的研究&#xff0c;都围绕着如何高效微调展开。微调是利用模型在大规模通用数据上学到的知识&#xff0c;通过有针对性的小规模下游任务数据&#xff0c;使模型更好地适应具体任务的训练方法。 在先前的工作中&#xff…

【挑战业余一周拿证】二、在云中计算 - 第 1 节 - 模块2 简介

第 1 节 - 模块2 简介 无论你的企业是属于像医疗、保健、制造、保险等等行业 , 再或者 , 您的服务是向全世界的数百万用户提供视频、、图片或者文字服务,你也需要服务器来为您的业务和应用程序提供支持,服务器的作用是帮助您托管应用程序并提供满足您业务需求的计算能力. 当你使…

显示Excel功能区或工具栏的方法不少,其中快捷方式最快

Microsoft Excel是Office套件中最复杂的工具之一&#xff0c;它提供了大量功能&#xff0c;其中大部分都是使用工具栏操作的。缺少工具栏使Excel很难完成工作。 如果Excel中没有这些关键元素&#xff0c;你将无法快速完成工作&#xff0c;因此&#xff0c;可以理解的是&#x…

Rust高性能网络框架:实战案例与代码解析

欢迎关注我的公众号lincyang新自媒体&#xff0c;回复关键字【程序员经典书单】&#xff0c;领取程序员的100本经典书单 大家好&#xff01;我是lincyang。 今天我们将深入探讨Rust编程语言在实际项目中的应用&#xff0c;并结合具体的代码示例来加深理解。 Rust因其内存安全…

JBase到JRT

JBase之前是站在之前基础上新做的java框架。所以带入一些老的历史习惯&#xff0c;比如库和空间都以LIS开头&#xff0c;实体只能是LIS.Model等。为了做到更通用的框架&#xff0c;需要剔除LIS特性&#xff0c;实体肯定不能只能叫LIS.Model了。同时之前只关注业务脚本化的事忘了…