线程同步

多线程同步

1)为什么需要多线程同步?
一块资源被多个线程同时操作的时候,当没有人任何操作,每个线程不知道什么时候开始执行什么时候结束,所以最终程序的运行结果会跟预想的不同
2)临界资源 临界区
临界资源:同一时刻只能够允许一个线程去访问的资源
临界区:访问临界资源的代码段

线程同步实际是为了保证线程安全

synchronized关键字

Synchornized关键字
防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读/写都可以通过同步的方式来进行

  • 1)Synchronized关键字提供了锁机制,能够去保证共享资源的互斥访问, 解决数据不一致的问题
  • 2)Synchornized的用法
  • 同步方法
 public synchornized void sync(){
    }
  • 同步代码块
    private final Object mutex = new Object();
    public void sync(){
        synchornized(mutex){
           ...
   }

代码练习:
两个线程,一个顺序输出1 2 3 4 5,一个逆序输出5 4 3 2 1

public class MyThread {
    public  synchronized void shun(){
        for(int i=1;i<=5;i++){
            System.out.println(Thread.currentThread().getName()+"顺序输出"+i);
        }
    }
    public void ni(){
        synchronized (MyThread.class){
                for (int i = 5; i > 0; i--) {
                    System.out.println(Thread.currentThread().getName() + "逆序输出" + i);
                }
            }
    }

    public static void main(String[] args) {
        MyThread demo=new MyThread();
         new Thread("线程1"){
             @Override
             public void run() {
                 demo.shun();
             }
         }.start();
         new Thread("线程2"){
             @Override
             public void run() {
                 demo.ni();
             }
         }.start();
    }
}

在这里插入图片描述

synchronized关键字是如何实现并发的呢
原子性:确保线程互斥的访问同步代码;

可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;

有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的作用是避免死锁

synchronized是如何获得锁的呢
同步代码块:
每一个对象都与一个monitor相关联,一个monitor lock只能被一个线程在同一时间锁获得

  • 1)如果monitor计数器为0,表示当前monitor lock未被获得,该线程获得之后就会对该计数器 +1-》monitorenter
    1. 如果monitor已经被线程锁拥有,则其他线程尝试获取mointor lock,会被陷入阻塞状态,直到moitor计数器变为0,才能够再次去获得monitor lock -》 monitorexit。
      在这里插入图片描述
      以上两个被红框框柱的就是获取锁和释放锁的过程。

那么同步方法是如何判断的呢
在这里插入图片描述
我们没有看到像是同步代码块中的monitorenter和exit的过程,只有一个ACC_SYNCHRONIZED标示符
JVM根据以上标示符去判断该方法是否是同步方法,如果是,执行的线程会先获取monitor lock,获取成功之后会去执行方法体,在方法体执行完之后释放monitor。方法执行期间,其他任何线程都没有办法去获得当前的monitor对象,只能阻塞。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
1.实例数据:存放类的属性数据信息,包括父类的属性信息;

2.对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

3.对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。比如锁膨胀就是借助Mark Word的偏向的线程ID。

MarkWord与锁的升级

Java对象的状态主要靠Mark Word来标记,主要有5种,大部分与线程有关。这里以64位JVM为例:
在这里插入图片描述
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向锁的时间戳。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

锁的升级

我们通常说的通过synchronized实现的同步锁,真实名称叫做重量级锁。但是重量级锁会造成线程排队(串行执行),且会使CPU在用户态和核心态之间频繁切换,所以代价高、效率低。为了提高效率,不会一开始就使用重量级锁,JVM在内部会根据需要,按如下步骤进行锁的升级:

  • 1.初期锁对象刚创建时,还没有任何线程来竞争,对象的Mark Word是下图的第一种情形,这偏向锁标识位是0,锁状态01,说明该对象处于无锁状态(无线程竞争它)。

  • 2.当有一个线程来竞争锁时,先用偏向锁,表示锁对象偏爱这个线程,这个线程要执行这个锁关联的任何代码,不需要再做任何检查和切换,这种竞争不激烈的情况下,效率非常高。这时Mark Word会记录自己偏爱的线程的ID,把该线程当做自己的熟人。如下图第二种情形。

  • 3.当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象并执行代码,锁对象的Mark Word就执行哪个线程的栈帧中的锁记录。如下图第三种情形。

  • 4.如果竞争的这个锁对象的线程更多,导致了更多的切换和等待,JVM会把该锁对象的锁升级为重量级锁,这个就叫做同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,这个监视器对象用集合的形式,来登记和管理排队的线程。
    在这里插入图片描述

