JUC3-共享模型之管程

news2024/9/22 17:40:51

共享带来的问题

分析

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

分析:

以上的结果可能是正数、负数、零

因为Java中对静态变量的自增、自减并不是原子操作,需要从字节码进行分析

例如对于i++而言(i为静态变量),会产生如下的JVM字节码指令:

i--:

而Java的内存模型如下,完成静态变量的自增、自减需要在主存和工作内存中进行数据交换:

如果是单线程,以上8行代码是顺序执行(不会交错),没有问题

但多线程下这8行代码可能交错运行

出现负数的情况:

出现正数的情况:

临界区Critical Section

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源

  • 多个线程读共享资源没有问题
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如:

static int counter = 0;

static void increment()
//临界区
{
    counter++;
}

static void decrement()
//临界区
{
    counter--;
}

竞态条件Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

为了避免临界区的竞态条件发生,有多种解决方案:

  • 阻塞式:synchronized,Lock
  • 非阻塞式:原子变量

synchronized解决方案

synchronized,俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住,这样就能保证拥有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换

注意:

虽然Java中互斥和同步都可以采用synchronized关键字来完成,但存在区别:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点

语法1

synchronized(对象) // 线程1, 线程2(blocked)
{
   临界区
}

解决:

理解:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给它,t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

图示:

 

思考:

synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

  • 如果把synchronized(obj)放在for循环的外面,如何理解?——原子性
  • 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎么运作?——不同的锁对象
  • 如果t1 synchronized(obj),而t2没有加会怎么样?——相当于都没有加锁

面向对象改进:

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                room.decrement();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("{}", room.getCounter());
    }
}

class Room {
    private int counter = 0;

    public void increment() {
        synchronized (this){
            counter++;
        }
    }

    public void decrement() {
        synchronized (this){
            counter--;
        }
    }

    public int getCounter() {
        synchronized (this){
            return counter;
        }
    }
}

语法2

成员方法:锁的是this对象

静态方法:锁的是类对象

线程八锁

考察synchronized锁住的是哪个对象

情况1:

12或21

情况2:

1s后122 1s后1

情况3:c方法没有synchronized,c和ab并行执行

3 1s后1 23 2 1s后1 或 2 3 1s后1

情况4: 两个对象n1和n2,不互斥

2 1s后1

情况5:a方法为静态方法,锁住的是类对象,与b方法锁住的对象不同,不互斥

2 1s后1

 

情况6:a、b都为静态方法

1s后122 1s后1

情况7:锁对象不同,不互斥

2 1s后1

情况8:锁对象相同,互斥

1s后122 1s后1

变量的线程安全

成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全

如果它们被共享,根据它们的状态是否能够改变,分为两种情况:

  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的

但局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用范围,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

每个线程调用test1()方法时,局部变量i会在每个线程的栈帧内存中被创建多份,因此不存在共享

局部变量的引用:

如果是成员变量

其中一种情况是,如果线程2还未add,线程1remove就会报错:

分析:

无论哪个线程的method2,引用的都是同一个对象中的list成员变量

将list修改为局部变量

分析:

list是局部变量,每个线程调用时会创建其不同实例,没有共享

如果把method2和method3的方法修改为public会不会有线程安全问题?

情况1:有其他线程调用method2和method3

  • 没有问题,不同线程创建不同的list对象

情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法

  • 有问题,两个线程共享一个资源

常用线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的类

这里的线程安全指的是多个线程调用它们同一个实例的某个方法时,是线程安全的

它们的每个方法是原子的,但它们方法的组合不是原子的

线程安全类方法的组合:

下面代码不是线程安全的

不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

Monitor

Java对象头

以32位虚拟机为例

普通对象

数组对象

其中Mark Word结构为

64位虚拟机Mark Word

Monitor(监视器/管程)

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

Monitor结构:

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

注意:

  • synchronized必须是进入同一个对象的monitor才有上述的效果
  • 不加synchronized的对象不会关联监视器,不遵从以上规则

原理:synchronized

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

轻量级锁对使用者是透明的,即语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁:

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

  • 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

  • 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁

