Java之同步锁

Posted uesowys

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java之同步锁相关的知识,希望对你有一定的参考价值。

1.  前言

众所周知,在单线程运行环境中,因为不存在资源竞争,所以不需要锁。但是,在多线程运行环境中,因为存在资源共享与竞争,为了合理分配资源以及公平地使用资源,所以需要锁。在计算机系统中,多线程需要多核处理器的支持,而每个核是以时间片的方式进行资源调度,一旦线程获取到时间片,则开始执行代码逻辑,线程没有获取时间片,则暂停执行代码逻辑。

Java支持同步锁(synchronization lock)机制以及可重入锁(re-entrant lock)机制,本章节主要描述同步锁的机制。

2.  同步锁定义

Java提供多种机制支持多线程之间的交互,包括同步锁机制、可重入锁机制以及内存可见性机制。其中最基本的是同步锁机制,同步锁机制使用监视器(monitors)实现。

每个Java对象都关联一个监视器(monitor),在多线程的运行环境中,当一个线程需要使用一个Java对象的同步锁机制的时候,都需要锁住(lock)这个对象对应的监视器,当不需要使用该对象的时候,都需要解锁(unlock)这个对象对应的监视器,同时,一次只能有一个线程允许锁住该对象对应的监视器,其他线程只能阻塞而等待监视器解锁。此外,一个线程能多次锁住同一监视器。

Java对象中使用同步锁机制的方式包括同步语句(synchronized statement)以及同步方法(synchronized method)。

2.1.  同步语句(synchronized statement)

Java语言提供的锁类型包括互相排斥锁类型以及互相共享锁类型,而同步锁是属于互相排斥锁类型,也称之为独占锁类型,当一个线程获取到同步锁(独占该锁,其他线程不能获取),则开始执行同步块,执行完毕解锁给其他线程使用。

同步语句也被称之为同步块,其使用的关键字是synchronized,语法形式是synchronized(表达式)同步块,其中表达式的计算结果是引用类型。代码示例如下所示:

以下步骤描述同步语句的执行逻辑:

  • 首先计算(表达式)中的表达式,从计算结果中获取到代表同步锁的引用对象V

  • 如果计算表达式的过程中发生异常,则结束执行当前同步块,如果锁引用对象V是空,则抛出空指针异常

  • 运行线程锁住V的监视器(monitor),锁不住,则当前运行线程阻塞等待

  • 运行线程执行同步块中的同步块

  • 如果执行同步块过程中无异常发生,执行完毕则解锁V的监视器锁,结束同步块,如果执行同步块过程中发生异常,解锁V的监视器锁,结束同步块,抛出异常

此外,同一个Java对象中,在出现同步块的逻辑中才存在监视器锁的竞争,在该Java对象中的其他域或者其他非同步块的逻辑可以正常地被其他线程同时执行。

2.2.  同步方法(synchronized method)

同步方式使用关键字synchronized修饰一个类方法,当前一个线程执行该类的同步方法之前,需要锁住该方法对应的监视器(monitor),如果是执行静态方法,则该监视器是属于类的,如果是执行非静态方法,则该监视器是属于类实例的。

同步方法与同步块的执行逻辑相同,执行完毕或者执行中途发生异常,则需要解锁对象的监视器。

Test类的同步方法的代码示例如下所示,getNonStaticA、getNonStaticA_same使用相同的Test类实例的监视器,getStaticA、getStaticA_same使用相同的Test类的监视器:

以下代码所示,在多线程的并发环境中使用同一个Test类实例,如果调用getNonStaticA的次数与调用setNonStaticA的次数相同,则nonstatic_a的值等于第一次调用的值。

3.  字节码分析

本章节从编译后字节码的角度分析同步块的执行逻辑,以下示例是Test类的main方法内使用同步块的字节码:

下表详细描述Test类的main方法字节码所表示的意义:

字节码

描述

0: new

新建的localLock对象

4: invokespecial

初始化对象的基本初始化方法

7: astore_1

保存localLock对象到变量1

8: aload_1

入栈

11: monitorenter

第一次进入监视器(获取监视器锁)

14: astore_3

保存localLock对象到变量3

15: monitorenter

第二次进入监视器(获取监视器锁)

16: getstatic

19: ldc

21: invokevirtual

执行同步块的业务逻辑

24: aload_3

监视器锁入栈

25: monitorexit

解锁第二次进入监视器

32: aload_2

监视器锁入栈

33: monitorexit

解锁第一次次进入监视器

Exception table:

异常表,异常流程需要使用该表

