首页 > 编程语言 >java中的锁

java中的锁

时间:2024-09-04 18:21:28浏览次数:12  
标签:Singleton java synchronized instance 线程 volatile null

关键字:synchronized

介绍

synchronized 是Java语言中的一个关键字,用于实现线程同步,以确保在多线程环境下对共享资源的访问是安全且一致的。它通过提供一种互斥机制来防止多个线程同时执行特定的代码区域,从而避免了数据不一致性和其他并发问题。以下是关于synchronized的一些关键点:

基本用途

  1. 同步方法:当synchronized应用于方法时,该方法成为同步方法。这意味着同一时间只有一个线程可以访问该方法。对于实例方法,锁是当前实例对象(this);对于静态方法,锁是类的Class对象。
  2. 同步代码块允许更细粒度的控制,你可以指定一个对象作为锁。只有获得了这个特定对象锁的线程才能执行该代码块。这对于减少锁的范围和提高并发性非常有用。

实现原理

  • 监视器锁(Monitor Lock):每个对象都有一个关联的监视器锁。当线程进入synchronized代码块或方法时,它会自动获取对象的监视器锁,并在退出时释放锁。
  • 锁升级:Java 6以后,为了提高效率,synchronized经历了锁升级的过程,包括偏向锁、轻量级锁和重量级锁,以适应不同的竞争程度。

特性

  • 原子性:确保被synchronized保护的代码块作为一个不可分割的单位执行,即操作不会被线程调度机制打断。
  • 可见性:通过监视器锁的规则,保证了变量的修改能够及时地被其他线程看到,因为线程在退出同步代码块之前会将修改的数据刷新到主内存中。
  • 不可中断性:一旦线程获得了synchronized锁,它会一直持有直到执行完毕或抛出异常,期间不会响应中断请求。

使用注意事项

  • 死锁:不当的使用synchronized可能导致死锁,特别是在多个锁依赖的场景中。
  • 性能考量:虽然synchronized在Java中已经过优化,但在高并发场景下,过度使用仍可能成为性能瓶颈。
  • 可重入性:同一个线程可以多次获取同一个对象的锁,不会造成自我阻塞。

与其他并发工具比较

相较于synchronized,Java并发包(java.util.concurrent)提供了更多高级并发工具,如ReentrantLock, Semaphore, CountDownLatch等,它们提供了更灵活的锁机制和更高的并发性能,但synchronized因其简单易用和编译器支持而在很多场景下仍然是首选。

使用

public class PreloadSingleton {
       
       public static PreloadSingleton instance = new PreloadSingleton();
   
       //其他的类无法实例化单例类的对象
       private PreloadSingleton() {
       };
       
       public static PreloadSingleton getInstance() {
              return instance;
       }
}
public class Singleton {
       
       private static Singleton instance=null;
       
       private Singleton(){
       };
       
       public static Singleton getInstance()
       {
              if(instance==null)
              {
                     instance=new Singleton();
              }
              return instance;
              
       }
}
单例模式和线程安全

