java synchronized详解

news2024/11/17 6:46:55

背景

在多线程环境下同时访问共享资源会出现一些数据问题,此关键字就是用来保证线程安全的解决这一问题。

内存可见的问题

在了解synchronized之前先了解一下java内存模型,如下图:
image.png

  1. 线程1去主内存获取x的值读入本地内存此时x的值为1,进行运算x+1此时线程1的x值为2,然后写入主内存;
  2. 此时在线程1先入主内存之前,此时线程2去主内存读取了x的值,它读取到的值是1
  3. 最后x的值在主内存里的值是2,线程2读取到的是1,出现了内存不可见的问题。

synchronized关键字的使用方式

修饰方法

public class SynchronizedMethodExample {

    private int counter = 0;

    // synchronized 修饰的方法
    public synchronized void increment() {
        // 这里的操作是原子的,同一时刻只有一个线程能够执行
        counter++;
        System.out.println("Incremented counter to: " + counter);
    }

    public static void main(String[] args) {
        SynchronizedMethodExample example = new SynchronizedMethodExample();

        // 创建多个线程,同时调用 increment 方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        try {
            // 等待两个线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的计数器值
        System.out.println("Final counter value: " + example.counter);
    }
}

由于 synchronized 修饰了 increment 方法,保证了同一时刻只有一个线程能够执行该方法。因此,两个线程在执行 increment 方法时会互斥,不会同时对 counter 进行操作。
执行结果

Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10

修饰同步代码块

public class SynchronizedBlockExample {

    private int counter = 0;
    private final Object lockObject = new Object();  // 用于同步的对象

    public void increment() {
        // 一些非同步的代码

        synchronized (lockObject) {
            // 需要同步的代码块
            counter++;
            System.out.println("Incremented counter to: " + counter);
        }

        // 继续执行非同步的代码
    }

    public static void main(String[] args) {
        SynchronizedBlockExample example = new SynchronizedBlockExample();

        // 创建多个线程,同时调用 increment 方法
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        try {
            // 等待两个线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的计数器值
        System.out.println("Final counter value: " + example.counter);
    }
}

在这个例子中,increment 方法包含了一个同步的代码块,使用 synchronized (lockObject)counter 进行递增操作。由于使用了 lockObject 作为同步对象,保证了两个线程在执行同步代码块时是互斥的,不会同时对 counter 进行操作。
执行结果:

Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10

修饰静态方法

public class SynchronizedStaticMethodExample {

    private static int counter = 0;

    // synchronized 修饰的静态方法
    public static synchronized void increment() {
        // 这里的操作是原子的,同一时刻只有一个线程能够执行
        counter++;
        System.out.println("Incremented counter to: " + counter);
    }

    public static void main(String[] args) {
        // 创建多个线程,同时调用静态方法 increment
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                SynchronizedStaticMethodExample.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                SynchronizedStaticMethodExample.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        try {
            // 等待两个线程执行完毕
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印最终的计数器值
        System.out.println("Final counter value: " + SynchronizedStaticMethodExample.counter);
    }
}

在这个例子中,increment 方法是一个静态方法,使用 synchronized 修饰。由于是静态方法,它锁定的是整个类的 Class 对象。两个线程无法同时调用 increment 方法,确保了对 counter 的递增操作是线程安全的。

Incremented counter to: 1
Incremented counter to: 2
Incremented counter to: 3
Incremented counter to: 4
Incremented counter to: 5
Incremented counter to: 6
Incremented counter to: 7
Incremented counter to: 8
Incremented counter to: 9
Incremented counter to: 10
Final counter value: 10

synchronized原理

java对象的组成

在讲原理之前我们先了解一下java对象的组成:
image.png
实例数据

  • 存放类的属性数据信息,包括父类的属性信息。

对齐填充

  • 由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

对象头

