java中的锁

关键字: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资源。
  • 非阻塞不代表无等待:失败的线程虽然没有被阻塞,但需要不断地重试,这也是一种等待状态。

  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值