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

简介: 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();
    }
}

由结果我们可以知道,线程一释放锁后,由操作系统唤醒线程二才能获取到锁。

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();
    }
}

如果线程一不加synchronized,那么共享变量的改变它就感知不到,以至于程序一直在运行中。

可重入性

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

可以理解为一个线程没有释放锁,然后又尝试再次加锁。

按照之前对锁的理解就是,锁没有释放,进行再次加锁就会进行阻塞,直到第一次的锁被释放,才能获取到第二个锁,但释放第一个锁也由该线程来进行,结果现在这个线程啥都干不了,也就只能形成死锁了。

这样的锁称其为不可重入锁。

我们的synchronized是可重入锁。

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

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

synchronized的使用方法

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

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)) {
            // 不断循环直到获取到锁
        }

如果获取锁失败,立即再尝试获取锁, 无限循环,直到获取到锁为止。 第一次获取锁失败, 第二次的尝试会在极短的时间内到来。

缺点:如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源。

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

读写锁

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

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
  • 两个线程都要写一个数据, 有线程安全问题.
  • 一个线程读另外一个线程写, 也有线程安全问题.
    读写锁就是把读操作和写操作区分对待。 Java 标准库提供了ReentrantReadWriteLock 类,实现了读写锁。
  • ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁。 这个对象也提供了 lock / unlock 方法进行加锁解锁

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

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

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

Synchronized 不是读写锁

想了解更多也可以看我的笔记专栏哈哈


相关文章
|
8月前
|
存储 Java 大数据
Java 大视界 -- Java 大数据在智能家居能源消耗模式分析与节能策略制定中的应用(198)
简介:本文探讨Java大数据技术在智能家居能源消耗分析与节能策略中的应用。通过数据采集、存储与智能分析,构建能耗模型,挖掘用电模式,制定设备调度策略,实现节能目标。结合实际案例,展示Java大数据在智能家居节能中的关键作用。
|
8月前
|
存储 数据采集 数据可视化
Java 大视界 -- 基于 Java 的大数据可视化在城市交通拥堵溯源与治理策略展示中的应用(191)
本项目探索了基于Java的大数据可视化技术在城市交通拥堵溯源与治理策略中的应用。通过整合多源交通数据,利用Java生态中的大数据处理与可视化工具,构建了交通拥堵分析模型,并实现了拥堵成因的直观展示与治理效果的可视化评估。该方案为城市交通管理提供了科学、高效的决策支持,助力智慧城市建设。
|
8月前
|
存储 分布式计算 Java
Java 大视界 -- Java 大数据在智能建筑能耗监测与节能策略制定中的应用(182)
本文探讨了Java大数据技术在智能建筑能耗监测与节能策略制定中的关键应用。通过Hadoop、Spark等技术实现能耗数据的存储、分析与可视化,结合实际案例,展示了Java大数据如何助力建筑行业实现节能减排目标。
|
8月前
|
人工智能 前端开发 安全
Java开发不可不知的秘密:类加载器实现机制
类加载器是Java中负责动态加载类到JVM的组件,理解其工作原理对开发复杂应用至关重要。本文详解类加载过程、双亲委派模型及常见类加载器,并介绍自定义类加载器的实现与应用场景。
339 4
|
9月前
|
机器学习/深度学习 分布式计算 供应链
Java 大视界 ——Java 大数据在智能供应链库存优化与成本控制中的应用策略(172)
本文围绕 Java 大数据在智能供应链库存优化与成本控制中的应用展开,剖析库存管理现状与挑战,阐述大数据技术应用策略,结合真实案例与代码给出实操方案,助力企业提升库存管理效能,降低运营成本。
|
9月前
|
Java 测试技术 API
现代化 java 分层开发实施策略与最佳实践指南
现代化Java分层开发采用清晰的多层架构,包括Controller、Service、Repository和DTO等核心层次。文章详细介绍了标准Maven/Gradle项目结构,各层职责与实现规范:实体层使用JPA注解,DTO层隔离数据传输,Repository继承JpaRepository,Service层处理业务逻辑,Controller层处理HTTP请求。推荐使用Spring Boot、Lombok、MapStruct等技术栈,并强调了单元测试和集成测试的重要性。这种分层设计提高了代码的可维护性、可测试
457 0
|
9月前
|
SQL Java 数据库
解决Java Spring Boot应用中MyBatis-Plus查询问题的策略。
保持技能更新是侦探的重要素质。定期回顾最佳实践和新技术。比如,定期查看MyBatis-Plus的更新和社区的最佳做法,这样才能不断提升查询效率和性能。
489 1
|
10月前
|
人工智能 JavaScript Java
Java反射机制及原理
本文介绍了Java反射机制的基本概念、使用方法及其原理。反射在实际项目中比代理更常用,掌握它可以提升编程能力并理解框架设计原理。文章详细讲解了获取Class对象的四种方式:对象.getClass()、类.class、Class.forName()和类加载器.loadClass(),并分析了Class.forName()与ClassLoader的区别。此外,还探讨了通过Class对象进行实例化、获取方法和字段等操作的具体实现。最后从JVM类加载机制角度解析了Class对象的本质及其与类和实例的关系,帮助读者深入理解Java反射的工作原理。
254 0
|
10月前
|
人工智能 Java 关系型数据库
Java——SPI机制详解
SPI(Service Provider Interface)是JDK内置的服务提供发现机制,主要用于框架扩展和组件替换。通过在`META-INF/services/`目录下定义接口实现类文件,Java程序可利用`ServiceLoader`动态加载服务实现。SPI核心思想是解耦,允许不同厂商为同一接口提供多种实现,如`java.sql.Driver`的MySQL与PostgreSQL实现。然而,SPI存在缺陷:需遍历所有实现并实例化,可能造成资源浪费;获取实现类方式不够灵活;多线程使用时存在安全问题。尽管如此,SPI仍是Java生态系统中实现插件化和模块化设计的重要工具。
462 0
|
10月前
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
493 32
AQS:Java 中悲观锁的底层实现机制