Java synchronized理解

一:概述

    java发生线程安全的有原因有两个因素:第一,存在共享资源(也称临界资源,不知道为什么取这破名字);第二,存在多条线程操作共享数据。为了解决此问题,我们需要在一个线程访问共享资源的时候,别的线程无法访问此资源,达到共享资源访问互斥的目的。在java中,关键字synchronized可以保证在同一时刻,只有一个线程可以访问某个方法或者模块代码,同时,synchronized还能保证共享资源在一个线程里面的变化可以反映到其他线程,也就是其他线程能够做到这个共享资源已经变化,从而取到最新的共享资源的值,这就是保证共享资源的可见性,完全代替volatile关键字。

二:synchronized的三种使用方式

    synchronized关键字主要有以下三种使用场景:
    1.修饰实例方法,此时锁住的是该实例对象,调用该实例方法时需要获取该实例的锁。
    2.修饰静态方法,此时锁住的是该类对象,调用该类的静态方法时需要获得该类的锁。
    3.修饰代码块,此时锁住的是该实例对象,执行该代码块时需要获得该实例的锁。

    synchronized作用于实例方法:

public class AccountingSync implements Runnable {

    //共享资源
    static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        AccountingSync as = new AccountingSync();
        Thread t1 = new Thread(as);
        Thread t2 = new Thread(as);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    
    //synchroniezed 修饰实例方法
    public synchronized void increase() {
        i++;
    }
    
    @Override
    public void run() {
        for(int j = 0 ; j < 10000 ; j++) {
            increase();
        }
    }
}

    输出结果如下:

20000

    在上例中,i++不具备原子性,他实现的流程是先读,再加1,后写入结果;在读和写之间可以别的线程打断;假设increase方法没有使用关键字synchronized修饰,那么t1线程调用i++,在完成自增后,把数据往回写;再写入操作完成之前,t2可能也去执行自增,此时t1线程的执行被打断,由于t1自增的结果还没有写回去,所以t2拿到的i的值是t1自增前的值,这样就会使得最终结果比20000小。如果加了synchronized关键字,在t1完成自增后写入过程之前,因为t1没有释放实例as的锁,所以t2拿不到这把锁,此时t2是无法操作i的,必须等待t1把i自增后的结果写回完成并释放as的锁之后,t2才有可能拿到as的锁,接着执行自增操作。因为一个对象只有一把锁,就算t1在同步执行increase方法之前,t2也不能执行别的被synchronized修饰的实例方法;但是t2可以执行没有被synchronized修饰的方法。

    如果有两个实例对象,他们的锁肯定就不一样了,如果t1去执行第一个实例对象的synchronized方法,此时t2去执行另一个对象synchronized方法,此时还是线程安全的;但是如果这两个方法都去操作共享资源,那么就会产生线程不安全的问题:

public class AccountingSyncFailure implements Runnable {

    //共享资源
    static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        //创建两个Runnable对象
        AccountingSyncFailure as = new AccountingSyncFailure();
        AccountingSyncFailure as1 = new AccountingSyncFailure();
        
        //两个Thread对应两个不同的Runnable对象
        Thread t1 = new Thread(as);
        Thread t2 = new Thread(as1);
        
        t1.start();
        t2.start();
        
//      //join的含义是当前线程等待thread线程终止后才能从thread.join返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
    
    //synchroniezed 修饰实例方法
    public synchronized void increase() {
        i++;
    }
    
    @Override
    public void run() {
        for(int j = 0 ; j < 10000 ; j++) {
            increase();
        }
    }
}

    在上例中,虽然创建了两个实例as1和as2,但是他们都去访问了共享资源i,所以最终的结果肯定比20000小。

    synchronized修饰类方法

    synchronized修饰类方法时,作用的是当前类的锁,如果线程t1调用实例的非静态 synhronized方法,那么另一个线程t2是可以调用这类的静态 synchronized方法的,不会发生互斥现象,因为访问静态synchronized方法需要的是持有类的锁,而访问实例synchronized方法需要的是持有对象的锁,不是同一把锁:

public class TestStatic implements Runnable{
    static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        
        TestStatic ts = new TestStatic();
        
