最近在准备寒假实习的面试,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->3 1-
>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 锁升级的过程可以有效地减少锁竞争,提高多线程并发性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