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

深入浅出聊聊synchronized

最近在准备寒假实习的面试,synchronized傻傻搞不明白,相信很多小伙伴像我一开始一样,所以就有了这篇博客,对大家有帮助的话,可以点个关注或者点个赞哦~

目录

一.简介

修饰普通同步方法

修饰静态方法

修饰同步代码块

二.synchronized的特性

原子性

可见性

可见性原理:

有序性

可重入性

三.锁升级的过程

1.无锁状态

2.偏向锁状态

3.轻量级锁状态

4.重量级锁状态


一.简介

synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致共享数据不安全,synchronized 关键字就可以用于代码同步。
synchronized主要有3种使用形式:
  • 修饰普通同步方法:
锁的对象是当前实例对象;
  • 修饰静态同步方法:
锁的对象是当前的类的Class字节码对象;
  • 修饰同步代码块:
锁的对象是synchronized后面括号里配置的对象,可以是某个对象,也可以是某个类的.class对象;

感觉很抽象,好,上代码!

修饰普通同步方法

不同的实例,锁就不一样,锁不共享,只能一个一个执行,实现同步操作;

/*** 修饰同步方法,锁的是当前实例对象*/
public class SynchronizedMethodExample {private int count = 0;// 修饰普通同步方法 - 锁的是当前实例对象public synchronized void increment() {System.out.println(Thread.currentThread().getName() + " 进入同步方法");try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}count++;System.out.println(Thread.currentThread().getName() + " 修改count: " + count);}public int getCount() {return count;}public static void main(String[] args) throws InterruptedException {System.out.println("=== 1. 修饰普通同步方法 ===");SynchronizedMethodExample example = new SynchronizedMethodExample();// 创建多个线程操作同一个对象Thread t1 = new Thread(() -> {for (int i = 0; i < 3; i++) {example.increment();}}, "线程A");Thread t2 = new Thread(() -> {for (int i = 0; i < 3; i++) {example.increment();}}, "线程B");t1.start();t2.start();Thread.sleep(10000);//主线程先睡眠,防止主线程执行完了,子线程还没执行完System.out.println("最终结果: " + example.getCount()); // 应该是6}
}

大家可以直接复制代码自己去跑一跑,我这里贴出运行结果

可以看到,每一次都是一个线程调用该方法,实现count变量值的修改,大家可以把syncronized关键字去掉,跑一跑看看结果,这里我直接贴出结果:

可以看到,出现了一种情况,就是一个线程进入了这个方法,在还未修改count变量的值前,另一个线程也进来了,这种情况是非常危险的,即所谓的线程安全问题产生了!

修饰静态方法

该类创建的所有对象都共享这把锁,不会出现阻塞情况;

