当前位置: 首页 > news >正文

【Java ee 初阶】多线程(8)

Synchronized优化:

一、锁升级

锁升级时一个自适应的过程,自适应的过程如下:

在Java编程中,有一部分的人不一定能正确地使用锁,因此,Java的设计者为了让大家使用锁的门槛更低,就在synchronized中引入了很多优化策略。

*首先,根据前面的学习,我们已经初步了解了自旋锁和重量级锁,那么图中的偏向锁是什么?

偏向锁,不是真正加锁,而只是做个标记,如果整个过程中,没有其他线程来竞争这个锁,偏向锁状态就始终保持,直到最终解锁。但是,如果在过程中,遇到其他线程也尝试来竞争这个锁,就可以在其他线程拿到锁之前,抢先获取这个锁。这样子仍然可以使其他线程产生阻塞,保证线程的安全。

锁升级的过程:

1.偏向锁->轻量级锁:说明该线程上发生了锁竞争

2.轻量级锁->偏向锁:竞争更加激烈

那我们如何去了解竞争的激烈程度呢?——JVM内部,统计了这个锁上面有多少个线程在等待获取,这一切都取决于JVM的实现。

而我们作为程序员,关心的是策略,而不是参数,因为参数都是可以调整的,但是策略是不变的。

锁升级,对于当前主流的JVM实现来说,是“不可逆”的,一旦升级了,就无法回头降级了

二、锁消除

有些代码,那你写了加锁,但是在JVM执行的时候会发现,这个地方没必要加锁,因此JVM就会自动把锁给去除掉

例如:我们知道,StringBuilder不带有synchronized,StringBuffer带有synchronized,因此可能有人在单线程环境下就使用StringBuffer,此时这个锁只在一个线程中,因此会被JVM给优化掉。

JVM的优化是一方面,咱们作为程序员,也不能够完全摆烂。

三、锁粗化

首先我们先了解一个概念——锁的粒度。加锁和解锁范围中代码越多,锁的粒度就越粗,反之锁的粒度就越细

如图我们可知这两段代码,上面那一段代码的粒度更粗,下面代码的粒度更细。

获取到一次锁,可能是一件不太容易的事情。因此有的时候,JVM会进行锁粗化,将加锁解锁的次数减少,以此提高效率。synchronized关键字,作为Java的关键字,底层实现是在JVM内部完成的,不是通过Java来实现的,而是通过C++来实现的。

总结:synchronized优化主要体现在:

1.锁升级

2.锁消除

3.锁粗化

四、CAS

CAS,全称compare and swap,比较和交换。

这是一串cas的伪代码(不是严格符合语法等待,知识用来描述一下逻辑)

address:是一个内存地址

expectValue,swapValue:是CPU寄存器

这段代码实现的内容是:拿着内存中的值,和寄存器1的值进行比较,如果二者相等,就把内存和寄存器2的值进行交换。一般来说,只关心内存中值的变化,而不关心寄存器2中发生了什么变化。上述的交换,也可以近似理解成赋值。

注意,上述所有的逻辑,都是通过“一个CPU指令”完成的。一条指令,意味着这是原子的!!没有线程安全的问题。

CPU的特殊指令完成了上述的所有操作—>操作系统封装了这个指令,形成了一个系统的API—>Java中又封装了操作系统的API,由unsafe包进行提供,这里提供的操作,比较底层,可能不安全。

CAS的典型应用

1.实现原子类

可以将原来的三步打包成原子的,通过AtomicInteger,原子的进行++ -- 等操作

例如:

package Thread;public class demo1 {public static int count = 0;
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i=0;i<5000;i++){count++;}});Thread t2 = new Thread(()->{for(int i=0;i<5000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);
}}

输出结果:

出现了运算符重载的问题!

什么是运算符重载:运算符重载就是让已有的运算符针对不同的类,也能够有对应的效果,Java并不支持。运算符重载的好处,就是可以用使代码的一致性更高,比如,定义一个类,“复数”通过运算符重载,就可以使得复数类和其他的数字类型相似,都可以进行加减乘除。定义一个矩阵类也可以通过运算符重载。

Java中认为,上述写法,容易被滥用,因此Java并不支持。

package Thread;import java.util.concurrent.atomic.AtomicInteger;public class demo1 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i=0;i<5000;i++){//下列这些操作,都是原子的,不加锁也能保持线程安全count.getAndIncrement();//count++//count.incrementAndGet();//++count//count.getAndIncrement();//count++//count.getAndDecrement();//--count//count.GetAndDecrement();//count--//count.getAndAdd(10);//count+=10;}});Thread t2 = new Thread(()->{for(int i=0;i<5000;i++){count.getAndIncrement();//count++}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);
}}

此时的输出:

这样的操作,不仅线程安全,而且效率更高,不涉及到阻塞。因此在实际开发中,如果有这种计数的需求,优先考虑原子类,而不是自己去加锁。

划红线的部分,就是JVM封装过的,比上述谈到的CAS还要更加复杂一点,但是他们的本质是一样的。

伪代码实现:

CAS如果一样,就说明在这两者之前没有其他线程修改value内存,此时就可以安全的对value进行修改了。如果CAS发现有其他线程插队,就会进入循环,重新load。此处的oldValue是寄存器,而不是变量。C语言曾经能够定义寄存器变量,但是后来也废除的,也就只有汇编能够直接操作寄存器了,和之前传统的++相比,在真正进行修改之前,又做了判定。

2.实现自旋锁

CAS实现自旋锁的伪代码