  • 标志位(Flags):
    • Mark Word 的一些位用于存储对象的状态标志,例如是否被锁定、是否是偏向锁、是否是轻量级锁、是否是GC标记等。这些标志位的组合形成对象的状态信息。
  • 锁信息:
    • 存储锁相关的信息,用于实现对象的同步机制。
    • 对象可以处于无锁状态、偏向锁状态、轻量级锁状态或重量级锁状态。
  • 哈希码:
    • 用于支持对象的哈希操作,例如在哈希表中查找对象。
    • 哈希码是对象的标识,有助于提高哈希表的性能。
  • 对象分代年龄:
    • 用于支持分代垃圾回收算法。
    • 标识对象被创建后经历的垃圾回收次数。
  • 其他:
    • 可能还包含其他与垃圾回收、锁定等相关的信息。

ObjectMonitor

HotSpot虚拟机源码中ObjectMonitor.hpp。

ObjectMonitor::ObjectMonitor() {
    _header       = NULL;         // 监视器头部,用于保存状态信息
    _count        = 0;            // 计数器,用于记录监视器的使用次数
    _waiters      = 0;            // 等待线程数
    _recursions   = 0;            // 当前线程对该锁的递归次数
    _object       = NULL;         // 监视的对象
    _owner        = NULL;         // 拥有锁的线程
    _WaitSet      = NULL;         // 等待队列,存储等待该锁的线程
    _WaitSetLock  = 0 ;           // 用于保护等待队列的锁
    _Responsible  = NULL ;        // 释放锁的线程
    _succ         = NULL ;        // 后继监视器
    _cxq          = NULL ;        // 入口等待队列
    FreeNext      = NULL ;        // 空闲监视器链表的下一个
    _EntryList    = NULL ;        // 入口列表
    _SpinFreq     = 0 ;           // 自旋频率
    _SpinClock    = 0 ;           // 自旋时钟
    OwnerIsThread = 0 ;           // 拥有者是否为线程
}

ObjectMonitor.hpp 是 HotSpot 虚拟机(OpenJDK 的默认虚拟机实现)中用于实现对象监视器的头文件。对象监视器在 Java 中由 synchronized 关键字提供支持,用于实现多线程之间的同步。以下是对 ObjectMonitor.hpp 的一些关键部分的简要解释:

  1. ObjectMonitor 结构体:
    ObjectMonitor 是一个结构体,表示对象监视器。它包含了维护监视器状态和控制线程访问的各种信息。主要字段包括:
  • header:用于保存监视器的状态信息,如锁的状态、等待队列等。
  • owner:指向当前拥有锁的线程。
  • wait_set:等待队列,用于存储等待该锁的线程。
  • 等等。
  1. ObjectMonitor 头部(header):
    ObjectMonitor 的头部包含了一系列标志位,用于表示锁的状态、等待队列的状态等。这些标志位在字节层面上被设置和检查,以进行对锁的操作。一些常见的标志位有:
  • INFLATED:表示锁已经被膨胀,即从轻量级锁升级为重量级锁。
  • CONTENTION:表示锁有竞争。
  • HELD_EXCLUSIVELY:表示锁被当前线程独占。
  1. 等待队列(WaitSet):
    ObjectMonitor 中包含一个等待队列,用于存储等待该锁的线程。线程在等待队列中等待时,它会进入等待状态,直到被唤醒。等待队列的管理涉及到线程的入队和出队等操作。
  2. 线程入队和出队:
    ObjectMonitor 定义了一些方法,用于线程的入队和出队操作。例如:
  • void enter(Handle h):线程尝试进入临界区。
  • void exit():线程退出临界区。
  • void wait(bool, jlong, jlong):线程进入等待状态。
  • void notify()void notifyAll():唤醒一个或所有等待线程。
  1. 锁的状态转换:
    ObjectMonitor 定义了一些方法来实现锁状态的转换,例如从无锁到轻量级锁、从轻量级锁到重量级锁等。这些状态的转换涉及到了底层的原子操作和 CAS(Compare and Swap)等机制。
  2. 适应性自旋锁:
    HotSpot 虚拟机中的 ObjectMonitor 还包括适应性自旋锁的机制,该机制用于在获取锁时进行自旋,以避免线程进入阻塞状态。适应性自旋锁的目标是根据程序运行时的历史信息来动态调整自旋次数,以提高性能。

image.png

  1. 当多个线程同时访问同步代码块时,首先会进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,同时count加1,若发现之前的owner的值就是指向当前线程的,recursions也需要加1。如果CAS尝试获取锁失败,则进入到EntryList中;

  2. 当获取锁的线程调用wait()方法,则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒;

  3. 当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁.

synchronized作用于同步代码块的字节码指令

在Java中,synchronized 作用于同步代码块的字节码指令主要涉及到 monitorentermonitorexit 指令。这两个指令用于实现监视器(monitor)的进入和退出,即获取和释放锁。
以下是一个简单的Java同步代码块的例子:

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