        Thread t1 = new Thread(ts);
        Thread t2 = new Thread(ts);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    
    //类方法,访问该方法需要获得类的锁,就是Class对象的锁
    public static synchronized void increase(){
        i++;
    }
    
    //实例方法,访问该方法需要获得实例对象的锁
    public synchronized void increase1() {
        i++;
    }
    
    @Override
    public void run() {
        for(int j = 0 ; j < 10000 ; j++) {
            increase();
        }
    }
}

    synchronized修饰同步代码块

    除了修饰方法,synchronized还可以修饰代码块,有些方法,可能只需要部分代码同步,其他的代码不会发生线程安全问题的话,此时只需要将需要同步的代码块用synchronized修饰即可:

public class TestCode implements Runnable {

    static TestCode tc = new TestCode();
    
    static int i = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(tc);
        Thread t2 = new Thread(tc);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    
    @Override
    public void run() {
        //tc也可以用this代替,建议用this
        synchronized (tc) {
            
            //这里可以根据自己的需要添加无需同步的逻辑
            
            for(int j = 0 ; j < 10000 ; j++) {
                i++;
            }
        }
    }
}

三:synchronized的实现原理

    讲了这么多同步同步同步,那么用于同步的关键字synchronized是怎么实现的呢?要想理解此问题,首先要理解虚拟机的运行时数据区:
运行时数据区

    注意,这是虚拟机的运行时数据区,并不是所谓的java内存模型。虚拟机栈存放栈帧,栈帧里面存放局部变量表,操作数栈,返回地址等信息;本地方法栈是与native方法相关的,不管;堆大家都知道,真正存放对象的地方;方法区(JDK1.8以后叫元数据区)是存放类信息和常量池等信息的。堆和方法区是线程共享的。假装有下面的代码:
A a = new A()

    o创建后,对象分布如下:
对象内存分布

    上图就是一个普通的对象被创建后的内存分布图,a就是我们常说的引用,栈里面的引用指向堆里面的对象,这很好理解。同时堆里面又有指针指向方法区的类信息,但这不是我们关注的重点,我们关注的是堆里面的实例对象,将堆里面的实例对象分解如下:
对象信息

    从图中可以看出,堆里面的对象可以分为三部分:
    1.对象头,分为Mark Word 和 类型指针两部分,类型指针指向方法区的类信息;Mark Word里面存放的是对象自身的运行时数据,比如hash码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等。

    2.实例数据,这里存放的就是对象的成员属性的值了,包括父类的成员属性,如果存放数组的话,还有数组的长度。
    3.对齐填充,由于虚拟机规定对象的起始地址必须是8字节的整数倍,如果不足8字节的某个整数倍,那么写入空数据填充,这个不太关注。

    从上面的分析看来,跟synchronized相关的,就是对象头了,对象头是synchronized实现的基础,重点分析。

    虚拟机一般用2个字节来存放对象的头信息,如果该对象是数组类型的话,那么用3个字节来存放头信息,多出来的那个用来存储数组长度。虚拟机使用markOop类型来描述Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间,32位虚拟机的markOop实现如下:
    1.hash: 保存对象的哈希码
    2.age: 保存对象的分代年龄
    3.biased_lock: 偏向锁标识位
    4.lock: 锁状态标识位
    5.JavaThread*: 保存持有偏向锁的线程ID
    6.epoch: 保存偏向时间戳

    markOop中不同的锁标识位,代表着不同的锁状态(盗图):
锁状态

    而不同的锁状态,Mark Word中存储的数据又不一样:
存储内容
    各数据大小如下所示(盗图):
数据大小

    我们先分析重量级锁也就是通常说synchronized的对象锁,从图中可以看出,该锁的标记位是10,其中指针ptr指向的是monitor(管程或者监视器锁)的起始地址;每个实例对象都有一个monitor对象与之关联,实例对象和monitor对象之间的关系有多种实现方式,如monitor可以与实例对象一起创建、销毁,或者当线程试图获得对象锁时自动生成。当一个monitor对象被一个线程持有时,它便处于锁定状态。monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

     ObjectMonitor对象有两个队列:_WaitSet和_EntryList;这两个集合用来保存ObjectMonitor对象列表,因为每个等待锁的线程会被封装成ObjectMonitor对象;_owner指向持有ObjectMonitor对象的线程;当多个线程同时访问同步代码时,首先会进入_EntryList集合,当线程获得对象的monitor后进入_owner属性,并把_owner属性设置为当前线程,同时将计数器_count加一;若线程调用wait()方法,该线程将放弃当前持有的monitor对象,然后_owner置空,_count减一,同时该线程进入_WaitSet集合等待被唤醒;若当前持有monitor对象的线程执行完毕,也会放弃monitor对象,并将_owner置空,_count减一,以便其他的线程获得该锁(等同于获得monitor对象)。调用过程如下:
