• Java中synchronized:特性、使用、锁机制与策略简析


    synchronized的特性

    互斥性

    synchronized确保同一时间只有一个线程可以进入同步块或同步方法,避免了多线程并发访问共享资源的冲突问题。
    synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
    下面我们来看一个例子,两个线程获取同一个锁,锁被占用后,剩下的那个线程就会进行阻塞等待。

    public class test2 {
        public static void main(String[] args) {
            Object object = new Object();
            Thread t1 =  new Thread(()->{
            //进入 synchronized 修饰的代码块, 相当于 加锁
              synchronized (object) {
                  for (int i = 0; i < 5; i++) {
                      System.out.println("线程t1获取锁");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                  }
              }
            //退出 synchronized 修饰的代码块, 相当于 解锁  
            });
            Thread t2 = new Thread(()->{
                synchronized (object) {
                    for (int i = 0; i < 5; i++) {
                        System.out.println("线程特t2获取锁");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    
    • 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
    • 32
    • 33

    在这里插入图片描述
    由结果我们可以知道,线程一释放锁后,由操作系统唤醒线程二才能获取到锁。

    synchronized的底层是使用操作系统的mutex lock实现的。

    可见性

    内存可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到修改的值。在多线程环境中,由于多个线程同时访问共享变量,每个线程都有自己的工作内存,而工作内存中保存了主内存中的部分数据副本。因此,当一个线程修改了共享变量的值,但这个修改尚未被刷新到主内存时,其他线程可能无法立即看到这个修改,而继续使用自己工作内存中的旧值,造成了内存不可见性。

    synchronized 既能保证原子性,也能保证内存可见性,一个线程对共享变量的修改对于其他线程是可见的。

    class Counter {
        public static int flag = 0;
    }
    
    public class test3 {
        public static void main(String[] args) {
            Object object = new Object();
            Thread t1 = new Thread(() -> {
                while (true) {
                    synchronized (object) {
                        if (Counter.flag != 0) {
                            break;
                        }
                    }
                }
                System.out.println("线程一知道了共享变量改为" + Counter.flag);
            });
            Thread t2 = new Thread(() -> {
                Scanner scanner = new Scanner(System.in);
                System.out.println("输入一个整数:");
                Counter.flag = scanner.nextInt();
            });
            t1.start();
            t2.start();
        }
    }
    
    
    • 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 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
    可以理解为一个线程没有释放锁,然后又尝试再次加锁。
    按照之前对锁的理解就是,锁没有释放,进行再次加锁就会进行阻塞,直到第一次的锁被释放,才能获取到第二个锁,但释放第一个锁也由该线程来进行,结果现在这个线程啥都干不了,也就只能形成死锁了。
    这样的锁称其为不可重入锁。

    我们的synchronized是可重入锁。
    在重入锁的内部有两个信息,分别为“程序计数器”和“线程持有者”

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

    synchronized的使用方法

    1. 直接修饰普通方法: 锁的 SynchronizedDemo 对象
    public synchronized void methond() {
    }
    
    • 1
    • 2
    1. 修饰静态方法: 锁的 SynchronizedDemo 类的对象
    public synchronized static void method() {
    }
    
    • 1
    • 2
    1. 修饰代码块: 明确指定锁哪个对象
    • 锁当前对象
    public void method() {
    synchronized (this) {
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 锁类对象
    public void method() {
    synchronized (SynchronizedDemo.class) {
    }
    }
    
    • 1
    • 2
    • 3
    • 4

    synchronized的锁机制

    1. 对象锁:可以将synchronized关键字直接应用于实例方法或实例代码块上。当一个线程进入被synchronized修饰的实例方法或实例代码块时,它会自动获取该对象的内置锁。只有当线程释放锁之后,其他线程才能进入同步块。

    2. 类锁:可以将synchronized关键字应用于静态方法或类代码块上。当一个线程进入被synchronized修饰的静态方法或类代码块时,它会自动获取该类的Class对象的内置锁。类锁是属于整个类的,对于同一个类的不同实例,他们共享同一个类锁。

    3. 锁对象:可以使用synchronized关键字加锁指定的对象。通过指定一个对象作为锁,多个线程可以根据这个对象来实现同步。当一个线程进入synchronized代码块时,它会尝试获取指定对象的内置锁,只有当线程释放锁之后,其他线程才能获得锁并执行同步代码。

    常见锁策略

    乐观锁与悲观锁

    悲观锁是在数据被使用前加锁,防止数据被其他线程修改。
    乐观锁则是在更新数据时检查数据是否被其他线程修改过,如果没有则更新成功,否则返回失败。
    Synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略。

    重量级锁与轻量级锁

    轻量级锁是一种优化的锁,它在CAS操作时使用CPU的自旋机制,如果自旋成功则获取到锁,否则进入睡眠状态。
    重量级锁是一种传统的锁,它依赖于操作系统的MutexLock(互斥锁)来实现,当有多个线程竞争同一个锁时,会阻塞其他线程等待释放。

    公平锁与非公平锁

    假设有A,B,C三个线程依次进行同一把锁的获取,线程A获取成功了,线程B与C获取失败。
    等待线程A释放锁后,线程B与C,如何获取锁在这里插入图片描述
    公平锁策略: 遵守 “先来后到”。B 比 C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。
    非公平锁策略:不遵守 “先来后到”。B 和 C 都有可能获取到锁。
    synchronized 是非公平锁

    可重入锁与不可重入锁

    可重入锁的意思就是允许同一个线程多次获取同一把锁。
    Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
    可以理解为一个线程没有释放锁,然后又尝试再次加锁。
    按照之前对锁的理解就是,锁没有释放,进行再次加锁就会进行阻塞,直到第一次的锁被释放,才能获取到第二个锁,但释放第一个锁也由该线程来进行,结果现在这个线程啥都干不了,也就只能形成死锁了。
    这样的锁称其为不可重入锁。

    synchronized 是可重入锁

    自旋锁

    为防止线程在抢锁失败后进入阻塞状态,经过很久才能再次被调度的情况。

    while (!locked.compareAndSet(false, true)) {
                // 不断循环直到获取到锁
            }
    
    • 1
    • 2
    • 3

    如果获取锁失败,立即再尝试获取锁, 无限循环,直到获取到锁为止。 第一次获取锁失败, 第二次的尝试会在极短的时间内到来。
    缺点:如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源。
    synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

    读写锁

    一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

    • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.

    • 两个线程都要写一个数据, 有线程安全问题.

    • 一个线程读另外一个线程写, 也有线程安全问题.

      读写锁就是把读操作和写操作区分对待。 Java 标准库提供了ReentrantReadWriteLock 类,实现了读写锁。

    • ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。

    • ReentrantReadWriteLock.WriteLock 类表示一个写锁。 这个对象也提供了 lock / unlock 方法进行加锁解锁

    读加锁和读加锁之间, 不互斥.
    写加锁和写加锁之间, 互斥.
    读加锁和写加锁之间, 互斥

    Synchronized 不是读写锁

    想了解更多也可以看我的笔记专栏哈哈在这里插入图片描述

  • 相关阅读:
    网关gateway-88
    获取需要对比的日期范围方法
    华为认证HCIA H12-811 Datacom数通考试真题题库【带答案刷题必过】【第五部分】
    《拼多多电商业务场景》批量采集产品数据根据关键词获取商品列表示例
    Qt读取Json文件(含源码+注释)
    ICML'22| 基于GNN的物理模拟器
    AWS认证SAA-C03每日一题
    C++随心记(四)
    摸鱼网站推荐,不会摸鱼的程序员不是好程序猿
    组合类替换嵌套内置类型实现多层嵌套业务
  • 原文地址:https://blog.csdn.net/st200112266/article/details/133100680