首页 > 编程语言 >Java-线程-synchronized

Java-线程-synchronized

时间:2024-05-15 12:54:17浏览次数:26  
标签:Java synchronized 对象 实例 线程 SynchronizedSymbolTable fSymbolTable

0.背景

文章目录层级较多,参考我这篇文章来进行展开,方便阅读:博客园SimpleMemory主题如何浮动目录显示

参考文章:

synchronized底层monitor原理


synchronized的底层是基于Java的监视器monitor的。

在Java中,monitor(监视器)是用来实现线程同步的一种基本机制。

它是一个控制结构,内置在每个Java对象中,用于解决多线程之间的互斥和协作问题。

我们先要知道以下2点:

  • 每个对象可以有1个与之关联的锁(也称为互斥锁或monitor锁),这把锁是在对象被用作同步时由 JVM 动态管理的。
  • 每个对象都有3个用于线程间通信的方法
    • wait()
    • notify()
    • notifyAll()

查看Object类,可以看到这几个方法。

其中,notify()notifyAll()wait(long timeout)都是native的,而wait()wait(long timeout, int nanos)则是基于wait(long timeout)的重载。

    public final native void notify();
	
	public final native void notifyAll();

	public final native void wait(long timeout) throws InterruptedException;

	public final void wait() throws InterruptedException {
        wait(0);
    }

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

由于Object是顶层父类,所以任意变量实例化后,都会自动继承其中的方法,wait()、notify()、notifyAll()亦是如此,任何实例对象都会自动具备。

image-20240511220815239

1.概念

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,源码由C++实现,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中src/share/vm/runtime/objectMonitor.hpp

在Java中,监视器(monitor)是与对象关联的,而不是与类、接口或抽象类直接关联。

这里的知识搜了下比较复杂,我们可以先这么简单的理解:

所有对象都有潜在的能力关联监视器,但并不是说每个对象都默认地与一个监视器实体积极关联。

对象的监视器(或说锁)只在必要时,即对象被用于synchronized时,才被激活和使用。

类比我们常规的加锁操作,锁这玩意,是一个外部的东西。

比如老生常谈的厕所这个例子,厕所门上的锁,相对于厕所本身,是个外部的、第三方的东西,我们依据它来裁定使用权。

两个线程出现问题了,涉及到竞态,我们就要寻求一个外部的东西来加锁,这个外部的东西,就是监视器。

这是一种思想,那我们抽象下,不用监视器,我们只要用一个外部的、第三方的能拿来判定的东西就好了。

比如说,分布式环境下,两个程序需要竞争了,我们是不是开始寻求一个外部的裁判redis,来,你们谁能给我拿到,谁就可以操作。

嗯,又比如我两都要去参加王者荣耀的线下比赛,但是我们都是打野位置,到底谁去?嗯 ,简单的方法就是今晚看谁先上2100分,这就是外部的一个判定条件。

就加了个synchronized关键字啊,都没有写锁的代码,当时是底层来帮你处理啦。

2.锁和锁的对象

我们可以先简单的归纳出一个基本的运行模式:

  • 当一个线程尝试进入一个由 synchronized 关键字同步的代码块或方法时,它需要获取那个对象的监视器。
  • 如果监视器是空闲的,则线程获取它,并进入同步代码块。
  • 如果监视器已经被另一个线程持有,则当前线程将阻塞,直到监视器被释放。

2.1 锁定

当使用 synchronized 关键字时,锁定某个对象或类的 Class 对象。

实际上意味着任何线程想要执行该对象或类中的同步代码部分,它必须首先获得那个特定对象或 Class 对象背后的锁。

什么是锁定,锁定就是说,你不能用了,你得等解锁后才能继续。

它是一个状态,代表着已经上锁了,不能乱用。

锁定xx,到底蕴含着什么意思啊?

锁定的关键,在于你锁定谁了,你就必须持有谁背后的那把锁,才能继续操作。

关键字持有,看到锁定,我们就要思考,谁持有?

