JavaEE初阶Day 14:多线程(12)

目录

  • Day 14 :多线程(12)
    • CAS的ABA问题
    • Callable接口
    • ReentrantLock
    • 信号量Semaphore
    • CountDownLatch
    • 集合类的多线程安全问题
      • 1. Collections.synchronizedList(new ArrayList)
      • 2. CopyOnWriteArrayList
      • 3. BlockingQueue
      • 4. ConcurrentHashMap

Day 14 :多线程(12)

回顾

锁策略

乐观锁VS悲观锁;轻量级锁VS重量级锁;自旋锁VS挂起等待锁;公平锁VS非公平锁;可重入锁VS不可重入锁;普通斥锁VS读写锁

synchronize基本特性与实现原理

  • 锁升级:偏向锁 --> 轻量级锁 --> 重量级锁
  • 锁消除:编译器优化
  • 锁粗化:编译器优化

CAS的ABA问题

使用CAS编写代码:比较然后再交换

在比较过程中:检查当前内存的值,是否被其他线程修改了,如果被修改了,就要稍后再重试,如果没被修改,接下来就可以直接修改,不会有线程安全问题,没有其他线程穿插执行。但是值没变 != 值没变过,有可能另一个线程把这个值从A变为B,再从B变为A了

ABA在大部分情况下没什么问题,但是在极端情况下,就可能产生bug

如何避免ABA问题,核心思路是引入版本号,约定版本号只能加不能减,每一次操作版本号都要+1,通过CAS判定版本号,如果版本号没有发生改变,数据就一定没有变过

Callable接口

Callable也是用来描述任务的,并且call方法带有返回值,表示这个线程执行结束会得到什么结果

package thread;

public class Demo39 {
    private static int sum = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建一个线程,让这个线程来实现 1 + 2 + 3 +......+ 1000
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for (int i = 0; i <= 1000; i++) {
                    result += i;
                }
                //此处为了把result告知主线程,就需要通过静态成员变量倒腾一下
                sum = result;
            }
        });
        t.start();
        t.join();

        //主线程获取得到结果
        System.out.println(sum);
    }
}

上述代码主线程与t线程耦合太大了,线程内部定义的局部变量是不能被其他线程获取得到的,线程更多,就会更麻烦

Callable就是为了更优雅的解决上述问题

package thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo40 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i <= 1000; i++) {
                    result += i;
                }
                return result;
            }
        };

        //创建线程,把callable搭载到线程内部执行
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        t.join();
        System.out.println(futureTask.get());
    }
}

创建线程的方式

  • 继承Thread
  • 使用Runnable
  • 使用lambda
  • 使用线程池/ThreadFactory
  • 使用Callable

ReentrantLock

ReentrantLock:可重入

package thread;

import java.util.concurrent.locks.ReentrantLock;

public class Demo41 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();
        try {
            //加锁
            locker.lock();
        } finally {
            //解锁
            locker.unlock();
        }
    }
}
  • ReentrantLock提供了公平锁的实现,synchronized只是非公平锁,ReentrantLock locker = new ReentrantLock(true);表示公平锁,false/不填写表示非公平锁

  • ReentrantLock提供tryLock操作,给加锁提供了更多的可操作空间,尝试加锁,如果锁已经被获取到了,直接返回失败,而不会像synchronized遇到锁竞争会阻塞等待,tryLock也可以去指定等待超时时间

  • ReentrantLock搭配Condition类完成等待通知,synchronized搭配wait与notify等待通知机制,Condition可以指定线程唤醒,多个线程wait,notify是唤醒随机一个

信号量Semaphore

信号量就是一个计数器,描述了可用资源的个数,围绕信号量有两个基本操作

  • P操作:计数器+1,申请资源(acquire)
  • V操作:计数器-1,释放资源(release)
package thread;

import java.util.concurrent.Semaphore;

public class Demo42 {
    public static void main(String[] args) throws InterruptedException {
        //4个可用资源
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
        semaphore.acquire();
        System.out.println("P操作");
    }
}

上述代码进行了5次P操作,但是信号量只有4个可用资源,所以在第5次P操作的时候,会出现阻塞等待