如果cas失败,有两种情况:

  • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

  • 当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

当退出synchronized代码块(解锁时),锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当时线程自旋成功(即这时持锁线程已经退出来同步块,释放了锁),这时当前线程就可以避免阻塞

自旋会占用cpu时间,单核cpu自旋就是浪费,多核cpu自旋才能发挥优势

在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能

Java 7之后不能控制是否开启自旋功能

自旋重试成功的情况:

自旋重试失败的情况:

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作

Java 6引入了偏向锁来做进一步优化:只有第一次使用cas将线程ID设置到对象的Mark Word头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有

偏向状态

撤销-调用对象hashCode

撤销-其他线程使用对象

撤销-调用wait/notify

批量重偏向

批量撤销

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

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

相关文章

三级_网络技术_29_网络安全技术

1.下列关于RAID的描述中&#xff0c;错误的是()。 服务器需要外加一个RAID卡才能实现RAID功能 RAID10是RAID0和RAID1的组合 些RAID卡可以提供SATA接口 RAID卡可以提供多个磁盘接口通道 2.下列关于RAID的描述中&#xff0c;错误的是()。 RAID5可靠性优于RAID1 些RAID卡可…

【Java 并发编程】(三) 从CPU缓存开始聊 volatile 底层原理

并发编程 三大问题 在并发编程中&#xff0c;原子性、有序性和可见性是三个重要的问题&#xff0c;解决这三个问题是保证多线程程序正确性的基础。原子性: 指的是一个操作不可分割, 要么全部执行完成, 要么不执行, 不存在执行一部分的情况.有序性: 有序性是指程序的执行顺序与…

Arco Chatter - 改变 Chatter Panel 交互体验的插件

关于 Chatter Panel Arco Chatter 插件是一款专门为 odoo 系统 Chatter Panel 设计的实用工具&#xff0c;它具有改变 Chatter Panel 展开/收起状态和调整 Chatter Panel 宽度的功能。 Arco Chatter 插件主要用于优化用户在 odoo 系统中的操作体验。通过插件功能&#xff0c;用…

【iOS】—— JSONModel