回顾下,synchronized 主要作用于3个地方:

  • 实例方法
  • 静态方法
  • 同步代码块

2.1.1 实例方法

synchronized 关键字用于实例方法时,它会锁定调用该方法的对象(通常称为 this 对象)。

这意味着,要执行这个同步方法,线程必须首先获得这个对象的锁。

public class Example {
    public synchronized void instanceMethod() {
        // code
    }
}

实例方法,必然是由某个实例对象调用的,假设这个实例对象是A。

假设我们有线程1和2要使用这个对象A的instanceMethod()方法。

现在,线程1获取到这个对象A上的锁后,线程1就锁定对象A,这个时候,线程2未持有对象A的锁,所以,线程2无法执行。

2.1.2 静态方法

synchronized 关键字用于静态方法时,它会锁定这个类的所有对象共享的 Class 对象。

因为静态方法是属于类的而不是某个对象,所以用类本身作为锁的对象。

public class Example {
    public static synchronized void staticMethod() {
        // code
    }
}

静态方法,是直接由类进行调用的。

假设我们有线程1和2要使用这个Example类的staticMethod()方法。

现在,线程1获取到这个Example类的锁后,线程1就锁定Example类,这个时候,线程2未持有Example类的锁,所以,线程2无法执行。

2.1.3 同步代码块

这种方式允许你指定锁定的对象,提供更细粒度的锁控制。

可以是任何对象,例如一个特定的对象实例、类的 Class 对象,或者是方法中的任何其他对象。

public class Example {
    private final Object lock = new Object();

    public void method() {
        synchronized(lock) {
            // synchronized block code
        }
    }
}

同步代码块,可能位于一个实例方法中,也可能位于一个静态方法中,比如下图这样。

image-20240511231219543

那么,还是两个线程1和线程2,他们运行method1()method2()会怎么样呢?

  • method1()

    • 这个方法使用一个非静态同步代码块,锁定的是 lock 对象。
    • lock 是一个实例变量,不同的实例将会拥有不同的 lock 对象。
    • 如果两个线程使用同一个实例调用 method1(),它们将会互相阻塞,因为它们试图获取同一个 lock 对象的监视器。
    • 如果每个线程使用不同的实例调用 method1(),它们将不会互相阻塞,因为每个实例的 lock 对象是不同的,它们各自锁定自己的锁对象。
  • method2()

    • 这个方法使用一个静态同步代码块,锁定的是 LOCK2 对象。
    • LOCK2 是一个静态变量,所有的 类实例共享同一个 LOCK2 对象。
    • 无论哪个线程或哪个实例调用 method2(),它们都会试图获取同一个 LOCK2 对象的监视器。
    • 意味着全局只有一个线程可以在任何时刻进入 method2() 的同步代码块,如果有多个线程尝试调用 method2(),它们将会因为 LOCK2 而互相阻塞。

2.2 扩展

上面的例子看下来,好像懂了,又好像没懂。到底有没有一个简单点的方法论,能让我判断xx线程能不能执行啊?

关键点在于,你要找到这把锁到底锁在哪,当前线程能拿到吗?

跟我重复背诵:1.锁在哪 2.能拿到吗

跟我重复背诵:1.锁在哪 2.能拿到吗

跟我重复背诵:1.锁在哪 2.能拿到吗

2.2.1 demo1

现在再来结合另一个例子演示下

package cn.yang37.thread;

import lombok.extern.slf4j.Slf4j;

/**
 * @description:
 * @class: MySync
 * @author: [email protected]
 * @date: 2024/5/15 11:06
 * @version: 1.0
 */
@Slf4j
public class MySync {

    public static void main(String[] args) {
        Sync sync = new Sync();

        new Thread(
                () -> {
                    sync.m1();
                }, "t1")
                .start();

        new Thread(
                () -> {
                    sync.m2();
                }, "t2")
                .start();

        log.info("mmmmmmmmmmmmmm");
    }
}

@Slf4j
class Sync {