锁其实就是特殊的信号量,如果信号量只有0、1两个取值,此时就称为”二元信号量“,本质就是一把锁

package thread;

import java.util.concurrent.Semaphore;

public class Demo43 {

    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(()->{
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

CountDownLatch

当我们把一个任务拆分成很多个的时候,可以通过这个工具类来识别任务是否整体执行完毕

package thread;

import java.util.concurrent.CountDownLatch;

public class Demo44 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(()->{
                System.out.println("线程启动" + id);
                try {
                    //假设这里是进行一些“下载”这样的耗时操作
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }

                System.out.println("线程结束" + id);
                latch.countDown();
            });

            t.start();
        }
        //通过await等待所有线程调用countDown
        latch.await();
        System.out.println("所有线程结束");
    }
}

await会阻塞等待,一直到countDown调用的次数和构造方法指定的次数一致的时候,await才会返回,await不仅仅能代替join,比如有1000个任务,交给4个线程的线程池来执行,如何判定1000个任务执行完了,也可以使用CountDownLatch来判定(这个过程没有线程真正结束)

集合类的多线程安全问题

ArrayList、LinkedList、Stack、Queue、HashMap…大部分都是线程不安全的

  • Vector自带了synchronized,Stack继承了Vector,也自带了synchronized
  • Hashtable也自带了synchronized

加锁不能保证线程一定安全,不加锁也不能确定线程一定不安全

手动加锁比较麻烦,标准库提供了一些其他的解决方案

1. Collections.synchronizedList(new ArrayList)

给ArrayList这些集合类,套一层壳,壳上是给关键方法都加了synchronized,就可以使ArrayList达到类似于vector的效果

2. CopyOnWriteArrayList

写时拷贝:在读的时候读取旧的数组,在写的时候,使用新的数组来写,当写完之后,用新的数组的引用,代替旧的数组的引用(引用赋值操作,是原子的),旧的空间就可以释放了

上述过程,没有任何加锁和阻塞等待,也就能确保读线程不会读出错误的数据

上述操作其实实用性非常高,有些服务器程序需要更新配置文件/数据文件,就可以采取上述策略

  • 显卡渲染画面到显示器就是按照写时拷贝的方式,在显示上一个画面的时候,在背后用额外的空间生成下一个画面,生成完毕了,使用下一个画面代替上一个画面

3. BlockingQueue

多线程使用队列,直接使用BlockingQueue即可

4. ConcurrentHashMap

多线程使用哈希表,HashMap是线程不安全的,Hashtable是带锁的,但是标准库提供了更好的代替,即ConcurrentHashMap

Hashtable加锁是简单粗暴给每个方法加了synchronized,相当于是针对this加锁,只要针对Hashtable上的元素进行操作,就会涉及到锁冲突

ConcurrentHashMap做出了优化

  • 使用**“锁桶”的方式,来代替“一把全局锁”,有效降低锁冲突的概率,即对每个哈希桶进行加锁**

    • 如果两个线程针对两个不同的链表进行操作,是不会涉及到锁冲突的,本身操作两个不同链表上的元素,也没修改“公共变量”,本身就不涉及到线程安全问题,上述提升的收益是非常大的,一个hash表,上面的hash桶的个数是非常多的,大部分的操作都没有锁冲突了(synchronized如果不产生锁冲突,就是偏向锁)
    • 另一方面,看起来锁对象多了,实际上也不会产生更多的额外开销,Java中每个对象都可以作为锁对象,就只需要把synchronized加到链表头节点上,就可以达成上述效果
  • 哈希表中的size,即使插入的元素是不同的链表上的元素,也会涉及到多线程修改同一变量,ConcurrentHashMap引入CAS,通过CAS的方式来修改size,也就避免了加锁操作

  • ConcurrentHashMap针对扩容操作做了特殊优化——化整为零,普通的HashMap要在一次put的过程中完成整个扩容过程,就会使put操作非常卡,ConcurrentHashMap会在扩容的时候,搞两份空间

    • 一份是扩容之前的空间
    • 一份是扩容之后的空间

    每次进行hash表的基本操作,都会把一部分数据从就空间搬到新空间,不是一口气搬完,分多次搬

    搬的过程中

    1. 插入:插入到新的上面

    2. 删除:新的旧的都要删除

    3. 查找:新的旧的都要查找