class SimpleCounter {// 静态同步方法 - 全类共用一把锁public static synchronized void staticTask(String threadName) {System.out.println(threadName + " 进入静态方法 ⚡");try { Thread.sleep(2000); } catch (Exception e) {}System.out.println(threadName + " 离开静态方法 ✅");}// 普通同步方法 - 每个对象有自己的锁public synchronized void instanceTask(String threadName) {System.out.println(threadName + " 进入实例方法 🔒");try { Thread.sleep(1000); } catch (Exception e) {}System.out.println(threadName + " 离开实例方法 ✅");}
}public class VerySimpleExample {public static void main(String[] args) {SimpleCounter obj1 = new SimpleCounter();SimpleCounter obj2 = new SimpleCounter();System.out.println("=== 场景1:静态方法 - 会阻塞 ===");new Thread(() -> SimpleCounter.staticTask("线程A")).start();new Thread(() -> SimpleCounter.staticTask("线程B")).start();// 等上面执行完try { Thread.sleep(3000); } catch (Exception e) {}System.out.println("\n=== 场景2:实例方法 - 不同对象不会阻塞 ===");new Thread(() -> obj1.instanceTask("线程C-obj1")).start();new Thread(() -> obj2.instanceTask("线程D-obj2")).start();// 等上面执行完try { Thread.sleep(3000); } catch (Exception e) {}System.out.println("\n=== 场景3:实例方法 - 同一对象会阻塞 ===");new Thread(() -> obj1.instanceTask("线程E-obj1")).start();new Thread(() -> obj1.instanceTask("线程F-obj1")).start();}
}

运行结果:

我们看场景二,obj2在obj1还没有执行完就能进入该方法,因为刚刚已经说过,该类创建的实例都共享一把锁,好比都能打开这个房间的钥匙;而场景3中,都是obj1这个对象,属于同一实例,因而出现了阻塞同步执行;

修饰同步代码块

和普通同步方法很类似

package 并发.Synchronized;public class SimpleSyncBlock {private int count = 0;private final Object lock = new Object(); // 自定义锁对象public void addCount() {// 同步代码块 - 使用自定义的lock对象作为锁synchronized (lock) {System.out.println(Thread.currentThread().getName() + " 进入同步块");// 模拟一些工作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count++;System.out.println(Thread.currentThread().getName() + " 修改count为: " + count);System.out.println(Thread.currentThread().getName() + " 离开同步块");}}public static void main(String[] args) {SimpleSyncBlock example = new SimpleSyncBlock();// 创建3个线程同时调用addCount方法Thread t1 = new Thread(() -> example.addCount(), "线程A");Thread t2 = new Thread(() -> example.addCount(), "线程B");Thread t3 = new Thread(() -> example.addCount(), "线程C");System.out.println("=== 开始测试同步代码块 ===");t1.start();t2.start();t3.start();try {Thread.sleep(5000);//避免主线程都执行完了三个子线程还没执行完;} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("最终count值: " + example.count);}
}

可以看到三个线程依次进入执行

二.synchronized的特性

  • 原子性

指的是在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不 执行。
上代码!
public class AtomicityExample {private int balance = 1000; // 账户余额// 没有同步 - 非原子操作public void withdrawUnsafe(int amount) {if (balance >= amount) {// 这里可能被其他线程打断,导致数据错误try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款成功,余额: " + balance);} else {System.out.println("余额不足");}}// 使用synchronized保证原子性public synchronized void withdrawSafe(int amount) {if (balance >= amount) {try {Thread.sleep(100); // 模拟处理时间} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款成功,余额: " + balance);} else {System.out.println("余额不足");}}public static void main(String[] args) throws InterruptedException {AtomicityExample account = new AtomicityExample();System.out.println("=== 非原子操作(会出现问题)===");// 创建多个线程同时取款for (int i = 0; i < 5; i++) {new Thread(() -> account.withdrawUnsafe(200), "线程" + i).start();}Thread.sleep(2000);System.out.println("\n=== 原子操作(synchronized保证)===");account.balance = 1000; // 重置余额for (int i = 0; i < 5; i++) {new Thread(() -> account.withdrawSafe(200), "线程" + i).start();}}
}

执行结果对比

  • 可见性

        一个线程修改了共享变量,其他线程能立即看到修改后的值。

package 并发.Synchronized.特性;public class SyncDemo {private static boolean flag = false;private static final Object lock = new Object(); // 锁对象public static void main(String[] args) throws InterruptedException {System.out.println("=== synchronized可见性演示 ===");// 线程A:修改flag(使用同步代码块)Thread threadA = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// ✅ 同步代码块 - 锁对象是lock,获取到对象锁,就执行括号内的方法synchronized (lock) {flag = true;System.out.println("线程A: 在同步代码块内设置 flag = true");}});// 线程B:读取flag(使用同步代码块)Thread threadB = new Thread(() -> {System.out.println("线程B: 开始检查flag");while (true) {// ✅ 同步代码块 - 锁对象是lock,执行完一次就释放synchronized (lock) {System.out.println("------线程B在执行------");if (flag) {System.out.println("线程B: 在同步代码块内检测到flag变为true!");break;}}try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}});threadB.start();threadA.start();threadA.join();threadB.join();System.out.println("演示完成!");}
}

执行结果:

解释:两个线程依次启动,但是由于线程A会睡眠1s,所以对象锁是B先拿到,然后线程B没每在循环体内循环一次,当执行完之后,就会释放锁,又拿到锁,又执行,就在锁释放与锁获取的间隙,线程A争抢到锁,完成修改,然后通过内存屏障,将flag的值刷新到主内存,线程B每次去主内存去拿值(这里就涉及java的内存模型JMM),当拿到更新之后的值后,就退出循环;

可见性原理:

synchronized可见性是通过内存屏障实现的,按可见性划分,内存屏障分为:
  • Load屏障:执行refresh,从其它处理器的高速缓冲、主内存,加载数据到自己的高速缓存、保证数据是最新的;
  • Store屏障:执行flush操作,自己处理器更新的变量的值,刷新到高速缓存、主内存去;
获取锁时,会清空当前线程工作内存中共享变量的副本值,重新从主内存中获取变量最新的值; 释放锁时,
会将工作内存的值重新刷新回主内存
int a = 0;
synchronize (this){ //monitorenter
// Load内存屏障int b = a; //读,通过Load内存屏障,强制执行refresh,保证读到最新的a = 10; //写,释放锁时会通过Store,强制flush到高速缓存或主内存
} //monitorexit
//Store内存屏障

synchornized加锁和自动释放锁是基于moniter监视器实现,底层是c++实现,如下的代码片段,他就是上锁和释放锁的字节码反编译看到的,为什么释放两次呢,因为隐式实现try-catch-finally避免代码抛异常而没有释放锁,这就解释了上面的代码的moniterenter和moniterexit操作;

  • 有序性

有序性是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定 就是我们编写代码时的顺序。
例如,instance = new Singleton()实例化对象的语句分为三步:
1、分配对象的内存空间; 2、初始化对象; 3、设置实例对象指向刚分配的内存地址;
上述第二步操作需要依赖第一步,但是第三步操作不需要依赖第二步,所以执行顺序可能为:1->2->31-
>3->2,当执行顺序为1->3->2时,可能实例对象还没正确初始化,我们直接拿到使用的时候可能会报错。
synchronized的有序性是依靠内存屏障实现的。按照有序性,内存屏障分为:
Acquire屏障:load屏障之后,加Acquire屏障。它会禁止同步代码块内的读操作,和外面的读写操作发生指 令重排;
Release屏障:禁止写操作,和外面的读写操作发生指令重排;
monitorenter 指令和 Load 屏障之后,会加一个 Acquire屏障,这个屏障的作用是禁止同步代码块里面的读操 作和外面的读写操作之间发生指令重排,在 monitorexit 指令前加一个Release屏障,也是禁止同步代码块里面的 写操作和外面的读写操作之间发生重排序。如下:
  • 可重入性

可重入指的就是一个线程可以多次执行synchronized,重复获取同一把锁
public class RenentrantDemo {
// 锁对象
private static Object obj = new Object();
public static void main(String[] args) {
// 自定义Runnable对象
Runnable runnable = () -> {
// 使用嵌套的同步代码块
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "第一次获取锁资源...");
synchronized (obj) {System.out.println(Thread.currentThread().getName() + "第二次获取锁资源...");synchronized (obj) {System.out.println(Thread.currentThread().getName() + "第三次获取锁资源...");}}}
};

可以再参考一下这些文章:

synchronized总结:怎么保证可见性、有序性、原子性?

https://www.cnblogs.com/firsthelloworld/p/18826929

https://blog.csdn.net/weixin_44978801/article/details/147314728

三.锁升级的过程

synchronized 锁升级机制也叫做锁膨胀机制,此机制诞生于 JDK 6 中。在 Java 6 及之前的版本中,synchronized 的实现主要依赖于操作系统的 mutex 锁(重量级锁),而在 Java 6 及之后的版本中,Java 对 synchronized 进行了升级,引入了锁升级的机制,可以更加高效地利用 CPU 的多级缓存,提升了多线程并发性能。

synchronized 锁升级的过程可以分为以下四个阶段:无锁状态、偏向锁、轻量级锁和重量级锁。其中,无锁状态和偏向锁状态都属于乐观锁,不需要进行锁升级,锁竞争较少,能够提高程序的性能。只有在锁竞争激烈的情况下,才会进行锁升级,将锁升级为轻量级锁状态。

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁;

1.无锁状态

当前无任何线程获取它,所以此过程不需要加锁,是无锁状态。当一个线程访问一个同步块时,如果该同步块没有被其他线程占用,那么该线程就可以直接进入同步块,并且将同步块标记为偏向锁状态。

2.偏向锁状态

在偏向锁状态下,同步块已经被一个线程占用,其他线程访问该同步块时,只需要判断该同步块是否被当前线程占用,如果是,则直接进入同步块。这个过程不需要进行任何加锁操作,仍然属于乐观锁状态。

3.轻量级锁状态

如果在偏向锁状态下,有多个线程竞争同一个同步块,那么该同步块就会升级为轻量级锁状态。此时,每个线程都会在自己的 CPU 缓存中保存该同步块的副本,并通过 CAS(Compare and Swap)操作来对同步块进行加锁和解锁。这个过程需要进行加锁操作,但相对于传统的 mutex 锁,轻量级锁的效率要高很多。

4.重量级锁状态

轻量级锁之后会通过自旋来获取锁,自旋执行一定次数之后还未成功获取到锁,此时就会升级为重量级锁,并且进入阻塞状态。

synchronized 锁升级的过程可以有效地减少锁竞争,提高多线程并发性。

http://www.dtcms.com/a/615217.html

相关文章:

  • 网站蜘蛛爬行赣州快车公众号
  • 深入理解Python模块和第三方库使用与管理
  • 郑州网站建设的软件奉贤高端网站建设
  • 【架构】安全(二)
  • 设置一个好的网站导航栏网站多久才会被收录
  • 毕设做网站 方面的论文广州公共交易中心
  • 公司网站建设方案汇报浙江省住房和建设厅网站
  • 论文研读|基于扩散过程的图像篡改定位
  • 网站优化需要金桥网站建设
  • jEasyUI 创建菜单按钮
  • C语言编译单元 | C语言编译过程的详细解析与优化技巧
  • 石家庄推广网站有做网站赚钱的吗
  • 黄冈网站开发油管代理网页
  • asp.net 发布网站 ftp云南做网站多少钱
  • 网站制作 网页显示不全建造网站需要什么
  • 当前网站开发什么语言帝国cms做的网站
  • Linux系统编程——信号
  • 做游戏网站有几个要素北京大兴黄村网站建设
  • 常州高端模板建站别人的域名解析到了我的网站上
  • 广东建设报网站施工企业管理费用包括哪些
  • 第四十二篇:MySQL索引深入:B+Tree原理、最左前缀原则、索引优化
  • Win10 用的 C 语言编译器 | 提升开发效率的最佳选择
  • 做百度快照要先有网站吗手机网站一键开发
  • 能源产品网站建设多少钱网站关键词下降
  • 华容道布局(1):40种经典布局及解法图解合集
  • 网站建设常用模板下载网站推广方法有哪些
  • 没电脑可以建网站吗泰国网站的域名
  • 编译型语言 | 解析编译型语言的特点与应用
  • html5网站后台制作北京南站在哪个街道
  • 企业网站cms 开源广东新闻联播