    public void m1() {
        try {
            // Thread.sleep(1000);
            log.info("11111111111111");
        } catch (Exception e) {
            log.error("error...", e);
        }
    }

    public void m2() {
        log.info("22222222222222");
    }
}

main方法中启动了2个线程,分别运行了m1、m2两个方法,这个时候,没有使用synchronized关键字,可以看到输出是无序的,依赖于cpu对t1、t2、main这3个线程的调度顺序。

  • m、2、1

image-20240515112241187

  • 2、1、m

image-20240515112214865

  • 2、m、1

image-20240515112305260

  • 1、m、2

image-20240515112320894

2.2.1.1 添加sleep

尝试为m1方法添加sleep(1000),可以看到,此时会最后输出。

因为线程1、2近似同时启动,由于线程1中会休眠1s,故最后输出信息。

image-20240515112711171

2.2.1.2 m1和m2添加synchronized

将m1和m2变成同步方法,现在再来运行,注意,m1中会sleep。

image-20240515113259592

现在可以看到,111将会固定比222先输出。

方法:看锁定的是谁,当前线程中能拿到吗?

此时,synchronized锁定的是当前对象,线程1和线程2都是访问的sync对象。

  Sync sync = new Sync();

线程1启动后,立马占用了sync这个对象的锁。此时,哪怕线程2想要运行,也拿不到锁,得等线程1先运行完释放。

所以现象就是,222肯定得等111输出完释放锁。

2.2.1.3 m2移除synchronized

image-20240515113910627

现在,又可以看到222比111先输出了。

还是刚才的点,因为m2不是同步方法,不需要获取锁,所以哪怕m1占用了也没事。

2.2.1.4 同时调用m1

image-20240515114037408

此时,由于t1先持有锁,t2必须等待,所以,t2固定后输出111。

2.2.1.5 不同的对象调用m1

image-20240515114209490

此时,由于线程1和2持有的锁不同,一个是sync对象的,一个是sync2对象的,所以不会等待,可以看到t2线程先输出的情况。

因为没有竞争,t1先输出也是正常的。

image-20240515114325566

2.2.1.6 修改为静态方法

虽然会告警不应该通过实例对象调用静态方法,但是此处为了演示,我们先忽略。

image-20240515114612875

此时,线程1固定线程2先输出,哪怕它们用的对象不同,因为,此时锁定的是Sync.class这个类,线程1和2是存在竞争的。

2.2.1.7 代码块

调整为静态代码块,使用this。

image-20240515114909857

this啥意思,就是调用方法的这个玩意,这里是谁,就是sync这个对象,所以,t1固定比t2先输出。

2.2.1.7 代码块中使用同一个对象

为m1添加入参,锁对象由入参传入。

image-20240515115135161

此时t1固定比t2先输出,因为它们都使用的o1对象。

2.2.1.8 代码块中使用不同的对象

调整传入的锁对象分别为o1和o2,可以看到,此时t2可能比t1先输出。

image-20240515115342691

t1比t2先输出,也是正常的。

image-20240515115308427

因为,此时锁定的对象是不同的,分别是o1和o2,所以,不存在竞争关系。

2.2.2 demo2

代码里搜索了下synchronized,咱也不知道这是哪个类,主打一个随机展示。

image-20240515121042324

看到这里有个synchronized,如何分析?

可以看到里面的fSymbolTable,所以关键点就是,fSymbolTable这个玩意咱们的线程1和线程2如何访问的。

找到这个fSymbolTable的来源,哦原来这是个成员变量,从构造方法传入的。

image-20240515121214346

看下这几个构造方法:

    public SynchronizedSymbolTable(SymbolTable symbolTable) {
        fSymbolTable = symbolTable;
    } // <init>(SymbolTable)

    // construct synchronized symbol table of default size
    public SynchronizedSymbolTable() {
        fSymbolTable = new SymbolTable();
    } // init()

    // construct synchronized symbol table of given size
    public SynchronizedSymbolTable(int size) {
        fSymbolTable = new SymbolTable(size);
    } // init(int)

