【web】java多线程(常见锁策略+synchronized原理)

简介: 主要介绍了共享锁VS独占锁、重入锁VS不可重入锁、公平锁VS不公平锁、乐观锁VS悲观锁和synchronized原理。

【大家好,我是爱干饭的猿,本文是多线程初级入门,主要介绍了共享锁VS独占锁、重入锁VS不可重入锁、公平锁VS不公平锁、乐观锁VS悲观锁和synchronized原理。

后续会继续分享网络原理及其他重要知识点总结,如果喜欢这篇文章,点个赞👍,关注一下吧】

上一篇文章:《【web】java多线程(单例模式+阻塞队列+定时器+线程池)》


🤞目录🤞

💖1. 常见的锁策略

1.1 共享锁 vs 独占锁(读写锁)

1.2 可重入锁 vs 不可重入锁

1.3 公平锁 vs 不公平锁

1.4 乐观锁 vs 悲观锁

1.5 互斥锁 vs 自旋锁(重量级锁 vs 轻量级锁)

💖2. synchronized锁的原理

2.1 synchronized锁的基本特点

2.2 synchronized锁的加锁过程

2.3 synchronized的锁优化操作

1. 锁消除优化

2. 锁粗化优化

3. 锁升级优化

4. synchronized的锁优化总结


🚌1. 常见的锁策略

1.1 共享锁 vs 独占锁(读写锁)

