• 【JavaEE初阶】 synchronized关键字详解


    本文重点:

    • synchronized 的特性
    • synchronized 使用示例
    • ava 标准库中的线程安全类

    🌴synchronized 的特性

    🚩互斥

    synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
    同一个对象 synchronized 就会阻塞等待.

    • 进入 synchronized 修饰的代码块, 相当于 加锁

    • 退出 synchronized 修饰的代码块, 相当于 解锁
      在这里插入图片描述
      synchronized用的锁是存在Java对象头里的。

    在这里插入图片描述
    可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).

    • 如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

    • 如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

    在这里插入图片描述

    后面排队的人我们可以称为“阻塞等待”

    阻塞等待:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

    注意:

    • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
    • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待.但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则

    补充:synchronized的底层是使用操作系统的mutex lock实现的

    🚩可重入

    synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

    那么什么是可重入?怎么自己把自己锁死呢?

    理解 “把自己锁死”:

    一个线程没有释放锁, 然后又尝试再次加锁

    • 按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待.

    • 直到第一次的锁被释放, 才能获取到第二个锁.

    • 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作.

    • 这时候就会死锁

    举个例子:

    一个滑稽老铁去上厕所,反锁厕所们后,然后不小心一个闪现出来了,还失忆了,这时候厕所没人,但是处于锁的状态,后面等待的人无法进入
    在这里插入图片描述
    这样的锁称为不可重入锁

    Java 中的== synchronized 是可重入锁==, 因此没有上面的问题

    代码示例

    public class Counter {
    	public int count = 0;
    	synchronized void increase() {
    		count++;
    	}
    	synchronized void increase2() {
    		increase();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面的代码中,

    • increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对== this 当前对象==加锁的.

    • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

    • 这个代码是完全没问题的.

    因为 synchronized 是可重入锁

    在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

    • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.

    • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

    🍀synchronized 的使用

    synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具
    体的对象来使用

    synchronized的使用方法可以分为以下三种:

    🚩直接修饰普通方法

    锁是当前实例对象 ,进入同步代码前要获得当前实例的锁

    锁的 SynchronizedDemo对象

    public class SynchronizedDemo {
    	public synchronized void methond() {
    	}
    }
    
    • 1
    • 2
    • 3
    • 4

    使用举例:

    多个线程访问同一个对象的同一个方法

    public class synchronizedTest implements Runnable {
        //共享资源
        static int i =0;
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run(){
            for (int j =0 ; j<10000;j++){
                increase();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            synchronizedTest test = new synchronizedTest();
            Thread t1 = new Thread(test);
            Thread t2 = new Thread(test);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    运行结果如下:
    在这里插入图片描述
    分析:当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,需要等到对象被释放后才能获取,但是在对象没有被释放前,其他线程可以访问非synchronized修饰的方法

    🚩修饰静态方法

    锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁

    锁的 SynchronizedDemo 类的对象

    public class SynchronizedDemo {
    	public synchronized static void method() {
    	
    		}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用举例

    public class synchronizedTest3 implements Runnable {
        //共享资源
        static int i = 0;
    
        /**
         * synchronized 修饰静态方法
         */
        public static synchronized void increase() {
            i++;
        }
    
        @Override
        public void run() {
            for (int j = 0; j < 10000; j++) {
                increase();
            }
        }
    }
    
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(new synchronizedTest3());
            Thread t2 = new Thread(new synchronizedTest3());
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(synchronizedTest3.i);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    结果如下:
    在这里插入图片描述
    分析:由例子可知,两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象

    🚩 修饰代码块

    同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

    • 锁当前对象
    public class SynchronizedDemo {
    	public void method() {
    		synchronized (this) {
    		
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 锁类对象
    public class SynchronizedDemo {
    	public void method() {
    		synchronized (SynchronizedDemo.class) {
    		
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    🧭synchronized 的使用总结:

    我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待

    两个线程分别尝试获取两把不同的锁, 不会产生竞争.
    在这里插入图片描述

    🎍Java 标准库中的线程安全类

    Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

    • ArrayList

    • LinkedList

    • HashMap

    • TreeMap

    • HashSet

    • TreeSet

    • StringBuilde

    但是还有一些是线程安全的. 使用了一些锁机制来控制

    • Vector (不推荐使用)

    • HashTable (不推荐使用)

    • ConcurrentHashMap

    • StringBuffer

    不推荐使用的原因是:当你使用这些类时,每一次都会伴随加锁操作,如果该操作不需要加锁,则就会造成时间浪费。

    注意String还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的

    ⭕总结

    关于《【JavaEE初阶】 synchronized关键字详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

  • 相关阅读:
    MySQL表的增删改查--你都知道吗?
    别把对象当Map
    基于java+springmvc+mybatis+vue+mysql的校园安全管理系统
    借助 DevChat AI 之力,成就我之全栈梦想
    网页JS自动化脚本(七)使用在线jQuery来操作元素
    基于JAVA音乐资源分享网站系统计算机毕业设计源码+系统+数据库+lw文档+部署
    一文玩转NGINX(对于NGINX,你真的了解吗?)
    Blend for Visual Studio 概述
    springboot整合quartz实现任务持久化(下)
    windows11使用docker部署安装minio
  • 原文地址:https://blog.csdn.net/m0_71731682/article/details/133778332