JSONModel源码 1. JSONModel介绍2. JSONModel的其他用法2.1 转换属性名称2.2 自定义错误 3. 源码分析3.1 - (id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err3.2 JSONModel持有的数据3.3 load3.4 JSONModel的init方法3.5 __inspectProperties方法3.6 JSONMode…

动态创建 Delphi 按钮的完整指南:基于配置文件的 `TGridPanel` 实现

在 Delphi 开发中&#xff0c;我们经常需要根据不同的配置动态生成 UI 元素。本文将带你通过一个完整的示例&#xff0c;演示如何根据配置文件动态创建按钮&#xff0c;并将它们排列在一个 TGridPanel 中。每个按钮的标题、链接、颜色和大小都将从配置文件中读取。 “C:\myApp\…

基于YOLOv8-pose的手部关键点检测(1)- 手部关键点数据集获取(数据集下载、数据清洗、处理与增强)

前言 手部姿态估计、手势识别和手部动作识别等任务时&#xff0c;可以转化为对手部关键点的分布状态和运动状态的估计问题。本文主要给出手部关键点数据集获取的方式。 总共获取三个数据集&#xff1a; handpose_v2&#xff1a;训练集35W张&#xff0c;验证集2.85W张&#xff1…

vim - vim模式及部分操作

文章目录 一、vim 基本介绍二、vim 的简单使用三、几种常用模式切换四、命令模式和底行模式的操作汇总 一、vim 基本介绍 vim 是一款多模式的编辑器。vim 中有很多子命令来进行代码的编写操作。 同时&#xff0c;vim 提供了不同的模式供我们选择。 在vim下的底行模式下通过:he…

如何查询婚姻状况信息?

1.使用在线查询工具&#xff1a;‌ 现在&#xff0c;‌也有一些在线查询工具&#xff0c;‌如“天远查”“全能查”等微信小程序&#xff0c;‌提供了婚姻状态查询服务。‌这些工具通常需要你提供一些基本信息&#xff0c;‌并可能收取一定的费用。‌在使用这些工具时&#xff…

WebRTC为何成为视频开发领域的首选技术? EasyCVR视频转码助力无缝视频通信

随着互联网的飞速发展&#xff0c;视频通信已成为日常生活和工作中不可或缺的一部分。从在线教育、视频会议到远程医疗、在线直播&#xff0c;视频开发的需求日益增长。在这些应用场景中&#xff0c;选择何种技术来构建视频系统至关重要。 目前&#xff0c;在很多视频业务的开…

文本纠错实现定位与标记

一、基于讯飞文本纠错实现前端标记定位&#xff0c;点击可以联动&#xff01;

VM下kali设置桥接网络

一、查看主机ip 1.winr输入cmd 2.进入终端输入ipconfig 3.查看ip 二、虚拟机网络设置 1.进入vm的虚拟网络编辑器 2.桥接网卡自己选&#xff0c;1是有线网卡2是无线网卡&#xff0c;选择记得点应用 3.虚拟机的网络适配器也要选择桥接模式 三、kali网络配置 1.打开kali终端编辑文…

基于Spring Boot的库存管理系统

TOC springboot265基于Spring Boot的库存管理系统 绪论 1.1 选题动因 在现在社会&#xff0c;对于信息处理方面&#xff0c;是有很高的要求的&#xff0c;因为信息的产生是无时无刻的&#xff0c;并且信息产生的数量是呈几何形式的增加&#xff0c;而增加的信息如何存储以及…

linux下QOS:理论篇

关于qos &#xff0c;也是linux下面必备功能之一&#xff0c;一般只需要结合iptables/etables/iproute2 和tc配合即可实现大部分功能. 网上讲这么方面的资料很多&#xff0c;大部分都讲tc命令的应用.这里就先从理论入手. QoS&#xff08;Quality of Service&#xff09;服务质…

WSL2 使用usbipd工具 连接USB设备

Connect USB devices | Microsoft Learn 使用开源工具usbipd&#xff0c;可以让usb设备连接WSL https://github.com/dorssel/usbipd-win usbipd list 可以查看连接到win上的设备。 把USB设备从win转移到WSL需要执行下面两个指令。 usbipd bind --busid <BUSID> usb…

Shiro-721 分析

前言 shiro-550漏洞的产生源自硬编码问题&#xff0c;在 1.2.4之前&#xff0c;密钥在代码中是固定的 而在1.2.5 < Apache Shiro < 1.4.1&#xff0c;我们再看AbstractRememberMeManager类&#xff0c;已经修改为生成随机的密钥 在本篇文章中&#xff0c;分析着重于代…

电子电气架构---EEA的发展趋势

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

RPA自动化流程机器人在财税管理中的作用

随着科技的飞速发展&#xff0c;人工智能和自动化技术正在改变市场竞争的格局&#xff0c;企业对于提高工作效率和准确性的要求日益提高。财税管理作为企业运营管理的核心环节之一&#xff0c;其数字化程度决定了企业发展的速度、广度和深度。因此&#xff0c;财税数字化成为各…

transformer-explainer

安装和启动 找到这个项目&#xff0c;然后装好了。 这个项目的目的如名字。 https://github.com/poloclub/transformer-explainerTransformer Explained: Learn How LLM Transformer Models Work with Interactive Visualization - poloclub/transformer-explainerhttps:/…

【算法】蚁群算法

一、引言 蚁群算法&#xff08;Ant Colony Optimization, ACO&#xff09;是一种模拟蚂蚁觅食行为的启发式搜索算法。它由Marco Dorigo于1992年提出&#xff0c;适用于解决组合优化问题&#xff0c;如旅行商问题&#xff08;TSP&#xff09;、车辆路径问题&#xff08;VRP&…

STM32初识

这边软件使用的是Keil5&#xff0c;主要介绍一下使用的一些注意事项。 创建工程部分&#xff1a; 创建工程方式有两种&#xff1a;使用Keil创建工程模板、 使用STM32CubeMX 新建一个工程 1.新建一个文件&#xff0c;添加文件&#xff1a; DOC工程说明 doc说明文档 Librarie…