【多线程与高并发】- synchronized锁的认知

简介: synchronized 是 Java 语言的一个关键字,它允许多个线程同时访问共享的资源,以避免多线程编程中的竞争条件和死锁问题。

synchronized锁的认知

😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 一个有梦有戏的人 @怒放吧德德
🌝分享学习心得,欢迎指正,大家一起学习成长!

synchronized锁的认知.jpg

简介

synchronized 是 Java 语言的一个关键字,它允许多个线程同时访问共享的资源,以避免多线程编程中的竞争条件和死锁问题。synchronized可以用来给对象或者方法进行加锁,当对某个对象或者代码块加锁时,同时就只能有一个线程去执行。这种就是互斥关系,被加锁的区域称为临界区,而里面的资源就是临界资源。当一个线程进入临界区的时候,另一个线程就必须等待。
在这里插入图片描述
synchronized可以限制对某个资源的访问,但是它锁的并不是资源本身,可以锁住某个对象,只有线程拿到这把锁之后才能够去访问临界资源。如下代码,在我们想执行对count变量进行操作的时候,线程需要拿到o这个对象。

public class T1_Synchronized01 {
    private int count = 1;
    private Object o = new Object();

    public void m1() {
        synchronized (o) { // 必须先拿到o这个锁
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

synchronized基础用法

1、通过对象进行锁

在代码里,可以通过创建一个对象,这样要想拿到临界资源,就必须先获得到这个对象的锁。

public class T1_Synchronized01 {
    private int count = 1;
    private Object o = new Object();

    public void m1() {
        synchronized (o) { // 必须先拿到o这个锁
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

2、通过this

使用this代表锁住的是当前对象,这种方法等同直接把synchronized关键字加在方法前。

public class T1_Synchronized01 {
    private int count = 1;

    public void m2() {
        synchronized (this) { // 必须先拿到this的锁
            count++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }

    public synchronized void m3() { // 与m2一样
        count++;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

}

3、锁定静态方法

锁定静态方法需要通过类.class,或者直接在静态方法上加上关键字。但是,类.class不能使用this来代替。注:在同一个类加载器中,class是单例的,这也就能保证synchronized能够只让一个线程访问临界资源。

public class T1_Synchronized01 {
    public static void m4() { // 静态方法
        synchronized (T1_Synchronized01.class) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public synchronized static void m5() {
       System.out.println(Thread.currentThread().getName());
    }
}

4、实验测试

①、首先测试一下,同步和非同步是否可以相互调用
定义两个线程,一个执行同步方法,一个执行非同步方法,如果不能够互相调用,那么,非同步线程就需要等待同步线程执行完之后在继续执行。

public class T3_Synchronized03 {
    public synchronized void one() {
        System.out.println(Thread.currentThread().getName() + " start one method");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + " end one method");
    }

    public void two() {
        System.out.println(Thread.currentThread().getName() + " execute two method");
    }

    public static void main(String[] args) {
        T3_Synchronized03 t = new T3_Synchronized03();
        new Thread(t::one, "第一个线程").start();
        new Thread(t::two, "第二个线程").start();
    }
}

从运行的结果可以看出是可以的。
image.png
②、读写不全加锁会怎样
通过购票与查询票数来进行模拟读写加锁问题。
首先,看以下代码是给读写都进行加锁了,在扣掉票数的时候,休眠了2秒,当线程执行了购票之后,通过多个线程去查询票数,每次启动线程会睡眠0.5秒。

public class T4_Synchronized {
    private int ticket = 100; // 模拟100张票

    public synchronized int getTicket() { // 读
        return this.ticket;
    }

    public synchronized void buy(int number) { // 写
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        ticket = ticket - number;
    }

    public static void main(String[] args) throws InterruptedException {
        T4_Synchronized bus = new T4_Synchronized();
        System.out.println("刚开始有票数:" + bus.getTicket());
        new Thread(() -> bus.buy(1)).start();
        for (int i = 1; i <= 10; i++) {
            Thread.sleep(500);
            int finalI = i;
            new Thread(() -> System.out.println("第" + finalI + "次查询余票数:" + bus.getTicket())).start();
        }

    }
}

运行之后,我们可以发现,数据是正确的,尽管是在查询的时候并没有睡眠0.5秒,显示数据依然是期望数据。
image.png
然而,当我们把读的锁去掉,运行代码,会发现,数据读出来了脏数据,为了更好的显示,查询票数的睡眠不要去掉。
image.png
③、synchronized的可重入性
定义一个类,类中有两个同步方法,他们锁的对象都是当前类,如果不能够重入,在one线程启动的时候就会死锁。在同步方法one中去调用同步方法two,当线程启动的时候,已经获取了对象的锁,等调用two方法的时候,同样是拿到了这个对象的锁。所以synchronized是可重入的。

public class T5_Synchronized {

    synchronized void one() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        two();
        System.out.println("one - thread-" + Thread.currentThread().getName() + " end");
    }

    synchronized void two() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("two - thread-" + Thread.currentThread().getName() + " end");
    }

    public static void main(String[] args) {
        T5_Synchronized t5 = new T5_Synchronized();
        new Thread(t5::one, "one1").start();
        new Thread(t5::one, "one2").start();
        new Thread(t5::one, "one3").start();
    }
}

实验结果
image.png
④、异常会释放锁
当线程执行过程中出现了异常,synchronized的锁会被释放,这样其他需要访问这个临界资源的线程就能进入执行。

public class T6_Synchronized {
    int count = 0;
    synchronized void add() {
        System.out.println("线程 " + Thread.currentThread().getName() + " start");
        while (true) {
            count++;
            System.out.println("线程 " + Thread.currentThread().getName() + " now count = " + count);
            if (count == 3) {
                throw new NullPointerException("人为异常");
            }
            if (count == 10) {
                throw new NullPointerException("测试结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T6_Synchronized t = new T6_Synchronized();
        new Thread(t::add, "1").start();
        Thread.sleep(1000);
        new Thread(t::add, "2").start();
    }
}

当第一次异常抛出时,线程2就立即进入执行。
image.png

synchronized锁的底层原理

synchronized实现锁的基础就是Java对象头,synchronized锁会将线程ID存入mark word(对象头由标记字)。关于mark word,先简要了解一下Java对象。
在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。synchronized主要是跟对象头有关系,在对象头中包含了标记字(mark word)、类指针(klass word)和 数组长度(array length)。也就是通过mark word的字节位数来表示各种锁状态。
Java对象 (2).png
synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,这种就是偏向锁,默认是认为不会有多个线程抢着用,mark word是通过64bit来表示的,通过最低2位也就是锁标志位,偏向锁与无锁的值是01,轻量级锁用00表示,重量级锁用10表示,标记了GC的用11表示,无锁与偏向锁低2位是一致的,在倒数第3位有1位来表示偏向锁位:值为1表示偏向锁。
在这里插入图片描述

这里引用一张掘金博客上的图: https://juejin.cn/post/6978882583492821023

synchronized锁升级

● synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,默认也就是使用偏向锁。
● 当第二个线程来争用的时候,此时第二个线程会占用cpu,循环等待锁的释放,这时候偏向锁也就升级为自旋锁。
● 当自旋10次之后,就会升级为重量级锁,重量级锁是不占用cpu,他是使用OS的。
当线程数较少、运行时间较短的时候是比较适合使用自旋锁,反之则比较适合重量级锁。

博文推荐

对于synchronized的学习,我也查阅了许多的相关信息,确实很不容易,我将认为不错的文章都推荐出来:
【并发编程系列2】synchronized锁升级原理分析(偏向锁-轻量级锁-重量级锁)_双子孤狼的博客-CSDN博客_java锁降级和升级

深入理解Java并发之synchronized实现原理_zejian_的博客-CSDN博客_javasynchronized原理

深入理解synchronized底层原理,一篇文章就够了! - 腾讯云开发者社区-腾讯云

Java 对象、对象头mark word、锁升级、对象占内存大小 - 掘金

深入理解Java的对象头mark word_了不起的盖茨比。的博客-CSDN博客_撖寡情憭尋ark word

Java知识进阶-程序员未接触的知识点之对象模型Klass Word-知识铺_实例

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

相关文章
|
2天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
13 3
|
28天前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
37 6
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
47 4
|
1月前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
1月前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?
|
2月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
30 2
|
2月前
|
Java Linux
【网络】高并发场景处理:线程池和IO多路复用
【网络】高并发场景处理:线程池和IO多路复用
63 2
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
47 0
|
2月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
31 0