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地位

  1. synchronized是Java关键字,被Java语言原生支持。
  2. synchronized是最基本的互斥同步手段。
  3. 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++,看上去只是一个操作,实际上包含三个动作:

  1. 读取count
  2. 将count加一
  3. 将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种情况(面试重点)

  1. 两个线程同时访问一个对象的同步方法;
    顺序执行,一把锁同时只有一个线程持有。

  2. 两个线程访问的是两个对象的同步方法;
    两个线程互不干扰,此时同步方法不起作用,因为synchronized锁的内容属于不同的实例。

  3. 两个线程访问的是synchronized的静态方法;
    顺序执行,一把锁同时只有一个线程持有。

  4. 同时访问同步方法与非同步方法;
    同步方法与非同步方法之间互不影响

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
两个线程的执行几乎同时开始同时结束,说明同步方法对未同步的方法没有任何影响。
  1. 访问同一个对象的不同的普通同步方法;
    顺序执行,一个把锁同一个时刻只能被一个线程所持有,两个不同的普通同步方法拿的都是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
  1. 同时访问静态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
  1. 方法抛出异常后,会释放锁;
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点核心思想

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待;

  2. 每一个实例都对应自己的一把锁,不同实例之间互不影响(例外: 锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁);

  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁;

可重入性质 - 理论部分

什么是可重入: 同一个线程的外层函数获得锁之后,内存函数可以直接再次获取该锁🔒。

好处: 避免死锁,提供封装性。

粒度: 线程而非调用

情况一: 证明同一个方法是可重入的

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使用注意点
  1. 锁对象不能为空。
  2. 作用域不宜过大。
  3. 避免死锁
  • 如何选择lock和synchronized关键字
  1. 能使用线程安全的工具类尽量避免使用这两种同步方法。
  2. 必须实现同步则优选选择synchronized,原因是
  1. synchronized关键字是Java语法级别的,编写的代码量少、代码执行的过程更加清晰易读。
  2. jdk6之前synchronized性能远小于ReentrantLock但是jdk7之后synchronized增加了很多优化方法其性能与ReentrantLock没有太大差异
  3. synchronized关键字下的线程无论是正常结束还是异常退出都会自动释放锁,而Lock接口的实现类实现的锁必须手动释放,则异常抛出的时候无法释放锁必须做好异常处理。
  1. 需要使用Lock接口的特性比如多个condition()特性时才推荐使用Lock接口下的子类。
posted @   Felix_Openmind  阅读(90)  评论(0编辑  收藏  举报
编辑推荐:
· 从 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》
*{cursor: url(https://files-cdn.cnblogs.com/files/morango/fish-cursor.ico),auto;}
点击右上角即可分享
微信分享提示