Java 8之前,ConcurrentHashMap基于分段锁的方式实现,引入若干个锁对象,每个锁对象管理若干个哈希桶,Java 8之后就把这种实现方式废弃了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java中的栈和队列

1.前言 在计算机科学中&#xff0c;数据结构是用来组织和存储数据的方式&#xff0c;以便可以高效地访问和修改。栈和队列是两种最基本的数据结构&#xff0c;它们在各种计算过程中都有广泛的应用。本文将介绍栈和队列的概念、特性以及它们的一些常见应用。 2.栈 2.1概念 栈…

Vue实现多角色登录,Vue-Router路由守卫控制权限页面

实现页面侧边栏和头部不变&#xff0c;当点击某个功能时&#xff0c;只有主体部分发生变化&#xff0c;这要用到子路由技术 我的项目结构如上&#xff0c;其中包含侧边栏和头部的文件是Manage.vue&#xff0c;主页面是Home.vue&#xff0c;个人页面是Person.vue&#xff0c;用户…

kaggle咖啡销售分析案例侧重可视化折线图条形图扇形图柱状图

目录 概述 环境依赖 数据描述 代码概述 导包 数据读取 统计缺失值 数据结构概述 描述统计 时间轴数据转换 月交易统计直方图 周交易统计图 小时数据转换 小时折线图 销售关系可视化统计 销售占比扇形图 价格箱线图 各类别多维度条形图统计 商店位置交易量折线…

9个技巧使你的Python代码更Pythonic!

如何区分漂亮和丑陋的代码&#xff1f; 更重要的是&#xff0c;如何写出漂亮的 Python 代码&#xff1f; 本文将通过初学者容易理解的例子展示9个神话般的Python技巧&#xff0c;以帮助你在日常工作中编写更多的Pythonic程序。 01 product() 使用 product() 函数避免嵌套的…

电缆故障测试仪的操作方法有哪些?

电缆故障测试仪是一种专业设备&#xff0c;用于检测电力电缆和通信电缆的各种故障。它采用多种技术手段&#xff0c;包括但不限于低压脉冲法、高压闪络法、直闪法和冲闪法。这些方法适用于不同类型的电缆故障&#xff0c;例如断线、接触不良、低阻接地、高阻接地、开路故障和短…

iOS开发 刻度盘 仪表盘,圆点按钮滑动控制,渐变色

最近项目需要&#xff0c;想做一个渐变色的刻度盘&#xff0c;圆形按钮滑动控制&#xff0c;所以 用oc写了一下&#xff0c;代码没附上&#xff0c;想看代码可以私信联系&#xff0c;效果如下图。 部分代码 self.drawCenter CGPointMake(self.frame.size.width / 2.0, self.f…

ubuntu22.04搭建dns内网

近期&#xff0c;需要在无网络的ubuntu环境下搭建内部可用的dns内网&#xff0c;总共花费3个工作日晚上&#xff0c;总算成功搭建&#xff0c;做个记录&#xff0c;记录踩坑记录&#xff0c;同时方便以后翻阅。 安装软件包&#xff1a; 有网络环境下&#xff0c;比较简单&…

科研基础与工具(论文搜索)

免责申明&#xff1a; 本文内容只是学习笔记&#xff0c;不代表个人观点&#xff0c;希望各位看官自行甄别 参考文献 科研基础与工具&#xff08;YouTube&#xff09; 搜索论文 Google Scholar 谷歌学术 涵盖面太全了&#xff0c;都收录&#xff0c;就会有很多低质量的论文…

基于STM32F103RCT6最小系统原理图和PCB

目录 1、原理图 2、PCB 3、3D图 资料下载地址&#xff1a;基于STM32F103RCT6最小系统原理图和PCB 1、原理图 2、PCB 3、3D图

解决Error in writing header file of the driver

