聊一聊synchronized的简单理解
相信熟悉java的同学对synchronized关键字也是非常熟悉了,似乎只要在涉及到线程安全的问题的问题中,加上synchronized关键字就对了!
比如下面这个我们比较常见的代码,懒汉式单例模式:
public class LazySimpleSinglethon { private static LazySimpleSinglethon singlethon = null; private LazySimpleSinglethon(){} public synchronized tatic LazySimpleSinglethon getInstance(){ if(singlethon==null){ //如果不加synchronizde会存在线程安全问题 singlethon = new LazySimpleSinglethon(); } return singlethon; } }
好,问题来了,为什么会有线程安全问题?什么是线程安全问题?《Java Concurrency In Practice》一书的作者Brian Goetz 对“线程安全“有一个比较恰到的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。” 看完这一段定义,我们可以理解为,当我们在编写代码的时候不需要考虑线程问题即不加上同步处理,程序的运行结果仍然会是正常的,那我们可以说这是线程安全的。比如上面这段代码,如果我们不加上synchronized关键字,那么在多线程环境下,很有可能会创建多个单例对象,很显然这不是我们想要的,也违背了单例模式的设计原则。
线程安全问题,说白了就是多个线程在操作共享数据时引发的数据安全问题。
在java语言中,使用synchronized关键字是我们使用互斥同步的方式(另外还有两种保证线程安全的方式:非阻塞同步和无同步方案)来保证线程安全的常用手段,那是不是只要涉及线程安全问题,我们都可以用synchronize来解决呢?使用synchronized关键字会不会对性能有什么影响?使用synchronized关键字的缺陷又在哪里呢?接下来我们就好好的聊一聊,synchronized关键字。
- synchrozized的用法:
- 修饰实例方法,作用于当前实例加锁,进入同步代码块前要获得当前实例的锁。
- 修饰静态方法,作用于当前类加锁,进入同步代码前要获得当前类对象的锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
简单的示例代码:
public class SyncDemo { //修饰实例方法 public synchronized void syncMethod(){ //对象锁 } public void syncCode(){ //修饰代码块 synchronized (this){ // 括号表示作用范围 this是对象级别 SyncDemo.class类级别 //保护存在线程安全的变量 } } //修饰静态方法 public synchronized static void syncStaticMethod(){ //类锁 //TODO } }
以上代码表示synchronized关键字的两种作用范围,一种作用于对象,一种作用于类。观察synchronized 的整个语法发现,synchronized(lock)是基于lock 这个对象的生命周期来控制锁粒度的,如果这个对象是类,那么作用范围是类级别,如果是对象,那么作用范围就是对象。显而易见类级别的范围要大于对象级别(类的生命周期>对象的生命周期)。
synchronized 关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要- 个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference ;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
我们在使用synchronized互斥同步的时候,互斥同步对性能影响最大是通过阻塞的实现,挂起线程和恢复线程的操作都需要从用户态转换到内核态去完成,这些操作会给操作系统的并发性能带来较大的压力,所有很长时间以来synchronized一直都被称之为重量级锁。当然除synchronize之外,我们还可以使用java.util.concurrent(J.U.C)包中的重入锁(ReentrantLock)来实现同步,这一篇我以后会介绍。
synchronized重量级锁会给操作系统的并发性能带来影响,加锁一定会带来性能的影响,那么该如何优化呢?hotspot作者经过研究发现,大多数情况下,锁不仅不存在多线程的竞争,而且总是由同一线程多次获得。基于这样的理论推测,线程对共享资源的竞争存在三种情况:
- 锁不存在多线程竞争的情况:每次获得锁的线程都是同一个线程。这种情况只需要在锁对象头中记录是否为偏向锁,1是,0不是,并且记录线程id。
- 锁存在线程较小竞争的情况:这个时候当第二个线程访问同步代码块时,发现锁的对象头中已经有偏向标记,于是先会撤销锁的偏向锁标记,升级为轻量级锁,通过多次cas操作来竞争锁,未抢占到锁的线程,会通过自旋重试来多次cas操作抢占锁。由于自旋会耗费cpu资源,这个自旋次数也不是无限次的,当达到来限度就会升级为重量级锁。所以轻量级锁的使用,实际上是自旋等待的线程可以较快获取锁的情况下,即锁的竞争比较小的情况下。
- 锁存在线程激烈竞争的情况:为避免多个线程出现的自旋等待耗费cpu资源,会将锁升级到重量级锁。
以上简单的描述了锁的升级过程,另外值得注意的是,synchronized关键字锁的升级这一块是不可逆的,无法从重量级锁再回到轻量级锁。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】