同步方法调用

    综上可知,monitor对象存在于对象头里面,这也是为什么我们常说锁的是对象,不是锁方法或者代码块的原因。下面看一个简单的例子:

public class Test{
    public int i;

    public void add(){
        synchronized(this){
            i++;
        }
    }
}

    这个例子非常简单,就是一个整形成员变量i和一个add方法,add里面的代码块添加了synchronized修饰,首先通过javac -g Test.java进行编译,然后用javap -verbose Test来查看他的字节码:

//class文件的路径
Classfile /home/tuhao/Test.class

  //文件创建的时间和大小
  Last modified 2018-8-6; size 452 bytes
  /MD5值
  MD5 checksum afb042fe0aa113f37c5dd70e791cdcfe
  //由Test.java这个文件编译而来
  Compiled from "Test.java"
//类名
public class Test
  //源文件
  SourceFile: "Test.java"
  //此文件支持的JDK最小版本
  minor version: 0
  //此文件支持的最大JDK版本(51代表JDK1.7)
  major version: 51
  //此类的修饰符,含义可以去网上查
  flags: ACC_PUBLIC, ACC_SUPER
  
//此类的常量池,常量池非常重要
Constant pool:
   #1 = Methodref          #4.#21         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#22         //  Test.i:I
   #3 = Class              #23            //  Test
   #4 = Class              #24            //  java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LTest;
  #14 = Utf8               add
  #15 = Utf8               StackMapTable
  #16 = Class              #23            //  Test
  #17 = Class              #24            //  java/lang/Object
  #18 = Class              #25            //  java/lang/Throwable
  #19 = Utf8               SourceFile
  #20 = Utf8               Test.java
  #21 = NameAndType        #7:#8          //  "<init>":()V
  #22 = NameAndType        #5:#6          //  i:I
  #23 = Utf8               Test
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/Throwable
{
  //成员属性i和他的修饰符
  public int i;
    flags: ACC_PUBLIC
  //Test的构造函数
  public Test();
    flags: ACC_PUBLIC
    //构造函数调用的流程,通过虚拟机指令来表示
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LTest;
  //重点关注add方法
  public void add();
    //add函数调用的流程,通过虚拟机指令来表示
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         //将局部变量表的第0个位置的参数(this)压入栈顶
         0: aload_0       
         //复制操作数栈栈顶的值,并插入到栈顶
         1: dup           
         //弹出栈顶的数据,存入局部变量表的第2个位置
         2: astore_1      
         //执行monitorenter指令,java虚拟机规范是这样解释这个指令的:进入一个对象的 monitor
         3: monitorenter                      //重点关注monitorenter指令
         //将局部变量表的第一个参数(this)压入操作数栈中
         4: aload_0       
         //复制操作数栈栈顶的值,并插入到栈顶
         5: dup           
         //获取属性i的值
         6: getfield      #2                  // Field i:I
         //将常量1压入栈顶(用于++)
         9: iconst_1      
        //将栈里面的数据相加(i加上常量1)
        10: iadd          
        //把自增后的结果写会给i
        11: putfield      #2                  // Field i:I
        ////将局部变量表的第二个参数(this)压入操作数栈中
        14: aload_1       
        //退出对象的monitorexit
        15: monitorexit                      //重点关注monitorexit指令
        //执行第24条指令
        16: goto          24
        19: astore_2      
        20: aload_1       
        21: monitorexit   
        22: aload_2       
        23: athrow        
        24: return        
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 5: 0
        line 6: 4
        line 7: 14
        line 8: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      25     0  this   LTest;
      StackMapTable: number_of_entries = 2
           frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class Test, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
           frame_type = 250 /* chop */
          offset_delta = 4

}

    对于字节码不懂的,可以看我之前写的笔记:https://www.jianshu.com/p/635aea3a0ae2

    通过字节码可以看出,在虚拟机层面实现同步的指令是monitorenter和monitorexit,monitorenter用于同步代码开始的位置,monitorexit用于同步代码结束的位置;当执行monitorenter指令的时候,执行同步代码块的线程就会试图获取(不一定能成功)当前实例对象所对应的 monitor 的所有权,那么:

    1.如果对象的 monitor 的进入计数器为 0,那调用同步代码的线程可以成功进入 monitor,以及将计数器值设置为 1;调用线程就是 monitor 的所有者。
    2.如果当前线程已经拥有对象的 monitor 的所有权,那它可以重入这 个 monitor,重入时需将进入计数器的值加 1。
    3.如果其他线程已经拥有对象的 monitor 的所有权,那当前线程将被阻塞,直到 monitor 的进入计数器值变为 0 时,重新尝试获取 monitor 的 所有权。

    注意,一个monitorenter指令可能会与一个或多个monitorexit指令配合实现Java 语言中 synchronized 同步语句块的语义。但monitorenter和monitorexit指令不会用来实现 synchronized方法的语义,尽管它们确实可以实现类似的语义。当一个 synchronized 方法被调用时,自动进入对应的 monitor,当方法返回时,自动退出 monitor,这些动作是 Java 虚拟机在调用和返回指令中隐式处理的,所以在上面的例子中,如果将synchronized修饰add方法而不是add方法里面的代码块,那么编译出来的字节码中是没有monitorenter和monitorexit指令指令的,不过在add方法的修饰符里面有个synchronized。在 Java 语言里面,同步的概念除了包括 monitor 的进入和退出操作以外,还包括有等待(Object.wait)和唤醒(Object.notifyAll 和 Object.notify)。这些操作包含在 Java 虚拟机提供的标准包 java.lang 之中,而不是通过 Java 虚拟机的指令集来显式支持(没事多看看java虚拟机规范,很有好处,而且此规范真的不难)。