Test类同步方法setNonStaticA的字节码如下所示,同步方法没有使用监视器的指令,只使用同步标识,JVM在执行字节码的时候,判断方法的同步标识,如果是同步方法则默认地执行监视器锁操作、解锁操作:

4.  原理分析

由以上字节码分析章节所述,同步块与同步方法的执行方式存在区别,但是两种同步锁方式都使用了相同的机制:监视器(monitor)。

4.1.  同步语句原理

同步块使用监视器涉及到两个字节码指令monitorenter、monitorexit,下面分别详细描述这两个指令。

4.1.1. monitorenter指令

monitorenter指令对应的操作数是引用对象,而每个对象对应一个监视器(monitor),当这个监视器拥有属主,则说明其已被调用线程锁定,调用线程使用monitorenter指令去获得引用对象监视器的拥有权的流程如下所述:

  • 如果引用对象监视器的进入次数e=0,则调用线程t1进入监视器并设置e=1,设置成功后调用线程t1成为该监视器的拥有者

  • 如果调用线程t1已经成为该监视器的拥有者,则再次进入监视器时,设置e=e+1

  • 如果其他线程t2已经拥有该监视器,则调用线程t1阻塞直到e=0,然后t1尝试进入监视器

如果监视器对应的引用对象为空,则抛出空指针异常。

4.1.2. monitorexit指令

monitorexit指令对应的操作数是引用对象,与monitorenter指令对应的是同一个引用对象。执行monitorexit指令的调用线程必须是引用对象监视器的拥有者,调用线程进入监视器设置进入次数e=e-1,如果e=0则调用线程退出监视器,调用线程不再是监视器的拥有者,然后,其他阻塞线程可以调用monitorenter指令进入监视器成为该监视器的拥有者。

如果监视器对应的引用对象为空,则抛出空指针异常。

4.2.  同步方法原理

同步方法的字节码执行方式与同步块的不同,同步方法是在当方法被调用时由JVM根据方法(method_info)提供的信息判断方法是否是同步方法,如果是同步方法,则自动地对引用对象的监视器加锁,当方法的代码逻辑被执行完成,JVM在被调用方法返回时对引用对象的监视器解锁。

4.2.1. 类文件method_info

Java对象类文件的字节码对应的数据结构如下所示,其中method_info字段是存储类对象的方法信息:

其中method_info的数据结构如下所示,其中access_flags标识了方法的各种类型,该类型的长度是2个字节共计16位,使用二进制mask运算的方式标识16位中的每个一字节位的信息:

access_flags标识的类型如下表所示,其中ACC_SYNCHRONIZED位的标识是同步方法:

Java多线程之Lock(锁)

Java多线程之Lock(锁)

定义:

在Java多线程中,可以使用synchronized关键字实现线程之间的同步,在JDK5后新增的ReentrantLock类同样可达到此效果,且在使用上比synchronized更加灵活。

特征:

  • 从JDK5开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步;

  • 同步锁使用Lock对象来充当;

  • import java.util.concurrent.locks.Lock
  • 以上接口时控制多个线程对共享资源进行访问的工具;

  • 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

synchronized与Lock的对比:

  • Lock是显式锁(手动开启和手动关闭锁);

  • synchronized是隐式锁,出了作用域后自动释放;

  • Lock只有代码块锁;

  • synchronized有代码块锁和方法锁;

  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且有更好的扩展性(提供更多的子类);

  • 优先使用顺序:

    • Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)。

示例:

package 线程同步;
import java.util.concurrent.locks.ReentrantLock;
?
//lock锁
public class TestLock2 {
   public static void main(String[] args) {
       lock lock = new lock();
?
       new Thread(lock,"A").start();
       new Thread(lock,"B").start();
       new Thread(lock,"C").start();
  }
}
class lock implements Runnable{
   int num=10;
?
   ReentrantLock ll=new ReentrantLock();
?
   @Override
   public void run() {
       while (true){
           try {
               ll.lock();      //加锁
               if (num>0){
                   try {
                       Thread.sleep(1000);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
                   System.out.println(Thread.currentThread().getName()+"得到了第"+num--);
              }else
                   break;
          }
           finally {
               ll.unlock();    //解锁
          }
      }
  }
}
?

备注:lock()方法和unlock()方法需要放在try-catch-finally中。

以上是关于Java之同步锁的主要内容,如果未能解决你的问题,请参考以下文章

Java同步锁——lock与synchronized 的区别转

作为Java开发程序员,java的语言环境包括

死磕 java同步系列之redis分布式锁进化史

死磕 java同步系列之redis分布式锁进化史

Java多线程之synchronized及其优化

JAVA并发之锁获取步骤及锁优化