1)偏向锁
CAS指令:(Compare And Swap)cpu层面的原子性操作指令,该指令存在三个参数,第一参数是目标地址,第二参数是值1,第三参数值2,指令会比较目标存储的值跟值1是否一致,如果一致目标地址会更新为新值,即值2。

如果一个线程获得了锁,那么锁就会进入偏向模式,锁标识位为01,是否为偏向锁为1,当这次线程再次请求锁的时候,不需要做同步操作,直接省略锁的获取阶段,提高系统的性能。这种场合下可能不存在锁竞争
锁竞争比较激烈的时候,偏向锁获取失败升级为轻量级锁
2)轻量级锁
轻量级锁所适应的线程交替执行同步快的场合
在代码进入同步代码快的时候,如果发现对象锁是无锁状态,在当前线程的栈帧中创建一个lock Record的空间, 存储对象Mark Word的拷贝,JVM使用CAS操作将对象Mark Word更新为指向Lock Record的引用,如果成功, 该线程拥有了这样的对象锁,对象Mark Word的锁标志位设置为00,表明该对象处于轻量级锁的状态;如果失败,锁竞争更加激烈,轻量级锁会升级为重量级锁

  • 补充:轻量级锁抢锁失败,JVM会使用自旋锁,不断尝试获取锁,jdk1.7默认启用

3)重量级锁
重量级锁使用会有操作系统的互斥量(MUTEX)和条件和(Conditionvariable)与其关联,在获取锁的过程修改操作系统层面的两个变量

LockRecord

在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。

Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构

  • Owner : 初始为NUll,如果有线程获取到monitor锁,Owner会保存线程唯一标识
  • EntryQ:阻塞所有试图获得monitor锁失败的线程
  • RcThis:blocked/Waiting在monitor上的所有线程个数
  • Nest:重入锁的个数
  • HashCode:保存对象的Hashcode
  • Candidate:用来避免不必要的阻塞或者额等待线程唤醒

monitor

  • 一种同步机制
  • 所有的Java对象天生可以作为monitor
  • 每一个对象自实例化之后都带有一把锁,这个锁称之为monitor锁/同步锁

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。

Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  • MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;

  • MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;

monitor 的数据结构

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时
1.首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;

2.若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;

3.若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

热门文章

暂无图片
编程学习 ·

MySQL 简洁速查手册

MySQL 速查手册 文章目录MySQL 速查手册0. 前言1. 开启/关闭数据库2. 数据库操作3. 数据表操作4. 字段操作5. 数据操作6. 运算符7. 高级查询(group by、having、order by、limit)8. 高级插入9. 高级删除10. 高级更新11. 联合查询12. 连接查询12.1 左外连接12.2 右外连接13. 子查…
暂无图片
编程学习 ·

vs2017试用期满了无法登陆怎么办

VS2017出现许可证过期解决方法 vs2017是免费社区,但是第一次试用的时候没有登录账户,提示试用期满不能进行操作。 查到有一下几种方法: 1、在帮助栏里输入序列号进行激活,此时必须处于许可证未过期状态进行激活,否则只能退出visual。 2、重新安装vs,对于一部分人来说是有…
暂无图片
编程学习 ·

Hadoop集群的四个配置文件的常用属性解析

在启动hadoop集群的守护线程时,一定会加载并运行相关的class字节码文件。通过common模块和hdfs模块里的源码可以看到,它们读取了相关的配置文件。hadoop-common-2.7.3-sources.jar下的org.apache.hadoop.conf.Configuration源文件的部分源码:package org.apache.hadoop.conf…
暂无图片
编程学习 ·

lex yacc flex bison

简介 lex与yacc是两个在Unix下的分别作词法分析和语法分析的工具, Linux对应flex与bison。 Yacc 与 Lex 快速入门 flex 和bison的安装和使用 Windows下安装lex(flex)与yacc(bison)
暂无图片
编程学习 ·

anaconda安装pytorch

CPU版本: https://pytorch.org/get-started/previous-versions/ #CPU only conda install pytorch1.2.0 torchvision0.4.0 cpuonly -c pytorch 参考博客 [1]https://blog.csdn.net/u014723479/article/details/103001861?utm_medium=distribute.pc_relevant.none-task-blog-B…
暂无图片
编程学习 ·

理解持续测试,才算理解DevOps

软件产品的成功与否,在很大程度上取决于对市场需求的及时把控,采用DevOps可以加快产品交付速度,改善用户体验,从而有助于保持领先于竞争对手的优势。作为敏捷开发方法论的一种扩展,DevOps强调开发、测试和运维不同团队间的协作与沟通。持续集成和持续测试是一个在迭代中构…
暂无图片
编程学习 ·

roarctf_2019_easy_pwn[off by one]