四:synchronized的优化

    上面分析了synchronized的使用方法和实现原理,但是必须注意到synchronized是一个重量级锁,效率较低,因为管程(monitor)是依赖于操作系统的Mutex Lock来实现的,在切换线程的时候,需要从用户态切换到内核态,这个切换需要比较长的时间,所以早期的synchronized是比较低效的。JDK1.6后,官方从虚拟机层面对synchronized进行了较大幅度的优化,为了减少获得锁和释放锁所带来的性能消耗,java引入和轻量级锁和偏向锁。

  1.偏向锁
    java官方解释,经过大量实验表明,大多数情况下,锁不存在多线程竞争,而是同一把锁经常被同一线程多次获取(既然是官方说的,我们就姑且相信吧);因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价,从而引入了偏向锁。偏向锁的核心思想是(完全没有必要去跟源码):如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word(上面介绍过,还贴了两张图)就进入偏向模式,此时Mark Word的结构也将变成偏向锁结构,锁标志位是01,此时并不会触发同步。如果此线程再次请求锁时,将无需再次获取锁,而是直接执行代码,这样就省去了申请锁(还有执行完了释放锁)的资源消耗。所以,在锁竞争不激烈的情况下,偏向锁能够大幅度提高性能。但是,在竞争比较激烈的场景下,偏向锁就失去了作用;因为竞争激烈的的场景,前一次申请锁的线程很有可能不是这次申请锁的线程;此时,偏向锁失败,失败后并不是直接升级为重量级锁,而是升级为轻量级锁。

    总的来说,偏向锁就是通过消除资源无竞争下的同步语义,达到提高性能的目的,偏向锁的获取过程如下:
    a.访问对象头Mark Word中偏向锁的标记位是否设置成了1,锁标记是否为01, 确认为可偏向状态。
    b.如果是可偏向状态,检查对象头保存的线程ID是否是当前线程的ID,如果是执行同步代码。
    c.如果Mark Word保存的线程ID不是当前的线程ID,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中的线程ID更新为当前线程ID,然后执行同步代码。
    d.如果竞争失败,那么到达全局安全点(safepoint,这个时间点上没有字节码在执行)后时,此前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码;撤销偏向锁的时候会有stop the world现象,也就是卡一下,时间很短,跟GC类似。
    e.执行同步代码

  2.轻量级锁
    在锁竞争激烈的情况下,如果偏向失败,升级为轻量级锁,此时Mark Word的结构也将变成轻量级锁的结构,锁标记位是00;轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。轻量级锁的后偶去过程如下:

    a.在执行同步代码时,如果对象锁状态为无锁(此时锁标记位是01,偏向锁标记位是0),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存放对象头的Mark Word拷贝,这个空间也叫做Displace Lock Record;此时,对象头和栈的状态如下(盗图):