哦,无参没有就新建,有就用这个,同时支持一个传递size的。

那么问题的关键点就是,多个线程能否同时访问到SynchronizedSymbolTable中的fSymbolTable。

    protected SymbolTable fSymbolTable;

我们可以看到方法都是非static的,即实例方法。

image-20240515122022353

实例方法怎么用,实例方法肯定是new出来的对象用的,那么,new出来的这个SynchronizedSymbolTable对象,内部的fSymbolTable是不是唯一的?

显然,每个类中的fSymbolTable这个变量是唯一的,多个线程通过同一个SynchronizedSymbolTable对象访问时,访问到的fSymbolTable是固定的。

故,我们可以有以下结论:

这个SynchronizedSymbolTable的addSymbol()方法是线程安全的

多个线程执行addSymbol()方法时,会锁住SynchronizedSymbolTable中的fSymbolTable,此时别的线程无法操作。


来来来,上上最新的CHATGpt 4o,让它分析下。

image-20240515122720816

2.2.2.1 整体角度
1.方法定义
public String addSymbol(String symbol) {
    synchronized (fSymbolTable) {
        return fSymbolTable.addSymbol(symbol);
    }
}
2.并发安全
  • 同步机制:该方法使用 synchronized 关键字对 fSymbolTable 对象加锁,确保同一时间只有一个线程能够执行 addSymbol 方法,防止并发问题。
  • 锁的对象:锁住的是 fSymbolTable 实例,即实际的 SymbolTable 对象。
3.竞争关系举例
  • 竞争关系:多个线程同时调用 addSymbol 方法时,只有一个线程能获取到 fSymbolTable 的锁,其它线程必须等待锁的释放。

  • 实际例子

    • 场景:假设有两个线程 Thread AThread B,都试图将不同的符号添加到 SymbolTable 中。

    • 操作流程

      • Thread A 调用 addSymbol,获取 fSymbolTable 的锁。

      • Thread A 完成 addSymbol 操作之前,Thread B 也调用 addSymbol,但必须等待 Thread A 释放锁。

      • Thread A 释放锁后,Thread B 获取锁并执行 addSymbol 方法。

4.实际应用例子

假设一个编译器使用 SynchronizedSymbolTable 来存储唯一的标识符。多个线程解析代码并将标识符添加到符号表:

java复制代码SynchronizedSymbolTable symbolTable = new SynchronizedSymbolTable();

// Thread A
new Thread(() -> {
    symbolTable.addSymbol("variableA");
}).start();

// Thread B
new Thread(() -> {
    symbolTable.addSymbol("variableB");
}).start();

在上述例子中,Thread AThread B 会竞争 fSymbolTable 的锁,确保每次只有一个线程能够向符号表中添加符号,从而保证并发安全。

2.2.2.2 成员变量角度

每个 SynchronizedSymbolTable 实例中的 fSymbolTable 变量是唯一的。换句话说,fSymbolTable 是每个 SynchronizedSymbolTable 对象的成员变量,每个实例都有自己的 fSymbolTable 对象。

1.实例唯一性
  • 每个 SynchronizedSymbolTable 实例在创建时,会初始化自己的 fSymbolTable 变量,指向一个新的或现有的 SymbolTable 对象。

  • 例如:

    SynchronizedSymbolTable table1 = new SynchronizedSymbolTable();
    SynchronizedSymbolTable table2 = new SynchronizedSymbolTable();
    
2.锁的作用
  • synchronized (fSymbolTable) 对象锁定 fSymbolTable 的实例,确保每次只有一个线程可以访问该 SymbolTable 实例的方法。
  • 因此,锁的范围仅限于每个 SynchronizedSymbolTable 实例的 fSymbolTable,不会影响其他 SynchronizedSymbolTable 实例。

3.原理

4.锁的升级

标签:Java,synchronized,对象,实例,线程,SynchronizedSymbolTable,fSymbolTable
From: https://www.cnblogs.com/yang37/p/18193611

