成都汇智动力老师直接接听

400-029-09** 400-029-0997 转 65635
查看完整号码
扫码拨号
微信扫码拨号

Java开发之高并发必备篇(五)——线程安全操作之synchronized

作者:汇智动力学院 来源:汇智动力学院 2022/10/6 14:18:21

提到并发编程大多数第*时刻想到的就是synchronized...

提到并发编程大多数第*时刻想到的就是synchronized同步锁了,synchronized也是面试中问的比较多的一个问题。在之前的文章中我们提到过线程安全的三个特性:原子性、可见性和有序性,并且说到了java中定义了一个关键字synchronized对于线程的这三个特性都实现了,这么说来synchronized关键字是可以保证线程安全的,那么如何使用synchronized来实现线程安全?它又是怎么样的一个实现原理呢?这篇文章我们将从下面的几个内容来聊聊synchronized这个关键字。
1.synchronized介绍 synchronized 是java中的一个关键字,翻译成汉语是同步的意思,它的作用呢就是被其修饰的方法或者代码块在同一时刻只能有一个线程进行访问和执行,只有当该线程执行完毕之后其他的线程才可以进行竞争访问,并且当前访问的线程可以重复的申请竞争访问资源。 可以想象为大家都在一个窗口买东西,同一时刻只能有一个人在买,其他人都在等待,不一样的是外面等待的人并不会排好队而是都在等着正在买东西的人买完之后争夺到买东西的机会,并且现在买的人买完之后还能申请继续争夺买东西的机会。 简单来说:synchronized可以修饰代码块和方法,它可以保证同一时刻只能有一个线程访问被修饰的代码块或者方法,从而保证了线程的安全。synchronized犹如一把锁,当一个线程访问之后就锁定访问的共享资源代码段,达到互斥的效果,从而保证了线程的安全,并且同一个线程可以获取同一把锁多次,达到可重入的效果。 synchronized特性 synchronized具有 原子性 、 可见性 、 有序性 和 可重入性 ; 原子性 可见性 synchronized保证有序性是因为unlock解锁操作之前必须把工作内存中数据同步回主内存来实现的;而主内存是所有线程都可以访问的共享内存,所以修改之后其它线程操作该数据时都可以看到被修改后的值。 有序性 synchronized实现有序性是因为当一个共享资源变量被lock锁定操作之后,同一时刻只能被一个线程使用,而单线程执行代码是没有指令重排等问题的,所以线程也是有序的。同样被lock锁定的共享资源排斥其他线程访问所以Synchronized也具有互斥性。 可重入性 synchronized的可重入性就是当一个线程调用synchronized代码持有对象锁的时候,如果调用了该对象的其他synchronized代码,那么可以重新持有该锁,即同一个线程可以获取同一把锁多次,所以synchronized具有可重入性。 2.synchronized的使用 synchronized同步锁主要分两种,一种是 对象锁 ,另外一种是 类锁 ; 对象锁 对象锁顾名思义锁的作用对象是实例对象,当synchronized修饰普通的方法或者代码块的时候,都可以指定锁的对象。因一个类可以有很多对象,所以对象锁是可以有多个的。 修饰普通方法: 被称为同步方法,其锁作用范围是这个普通方法的所有代码,作用的对象是调用这个普通方法的对象。 修饰代码块: 被称为同步代码块,其锁作用范围是这个代码块的所有代码,作用的对象是调用这个代码块的对象。 类锁 每个类都只有一个对应的Class对象(反射对象),类锁其作用的对象就是类的Class对象了,或者当锁的对象为一个静态对象的时候也是类锁。当synchronized修饰静态方法或者代码块的时候都可以使用类锁。 修饰静态方法/代码块: 其锁的作用范围为定义的静态方法或代码块的代码。作用的对象就是在调用该静态方法或代码块的所有对象。 下面我们还是以之前卖票的MyRunnable为例看看对象锁和类锁的使用。 对象锁的使用代码如下:
运行结果如图:
分析: 通过运行我们发现,当使用了同步代码块或者同步方法的对象锁方法实现,线程就是同步执行的了。 值得注意的是对象锁中同步普通的方法锁的对象是this即锁的作用对象是当前的MyRunnable对象,而对象锁的同步代码块锁的对象可以是this也可以是Object类型。 synchronized(this)和synchronized(obj)的区别: synchronized(this)所的作用对象是当前访问的对象,而synchronized(obj)的作用对象是obj,如果多个线程共用一个obj对象那么执行的时候还是同步执行的,如果每个线程obj锁的对象不同那么还是异步执行。如下代码就是异步执行:
另外同步代码块的方式因为可以控制锁的代码范围即控制锁的粒度,所以有些场合下使用同步代码块的效率要更高。 类锁的使用代码如下:
因为锁的是class对象或者静态对象,所以我们测试时候可以每个线程创建不一样的MyRunable2对象来进行测试,代码如下:
运行结果如下:
分析: 通过测试我们发现类锁对于锁定的类class的所有对象都成立。 3.synchronized原理 为了研究synchronized的原理,我们就需要对使用这个关键字的java文件编译之后生成的class文件进行反编译,查看下java字节码对应的机器指令是怎么样的。 Java代码是这样的:
通过jdk自带的javap工具对SyncTest.class文件进行反编译获取字节码指令,执行命令“ java p-v SyncTest ”,然后获取到反编译的结果如图所示:
同步代码块
同步方法 我们可以看到使用同步代码块的test方法中看到两个熟悉的指令monitorenter、monitorexit,即遇到synchronized的时候执行monitorenter指令获取到锁,而当方法运行结束时执行monitorexit指令释放锁。其他指令有兴趣的话可以百度“JVM虚拟机字节码指令表”查看具体含义。 monitorenter 和 monitorexit 通过官方介绍的这两个指令进行翻译之后的大体上是这样的: monitorenter 和 monitorexit 的 执行流程图如下:
而在同步方法test2的反编译字节码中并没有看到monitorenter和monitorexit两个指令,但是发现图中红色框中标记了一个flags值为ACC_SYNCHRONIZED。 ACC_SYNCHRONIZED介绍如下: “方法级同步是隐式执行的,作为方法调用和返回的一部分。同步方法在运行时常量池的methodinfo结构中通过ACCSYNCHRONIZED标志进行区分,该标志由方法调用指令检查。当调用为其设置了ACC_SYNCHRONIZED的方法时,执行线程进入monitor监视器,调用方法本身,然后退出监视器,不管方法调用是正常完成还是突然完成。在执行线程拥有监视器期间,其他线程不能进入监视器。如果在调用synchronized方法的过程中抛出异常,并且synchronized方法不处理该异常,则在将异常从synchronized方法中重新抛出之前,该方法的监视器将自动退出”。 通过上面的描述我们知道了同步方法通过标志值为ACC_SYNCHRONIZED也可以获取到monitor锁,并在方法结束的时候会释放monitor锁,从而也达到了同步的效果。 4.JDK1.6对synchronized的优化 上述介绍了synchronized的使用和原理,我们发现虽然synchronized锁实现了并发安全,但是它有点“重”,因为当一个线程访问同步方法或者代码块获取锁了之后,其他的线程都处于等待阻塞状态,浪费CPU的资源,并且频繁的获取和释放锁也消耗CPU的性能等等,所以以前一提到synchronized大家都说它是一个重量级锁。但是到JDK1.6的时候就对synchronized进行了各种优化来提高它的效率,如JVM会对java代码进行锁粗化、锁消除处理,适应性自旋解决自旋占用大量CPU资源问题,并且加入了偏向锁和轻量级锁等对锁进行了升级优化,最后才是重量级锁。 锁粗化 加锁的共享资源范围越小,那么其他线程等待阻塞的时间就会越短,这样明显比对大范围资源加锁效率高。但是加锁和释放锁也需要时间和消耗资源的,如果出现频繁的加锁和释放锁操作那么就会导致消耗CPU性能,锁粗化就是解决这种问题的。锁粗化就是在出现很小范围内代码进行连续加锁释放锁操作的时候,对其锁的范围进行扩大,这样锁就变成了外部的一个,避免了小范围频繁的锁操作。典型的案例就是for循环,如下:
锁消除 锁消除是指当java进行JIT (Just-In-Time) 编译(即时编译:程序运行时把Class文件字节码编译成本地机器码来提高执行效率)运行程序的时候,通过上下文进行逃逸分析(逃逸分析:如果变量被方法中使用,又被方法外使用,那么这个变量就发生了逃逸)发现如果变量发生了逃逸那么应该保持锁,如果没有发生逃逸那么不存在竞争资源的问题从而会把锁消除掉,案例如下:
我们知道StringBuffer是一个线程安全的类,它的append方法被synchronized修饰,但是此处因为sb变量只是一个局部变量,sb 的所有引用不会 “逃逸” 到 test方法之外其他线程无法访问控制到它,所以即使append方法操作有锁,JVM即使编译后就会把这个锁消除掉,上述代码就会忽略掉同步锁而执行。 悲观锁、乐观锁、CAS的概念 悲观锁 : 在使用synchronized的时候,如果一个线程获取到锁,那么它就非常的悲观,认为其他线程访问共享资源会出现冲突,所以其他线程会被阻塞。 CAS操作: compare and swap意思是比较并交换,CAS操作中有三个参数:内存位置(V)、预期原值(A)和新值(B);如果内存位置的值与预期原值相匹配,那么会自动将该值更新为新值 ,如果不一样那么重新计算直到一直为止。 乐观锁: CAS的操作就属于乐观锁,不加锁,而是认为多线程访问共享资源不会出现冲突的情况,如果出现了冲突那么就重试,直到内存值和预期值不冲突为止。 锁升级 JDK1.6后synchronized的锁状态总共有4种:无锁—>偏向锁—>轻量级锁—>重量级锁,锁的升级顺序是从无锁到重量级的顺序,锁只能升级不能降级。 在说锁的升级原理之前呢,我们先了解下我们的对象,大部分对象都是存储在堆中,而对象的组成主要有三部分: 对象头 、 实例数据 、 对齐填充 ; 对象头 对象头由MarkWord 、指向类的指针、以及数组长度三部分组成,这里我们需要着重熟悉的就是MarkWord部分。MarkWord 用于存储对象的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。MarkWord 的内容变化会随着锁的升级而变化。具体变化如下表:
实例数据 对象真正存储的有效信息,也就是代码中定义的各种字段内容。 对齐填充 对齐填充没有特别的含义不是必然存在的,它仅仅起着占位符的作用。 无锁 当对象已创建存储在内存中,它对象头MarkWord锁标志默认就是无锁状态,无锁状态不存在资源的锁定。 偏向锁 很多时候可能一段同步代码总是被一个线程多次访问,这时候并不存在多线程竞争的问题,这时候就是加入偏向锁,使得该线程在后续访问中自动获取到锁,降低了频繁获取锁释放锁代码的资源消耗。 原理是当一个线程执行同步方法或者代码块的时候,首先从对象头中的MarkWord中获取是否是偏向锁标志: (1)如果标志为0证明当前为无锁状态,就会将当前线程的ID添加到对象头的MarkWord中,然后将是否是偏向锁标志改为1,再执行同步代码; (2)如果标志为1证明已经是偏向锁状态,那就从MarkWord中获取到偏向线程ID跟当前线程ID比较,如果一样则不需要再次获取锁直接执行同步代码;如果不一样执行CAS操作将MarkWord的线程ID设置为当前线程ID,设置成功则执行同步代码,如果CAS操作失败证明存在多线程竞争情况,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁。 轻量级锁 轻量级锁是指当前线程是偏向锁但是被其他线程访问的时候则升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 自旋:线程的阻塞和唤醒需要C P U从用户态和内核态进行转换,比较耗时,所以JVM在发现锁被一个线程占用的时候,并不会让其他线程阻塞而是一直循环检测锁是否被释放,当然自旋的次数有限制(可以通过JVM参数-XX:PreBlockSpin 修改),如果达到次数还是没有获取锁才会被挂起。 升级为轻量级锁主要有两种情况,第*种就是我们说的当前线程的偏向锁被其他线程访问的时候会把当前线程升级为轻量级锁;另外一种就是关闭了偏向锁功能(JVM参数 -XX:-UseBiasedLocking )。 如果当前线程获取到的是轻量级锁,锁标志为00,如果还有一个线程访问的时候就会进行自旋,但是如果自旋超过了设定的自旋次数,这个线程还是会阻塞,或者在线程自旋的过程中又有其他线程访问了那么就会把轻量级锁升级为重量级锁。 重量级锁 当轻量级锁升级为重量级锁之后,锁的标志改为10,就会变成我们最初所说的现象,一个线程访问其他线程都阻塞,并且重量级锁底层依赖的是操作系统的互斥锁(Mutex Lock)实现的,线程的切换需要用户态和内核态的转换,比较耗时效率低。 最后我们使用张流程图简单的来总结下锁的升级过程:

