Java关键字:synchronized
synchronized简介: 作用、地位、不控制并发的后果
两种用法: 对象锁和类锁
多线程同步方法的7种情况: 是否是static、synchronized
synchronized:可重入、不可中断
加锁解锁原理、可重入原理、可见性原理
synchronized缺陷
synchronized优化
synchronized作用
Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors:
if an object is visible to more than one thread, all reads or writes to that object's variables are done through
synchronized methods.
【synchronized:能够保证【在同一时刻最多只有一个线程执行该段代码】,以保证【并发安全】的效果。】
synchronized地位
- synchronized是Java关键字,被Java语言原生支持。
- synchronized是最基本的互斥同步手段。
- synchronized是并发编程中元老界别的角色,是并发编程的基础。
不使用并发手段的结果?
public class DisappearedRequest1 implements Runnable{ static DisappearedRequest1 dr01 = new DisappearedRequest1(); static int i = 0; public static void main(String [] args) throws InterruptedException { Thread thread01 = new Thread(dr01); Thread thread02 = new Thread(dr01); thread01.start(); thread02.start(); thread01.join(); thread02.join(); System.out.println("i = " + i);// 结果不定 } @Override public void run() { for(int j = 0; j < 100000; j++){ i++; } } }
不使用并发手段会有什么结果?
代码实战: 两个线程同时a++,最后结果会比预计的少。
原因:
count++,看上去只是一个操作,实际上包含三个动作:
- 读取count
- 将count加一
- 将count值写入内存中
但是如果A线程将 i++ 计算为 i=201计算之后在放入内存之前又被另一个B线程执行,B线程执行是i=200的时刻没有读到i=201,这样就会造成数据不一致性。线程不安全情况出现。
synchronized的两个用法
🍎对象锁
对象锁包括【方法锁(默认锁对象为this当前实例对象)】和【同步代码块锁(自己指定锁对象)】
- 代码块形式: 手动指定锁对象
public class SynchronizedObjectCodeBlock2 implements Runnable{ static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2(); @Override public void run() { synchronized (this){ System.out.println("对象锁的同步代码块形式, Thread-Name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结果"); } } public static void main(String[] args) { Thread thread01 = new Thread(instance); Thread thread02 = new Thread(instance); thread01.start(); thread02.start(); while(thread01.isAlive() || thread02.isAlive()){ } System.out.println("finished"); } } // 对象锁的同步代码块形式, Thread-Name: Thread-0 // Thread-0运行结果 // 对象锁的同步代码块形式, Thread-Name: Thread-1 // Thread-1运行结果 // finished // =================================采用不同锁对象=================================== // 不同的线程持有不同的锁对象可以实现宏观上并发执行 Object lock01 = new Object(); // 第一个锁对象 Object lock02 = new Object(); // 第二个锁对象 @Override public void run() { synchronized (lock01){ System.out.println("对象锁的同步代码块形式, Thread-Name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println("lock01结束: " + Thread.currentThread().getName() + "运行结果"); } synchronized (lock02){ System.out.println("对象锁的同步代码块形式, Thread-Name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println("lock02结束: " + Thread.currentThread().getName() + "运行结果"); } }
- 方法锁形式: synchronized修饰普通方法,锁对象默认为this
public class SynchronizedObjectMethod3 implements Runnable{ static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3(); public static void main(String[] args) { Thread thread01 = new Thread(instance); Thread thread02 = new Thread(instance); thread01.start(); thread02.start(); while(thread01.isAlive() || thread02.isAlive()){ } System.out.println("finished"); } public synchronized void method(){ System.out.println("对象锁的方法" + " Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){e.printStackTrace();} } @Override public void run() { method(); } } // 对象锁的方法 Thread's name: Thread-0 // 对象锁的方法 Thread's name: Thread-1 // finished
🍎类锁
类锁包括【synchronized修饰的静态方法】和【指定锁为Class对象】
本质: 所谓的类锁,不过是Class对象的锁而已。
作用: 类锁只能同一个时刻被一个对象拥有。
概念: Java类可以有多个对象,但是只有1个Class对象
- 形式一: synchronized加在static方法上
public class SynchronizedClassStatic4 implements Runnable{ static SynchronizedClassStatic4 instance01 = new SynchronizedClassStatic4(); static SynchronizedClassStatic4 instance02 = new SynchronizedClassStatic4(); public static synchronized void method(){ System.out.println("类锁的第一种形式: static方法" + ", Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){e.printStackTrace();} System.out.println("运行结束!"); } @Override public void run() { method(); } public static void main(String[] args) { Thread thread01 = new Thread(instance01); Thread thread02 = new Thread(instance02); thread01.start(); thread02.start(); while(thread01.isAlive() || thread02.isAlive()){} System.out.println("finished"); } } // 类锁的第一种形式: static方法, Thread's name: Thread-0 // 运行结束! // 类锁的第一种形式: static方法, Thread's name: Thread-1 // 运行结束! // finished
- 形式二: synchronized加在(*.class)代码块上
public class SynchronizedClassClass implements Runnable{ static SynchronizedClassClass instance01 = new SynchronizedClassClass(); static SynchronizedClassClass instance02 = new SynchronizedClassClass(); public static void main(String[] args) { Thread thread01 = new Thread(instance01); Thread thread02 = new Thread(instance02); thread01.start(); thread02.start(); while(thread01.isAlive() || thread02.isAlive()){} System.out.println("finished"); } @Override public void run() { method(); } private void method(){ synchronized (SynchronizedClassClass.class){ System.out.println("类锁的第二种形式: Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){e.printStackTrace();} System.out.println("运行结束!"); } } } // 类锁的第二种形式: Thread's name: Thread-0 // 运行结束! // 类锁的第二种形式: Thread's name: Thread-1 // 运行结束! // finished
消失的请求解决办法
🌈多线程访问同步方法的7种情况(面试重点)
-
两个线程同时访问一个对象的同步方法;
顺序执行,一把锁同时只有一个线程持有。 -
两个线程访问的是两个对象的同步方法;
两个线程互不干扰,此时同步方法不起作用,因为synchronized锁的内容属于不同的实例。 -
两个线程访问的是synchronized的静态方法;
顺序执行,一把锁同时只有一个线程持有。 -
同时访问同步方法与非同步方法;
同步方法与非同步方法之间互不影响
public class SynchronizedYesAndNo6 implements Runnable{ static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6(); public static void main(String[] args) { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()){} System.out.println("finished"); } @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())){ method1(); }else{ method2(); } } public synchronized void method1(){ System.out.println("同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } public void method2(){ System.out.println("未同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } } // 同步的普通方法 Thread's name: Thread-0 // 未同步的普通方法 Thread's name: Thread-1 // Thread-1运行结束! // Thread-0运行结束! // finished
两个线程的执行几乎同时开始同时结束,说明同步方法对未同步的方法没有任何影响。
- 访问同一个对象的不同的普通同步方法;
顺序执行,一个把锁同一个时刻只能被一个线程所持有,两个不同的普通同步方法拿的都是this对象
所以会按照线程调用的顺序依次执行。
public class SynchronizedDifferentMethod7 implements Runnable{ static SynchronizedDifferentMethod7 instance = new SynchronizedDifferentMethod7(); public static void main(String[] args) { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()){} System.out.println("finished"); } @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())){ method1(); }else{ method2(); } } public synchronized void method2(){ System.out.println("method2同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } public synchronized void method1(){ System.out.println("method1同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } } // method1同步的普通方法 Thread's name: Thread-0 // Thread-0运行结束! // method2同步的普通方法 Thread's name: Thread-1 // Thread-1运行结束! // finished
- 同时访问静态synchronized和非静态synchronized方法;
静态同步方法和非静态同步方法他们加的是不一样的锁,静态同步方法加的是类锁,非静态同步方法加的是对象锁,他们之间互不影响,所以宏观上会并行执行。
public class SynchronizedStaticAndNormalMethod8 implements Runnable{ static SynchronizedStaticAndNormalMethod8 instance = new SynchronizedStaticAndNormalMethod8(); public static void main(String[] args) { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()){} System.out.println("finished"); } @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())){ method1(); }else{ method2(); } } // 非静态普通同步方法 public synchronized void method2(){ System.out.println("method2同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } // 静态类同步方法 public synchronized static void method1(){ System.out.println("method1同步的类方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } } // method2同步的普通方法 Thread's name: Thread-1 // method1同步的类方法 Thread's name: Thread-0 // Thread-0运行结束! // Thread-1运行结束! // finished
- 方法抛出异常后,会释放锁;
public class SynchronizedException9 implements Runnable{ static SynchronizedException9 instance = new SynchronizedException9(); public static void main(String[] args) { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); while(t1.isAlive() || t2.isAlive()){} System.out.println("finished"); } @Override public void run() { if("Thread-0".equals(Thread.currentThread().getName())){ method01(); }else{ method02(); } } // 非静态普通同步方法 public synchronized void method01(){ System.out.println("method01同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } throw new RuntimeException(); } // 非静态普通同步方法 public synchronized void method02(){ System.out.println("method02同步的普通方法 Thread's name: " + Thread.currentThread().getName()); try{ Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束!"); } } // method01同步的普通方法 Thread's name: Thread-0 // Exception in thread "Thread-0" java.lang.RuntimeException // at ink.openmind.concurrentlearn.SynchronizedException9.method01(SynchronizedException9.java:39) // at ink.openmind.concurrentlearn.SynchronizedException9.run(SynchronizedException9.java:25) // at java.base/java.lang.Thread.run(Thread.java:834) // method02同步的普通方法 Thread's name: Thread-1 // Thread-1运行结束! // finished
7种情况总结: 3点核心思想
-
一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待;
-
每一个实例都对应自己的一把锁,不同实例之间互不影响(例外: 锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁);
-
无论是方法正常执行完毕或者方法抛出异常,都会释放锁;
可重入性质 - 理论部分
什么是可重入: 同一个线程的外层函数获得锁之后,内存函数可以直接再次获取该锁🔒。
好处: 避免死锁,提供封装性。
粒度: 线程而非调用
情况一: 证明同一个方法是可重入的
public class SynchronizedRecursion10 { int a = 0; public static void main(String[] args) { SynchronizedRecursion10 instance = new SynchronizedRecursion10(); instance.method1(); } // 1. 证明同一个方法是可重入的 private synchronized void method1(){ System.out.println("method1中a = " + a); if(a == 0){ a ++; method1(); } } // method1中a = 0 // method1中a = 1 }
情况二: 证明可重入不要求是同一个方法
public class SynchronizedOtherMethod11 { public synchronized void method1(){ System.out.println("===method1==="); method2(); } private synchronized void method2() { System.out.println("===method2==="); } public static void main(String[] args) { SynchronizedOtherMethod11 instance = new SynchronizedOtherMethod11(); instance.method1(); } } //===method1=== //===method2===
情况三: 证明可重入不要求是同一个类中的
public class SynchronizedSuperClass12 { public synchronized void doSth(){ System.out.println("===父类方法==="); } } class Son extends SynchronizedSuperClass12{ @Override public synchronized void doSth(){ System.out.println("===子类方法==="); super.doSth(); // 调用父类方法 } public static void main(String[] args) { Son son = new Son(); son.doSth(); } } //===子类方法=== //===父类方法===
不可中断性质
一旦这个锁已经被别人获得了,如果我还想获得,只能选择等待或阻塞,直到别的线程释放这个锁。如果别人用于不释放锁,那么我只能永远地等下去。
加锁和释放锁地原理
public class SynchronizedToLock13 { Lock lock = new ReentrantLock(); public synchronized void method1(){ System.out.println("Synchronized形式地同步锁"); } public void method2(){ lock.lock(); try{ System.out.println("lock形式地锁"); }finally{ lock.unlock(); } } public static void main(String[] args) { SynchronizedToLock13 instance = new SynchronizedToLock13(); instance.method1(); instance.method2(); // synchronized形式地同步锁 // lock形式地锁 } }
现象、时机、深入JVM字节码
E:\A_JavaEE\base-learn\src\ink\openmind\concurrentlearn>javap -verbose Decompilation14.class Classfile /E:/A_JavaEE/base-learn/src/ink/openmind/concurrentlearn/Decompilation14.class Last modified 2021年9月7日; size 499 bytes MD5 checksum 40bd9113d0f02182da047a5adc17b734 Compiled from "Decompilation14.java" public class ink.openmind.concurrentlearn.Decompilation14 minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #4 // ink/openmind/concurrentlearn/Decompilation14 super_class: #2 // java/lang/Object interfaces: 0, fields: 1, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#18 // java/lang/Object."<init>":()V #2 = Class #19 // java/lang/Object #3 = Fieldref #4.#20 // ink/openmind/concurrentlearn/Decompilation14.obj:Ljava/lang/Object; #4 = Class #21 // ink/openmind/concurrentlearn/Decompilation14 #5 = Utf8 obj #6 = Utf8 Ljava/lang/Object; #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 insert #12 = Utf8 (Ljava/lang/Thread;)V #13 = Utf8 StackMapTable #14 = Class #22 // java/lang/Thread #15 = Class #23 // java/lang/Throwable #16 = Utf8 SourceFile #17 = Utf8 Decompilation14.java #18 = NameAndType #7:#8 // "<init>":()V #19 = Utf8 java/lang/Object #20 = NameAndType #5:#6 // obj:Ljava/lang/Object; #21 = Utf8 ink/openmind/concurrentlearn/Decompilation14 #22 = Utf8 java/lang/Thread #23 = Utf8 java/lang/Throwable { public ink.openmind.concurrentlearn.Decompilation14(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class java/lang/Object 8: dup 9: invokespecial #1 // Method java/lang/Object."<init>":()V 12: putfield #3 // Field obj:Ljava/lang/Object; 15: return LineNumberTable: line 10: 0 line 11: 4 public void insert(java.lang.Thread); descriptor: (Ljava/lang/Thread;)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_0 1: getfield #3 // Field obj:Ljava/lang/Object; 4: dup 5: astore_2 6: monitorenter // 🍎 7: aload_2 8: monitorexit // 🍉 9: goto 17 12: astore_3 13: aload_2 14: monitorexit // 🍉 15: aload_3 16: athrow 17: return Exception table: from to target type 7 9 12 any 12 15 12 any LineNumberTable: line 14: 0 line 16: 7 line 17: 17 StackMapTable: number_of_entries = 2 locals = [ class ink/openmind/concurrentlearn/Decompilation14, class java/lang/Thread, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 } SourceFile: "Decompilation14.java"
- monitorenter:
若monitor的进入次数为0,线程可以进入,并将monitor进入的次数设为1,当前线程成为montiro的owner; 若线程已拥有monitor的所有权,允许它重入monitor,进入一次次数+1 ; 若其他线程已经占有monitor,当前尝试获取monitor的线程会被阻塞,直到进入次数为变0,才能重新被再次获取。
- monitorexit:
能执行monitorexit指令的线程,一定是拥有当前对象的monitor所有权的。当执行monitorexit指令计数器减到为0时,当前线程就不再拥有monitor所有权。其他被阻塞的线程即可再一次去尝试获取这个monitor的所有权。 上面编译出来的指令,其实monitoreexit是有两个:需要保证如果同步代码块执行抛出了异常,则也需要释放锁对象。
可重入原理: 加锁次数计数器
- JVM负责跟踪对象被加锁的次数。
- 线程第一次给对象加锁的时候,计数变为1,每当这个相同的线程在次对象上再次获得锁时,计数会递增
- 当该对象上的加锁次数计数器变为0完全释放锁,则其他阻塞线程就有机会获得该对象上的锁
可见性原理: 内存模型
主内存是线程之间通信的桥梁。
synchronized缺陷
- 效率低: 锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
- 不够灵活: 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
- 无法知道是否成功获得锁
常见面试题
- synchronized使用注意点
- 锁对象不能为空。
- 作用域不宜过大。
- 避免死锁
- 如何选择lock和synchronized关键字
- 能使用线程安全的工具类尽量避免使用这两种同步方法。
- 必须实现同步则优选选择synchronized,原因是
- synchronized关键字是Java语法级别的,编写的代码量少、代码执行的过程更加清晰易读。
- jdk6之前synchronized性能远小于ReentrantLock但是jdk7之后synchronized增加了很多优化方法其性能与ReentrantLock没有太大差异
- synchronized关键字下的线程无论是正常结束还是异常退出都会自动释放锁,而Lock接口的实现类实现的锁必须手动释放,则异常抛出的时候无法释放锁必须做好异常处理。
- 需要使用Lock接口的特性比如多个condition()特性时才推荐使用Lock接口下的子类。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
2020-09-07 视图渲染+Spring MVC进阶技术 -- 《Spring In Action》