溢出一个字节,修改size域 exp from pwn import *context.log_level = debugdef debug_pause():log.info(proc.pidof(p))pause()def create_note(size, ):p.sendlineafter(choice:, str(1))p.sendlineafter(size:, str(size))def write_note(index, size, content):p.sendlineaf…
暂无图片
编程学习 ·

git命令大全

Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。 Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 Git官方网站:https://git-scm.com/ 原理图Workspace:工作区 Index / Stage:暂存区 Repository:仓库…
暂无图片
编程学习 ·

ECharts雷达图详细配置说明

// 指定图表的配置项和数据 var option = {backgroundColor: rgba(204,204,204,0.7 ),// 背景色,默认无背景 rgba(51,255,255,0.7)title: {text: 各教育阶段男女人数统计,link: https://www.xxx.com,target: blank,top: 5%,left: 3%,textStyle: {color: #fff,fontSize: 20,…
暂无图片
编程学习 ·

MySql简单入门_第四篇(1)_视图

4、使用视图【将视图用于检索SELECT 而不用于更新INSERT,UPDATE和DELETE】视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询。视图提供了一种MySQL的SELECT语句层次的封装,可用来简化数据处理以及重新格式化基础数据或保护基础数据。【视图仅仅是用…
暂无图片
编程学习 ·

Java工具类-使用RSA验签

1 私钥签名public static String signByKey(String content,String privateKey) {PKCS8EncodedKeySpec sp = new PKCS8EncodedKeySpec(new BASE64Decoder().decodeBuffer(privateKey));KeyFactory keyFactory = KeyFactory.getInstance("RSA");PrivateKey key = keyF…
暂无图片
编程学习 ·

PAT 甲级 1013 Battle Over Cities (25分)

题目 1013 Battle Over Cities (25分) It is vitally important to have all the cities connected by highways in a war. If a city is occupied by the enemy, all the highways from/toward that city are closed. We must know immediately if we need to repair any othe…
暂无图片
编程学习 ·

Zabbix简介

一、Zabbix介绍 Zabbix是一个企业级的、开源的、分布式的监控套件 Zabbix可以监控网络和服务的监控状况. Zabbix利用灵活的告警机制,允许用户对事件发送基于Email的告警. 这样可以保证快速的对问题作出响应. Zabbix可以利用存储数据提供杰出的报告及图形化方式. 这一特性将帮助…
暂无图片
编程学习 ·

STM32CubeIDE TFT-LCD显示

随言:TFT-LCD的8080并口时序可以与ST的FSMC总线上操作SRAM的时序类似。故把TFT-LCD挂在SRAM上就能想操作SRAM一样操作TFT-LCD显示了。主要是STM32CubeIDE的时序图形配置。剩下的就是移植LCD显示厂商的驱动和寄存器设置,因为这部分设置太多了,自己看手册设置非常繁琐。重要是…
暂无图片
编程学习 ·

mybati中动态标签「if」没有生效的原因

一、问题: <if test="carrier != null and carrier != and carrier !=0">AND CARRIER = #{carrier} </if>我们在接口设置传入的字段类型为String,要在carrier字段不为null,空字符串,和”0“的时候增加以上条件,但是以上当carrier等于"0"时…
暂无图片
编程学习 ·

vulnhub靶机-djinn3

1、靶机ip:192.168.0.110(开机就提示:不是所有的都需要扫描发现主机)2、扫描靶机端口root@kali:~# nmap -A -p- 192.168.0.110 Starting Nmap 7.80 ( https://nmap.org ) Nmap scan report for 192.168.0.110 Host is up (0.0011s latency). Not shown: 65531 closed ports…
暂无图片
编程学习 ·

android 防止重复点击

1、kotlin实现 通过 Kotlin 拓展, 在拓展类中新增两个方法 fun View.OnClickListener.initSingleClickListener(vararg views: View) { views.map { it.setOnSingleClickListener(this) } } fun View.setOnSingleClickListener(listener: View.OnClickListener) { setOnClickLi…
暂无图片
编程学习 ·

6台路由器ospf+rip实验

一、环境OSPF路由协议是一种典型的链路状态(Link-state)的路由协议,一般用于同一个路由域内。在这里,路由域是指一个自治系统(Autonomous System),即AS,它是指一组通过统一的路由政策或路由协换路由信息的网络。在这个AS中,所有的OSPF路由器都维护一个相同的描述这个A…
暂无图片
编程学习 ·

JMXTrans入门教程

概述 官网 GitHub JMX JMX,即Java Management Extensions,监控Java应用程序系统运行的状态信息,通过分析JMX信息,可用于监控应用程序运行状态、优化程序、排查问题。 JMXTrans JMXTrans是一款开源的JMX指标采集工具,使用简单方便,无需编写代码,只需要配置文件就可以轻松…