相关文章

  • 【Elasticsearch】系统已经配置了JAVA_HOME,ElasticSearch源码还是操作gradle失败(JAVA_
    先看下报错内容吧,如下:FAILURE:Buildcompletedwith2failures.1:Taskfailedwithanexception.-----------*Where:Buildfile'/Users/liubolun/IdeaProjects/elasticsearch/benchmarks/build.gradle'line:20*Whatwentwrong:Aproblemoccurredevalu......
  • 【java】【集合类】HashMap之扩容原理
    一、什么是HashMap?HashMap数据结构为数组+链表(JDk1.7),JDK1.8中增加了红黑树,其中:链表的节点存储的是一个Entry对象,每个Entry对象存储四个属性(hash,key,value,next)二、为什么要使用HashMap?对于要求查询次数特别多,查询效率比较高同时插入和删除的次数比较少的情况下,通常会选择Arra......
  • Unrecognized option: --add-opens java.base/java.lang=ALL-UNNAMED
    由于Java中有关反射相关的功能自从JDK9就开始进行了限制,因此如果还想继续在JDK9以及更高的版本中使用反射相关的功能,需要添加JVM启动参数,--add-opensjava.base/java.lang=ALL-UNNAMED。由于现在的项目都是用Docker部署的,所以紧接着在DockerFile中添加了这个JVM启动参数,如下......
  • Java的基础语法
    Java的基础语法1、注释、标识符、关键字Java中的注释有三种,注释并不会被执行,是给人看的。单行注释//注释文字只能够注释一行。多行注释/*多行注释文字*/能够注释一段文字。文档注释/***abcd*aaaa*/和JavaDoc结合使用标识符Java所有的组成部分都需要名字......
  • CPLEX 初识 -- JAVA实现
    CPLEX初识--JAVA实现本文参考《运筹优化常用模型、算法及案例实战》,同时也是笔者用来记录自己所学知识,如有问题欢迎交流讨论~1环境配置&模型建立需要装配jar包及配置VMoptions,如下图所示:-Djava.library.path="/Applications/CPLEX_Studio2211/java"一般使用IloCple......
  • JAVA爬虫使用Selenium自动翻页
    关于Maven<dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.15.3</version></dependency><dependency><groupId>org.seleniumhq.selenium</groupId>......
  • 面试题-JAVA基础
    JAVA有几种基本数据类型,各种类型占用字节大小?64位JVM中,int的长度是多数?Java的Integer缓存池大小是多少?Java中应该使用什么数据类型来描述价格?什么是装箱和拆箱?Java中的值传递和引用传递有什么区别?java8和java9的String类型的区别String,StringBuilder,StringBuffer区别......
  • 【JavaWeb】前后端分离SpringBoot项目快速排错指南
    1发起业务请求打开浏览器开发者工具,同时显示网络(Internet)和控制台(console)接着,清空控制台和网络的内容,如下图然后,点击你的业务按钮,发起请求。首先看控制台有没有报错信息,这是最直观的,看不懂英文就去翻译一下,百度一下如果控制台的信息并没有什么价值,或者指明是后端的问题,那么......
  • java poi之XWPFDocument读取word内容并创建新的word(获取表格所有图片)
    Poi的Word文档结构介绍1、poi之word文档结构介绍之正文段落一个文档包含多个段落,一个段落包含多个Runs,一个Runs包含多个Run,Run是文档的最小单元获取所有段落:Listparagraphs=word.getParagraphs();获取一个段落中的所有Runs:ListxwpfRuns=xwpfParagraph.getRuns();获取......
  • Java面试题 - Java基础
    参考教程【本文参考自以下文章,部分图片及代码片段也取自以下文章,如果构成侵权,请联系我进行修改/删除】【如果构成侵权,请联系我进行修改/删除】【如果构成侵权,请联系我进行修改/删除】【如果构成侵权,请联系我进行修改/删除】自学精灵-首页(本文几乎所有的内容都是自学精灵上......