二、算法的设计原则   ①、正确性:首先,算法应当满足以特定的“规则说明”方式给出的需求。其次,对算法是否“正确”的理解可以有以下四个层次:   一、程序语法错误。   二、程序对于几组输入数据能够得出满足需要的结果。   三、程序对于精心选择的、典型、苛刻切带有刁难性的几组输入数据能够得出满足要求的结果。   四、程序对于一切合法的输入数据都能得到满足要求的结果。   PS:通常以第 三 层意义的正确性作为衡量一个算法是否合格的标准。   ②、可读性:算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解;另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试。   ③、健壮性:当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且,处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理。   ④、高效率与低存储量需求:通常算法效率值得是算法执行时间;存储量是指算法执行过程中所需要的*大存储空间,两者都与问题的规模有关。   前面三点 正确性,可读性和健壮性相信都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说“A算法比B算法快两倍”之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。所以描述算法的速度必须要和数据项的个数联系起来。也就是“大O”表示法,它是一种算法复杂度的相对表示方式,这里我简单介绍一下,后面会根据具体的算法来描述。   相对(relative):你只能比较相同的事物。你不能把一个做算数乘法的算法和排序整数列表的算法进行比较。但是,比较2个算法所做的算术操作(一个做乘法,一个做加法)将会告诉你一些有意义的东西;   表示(representation):大O(用它*简单的形式)把算法间的比较简化为了一个单一变量。这个变量的选择基于观察或假设。例如,排序算法之间的对比通常是基于比较操作(比较2个结点来决定这2个结点的相对顺序)。这里面就假设了比较操作的计算开销很大。但是,如果比较操作的计算开销不大,而交换操作的计算开销很大,又会怎么样呢?这就改变了先前的比较方式;   然后我们再说说算法的存储量,包括:    程序本身所占空间;    输入数据所占空间;    辅助变量所占空间;   一个算法的效率越高越好,而存储量是越低越好。 三、算法的分类 算法可以宏泛的分为三类: 一,有限的,确定性算法 这类算法在有限的一段时间内终止。他们可能要花很长时间来执行指定的任务,但仍将在一定的时间内终止。这类算法得出的结果常取决于输入值。 二,有限的,非确定算法 这类算法在有限的时间内终止。然而,对于一个(或一些)给定的数值,算法的结果并不是的或确定的。 三,无限的算法 是那些由于没有定义终止定义条件,或定义的条件无法由输入的数据满足而不终止运行的算法。通常,无限算法的产生是由于未能确定的定义终止条件。 Java中常见的算法有: ①、排序 排序就是对一组数据按照一定的顺序(从大到小或者从小到大)进行排序; 常见排序如下: 简单排序:冒泡排序、选择排序、插入排序; 高级排序:快速排序、希尔排序、归并排序、基数排序、鸡尾酒排序等等; ②、递归 递归是一种直接或者间接调用自身的一种算法,递归的目的是简化程序设计使程序更加易读; ③、查找 在一些(有序的/无序的)数据元素中,通过一定的方法找出与给定关键字相同的数据元素就叫做查找; ④、统计 指对有关数据的搜集、整理、计算、分析、解释、表述等的活动。 往期文章 墙裂推荐 [1] Java开发之高并发必备篇(四)——线程的状态、调度和操作方法 [2] Java开发之高并发必备篇(三)——线程的内存模型 [3 ] Java开发之高并发必备篇(二)——线程为什么会不安全? 原创视频 墙裂推荐 2022转行软件测试之校区资讯 ↓↓↓

活动福利 // 1 毕业礼包 | 毕业学员免费赠送《软件测试技术大咖专题课》,助力学员早日突破高薪瓶颈 // 2 入职礼包 |就业学员免费赠送《Java语言开发视频课》及全套源代码,市场价值12800元 // 3 推荐有奖 |推荐好友成功报名,立得丰厚“伯乐”红包 (欢迎详询校区老师)

详询软件测试&开发培训事宜

添加微信咨询
杨老师 @成都汇智动力

专业解答各类课程问题、介绍师资和学校情况

微信号:186******73

立即咨询

“成都汇智动力”是成都汇智动力信息技术有限公司在教育宝平台开设的店铺,若该店铺内信息涉嫌虚假或违法,请点击这里向教育宝反馈,我们将及时进行处理。

机构评分

环境:4.5师资:4.5服务:4.0效果:4.0

公示信息

店铺名称:成都汇智动力

单位名称:成都汇智动力信息技术有限公司

账号名称:cdhzdl(180******07)

所属城市:四川成都

入驻时长:11年

在线客服:在线聊

微信咨询

返回顶部