synchronized的优化机制和一些多线程的常见类
admin
2024-01-20 17:50:56
0

1.synchronized

1.1.我们现在知道常用的锁策略

  1. 乐观锁 vs 悲观锁
  2. 读写锁 vs 普通的互斥锁
  3. 重量级锁 vs 轻量级锁
  4. 挂起等待锁 vs 自旋锁
  5. 公平锁 vs 非公平锁
  6. 可重入锁 vs 不可重入锁

那synchronized基于这些策略有哪些特性呢?

  1. synchronized 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.
  2. synchronized 不是读写锁,是普通互斥锁
  3. synchronized 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁.
  4. synchronized 实现轻量级锁的时候大概率用到的自旋锁策略,重量级锁的部分基于挂起等待实现
  5. synchronized 是一种非公平锁
  6. synchronized 是一种可重入锁

1.2.synchronized 的一些锁优化机制(jdk 1.8)

1.2.1.锁膨胀

JVM 将 synchronized 锁分为四种情况.

  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

四种锁状态也会根据实际情况依次进行升级.

  1. 没有加锁的状态
  2. 首个线程加锁,就会进入偏向锁状态(只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程,并没有真的加锁)
  3. 如果偏向锁后续有其他线程来竞争该锁,那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.(自适应的自旋锁)
  4. 如果竞争进一步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁(用到内核提供的mutex.)

1.2.2.锁粗化

锁粗化涉及到锁的粒度

  • 锁的粒度: 加锁代码涉及到的范围.(不是整体多个加锁总和的范围,而是单个锁涉及的范围)
  1. 加锁代码的范围越大,认为锁的粒度越粗.
  2. 加锁代码的范围越小,则认为粒度越细
  • 如果两次加锁之间隔的代码比较多,不会影响整体运行状态的情况下,一般不会优化.
  • 如果之间间隔比较小(中间间隔的代码少),间隔过少的时候可能涉及多次释放锁后,又马上申请锁的操作.就很可能触发这个优化(JVM 就会自动把锁粗化)

1.2.3.锁消除

有这么一种情况: 有些代码,明明不用加锁,结果你给加上锁了.编译器就会发现这个加锁好像没啥必要,就直接把锁给去掉了(锁消除).

当然你可能说,我不会乱加锁的.不过,有的时候加锁操作并不是那么明显,稍不留神就会做出了这种错误的决定.

例如: StringBuffer, Vector…这些类,在标准库中进行了加锁操作,而我们在单个线程中用到这些类的时候,就是单线程进行了加锁解锁.而我们并不会发觉,不过我们也不用担心.因为编译器会发现并处理这一类情况,也就是上述的锁消除.

2.Callable接口