(1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费

(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。

不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:

memory=allocate();//1:初始化内存空间
 
ctorInstance(memory);//2:初始化对象
 
instance=memory();//3:设置instance指向刚分配的内存地址

jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。

synchronized用法1-同步方法
public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (Singleton.class) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

synchronized用法2-同步代码块

我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁

public class Singleton {
       private static Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (Singleton.class) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}
顺序性考虑

上述讨论得知new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。

public class Singleton {
       private static volatile Singleton instance = null;
       private Singleton() {
       };
       public static synchronized Singleton getInstance() {
              if (instance == null) {
                     synchronized (instance) {
                           if (instance == null) {
                                  instance = new Singleton();
                           }
                     }
              }
              return instance;
       }
}

到此,我们就保证了懒加载的线程安全。

关键字:volatile

volatile是Java中的一个关键字,它用于修饰变量,以确保在多线程环境中的可见性一定程度的有序性,但不保证原子性。以下是volatile关键字的主要作用和特性:

可见性(Visibility)

当一个变量被声明为volatile时,任何对其的写操作都会立即刷新到主内存中,而任何读操作都会直接从主内存中读取最新的值。这意味着,一旦一个线程修改了volatile变量的值,其他线程可以立即看到这个变化,消除了缓存不一致的问题,保证了共享变量的可见性。

有序性(Ordering)

volatile除了保证可见性外,还禁止了指令重排序(Instruction Reordering)。在没有volatile修饰的情况下,编译器和处理器为了优化性能可能会对指令进行重新排序,这在单线程环境中没有问题,但在多线程环境下可能导致不一致的行为。使用volatile可以确保对volatile变量的读/写操作不会与其他内存操作重排序,从而保证了操作的有序性。

不保证原子性(Non-Atomicity)

尽管volatile提供了可见性和一定的有序性,但它并不能保证复合操作(i++)的原子性。例如,一个线程执行i++操作实际上包含了读取、修改、写回三个步骤,即使ivolatile的,这个操作仍然不是原子的,因此在多线程环境下可能会出现问题。为了确保原子性,通常需要配合synchronized块或使用java.util.concurrent包下的原子类(如AtomicInteger)。

使用场景

  • 适用于状态标记量,如flag变量,用于控制线程的启动、停止或某个状态的开关。
  • 读多写少的场景,当一个变量被频繁读取,但很少修改时,使用volatile可以减少锁的开销。
  • 双重检查锁定(Double-Checked Locking)模式中,用于确保实例化过程的可见性。

锁升级


锁升级是Java中针对synchronized关键字实现的一种优化策略,旨在减少线程间竞争锁的开销,提高并发性能。这一机制主要发生在Java 6及之后的版本中,主要包括以下几个阶段:

1. 偏向锁(Bias Locking)

  • 目的:大多数情况下,锁只会被一个线程反复获得,偏向锁就是为了这种场景设计的优化。它会偏向于第一个获得它的线程,之后的运行过程中,如果该锁没有被其他线程尝试获取,持有偏向锁的线程不需要进行同步操作。
  • 升级:当其他线程尝试获取这个偏向锁时,偏向锁就会升级为轻量级锁。

2. 轻量级锁(Lightweight Locking)

  • 目的:如果偏向锁被另一个线程竞争,那么偏向锁就会升级为轻量级锁。轻量级锁是为了解决锁竞争较轻的情况,它通过在堆栈上创建锁记录(Lock Record)来避免传统的重量级互斥锁带来的系统调用开销。
  • 操作:线程会尝试将对象头的Mark Word复制到自己的栈帧中创建的Lock Record中,并尝试用CAS操作将对象头的Mark Word更新为指向Lock Record的指针。如果成功,表示获取锁成功;如果失败,则说明有竞争,会进一步升级。
  • 升级:当锁竞争进一步加剧,轻量级锁会膨胀为重量级锁。

3. 重量级锁(Heavyweight Locking)

  • 目的:当多个线程同时竞争锁,且通过轻量级锁无法有效解决竞争时,锁就会膨胀为重量级锁。这时,Java虚拟机会使用操作系统的互斥量(Mutex)来实现线程同步,这会导致线程阻塞和唤醒,开销较大。
  • 特点:重量级锁是传统意义上的互斥锁,它会导致未获取到锁的线程阻塞,等待操作系统调度。

锁降级

除了锁升级,还存在锁降级的概念,即从重量级锁降级为轻量级锁,甚至是偏向锁。但是,实际应用中锁降级并不常见,主要还是关注于锁的升级路径来优化并发性能。

CAS

CAS(Compare and Swap)操作是一种在多线程环境下的非阻塞同步技术,用于实现原子性的更新操作。这项技术广泛应用于Java的并发编程中,尤其是在java.util.concurrent.atomic包下的原子类中,比如AtomicIntegerAtomicLong等。下面是关于CAS操作的关键点:

工作原理

CAS操作包含三个操作数:

  • 内存值(V):要更新的变量在内存中的当前值。
  • 预期值(A):执行CAS操作前,线程期望V应该具有的值。
  • 新值(B):如果V的值确实等于A,那么需要更新的新值。

执行过程如下:

  1. 比较:CAS首先比较内存位置V的当前值与预期值A是否相等。
  2. 交换:如果相等,说明没有其他线程改变过这个值,此时将内存值更新为新值B,并返回true,表示更新成功。如果不相等,说明已经有其他线程改变了这个值,此时不进行任何操作,并返回false,表示更新失败。

特点

  • 原子性:CAS操作是CPU级别的原子指令,保证了读-改-写的整个过程不会被中断,从而避免了数据的不一致性。
  • 乐观锁:CAS基于一种乐观的并发策略,它总是认为自己可以成功完成操作,而不像悲观锁那样每次操作前都先锁定资源。
  • 非阻塞:失败的线程不会被挂起或阻塞,而是被告知失败后可以再次尝试,这减少了线程上下文切换的开销,提高了系统的吞吐量。

应用场景

  • 原子变量更新:如原子类的递增、递减操作。
  • 无锁数据结构:构建如无锁队列、无锁栈等高性能并发数据结构。
  • 轻量级锁实现:在Java中的锁实现中,轻量级锁的获取和释放也使用了CAS操作。

注意事项

  • ABA问题:如果一个值从A变为B再变回A,CAS操作无法识别这种变化,可能会导致错误的结果。通常通过添加版本号或使用AtomicStampedReference来解决。
  • 循环开销:在高竞争的场景下,CAS可能需要多次重试,这可能导致大量的循环(自旋),消耗CPU资源。
  • 非阻塞不代表无等待:失败的线程虽然没有被阻塞,但需要不断地重试,这也是一种等待状态。

标签:Singleton,java,synchronized,instance,线程,volatile,null
From: https://blog.csdn.net/weixin_45143101/article/details/141900886

相关文章

  • JavaWeb
    JavaWeb1.Servlet<?xmlversion="1.0"encoding="UTF-8"?><web-appxmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="......
  • 关于Java链表的一些操作以及力扣原题刷刷刷——反转链表、删除链表的倒数第N个节点
    1、反转链表1.1环境准备,可以自己先尝试实现/***@AuthorMiku*@Date2024/09/0209:54*@DescriptionTODO*@Version1.0*/publicclassSolution{staticclassListNode{intval;ListNodenext;ListNode(intval){......
  • 探秘JavaScript深度领域:精通面向对象编程、虚拟DOM等核心技术
    JaScript作为前端开发的核心技术之一,凭借其强大的灵活性和广泛的应用场景,吸引了大量开发者深入学习。在探秘JaScript的深度领域时,面向对象编程和虚拟DOM等核心技术无疑是两个重要的学习方向。面向对象编程(OOP)在JaScript中扮演着重要角色。虽然JaScript是一种基于原型的语言,而非传......
  • 深入理解JavaScript类与对象:揭秘类字段和静态属性的妙用,js静态属性和实例属性
    在深入理解JaScript类与对象的过程中,类字段和静态属性是两个关键的概念,掌握它们的用法可以让你在实际开发中更加得心应手。虽然JaScript在ES6之前并没有类的概念,但通过ES6及以后的版本,引入了类语法糖,使得我们能够更直观地定义和使用类。类字段是指在类中直接定义的属性,而不是在构......
  • java JRMP学习
    JavaJRMP反序化RMI依赖的通信协议为JRMP(JavaRemoteMessageProtocol,Java远程消息交换协议),该协议为Java定制,基于TCP/IP之上,RMI协议之下,当需要进行RMI远程方法调用通信的时候要求服务端与客户端都为Java编写。、这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的......
  • java常用关键字
    类别关键字说明访问控制private私有的protected受保护的public公共的default默认类、方法和变量修饰符abstract声明抽象class类extends扩充、继承final最终值、不可改变的implements实现(接口)interface接口native本地、原......
  • java 二次反序列化
    java二次反序列化SignedObject该类是java.security下一个用于创建真实运行时对象的类,更具体地说,SignedObject包含另一个Serializable对象。先看其构造函数方法。看到参数接受一个可序列化的对象,然后又进行了一次序列化,继续看到该类的getObject方法(这是个getter方法......
  • Java之数据类型扩展及面试题讲解
    整数代码运行前运行后浮点数一些面试讲解关于银行业务怎么表示?钱如果我们用float和double来表示的话运行前运行后可以看出都是0.1,然而运行后却出现false(错误)的标志运行前运行后可以看出d2=d1=1,所以d2与d1不相等,然而却出现了true(正确)首先浮点数表现......
  • 【学习】【JavaScript 安全】JS代码混淆技术
    一、布局混淆1.1删除无效代码1.2标识符重命名二、数据混淆2.1数字混淆2.1.1进制转换2.1.2数学技巧2.1.3数字拆解2.2布尔混淆2.2.1类型转换2.2.2构造随机数2.3字符串混淆2.4undefined和null混淆......
  • 庖丁解java(一篇文章学java)
    (大家不用收藏这篇文章,因为这篇文章会经常更新,也就是删除后重发) 一篇文章学java,这是我滴一个执念...当然,真一篇文章就写完java基础,java架构,java业务实现,java业务扩展,根本不可能.所以,这篇文章,就是一个索引,索什么呢?   请看下文.关于决定开始写博文的介绍......