synchronized关键字浅析

背景

在实际工程实践中,多线程并发执行场景十分常见。所谓线程安全性即是多线程并发执行场景中需要保证的基本要求,如果不能保证线程安全性,那么势必会在实际工程实践中产生错误数据、甚至严重且不易察觉的异常处理,导致最终结果的不确定性。对于临界资源,或者是必须串行操作的流程,势必需要保证多个线程中每次仅有一个线程持有或仅有一个线程进入。如何保证多个线程由并行转串行,去持有临界资源或进入必须串行操作的流程呢?计算机领域中提供了“锁”的概念来进行保证,各种语言中都提供了对锁的实现。我们在这里仅针对JAVA语言中的锁进行分析。

synchronized关键字的原理

在早期的JAVA程序中,通常是使用synchronized关键字来保证线程安全性的。synchronized关键字,顾名思义表达的是同步的意思。也就是说,被synchronized关键字修饰的方法、语句块同一时刻只能有一个线程进入。

很多同学都会很好奇,究竟synchronized关键字的原理是怎样的,才能做到同一时刻只能有一个线程进入呢?其实是JVM来对此进行实现和保证的。每一个JAVA对象,其都对应一个Monitor(监视器)。标注有synchronized的语句块(如果我们将函数也看做是语句块,只不过是一段包装在函数内部的语句块)的头部和尾部,在JDK编译的时候,加入了monitorenter和monitorexit指令。通过对monitor的排他性获取,实现了同一时间仅有一个线程可以进入synchronized语句块,当语句执行完毕之后,线程释放monitor,从而使得其他等待获取monitor的线程依次获取到monitor,串行进入语句块,实现并行转串行。

使用方式

synchronized关键字的使用方式主要有以下三种:

  1. 修饰非静态方法
  2. 修饰静态方法
  3. 修饰语句块

修饰非静态方法的方式保证同一个对象(注意与静态方法进行区分)的这个方法内部同一时刻仅有一个线程可以进入;

相对应的,修饰静态方法的方式保证的是同一个类(注意不再是类的对象了,因为是静态方法)的这个方法内部同一时刻仅有一个线程可以进入。

修饰语句块的方式与修饰非静态方法的方式类似。

synchronized关键字的局限性

从上述的原理描述中,我们不难看出,由于synchronized是通过monitorenter和monitorexit来对语句块进行加锁,保证单一线程进入的,因此具备以下问题:

  1. 易死锁;
  2. 无法设置尝试获取锁的超时时间,一旦synchronized语句块中需要进行耗时的操作时,同时等待的线程的任务完成时间不可控且不可预期。

我们现在针对这二者进行简单的描述。

对于1,参见下述例子:

public class DeadLockDemo {

  public Integer aInt = new Integer(1);
  public Integer bInt = new Integer(1);

  public static void main(String[] args) {
    DeadLockDemo deadLockDemo = new DeadLockDemo();
    Thread thread1 = new Thread(() -> {
      synchronized (deadLockDemo.aInt) {
        System.out.println("in thread1");
        try {
          Thread.currentThread().sleep(5000);
          System.out.println("waiting for getting bInt object");
          synchronized (deadLockDemo.bInt) {
            System.out.println("got bInt object");
          }

        } catch (InterruptedException e) {
          e.printStackTrace();
        }

      }
    });
    thread1.start();
    Thread thread2 = new Thread(() -> {
      synchronized (deadLockDemo.bInt) {
        System.out.println("in thread2");
        try {
          Thread.currentThread().sleep(5000);
          System.out.println("waiting for getting aInt object");
          synchronized (deadLockDemo.aInt) {
            System.out.println("got aInt object");
          }

        } catch (InterruptedException e) {
          e.printStackTrace();
        }

      }
    });
    thread2.start();

  }
}

从中不难看出,共有两个线程。线程1首先获取aInt对象的锁,然后尝试获取bInt对象的锁。线程2与线程1恰恰相反。最终导致,线程1持有aInt对象的锁并尝试获取bInt对象的锁的同时,线程2也在尝试获取aInt对象的锁,导致二者均无法继续向下运行。

由于synchronized无法提供超时机制,因此一旦出现死锁很难通过超时机制解锁。

对于2,我们就不构造代码实例了。

改进方案

上边提出了两个比较影响实际工程使用的问题,JAVA后续提出了ReentrantLock(可重入锁)对此进行了改进。

ReentrantLock记录了尝试获取锁的线程数,并维护了等待线程的队列,提供公平方式(正常排队,根据到达等待时间点的先后顺序安排解锁后获取锁的顺序)和非公平方式(插队抢占模式),当解锁后将锁分配给后续等待的线程。此外,ReentrantLock提供了超时时间,使得等待时间更可控。

在后面的文章中,会重点介绍ReentrantLock的原理。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. synchronized关键字在使用层面的理解 synchronized关键字Java中用来实现线程同步的关键字,可以修饰方法和代码块。当线程访问被synchronized修饰的方法或代码块时,需要获取对象的,如果该已被其他线程获取,则该线程会进入阻塞状态,直到获取到为止。synchronized关键字可以保证同一时刻只有一个线程能够访问被定的方法或代码块,从而避免了多线程并发访问时的数据竞争和一致性问题。 2. synchronized关键字在字节码中的体现 在Java代码编译成字节码后,synchronized关键字会被编译成monitorenter和monitorexit指令来实现。monitorenter指令对应获取操作,monitorexit指令对应释放操作。 3. synchronized关键字在JVM中的实现 在JVM中,每个对象都有一个监视器(monitor),用来实现对象。当一个线程获取对象后,就进入了对象的监视器中,其他线程只能等待该线程释放后再去竞争synchronized关键字的实现涉及到对象头中的标志位,包括标志位和重量级标志位等。当一个线程获取后,标志位被设置为1,其他线程再去获取时,会进入自旋等待或者阻塞等待状态,直到标志位被设置为0,即被释放后才能获取。 4. synchronized关键字在硬件方面的实现 在硬件层面,的实现需要通过CPU指令和总线来实现。当一个线程获取时,CPU会向总线发送一个请求信号,其他CPU收到该信号后会进入自旋等待状态,直到被释放后才能获取。总线可以保证多个CPU之间的原子操作,从而保证的正确性和一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值