在源代码里面更新了一批常规的内容&#xff0c;编译的时候遇到一个error&#xff0c;一大片都是红的。XXX是项目名称。 Description Resource Path Location Type Generator: ERROR: Error in writing header file of the driver XXX Cpu Processor Expert Problem 表面意思是…

nvm下载的node没有npm

nvm下载的node没有npm 相信大家最近可能发现自己使用的nvm下载nodejs没有npm了。 会出现这种情况&#xff1a; C:\Users\89121>nvm install 15 Downloading node.js version 15.14.0 (64-bit)... Complete Downloading npm version 7.7.6... Download failed. Rolling Bac…

门禁管理系统服务器如何内网映射让外网访问?

禁管理系统整体解决方案,可实现请假出入联动、门状态监控、电子地图、非法闯入报警、远程开门、红外防夹、智能统计等功能&#xff0c;应用非常广泛。 如果门禁管理系统部署在没有公网IP的本地服务器上&#xff0c;如何设置&#xff0c;能让外网互联网上也能登录访问内部的管理…

04 JavaScript学习:输出

JavaScript 没有任何打印或者输出的函数。 JavaScript 显示数据 JavaScript 可以通过不同的方式来输出数据&#xff1a; 使用 window.alert() 弹出警告框。使用 document.write() 方法将内容写到 HTML 文档中。使用 innerHTML 写入到 HTML 元素。使用 console.log() 写入到浏…

怎样在外网登录访问CRM管理系统?

一、什么是CRM管理系统&#xff1f; Customer Relationship Management&#xff0c;简称CRM&#xff0c;指客户关系管理&#xff0c;是企业利用信息互联网技术&#xff0c;协调企业、顾客和服务上的交互&#xff0c;提升管理服务。为了企业信息安全以及使用方便&#xff0c;企业…

SQLAIchemy 异步DBManager封装-01入门理解

前言 SQLAlchemy 是一个强大的 Python SQL 工具包和对象关系映射&#xff08;ORM&#xff09;系统&#xff0c;是业内比较流行的ORM&#xff0c;设计非常优雅。随着其2.0版本的发布&#xff0c;SQLAlchemy 引入了原生的异步支持&#xff0c;这极大地增强了其在处理高并发和异步…

C++11的更新介绍(lamada、包装器)

&#x1fa90;&#x1fa90;&#x1fa90;欢迎来到程序员餐厅&#x1f4ab;&#x1f4ab;&#x1f4ab; 主厨&#xff1a;邪王真眼 主厨的主页&#xff1a;Chef‘s blog 所属专栏&#xff1a;c大冒险 总有光环在陨落&#xff0c;总有新星在闪烁 lambda表达式 C98中的一个…

51-42 NÜWA:女娲,统一的多模态预训练模型

21年11月&#xff0c;微软、北大联合发布了NUWA模型&#xff0c;一个统一的多模态预训练模型&#xff0c;在 8 个下游任务上效果惊艳。目前该项目已经发展成为一系列工作&#xff0c;而且都公开了源代码。 Abstract 本文提出了一种统一的多模态预训练模型N̈UWA&#xff0c;该…

FPGA中按键程序设计示例

本文中使用Zynq 7000系列中的xc7z035ffg676-2器件的100MHz PL侧的外部差分时钟来检测外部按键是否按下&#xff0c;当按键被按下时&#xff0c;对应的灯会被点亮。当松开按键时&#xff0c;对应的灯会熄灭。 1、编写代码 新建工程&#xff0c;选用xc7z035ffg676-2器件。 点击…

递归——汉诺塔

汉诺塔 法国数学家爱德华卢卡斯曾编写过一个印度的古老传说&#xff1a;在世界中心贝拿勒斯&#xff08;在印度北部&#xff09;的圣庙里&#xff0c;一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候&#xff0c;在其中一根针上从下到上地穿好了由大到小的64片金…

通过拖拽动态调整div的大小

最近遇到一个需求&#xff0c;页面展示两块内容&#xff0c;需要通过拖拽可以动态改变大小&#xff0c;如下图&#xff1a; 实现思路&#xff1a;其实就是改变div样式的width&#xff0c;本质上就是Dom操作。 完整代码&#xff1a;&#xff08;基于vue2项目实践&#xff09; …