Lock Record

    b.拷贝对象头的Mark Word到当前线程的栈帧的Lock Record中。

    c.拷贝成功后,虚拟机使用CAS操作尝试将对象头的Mark Word的更新为指向Lock Record的指针,并将Lock Record里面的_owner指针指向对象头的Mark Word;如果更新成功,则执行步骤d,否则执行步骤e;此时,对象头和栈的状态如下(盗图):
Lock Record1

    d.如果更新成功,那么该线程就拥有了该对象的锁,并且对象头的Mark Word设置成00(轻量级锁的标记位)。

    e.如果更新失败,虚拟机首先检查对象头的Mark Word是否指向当前线程的栈帧,如果指向,说明当前线程获得了该对象的锁,那么直接执行同步代码;如果不指向,说明多个线程在竞争同一把锁,轻量级锁要升级为重量级锁,将锁状态置为10(重量级锁标记位),Mark Word中存储的就是指向重量级锁的指针(?),后面等待锁的线程也要进入阻塞状态,而当前线程则通过自旋锁来获取锁,自旋是为了不让线程阻塞,采用轮询的方式去获取锁。

  3.自旋锁
    轻量级锁失败后,虚拟机为了避免竞争失败的线程挂起,还会采取自旋锁来优化。假装t1线程竞争失败后,t2正在执行同步代码;如果t1失败后立马挂起,就会由用户态转到内核态,这个开销是较大的;如果t2执行同步代码很快,共享资源马上得到释放,轮到t1去执行,可是t1正在老老实实的切换(快来欺负老实人)状态,好不容易等他切到挂起状态,又发现轮到自己访问共享资源了,然后又老老实实的切回来,多费事。自旋锁就是虽然一个线程虽然现在竞争失败了,但是假设持有锁的线程很快就能执行完毕,那么失败的线程等等再去访问共享资源又有何妨?没必要真的取切换状态,因为切换的成本太高了。根据官方解释,经过大量实验表明,大多数情况下,线程持有锁的时间不会太长(既然是官方说的,我们就姑且相信吧),所以就让竞争失败的线程做若干次空循环(这就是所谓的自旋),经过循环,再去访问共享资源,此时之前持有锁的线程很有可能已经访问完毕了,这样这个失败的线程就可以持有锁了,然后执行同步代码。当然了,也不能老在那循环,因为空循环也是占用CPU资源的,所以多次循环后还没有拿到锁,那就真的挂起了。

  3.锁消除
    锁消除是一种更为彻底的优化手段。虚拟机在进行JIT编译(即时编译)时,通过扫描上下文,去除不可能存在共享资源竞争的锁,这样可以省去无谓的申请锁的时间和开销。比如下面的例子:

public class TestBuffer {

    public static void main(String[] args) {
        TestBuffer tb = new TestBuffer();
        for(int i = 0 ; i < 10000; i++) {
            tb.add("a", "b");
        }
    }
    
    public void add(String s1 , String s2) {
        //StringBuffer本身就是线程安全的,append也被synchronized
        //修饰过,sb又是局部变量,并不会被其他线程使用,所以sb不会存
        //在资源竞争的问题所以append方法的synchronized可以被消除掉
        StringBuffer sb = new StringBuffer();
        sb.append(s1).append(s2);
    }
}

    当然,上面仅仅是举一个例子,毕竟在一个for循环里面去大量创建StringBuffer对象并不是上面好的写法。