读锁Shared Lock(s锁)和 写锁Exclusive Lock(x锁)

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

    • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行 加锁解锁
    • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进 行加锁解锁
      • 读加锁 + 读加锁       不互斥
      • 写加锁 + 写加锁        互斥
      • 读加锁 + 写加锁        互斥

      当业务中,读的次数远大于写的次数时,共享锁优于独占锁

      synchronized 锁 不是读写锁,是独占锁

      public class Main {
          public static void main(String[] args) {
              ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
              Lock readLock = readWriteLock.readLock();
              Lock writeLock = readWriteLock.writeLock();
              readLock.lock();
              Thread t = new Thread(){
                  @Override
                  public void run() {
                      readLock.lock();
                      System.out.println("读+读 可以访问到");
                  }
              };
              t.start();
          }
      }

      image.gif

      1.2 可重入锁 vs 不可重入锁

      可重入锁(ReentrantLock):允许同一个线程多次申请同一把锁

      比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入 锁(因为这个原因可重入锁也叫做递归锁)。

      Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类

      synchronized 锁 是可重入锁

      而 Linux 系统提供的 mutex 是不可重入锁。

      不可重入锁:在一个线程加锁后,第二次加同样的锁阻塞了,就是不可重入锁

      public class Main {
          public static void main(String[] args) {
              // 同一个线程申请同一把锁
              Lock lock = new ReentrantLock();
              lock.lock(); // main 线程锁
              lock.lock(); // 如果能申请同一把锁,就能执行下面的代码
              System.out.println("可执行之后的代码");
          }
      }

      image.gif

      1.3 公平锁 vs 不公平锁

      公平(fair):按照申请锁的次序获取到锁

      公平锁:先到先得,要维护一个阻塞队列,所以公平锁实复杂。

      不公平锁:不遵守先到先得的顺序,后来的线程运气好刚来就得到锁。

        • 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序
        • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景

        synchronized 锁是不公平锁

        public class Main {
            public static void main(String[] args) {
                Lock lock = new ReentrantLock(true);   // 公平锁
                Lock lock2 = new ReentrantLock(false); // 不公平锁
                Lock lock3 = new ReentrantLock();          // 默认不公平锁
            }
        }

        image.gif

        总结:synchronized 锁是 独占锁 + 可重入锁 + 不公平锁

        1.4 乐观锁 vs 悲观锁

        严格来讲,乐观锁和悲观锁是实现并发控制的两种解决方案,和“锁”的概念不是一个层级的概念。

        乐观锁:评估后,并发情况中,多个线程修改一个共享资源的情况比较少,可以采用轻量级锁(无锁)

        悲观锁:多个线程会频繁地修改同一个共享资源,必须使用互斥的方式(锁lock)来进行并发控制。

        Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

        1.5 互斥锁 vs 自旋锁(重量级锁 vs 轻量级锁)

        锁的实现导致的锁的种类不同:

        默认情况下,我们的锁的实现,是采用OS提供的锁(mutex锁:互斥锁) 一旦请求锁失败,会导致当前线程(请求锁失败的线程)会放弃CPU,进入阻塞状态,把自己加到锁的阻塞队列中,等待被唤醒。 必须进入到内核态,一旦放弃CPU,再到获取CPU,时间 相隔很久(站在CPU指令角度)

        性能太低。

        思考不需要进行触发线程调度的锁的实现方式。

        CAS(Compare and swap

        1. 比较 A 与 V 是否相等。(比较)

        2. 如果比较相等,将 B 写入 V。(交换)

        3. 返回操作是否成功。

        image.gif编辑

        硬件提供了CAS机制-> OS提供了CAS机制->JVM提供了CAS机制

        image.gif编辑

        自旋锁:

          • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁
          • 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是 不消耗 CPU 的)

          🚌2. synchronized锁的原理

          2.1 synchronized锁的基本特点

          策略:可重入的+不公平的+独占锁

          基本特点:

            1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
            2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
            3. 实现轻量级锁的时候大概率用到的自旋锁策略
            4. 是一种不公平锁
            5. 是一种可重入锁
            6. 不是读写锁

            2.2 synchronized锁的加锁过程

            JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

            image.gif编辑

            1) 偏向锁:第一个尝试加锁的线程, 优先进入偏向锁状态  

            2) 轻量级锁:随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁). 此处的轻量级锁就是通过 CAS 来实现

            3) 重量级锁:如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁 此处的重量级锁就是指用到内核提供的 mutex

            什么是偏向锁?

            偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线 程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态

            2.3 synchronized的锁优化操作

            1. 锁消除优化

            Vector v = new Vector(); V....  

            前提:Vector为了做到线程安全,每个方法都用synchronized 修饰了

            但是当实际上,我们的代码中只有主线程->所有做线程保护的操作都是无用功((加锁、释放锁)编译器+JVM判断出只有一个线程时,就会消除掉所有锁的操作,提升性能!!

            public class Main {
                private static synchronized void method(){
                    System.out.println("锁优化");
                }
                // 有没有锁无关紧要
                public static void main(String[] args) {
                    method();
                }
            }

            image.gif

            2. 锁粗化优化

            前提:已经没办法进行锁消除的情况下

            public class Main2 {
                static class MyThread extends Thread{
                    int i = 0;
                    // 加锁粒度过细,性能较低
                    @Override
                    public synchronized void run() {
                        i++;
                    }
                }
                static class MyThread2 extends Thread{
                    int i = 0;
                    // 加锁粒度适中,提升性能
                    @Override
                    public synchronized void run() {
                        i++;
                        i++;
                        i++;
                    }
                }
            }

            image.gif

            3. 锁升级优化

            就是synchronized 锁的加锁过程

            image.gif编辑

            image.gif编辑

            image.gif编辑

            image.gif编辑

            4. synchronized的锁优化总结

              1. 锁消除,能消除,尽量消除
              2. 锁粗化,看粒度是不是太细,尝试粗化
              3. 锁升级
                1. JVM发现一定是多线程场景来了 大部分对象不会被当成锁来使用+对象头空间始终存在->对象头里就可以暂存一些其他信息
                2. 【从第一个线程尝试加锁到第二个线程尝试加锁】期间 该对象锁,偏向于第一个线程。这个线程过来的时候,走的是快速通道 ->对象头来保存偏向哪个线程
                3. 【从有多个线程参与抢锁开始】︰一旦退出偏向状态,就无法回到偏向状态了 优先尝试使用轻量级锁(cas + spin lock,不进入内核态,只在JVM的用户态,解决锁的竞争问题)->对象头里保存轻量级锁的地址
                4. 对象头里保存轻量级锁的地址 spin之后,还拿不到锁or自适应的情况下,spin之后,仍然拿不到锁走到重量级锁(放弃CPU,阻塞。需要内核态参与) ->对象头里保存重量级锁的地址

                  分享到此,感谢大家观看!!!

                  如果你喜欢这篇文章,请点赞关注吧,或者如果你对文章有什么困惑,可以私信我。

                  🏓🏓🏓

                  相关文章
                  |
                  24天前
                  |
                  安全 Java 开发者
                  【JAVA】封装多线程原理
                  Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
                  |
                  2月前
                  |
                  缓存 安全 算法
                  Java 多线程 面试题
                  Java 多线程 相关基础面试题
                  |
                  10月前
                  |
                  安全 Java
                  深入理解Java并发编程:线程安全与性能优化
                  【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
                  73 0
                  |
                  10月前
                  |
                  存储 安全 Java
                  深入理解Java并发编程:线程安全与锁机制
                  【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
                  |
                  7月前
                  |
                  存储 安全 Java
                  解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
                  【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
                  90 1
                  |
                  8月前
                  |
                  安全 Java 开发者
                  Java并发编程中的线程安全问题及解决方案探讨
                  在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
                  132 0
                  |
                  9月前
                  |
                  安全 Java 开发者
                  Java并发编程中的线程安全策略
                  在现代软件开发中,Java语言的并发编程特性使得多线程应用成为可能。然而,随着线程数量的增加,如何确保数据的一致性和系统的稳定性成为开发者面临的挑战。本文将探讨Java并发编程中实现线程安全的几种策略,包括同步机制、volatile关键字的使用、以及java.util.concurrent包提供的工具类,旨在为Java开发者提供一系列实用的方法来应对并发问题。
                  70 0
                  |
                  10月前
                  |
                  安全 Java 容器
                  Java一分钟之-并发编程:线程安全的集合类
                  【5月更文挑战第19天】Java提供线程安全集合类以解决并发环境中的数据一致性问题。例如,Vector是线程安全但效率低;可以使用Collections.synchronizedXxx将ArrayList或HashMap同步;ConcurrentHashMap是高效线程安全的映射;CopyOnWriteArrayList和CopyOnWriteArraySet适合读多写少场景;LinkedBlockingQueue是生产者-消费者模型中的线程安全队列。注意,过度同步可能影响性能,应尽量减少共享状态并利用并发工具类。
                  93 2
                  |
                  10月前
                  |
                  安全 Java
                  Java中的并发编程:理解并发性与线程安全
                  Java作为一种广泛应用的编程语言,在并发编程方面具有显著的优势和特点。本文将探讨Java中的并发编程概念,重点关注并发性与线程安全,并提供一些实用的技巧和建议,帮助开发人员更好地理解和应用Java中的并发机制。
                  |
                  10月前
                  |
                  缓存 安全 Java
                  Java并发编程中的线程安全问题及解决方法
                  在Java编程中,线程安全是一个至关重要的问题,特别是在并发编程中。本文将探讨Java并发编程中常见的线程安全问题,包括数据竞争、死锁和内存可见性,并介绍了相应的解决方法,如使用同步锁、并发容器和原子类等技术,以确保多线程环境下程序的正确性和性能。
                  69 2