美文网首页
Java synchronized理解

Java synchronized理解

作者: 涂豪_OP | 来源:发表于2018-08-06 20:45 被阅读104次

    一:概述

        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

    相关文章

      网友评论

          本文标题:Java synchronized理解

          本文链接:https://www.haomeiwen.com/subject/unswvftx.html