四:synchronized的关键点

  1.synchronized的重入性
    如果一个线程持有了一个对象的锁,然后再次访问该对象的共享资源时,这种现象叫做重入, 请求将会成功。比如线程在获得对象锁后,去执行这个对象的同步方法,在执行方法的过程中又去执行另个同步方法,也就是拿到这个对象的锁后再去请求该锁,这是允许的。总结起来就是,一个线程拿到对象锁后再次请求锁,这是允许的,这就是synchronized的重入性。

public class TestAgain implements Runnable{
    
    static TestAgain ta = new TestAgain();
    
    static int a , b = 0;
    
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(ta);
        Thread t2 = new Thread(ta);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a : " + a + " , b : " + b);
    }


    @Override
    public void run() {
        for(int i = 0 ; i < 10000; i++) {
            //申请当前对象的锁
            synchronized(this) {
                a++;
                //再次请求,这是允许的,也会成功的
                increase();
            }
        }
    }
    
    //同步方法
    public synchronized void increase() {
        b++;
    }
}

    在上例中,synchronized(this)已经拿到了该对象的锁,然后在执行同步方法increase的时候,又会去申请该对象的锁,这是可以的,没毛病。需要注意的是,子类继承父类时,子类也可以通过重入性调用父类的同步方法。记得上面介绍montior的时候有个属性_count,每次重入时,_count都会+1。

  2.线程中断与synchronized

    java提供了下面三种方法使得线程中断:

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

    当一个线程处于被阻塞状态,或者试图执行一个阻塞操作时,使用Thread.interrupt()中断该线程,此时会抛出一个InterruptedException异常,同时中断状态将被复位,也就是从中断状态变成非中断状态:

public class TestInterrupt {
    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread() {
            public void run() {
                //无限循环
                try {
                    //while在try语句块里面,通过异常中断可以退出run方法
                    while(true) {
                        //当线程处于阻塞状态,线程必须捕获,无法向外抛出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interrupt when sleeping");
                    boolean interrupt = this.isInterrupted();   
                    //中断状态被复位
                    System.out.println("interrrupt : " + interrupt);
                    }
            };
        };
        
        //启动线程
        t1.start();
        
        //这里也睡2秒
        TimeUnit.SECONDS.sleep(2);
        
        //主动中断处于阻塞状态的线程
        t1.interrupt();
    }
}

    这里创建一个子线程,在子线程的while循环里面使得子线程无限睡眠;然后在主线程睡眠两秒,然后主动中断处于睡眠状态的子线程,输出结果如下:

Interrupt when sleeping
interrrupt : false

    可以看到,子线程的中断状态果然被复位了。

    但是中断操作对于正在等待获取锁对象的线程来说,并不起作用;也就说,如果一个线程正在等待对象锁,那么这个线程要么继续等待,要么拿到锁,执行被synchronized修饰的代码,就算你手动中断也无效:

public class SynchronizedBlock implements Runnable{
    
    //在构造器里面创建子线程执行同步方法
    public SynchronizedBlock() {
        new Thread() {
            public void run() {
                test();
            };
        }.start();
    }
    
    
    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlock sb = new SynchronizedBlock();
        Thread t = new Thread(sb);
        
        t.start();
        
        TimeUnit.SECONDS.sleep(1);
        
        //主动中断线程,但是run方法的log将不会被打印
        t.interrupt();
        
    }
    
    //随便定义一个同步方法
    public synchronized void test() {
        System.out.println("call test method");
        while(true) {
        }
    }

    //自己的run方法
    @Override
    public void run() {
        while(true) {
            //如果主线程中断了,那么打出log
            if(Thread.interrupted()) {
                System.out.println("线程中断");
            }else {
                //没有中断就调用test方法
                test();
            }
        }
    }
}

    上例中,在构造函数里面创建一个子线程并运行,然后在main方法里面再运行线程,然后主动中断线程,输出结果如下:

call test method

    可以看到,"线程中断"这行log没有打印出来,说明正在等待对象锁的线程是无法被打断的。

摘自:https://blog.csdn.net/javazejian/article/details/72828483 (略有修改)
引用:https://blog.csdn.net/zqz_zqz/article/details/70233767

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容