深入浅出聊聊synchronized
最近在准备寒假实习的面试,synchronized傻傻搞不明白,相信很多小伙伴像我一开始一样,所以就有了这篇博客,对大家有帮助的话,可以点个关注或者点个赞哦~
目录
一.简介
修饰普通同步方法
修饰静态方法
修饰同步代码块
二.synchronized的特性
原子性
可见性
可见性原理:
有序性
可重入性
三.锁升级的过程
1.无锁状态
2.偏向锁状态
3.轻量级锁状态
4.重量级锁状态
一.简介
- 修饰普通同步方法:
- 修饰静态同步方法:
- 修饰同步代码块:
感觉很抽象,好,上代码!
修饰普通同步方法
不同的实例,锁就不一样,锁不共享,只能一个一个执行,实现同步操作;
/*** 修饰同步方法,锁的是当前实例对象*/
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),当拿到更新之后的值后,就退出循环;
可见性原理:
- 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操作;

-
有序性

-
可重入性
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 锁升级的过程可以有效地减少锁竞争,提高多线程并发性。