如果owner为null,解锁状态,否则就会保存持有锁的线程的引用。如果锁已经被占用了,这个循环,就会快速地反复地循环。循环体中是没有任何sleep。这是一种消耗cpu,换来尽快地加锁速度。但是,如果锁竞争很激烈,大量的线程都会这样自旋,就会消耗非常多的cpu,那么cpu就负担不起了,锁释放之后,这些线程还是要竞争,还是意味着大量的线程是无法第一时间拿到锁的。

下面的赋值操作天然就是原子的,判定-赋值(check and set),典型的非原子的操作。

CAS的ABA问题

通过CAS来判定,当前load到寄存器的内容和内存的内容是否一致,如果一致,就认为没有其他线程修改过这个变量,接下来本线程的修改就是安全的。

然而,这里面存在着一个缺陷:可能出现这种情况:另一个线程又把内存的值从A改成B,又从B改回了A,此时CAS是感知不到的,仍然会认为没有其他线程修改过的。

ABA问题,通常情况下都是没事的,即使其他线程真的修改了,由于又修改回了原来的值,所以ABA现象不一定给程序引起BUG,但是如果遇到特别极端的场景,那还是有可能的,下面是一个非常极端的例子:

上述场景下,由于ABA问题,导致了重复扣款。

那么,针对ABA问题,我们应该如何解决呢?上述问题之所以出现ABA问题,是因为他针对余额的修改可能加,也可能减,如果改变成“只能加,不能减”,那么就可以解决上述问题。因此,我们引入版本号,约定每次修改,都需要对版本号+1,并且每次CAS比较的时候都是比较版本号是否相同(相同的话,进行版本号+1 和 余额修改)

我们可以定义一个类,包含版本号和余额,如下:

五、Callable接口

callable接口,类似于Runnable,也是属于JUC(java.util.concurrent)这个包的。call自定义返回值,run返回值是void。

接下来让我们利用Callable接口创建一个线程,并且通过这个线程计算1+2+3+...+100

package Thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class demo2 {
public static void main(String[] args) throws InterruptedException, ExecutionException {//callable 带有泛型参数 泛型参数就是返回值类型Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for(int i=0;i<=100;i++){result+=i;};return result;};};FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();//get会阻塞等待线程执行完毕,拿到返回值System.out.println(futureTask.get());
}
}

创建线程的方式:1.继承Thread 2.实现Runnable 3.基于lambda(本质还是Runnable)4.实现Callable 5.基于线程池

六、ReentranLock

ReentrantLock是一把比较传统的锁,在synchronized还不成熟的时候,这个锁就是进行多线程编程加锁的主要方案

package Thread;import java.util.concurrent.locks.ReentrantLock;public class demo3 {private static int count = 0;public static void main(String[] args) throws InterruptedException {ReentrantLock locker = new ReentrantLock();Thread t1 = new Thread(()->{for(int i=0;i<5000;i++){//加锁locker.lock();count++;//解锁locker.unlock();}});Thread t2 = new Thread(()->{for(int i=0;i<5000;i++){locker.lock();count++;locker.unlock();}   });t1.start();t2.start();t1.join();t2.join();System.out.println(count);}}

这种写法,容易把unlock遗漏

换成这种写法,不太美观

区别与联系

1.synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)

   ReentrantLock是标准库的一个类,在JVM外实现的(基于Java实现的)

2.synchronized使用的时候不需要手动释放锁

  ReentantLock使用的时候需要手动释放,使用的时候更加灵活,但是也容易遗漏。

3.synchronized是非公平锁,ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启公平锁模式

4.synchronized在申请锁失败的时候会死等,ReentrantLock可以通过trylock的方式等待一段时间就放弃(这是synchronized不具备的特点,trylock加锁失败的时候会直接放弃)

5.ReentrantLock拥有更强大的唤醒机制,synchronized是通过Object的wait /notify实现 等待-唤醒。每次唤醒的是一个随机等待的线程 ReentrantLock搭配Condition类来实现等待-唤醒,九二一更加精确控制唤醒某个指定的线程

相关文章:

  • Ubuntu日志文件清空的三种方式
  • 嵌入式通信协议总览篇:万物互联的基石
  • 滚动条样式
  • Ubuntu 配置网络接口端点(静态 IP 地址)详细教程
  • 紫光同创FPGA实现HSSTHP光口视频传输+图像缩放,基于Aurora 8b/10b编解码架构,提供3套PDS工程源码和技术支持
  • 如何有效防御服务器DDoS攻击
  • Tiny Machine Learning在人类行为分析中的全面综述
  • spring4.x详解介绍
  • 力扣热题100之反转链表
  • vue3 element-plus 输入框回车跳转页面问题处理
  • 《Python星球日记》 第43天:机器学习概述与Scikit-learn入门
  • 协方差与皮尔逊相关系数:从定义到应用的全面解析
  • Coze平台 搭建「AI美食视频制作工作流」的详细实现方案
  • Java消息队列性能优化实践:从理论到实战
  • JVM的双亲委派模型
  • Spark 之 YarnCoarseGrainedExecutorBackend
  • Kubernetes学习笔记
  • Python训练营打卡——DAY18(2025.5.7)
  • 按拼音首字母进行排序组成新的数组(vue)
  • Prometheus实战教程:k8s平台-Redis监控案例
  • 母亲节书单|关于生育自由的未来
  • 昆明一学校门外小吃摊占满人行道,城管:会在重点时段加强巡查处置
  • 中国海外发展:今年前4个月销售665.8亿元,花费305亿元拿地
  • 警方通报男子地铁上拍视频致乘客恐慌受伤:列车运行一度延误,已行拘
  • 5天完成1000多万元交易额,“一张手机膜”畅销海内外的启示
  • 47本笔记、2341场讲座,一位普通上海老人的阅读史