    public void synchronizedMethod() {
        synchronized (lock) {
            // 同步代码块
            // ...
        }
    }
}

对应的字节码大致如下:

public synchronizedMethod()V
   L0
    LINENUMBER 7 L0
    GETSTATIC SynchronizedExample.lock : Ljava/lang/Object;
    DUP
    ASTORE 1
    MONITORENTER   // monitorenter 指令,获取锁
    // 同步代码块的字节码指令
    // ...
   L1
    LINENUMBER 9 L1
    ALOAD 1
    MONITOREXIT    // monitorexit 指令,释放锁
    ATHROW

解释一下上述字节码:

  1. GETSTATIC SynchronizedExample.lock : Ljava/lang/Object;: 获取 lock 字段的值,即获取锁对象。
  2. DUP: 复制栈顶数值,用于后续的 ASTORE 1 操作。
  3. ASTORE 1: 将锁对象的引用存储到局部变量1。
  4. MONITORENTER: 进入监视器,即获取锁。如果锁已经被其他线程占用,当前线程将阻塞等待。
  5. 同步代码块的具体实现。
  6. ALOAD 1: 将之前存储的锁对象引用加载到栈顶。
  7. MONITOREXIT: 退出监视器,即释放锁。
  8. ATHROW: 抛出异常,确保在任何情况下都会释放锁。

synchronized作用于方法字节码指令

以下是一个简单的例子,演示了 synchronized 修饰方法的字节码指令:

public class SynchronizedMethodExample {
    private int counter = 0;

    public synchronized void synchronizedMethod() {
        counter++;
    }
}

对应的字节码可能类似于:

public class SynchronizedMethodExample {
    private int counter;

    public SynchronizedMethodExample() {
        counter = 0;
    }

    public synchronized void synchronizedMethod() {
        // 获取锁
        monitorenter

        try {
            // 同步代码块
            counter++;
        } finally {
            // 释放锁
            monitorexit
        }
    }
}

在这个例子中:

  1. monitorenter 指令:在进入同步代码块之前获取锁。
  2. monitorexit 指令:在同步代码块执行完毕后释放锁。

synchronized锁的优化

JDK1.5之前,synchronized是属于重量级锁,重量级需要依赖于底层操作系统的Mutex Lock实现,然后操作系统需要切换用户态和内核态,这种切换的消耗非常大,所以性能相对来说并不好。

  1. 偏向锁(Biased Locking):
    • 在程序刚启动时,对象的锁大多数情况下只被一个线程所持有。为了提高性能,引入了偏向锁机制。
    • 当一个线程获取了对象的锁后,会在对象头的 Mark Word 中记录这个线程的 ID,表示这个锁被偏向于该线程。之后,该线程再次进入同步块时,无需再竞争锁,直接获得。
    • 偏向锁的目标是降低无竞争情况下的锁操作的开销。
  2. 轻量级锁(Lightweight Locking):
    • 当多个线程争夺同一个锁时,偏向锁会升级为轻量级锁。
    • 轻量级锁使用 CAS 操作来避免传统的互斥量(Mutex)的开销。如果有多个线程竞争同一个锁,会使用 CAS 操作来尝试获取锁,而不是阻塞线程。
  3. 自旋锁和自适应自旋锁:
    • 在无法获取锁时,线程可能会进行一定次数的自旋等待。自旋是一种忙等待的策略,避免了线程的阻塞和唤醒带来的开销。
    • 自适应自旋锁会根据锁的持有时间和竞争情况来动态调整自旋的次数,以在不同的场景下取得更好的性能。
  4. 锁消除和锁粗化:
    • 锁消除是指在编译期间,对一些明显不会发生竞争的锁进行消除,从而减少锁的使用。
    • 锁粗化是指将多个连续的操作都加锁,从而减少加锁和解锁的次数。

总结

synchronized 是 Java 中用于实现线程同步的关键字,它提供了对代码块、方法以及静态方法的同步支持。以下是关于 synchronized 锁的总结:

  1. 对象锁:
  • synchronized 可以用于实现对对象的同步,确保同一时刻只有一个线程能够访问被同步的代码块或方法。
  • 对象锁的粒度可以是对象实例(实例方法)或类(静态方法)。
  1. 方法锁:
  • synchronized 可以直接修饰方法,使整个方法具有同步性。此时,锁对象是方法所属的对象实例。
  • 修饰实例方法时,锁对象是方法调用的实例;修饰静态方法时,锁对象是类的 Class 对象。
  1. 代码块锁:
  • synchronized 还可以用于修饰代码块,指定锁的粒度更加灵活。
  • 在代码块中,需要指定一个对象作为锁,多个线程只有在获取了相同的锁时才会争夺执行权。
  1. 锁的释放:
  • 当一个线程获得了对象锁后,其他线程必须等待该线程释放锁才能进入同步代码块或方法。
  • 如果线程执行的同步代码块出现异常,锁会被自动释放。
  1. 偏向锁、轻量级锁和重量级锁:
  • 为了减小锁的操作开销,Java 中引入了偏向锁、轻量级锁和重量级锁的概念。
  • 在无竞争的情况下,偏向锁能够提高性能;在低竞争的情况下,轻量级锁可以减小锁的争用开销。
  1. 可重入性:
  • synchronized 具有可重入性,即同一个线程可以多次获得同一把锁而不会出现死锁。
  1. 性能优化:
  • Java 虚拟机和编译器对 synchronized 进行了一系列优化,包括偏向锁、轻量级锁、自适应自旋等,以提高多线程程序的性能。

总体而言,synchronized 是一种简单而有效的同步机制,用于确保多线程程序中对共享资源的安全访问。然而,在一些高并发场景下,可能需要考虑使用更灵活的锁机制,如 java.util.concurrent 包中提供的锁。

参考

https://zhuanlan.zhihu.com/p/377423211
https://juejin.cn/post/6973571891915128846

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1283863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux(14):进程管理

一个程序被加载到内存当中运作&#xff0c;那么在内存内的那个数据就被称为进程(process)。 进程是操作系统上非常重要的概念&#xff0c;所有系统上面跑的数据都会以进程的型态存在。 进程 在 Linux底下所有的指令与能够进行的动作都与权限有关&#xff0c;而系统如何判定权…

大数据技术学习笔记(四)—— HDFS

目录 1 HDFS 概述1.1 HDFS 背景与定义1.2 HDFS 优缺点1.3 HDFS 组成架构1.4 HDFS 文件块大小 2 HDFS的shell操作2.1 上传2.2 下载2.3 HDFS直接操作 3 HDFS的客户端操作3.1 Windows 环境准备3.2 获取 HDFS 的客户端连接对象3.3 HDFS文件上传3.4 HDFS文件下载3.5 HDFS删除文件和目…

Vue项目解决van-calendar 打开下拉框显示空白(白色),需滑动一下屏幕,才可正常显示

问题描述&#xff0c;如图 ipad(平板&#xff09;或者 H5移动端引入Vant组件的日历组件&#xff08;van-calendar&#xff09;&#xff0c;初始化显示空白&#xff0c;需滚动一下屏幕&#xff0c;才可正常显示 解决方法 需在van-calendar上绑定open"openCalendar"事件…

升辉清洁IPO:广东清洁服务“一哥”还需要讲好全国化的故事

近日&#xff0c;广东物业清洁服务“一哥”升辉清洁第四次冲击IPO成功&#xff0c;拟于12月5日在香港主板挂牌上市。自2021年4月第一次递交招股书&#xff0c;时隔两年半&#xff0c;升辉清洁终于拿到了上市的门票。 天眼查显示&#xff0c;升辉清洁成立于2000年&#xff0c;主…

基于SpringBoot+Vue的前后端分离的房屋租赁系统2

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 开发过程中&#xff0…

最强AI之风袭来,你爱了吗?

2017年&#xff0c;柯洁同阿尔法狗人机大战&#xff0c;AlphaGo以3比0大获全胜&#xff0c;一代英才泪洒当场...... 2019年&#xff0c;换脸哥视频“杨幂换朱茵”轰动全网&#xff0c;时至今日AI换脸仍热度只增不减&#xff1b; 2022年&#xff0c;ChatGPT一经发布便轰动全球&a…

聚焦工业生产安全,汉威科技推出最轻最小迷你便携式单气体检测仪

有毒有害气体、可燃气体等是工业生产中常见的危险因素。我国已经出台了多项法律法规、行业标准&#xff0c;在石油、化工、钢铁冶金、危化品、矿业等行业以及有限空间相关场所作业&#xff0c;应当佩戴便携式气体检测仪&#xff0c;以保障作业人员的安全。 然而&#xff0c;近年…

从零开始搭建博客网站-----框架页

实现效果如下 发布的功能还没有实现&#xff0c;仅仅实现了简单的页面显示 关键代码如下 <template><div class"layout"><el-header class"header"><div class"logo">EasyBlog</div></el-header><el-c…

初识动态规划算法(题目加解析)

文章目录 什么是动态规划正文力扣题第 N 个泰波那契数三步问题使用最小花费爬楼梯 总结 什么是动态规划 线性动态规划&#xff1a;是可以用一个dp表来存储内容&#xff0c;并且找到规律存储,按照规律存储。让第i个位置的值等于题目要求的答案 >dp表&#xff1a;dp表就是用一…

Trello软件:从功能、使用技巧到替代软件等,一文弄懂项目管理必备工具!

Trello是什么&#xff1f; Trello是一款基于Web的可视化项目管理工具&#xff0c;它旨在提供一种灵活、易于理解和使用的任务管理系统。Trello将复杂的项目管理流程简化为直观的卡片和列表&#xff0c;使团队成员能轻松看到整个项目的状态&#xff0c;并了解自己的责任和任务。…

【React设计】React企业级设计模式

Image Source : https://bugfender.com React是一个强大的JavaScript库&#xff0c;用于构建用户界面。其基于组件的体系结构和构建可重用组件的能力使其成为许多企业级应用程序的首选。然而&#xff0c;随着应用程序的规模和复杂性的增长&#xff0c;维护和扩展变得更加困难。…

面试官:说说synchronized与ReentrantLock的区别

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

python pyaudio 录取语音数据

python pyaudio 录取语音数据 pyaudio安装方法&#xff1a; pip install pyaudio如果这个不行&#xff0c;可以尝试&#xff1a; pip install pipwin pipwin install pyaudio代码如下&#xff1a; import pyaudio import waveRESPEAKER_RATE 44100 # 采样率&#xff0c;每…

LoadBalancer将服务暴露到外部实现负载均衡purelb-layer2模式配置介绍

目录 一.purelb简介 1.简介 2.purelb的layer2工作模式特点 二.layer2的配置演示 1.首先准备ipvs和arp配置环境 2.purelb部署开始 &#xff08;1&#xff09;下载purelb-complete.yaml文件并应用 &#xff08;2&#xff09;查看该有的资源是否创建完成并运行 &#xff…

数字化转型浪潮中,施耐德电气如何用技术革新引领未来?

作为一家187年的老牌企业&#xff0c;施耐德电气不仅见证了科技的演进&#xff0c;也是数字化转型潮流中的先行者。在近日的施耐德电气数字化战略暨软件创新沟通会上&#xff0c;施耐德电气全球执行副总裁&#xff0c;首席数字官Peter Weckesser&#xff1b;施耐德电气副总裁、…

MySQL笔记-第03章_基本的SELECT语句

视频链接&#xff1a;【MySQL数据库入门到大牛&#xff0c;mysql安装到优化&#xff0c;百科全书级&#xff0c;全网天花板】 文章目录 第03章_基本的SELECT语句1. SQL概述1.1 SQL背景知识1.2 SQL语言排行榜1.3 SQL 分类 2. SQL语言的规则与规范2.1 基本规则2.2 SQL大小写规范 …

PoE技术详解

标准的五类网线有四对双绞线&#xff0c;IEEE 802.3af和IEEE 802.3at允许两种用法&#xff1a;通过空闲线对供电或者数据线对供电。IEEE 802.3bt允许通过空闲线对供电、通过数据线对供电或者空闲线对和数据线对一起供电&#xff0c;如图16.1所示。 图 16.1 PoE供电线对 当在一…

2023年12月4日:多继承

代码 #include <iostream>using namespace std;class Sofa { private:string sit;int *len; public:Sofa(){cout << "Sofa::无参构造函数" << endl;}Sofa(string sit,int len):sit(sit),len(new int(len)){cout << "Sofa::有参构造函数…

力扣124. 二叉树中的最大路径和(java DFS解法)

Problem: 124. 二叉树中的最大路径和 文章目录 题目描述思路解题方法复杂度Code 题目描述 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#xff0c;且不一定经…

Python流程控制【侯小啾python基础领航计划 系列(十)】

Python流程控制【侯小啾python基础领航计划 系列(十)】 大家好,我是博主侯小啾, 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ�…