2.1.Callable接口的使用

Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.所以 FutureTask 就可以负责这个 等待结果出来 的工作.

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成一个任务. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask .此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 task.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;/*** Callable 接口* java.util.concurrent* 

* Callable 解决了 Runnable 不方便返回结果的问题*/ public class Test {public static void main(String[] args) {Callable callable = new Callable() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};//创建线程,完成任务//为了让线程执行 callable 中的任务,光使用构造方法还不够,还需要一个辅助的类//FutureTask 运行结果会保存在这个类中FutureTask task = new FutureTask<>(callable);//任务Thread t = new Thread(task);t.start();try {//如果线程的任务没有执行完,get就会阻塞,一直阻塞到,有return 的结果System.out.println(task.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}} }

2.2.Callable 和 Runnable 比较

  • 都是描述一个任务
  • Callable 描述的是带有返回值的任务.
  • Runnable 描述的是不带返回值的任务.

3.JUC(java.util.concurrent) 中的常见类

3.1.ReentrantLock:可重入锁

有三个主要的方法

  1. lock()方法 : 加锁,如果获取不到锁就死等.
  2. unlock()方法 : 解锁
  3. trylock(超时时间)方法 : 也是加锁,不同于lock()方法的是这个方法懂得放弃加锁失败,不会死等,而是等待一定的时间之后就放弃加锁

相对于习惯使用 synchronized 的我来说,这里加锁和解锁 分开的做法,是不太友好的,因为很容易遗漏 unlock(),出现锁死.

3.1.1.ReentrantLock与synchronized的区别

  1. synchronized是一个关键字(背后的逻辑是JVM内部实现的,C++),ReentrantLock是一个标准库中的类(背后的逻辑是Java代码写的)

  2. synchronized 不需要手动释放锁,出了代码块,锁自然释放.ReentrantLock必须要手动释放锁,要谨防忘记释放.

  3. synchronized 如果竞争锁的时候失败,就会阻塞等待.但是ReentrantLock除了阻塞等待这一手之外,还有一手, trylock()超时失败了会返回.

  4. synchronized是一个非公平锁.ReentrantLock 提供了非公平和公平锁两个版本!在构造方法中,通过参数就可以来指定当前是公平锁还是非公平锁.

    • ReentrantLock locker = new ReentrantLock(true);
    • 参数为true 时该类为公平锁
    • 参数默认是false 默认该类为非公平锁

3.1.1.ReentrantLock类使用

import java.util.concurrent.locks.ReentrantLock;/*** ReentrantLock : 可重入锁   */
public class Test2 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock(true);//参数为true 为公平锁  默认为false 非公平锁locker.lock();//加锁try {} finally {locker.unlock();//解锁}}
}

3.2.Semaphore:信号量

锁就相当于一个二元信号量,可用资源只有一个.计数器非0,即1

Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

类的参数为信号量个数

3.2.1.PV操作

  1. acquire()方法 P操作(申请资源):无资源的时候获取会阻塞等待资源释放
  2. release()方法 V操作(释放资源)

3.2.2.Semaphore类使用

import java.util.concurrent.Semaphore;/*** Semaphore 信号量* 锁:二元信号量, 可用资源就一个,计数器 非0,即1*/
public class Test {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(4);//参数为4 代表可用资源有 4 个//申请资源 P操作semaphore.acquire();//1System.out.println("申请成功");semaphore.acquire();//2System.out.println("申请成功");semaphore.acquire();//3System.out.println("申请成功");semaphore.acquire();//4System.out.println("申请成功");
//        semaphore.acquire();//5 阻塞等待
//        System.out.println("申请成功");semaphore.release();//释放资源 V操作semaphore.acquire();System.out.println("申请成功");}
}

3.3.CountDownLatch 同时等待N个任务执行结束.

类的参数.表示需要等待任务的个数.

3.3.1.方法使用

  • countDown()方法.任务调用此方法表示任务结束.此时CountDownLatch内部的计数器减1.
  • await()方法.调用此方法的线程阻塞等待所有任务执行完毕. 就是计数器减为0的时候.

3.3.2.CountDownLatch类使用

import java.util.concurrent.CountDownLatch;/*** CountDownLatch 同时等待 N 个任务执行结束*/
public class Test {public static void main(String[] args) throws InterruptedException {//构造方法的参数表示 有多少个选手参赛CountDownLatch countDownLatch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {Thread t = new Thread(() -> {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "到达终点");countDownLatch.countDown();});t.start();}countDownLatch.await();//等待所有线程到达System.out.println("比赛结束");}
}

3.4.CopyOnWriteArrayList 写时拷贝

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy.复制出一个新的容器后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器.

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为只读的时候不会有线程安全问题.

3.5.ConcurrentHashMap 多线程环境下可以使用的哈希表

ConcurrentHashMap 相比于 Hashtable 又做出了一系列的改进和优化

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁.加锁的方式仍然是用 synchronized,但不是对整个对象加锁, 而是 “锁桶” (用每个链表的头结点作为锁对象,让锁加到每个链表的头结点上), 大大降低了锁冲突的概率.
  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容方式: 化整为零
    • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
    • 扩容期间, 新老数组同时存在.
    • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
    • 搬完最后一个元素再把老数组删掉.
    • 这个期间, 插入只往新数组加.
    • 这个期间, 查找需要同时查新数组和老数组.

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...