精华内容
下载资源
问答
  • 在eclipse开启debug模式的时候,即使synchronized这段代码没有打断点,但还是会跳到此处进行断点, 解决方法是在preferences中, java–>debug 取消勾选此2项 eclipse版本为Neon. ...

    在eclipse开启debug模式的时候,即使synchronized这段代码没有打断点,但还是会跳到此处进行断点,
    解决方法是在preferences中, java–>debug 取消勾选此2项
    eclipse版本为Neon.

    展开全文
  • 深入理解Java并发之synchronized实现原理

    万次阅读 多人点赞 2017-06-04 17:44:44
    需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前...

    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
    http://blog.csdn.net/javazejian/article/details/72828483
    出自【zejian的博客】

    关联文章:

    深入理解Java类型信息(Class对象)与反射机制

    深入理解Java枚举类型(enum)

    深入理解Java注解类型(@Annotation)

    深入理解Java类加载器(ClassLoader)

    深入理解Java并发之synchronized实现原理

    Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    深入理解Java内存模型(JMM)及volatile关键字

    剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理

    剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

    并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

    本篇主要是对Java并发中synchronized关键字进行较为深入的探索,这些知识点结合博主对synchronized的个人理解以及相关的书籍的讲解(在结尾参考资料),如有误处,欢迎留言。

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

    synchronized的三种应用方式

    synchronized关键字最主要有以下3种应用方式,下面分别介绍

    • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

    • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

    • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    synchronized作用于实例方法

    所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

    public class AccountingSync implements Runnable{
        //共享资源(临界资源)
        static int i=0;
    
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            AccountingSync instance=new AccountingSync();
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
        /**
         * 输出结果:
         * 2000000
         */
    }

    上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

    public class AccountingSyncBad implements Runnable{
        static int i=0;
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新实例
            Thread t1=new Thread(new AccountingSyncBad());
            //new新实例
            Thread t2=new Thread(new AccountingSyncBad());
            t1.start();
            t2.start();
            //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }

    上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

    synchronized作用于静态方法

    当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码

    public class AccountingSyncClass implements Runnable{
        static int i=0;
    
        /**
         * 作用于静态方法,锁是当前class对象,也就是
         * AccountingSyncClass类对应的class对象
         */
        public static synchronized void increase(){
            i++;
        }
    
        /**
         * 非静态,访问时锁不一样不会发生互斥
         */
        public synchronized void increase4Obj(){
            i++;
        }
    
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            //new新实例
            Thread t1=new Thread(new AccountingSyncClass());
            //new心事了
            Thread t2=new Thread(new AccountingSyncClass());
            //启动线程
            t1.start();t2.start();
    
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

    synchronized同步代码块

    除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        @Override
        public void run() {
            //省略其他耗时操作....
            //使用同步代码块对变量i进行同步操作,锁对象为instance
            synchronized(instance){
                for(int j=0;j<1000000;j++){
                        i++;
                  }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }
    

    从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

    //this,当前实例对象锁
    synchronized(this){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }
    
    //class对象锁
    synchronized(AccountingSync.class){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

    了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

    synchronized底层语义原理

    Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

    理解Java对象头与Monitor

    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

    而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

    虚拟机位数头对象结构说明
    32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

    其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

    锁状态25bit4bit1bit是否是偏向锁2bit 锁标志位
    无锁状态对象HashCode对象分代年龄001

    由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

    其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,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,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

    由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

    synchronized代码块底层原理

    现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下

    public class SyncCodeBlock {
    
       public int i;
    
       public void syncTask(){
           //同步代码库
           synchronized (this){
               i++;
           }
       }
    }

    编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
      Last modified 2017-6-2; size 426 bytes
      MD5 checksum c80bc322c87b312de760942820b4fed5
      Compiled from "SyncCodeBlock.java"
    public class com.zejian.concurrencys.SyncCodeBlock
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
      //........省略常量池中数据
      //构造函数
      public com.zejian.concurrencys.SyncCodeBlock();
        descriptor: ()V
        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 7: 0
      //===========主要看看syncTask方法实现================
      public void syncTask();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter  //注意此处,进入同步方法
             4: aload_0
             5: dup
             6: getfield      #2             // Field i:I
             9: iconst_1
            10: iadd
            11: putfield      #2            // Field i:I
            14: aload_1
            15: monitorexit   //注意此处,退出同步方法
            16: goto          24
            19: astore_2
            20: aload_1
            21: monitorexit //注意此处,退出同步方法
            22: aload_2
            23: athrow
            24: return
          Exception table:
          //省略其他字节码.......
    }
    SourceFile: "SyncCodeBlock.java"

    我们主要关注字节码中的如下代码

    3: monitorenter  //进入同步方法
    //..........省略其他  
    15: monitorexit   //退出同步方法
    16: goto          24
    //省略其他.......
    21: monitorexit //退出同步方法

    从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

    synchronized方法底层原理

    方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

    public class SyncMethod {
    
       public int i;
    
       public synchronized void syncTask(){
               i++;
       }
    }

    使用javap反编译后的字节码如下:

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
      Last modified 2017-6-2; size 308 bytes
      MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
      Compiled from "SyncMethod.java"
    public class com.zejian.concurrencys.SyncMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool;
    
       //省略没必要的字节码
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
          LineNumberTable:
            line 12: 0
            line 13: 10
    }
    SourceFile: "SyncMethod.java"

    从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

    Java虚拟机对synchronized的优化

    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

    偏向锁

    偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

    轻量级锁

    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

    自旋锁

    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

    锁消除

    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

    /**
     * Created by zejian on 2017/6/4.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     * 消除StringBuffer同步锁
     */
    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
            //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    
    }

    关于synchronized 可能需要了解的关键点

    synchronized的可重入性

    从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        static int j=0;
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
    
                //this,当前实例对象锁
                synchronized(this){
                    i++;
                    increase();//synchronized的可重入性
                }
            }
        }
    
        public synchronized void increase(){
            j++;
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

    线程中断与synchronized

    线程中断

    正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

    //中断线程(实例方法)
    public void Thread.interrupt();
    
    //判断线程是否被中断(实例方法)
    public boolean Thread.isInterrupted();
    
    //判断是否被中断并清除当前中断状态(静态方法)
    public static boolean Thread.interrupted();

    当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下代码将演示该过程:

    public class InterruputSleepThread3 {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    //while在try中,通过异常中断就可以退出run循环
                    try {
                        while (true) {
                            //当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
                            TimeUnit.SECONDS.sleep(2);
                        }
                    } catch (InterruptedException e) {
                        System.out.println("Interruted When Sleep");
                        boolean interrupt = this.isInterrupted();
                        //中断状态被复位
                        System.out.println("interrupt:"+interrupt);
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            //中断处于阻塞状态的线程
            t1.interrupt();
    
            /**
             * 输出结果:
               Interruted When Sleep
               interrupt:false
             */
        }
    }

    如上述代码所示,我们创建一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。这里有些人可能会诧异,为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单,前者使用时并没有明确的单位说明,而后者非常明确表达秒的单位,事实上后者的内部实现最终还是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。ok~,除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        System.out.println("未被中断");
                    }
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
    
            /**
             * 输出结果(无限执行):
                 未被中断
                 未被中断
                 未被中断
                 ......
             */
        }
    }

    虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:

    public class InterruputThread {
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(){
                @Override
                public void run(){
                    while(true){
                        //判断当前线程是否被中断
                        if (this.isInterrupted()){
                            System.out.println("线程中断");
                            break;
                        }
                    }
    
                    System.out.println("已跳出循环,线程中断!");
                }
            };
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.interrupt();
    
            /**
             * 输出结果:
                线程中断
                已跳出循环,线程中断!
             */
        }
    }

    是的,我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程,注意非阻塞状态调用interrupt()并不会导致中断状态重置。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:

    public void run(){
        try {
        //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
        while (!Thread.interrupted()) {
            TimeUnit.SECONDS.sleep(2);
        }
        } catch (InterruptedException e) {
    
        }
    }

    中断与synchronized

    事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下

    /**
     * Created by zejian on 2017/6/2.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class SynchronizedBlocked implements Runnable{
    
        public synchronized void f() {
            System.out.println("Trying to call f()");
            while(true) // Never releases lock
                Thread.yield();
        }
    
        /**
         * 在构造器中创建新线程并启动获取对象锁
         */
        public SynchronizedBlocked() {
            //该线程已持有当前实例锁
            new Thread() {
                public void run() {
                    f(); // Lock acquired by this thread
                }
            }.start();
        }
        public void run() {
            //中断判断
            while (true) {
                if (Thread.interrupted()) {
                    System.out.println("中断线程!!");
                    break;
                } else {
                    f();
                }
            }
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedBlocked sync = new SynchronizedBlocked();
            Thread t = new Thread(sync);
            //启动后调用f()方法,无法获取当前实例锁处于等待状态
            t.start();
            TimeUnit.SECONDS.sleep(1);
            //中断线程,无法生效
            t.interrupt();
        }
    }

    我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();但并不能中断线程。

    等待唤醒机制与synchronized

    所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

    synchronized (obj) {
           obj.wait();
           obj.notify();
           obj.notifyAll();         
     }

    需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

    ok~,篇幅已比较长了,关于synchronized,我们就暂且聊到这。如有错误,欢迎指正,谢谢~~。

    本篇的主要参考资料:
    《Java编程思想》
    《深入理解Java虚拟机》
    《实战Java高并发程序设计》

    感觉文章给你带来收获就赞一个吧!

    展开全文
  • java synchronized

    2021-03-17 19:55:14
    java synchronized一、简介常用的有如下几种方式synchronized(this)、synchronized(Object)与synchronized修饰静态方法、synchronized(class)二、修饰本身对象如下两种写法是具有一样的效果,都是锁住自己本身的对象...

    java synchronized

    一、简介

    常用的有如下几种方式

    synchronized(this)、synchronized(Object)与synchronized修饰静态方法、synchronized(class)

    二、修饰本身对象

    如下两种写法是具有一样的效果,都是锁住自己本身的对象,当同一对象多次调用时,同步锁会起作用。

    1.synchronized代码块

    写法一:

    public synchronized void method()

    {

    // todo

    }

    写法二:

    public void method()

    {

    synchronized(this) {

    // todo

    }

    }

    2.非synchronized代码块

    class Counter implements Runnable{

    private int count;

    public Counter() {

    count = 0;

    }

    public void countAdd() {

    synchronized(this) {

    for (int i = 0; i 5; i ++) {

    try {

    System.out.println(Thread.currentThread().getName() + ":" + (count++));

    Thread.sleep(100);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    }

    }

    }

    //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized

    public void printCount() {

    for (int i = 0; i 5; i ++) {

    try {

    System.out.println(Thread.currentThread().getName() + " count:" + count);

    Thread.sleep(100);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    }

    }

    public void run() {

    String threadName = Thread.currentThread().getName();

    if (threadName.equals("A")) {

    countAdd();

    } else if (threadName.equals("B")) {

    printCount();

    }

    }

    }

    调用代码

    Counter counter = new Counter();

    Thread thread1 = new Thread(counter, "A");

    Thread thread2 = new Thread(counter, "B");

    thread1.start();

    thread2.start();

    结果

    A:0

    B count:1

    A:1

    B count:2

    A:2

    B count:3

    A:3

    B count:4

    A:4

    B count:5

    上面代码中countAdd是一个synchronized的,printCount是非synchronized的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。

    三、同步一段代码

    当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

    class Test implements Runnable

    {

    private byte[] lock = new byte[0]; // 特殊的instance变量

    public void method()

    {

    synchronized(lock) {

    // todo 同步代码块

    }

    }

    public void run() {

    }

    }

    四、修饰类

    如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁,那也就是说对任何对象都是同步的。

    1.修饰类

    class ClassName {

    public void method() {

    synchronized(ClassName.class) {

    // todo

    }

    }

    }

    2.修饰静态方法

    class SyncThread implements Runnable {

    public synchronized static void method() {

    //todo

    }

    }

    参考:https://blog.csdn.net/luoweifu/article/details/46613015

    java synchronized 相关文章

    基于C语言的java串口通信程序

    目录 1.前言 2.windows ? 串口通信API 3.C/C++封装 ? 动态运行库 4.JAVA-JNI ? java程序调用C++程序 一、前言 ensp?;写这个博客主要是因为自己想用java写一个小小的后端服务器,其中要处理由51单片机传送来的一些数据。单片机的数据由USB转串口发送至上位机

    java-字符流

    前言 昨天学习了java的字节流,可以用字节流来读写文件。但是读取和写入的内容并不是内容本身,而是其对应的ASCII码。今天学习的字符流就可以规避这个问题,将内容以字符的形式进行读写。 使用字符流读取文件 1 package IO流; 2 3 import java.io.FileReader

    Java进阶专题(二十五) 分布式锁原理与实现

    前言 ?现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计。那么在这一个系统中,就会存在若干个微服务,而且服务间也会产生相互通信调用。那么既然产生了服务调用,就必然会存在服务调用延迟或失败的问题。当出现这种问题,服务端会进行重试等

    Java 经验书

    目录 目录 目录 正文 字节 转 String Base64Encoder 的使用 正文 字节 转 String 在开发中,会遇到各种 byte 类型转换为 String 的情况 下面做些记录 // 1. String.valueOf() 效率最高的方法String s = String.valueOf('c'); String s = String.valueOf(new c

    Java常用的函数式接口

    Supplier T 接口 java.util.function.Supplier T 接口仅包含一个无参的方法: T get() :用来获取一个泛型参数指定类型的对象数据。 Supplier T 接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的 get方法就会产生什么类型的数据。 示例 pack

    实验-继承super.doc

    Account.java package com.atguigu.exer2;public class Account { private int id;//账号 private double balance;//余额 private double annualInterestRate;//年利率 public Account(int id, double balance, double annualInterestRate) { super(); this.

    一篇文章图文并茂地带你轻松学完 JavaScript 闭包

    JavaScript 闭包 为了更好地理解 JavaScript 闭包,笔者将先从 JavaScript 执行上下文以及 JavaScript 作用域开始写起,如果读者对这方面已经了解了,可以直接跳过。 1. 执行上下文 简单来说, JavaScript 有三种代码运行环境,分别是: Global Code 是 JavaS

    Java基础(10) | 抽象

    #什么是抽象方法 加上abstract关键字,然后去掉方法体,直接分号结束 抽象方法所在的类必须是抽象类才行,在class之前写上abstract即可 public abstract class Animal{ //抽象类 public abstract void eat(); //抽象方法} # 注意事项 抽象方法所在的类必须是

    专业Java报表工具Stimulsoft Reports.Java常见问题解答(二)

    Stimulsoft Reports.Java是一个专为在Java应用程序中的报表进行交互和处理的报表工具。 Java技术允许在不同的平台、不同的操作系统和不同的硬件上使用程序。正因为这样,Stimulsoft Reports.Java现在几乎在任何地方都可用,同时还是一个拥有高质量的功能和用

    Unit5Java数据类型

    Unit5Java数据类型 一、Java标识如何命名 标识符就是名字,包括变量名,类名,方法名 Java标识符命名规则:由数字,字母,$组成,不能以数字开头,不能是关键字 二、Java关键字 abstract continue for new switch assert default goto package synchronized

    展开全文
  • Synchronized 是Java多线程高并发的灵魂,要想学好多线程编程,Synchronized 是绕不开的,一篇 Synchronized 基础用法讲解分享给大家,希望能给大家带来帮助!!

    Synchronized 作用介绍

    Synchronized 是 Java 中的关键字,是一种同步锁。它能保证在同一时刻最多只有一个线程执行该段代码,从而达到保证并发安全的效果。

    为了能更好的理解 Synchronized 的作用,我们先来看个并发不安全的例子。

    例子很简单,有两个英雄对同一个 boss 发起攻击,boss 血量为10000点,每受到一次攻击就会减少1点血,我们让这两个英雄都对 boss 发起5000次攻击。

    定义一个Boss类

    @Data
    @AllArgsConstructor
    public class Boss {
    
        private String bossName;
        private int bossHp;
    
        /**
         * 受到攻击
         *
         * 每次受到攻击固定会掉1点血
         */
        public void beAttacked(){
            this.bossHp --;
        }
    }
    

    创建一个10000点血的 boss 对象,并模拟两个英雄(线程)同时对这个 boss 对象发起5000次攻击。

    public static void main(String[] args) {
    
    	// 创建一个有10000点血的 boss 对象
    	Boss boss = new Boss("大魔王",10000);
    
    	// 创建两个英雄,分属不同线程,每个英雄都会对 boss 发起 5000 次攻击
    	Thread hero1 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } });
    	Thread hero2 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } });
    
    	// 两个英雄同时发起攻击
    	hero1.start();
    	hero2.start();
    
    	// 让主线程等待两个子线程运行结束后,再继续执行后边逻辑
    	try {
    		hero1.join();
    		hero2.join();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    
    	// 输出 boss 剩余血量
    	System.out.println(String.format("[%s] 剩余血量为:%d", boss.getBossName(), boss.getBossHp()));
    }
    

    执行一下main方法,得到如下输出:

    [大魔王] 剩余血量为:1795

    咦,boss 有10000点血,两个英雄每个对 boss 发起5000次攻击,每次攻击减少1点血,那通过计算 10000 - 2 * 5000 * 1 = 0,最后 boss 应该剩余0点血才对呀,为什么还会剩余这么多血量呢?

    其实原因是这样的,在 boss 减血的方法中,this.bossHp -- 这个操作,实际上需要三个动作才能完成:

    1. 从主内存中读取 this.bossHp 的值到自己的本地工作内存;
    2. 在本地工作内存中将 this.bossHp 值减1;
    3. 将 this.bossHp 的新值写回到主内存中。

    类似于下边这个模型
    在这里插入图片描述
    当两个线程对 boss 发起攻击时,如出现下边这样的时序逻辑,则就可能会出现少减血的情况。(假设当前 bossHp 值为10)

    主内存hero1线程本地内存hero2线程本地内存
    bossHp = 10读取 bossHp 值到本地内存
    (bossHp = 10)
    bossHp = 10将 bossHp 的值减1
    (bossHp = 9)
    读取 bossHp 值到本地内存
    (bossHp = 10)
    bossHp = 9
    (bossHp 的值被 hero1 线程改为9)
    将 bossHp 的新值 9 写回主内存将 bossHp 的值减1
    (bossHp = 9)
    bossHp = 9
    (bossHp 的值被 hero2 线程改为9)
    将 bossHp 的新值 9 写回主内存

    从上边的时序逻辑中可以看到,两次都将 bossHP 值修改成了9,这样就导致了少减血的情况。而防止出现这种并发问题的一个解决办法,就是使用 Synchronized 。

    下边,我们就先加上 Synchronized 关键字再看下效果。

    @Data
    @AllArgsConstructor
    public class Boss {
    
        private String bossName;
        private int bossHp;
    
        /**
         * 受到攻击
         *
         * 每次受到攻击固定会掉1点血
         */
        public synchronized void beAttacked(){
            this.bossHp --;
        }
    }
    

    只需要在 beAttacked() 方法前边加上 synchronized ,然后再来运行一下之前的 main 方法,这次我们就可以得到正确的输出值 0 了。

    Synchronized 加锁原理简析

    使用 synchronized 同步锁的方式,有的同学认为是对后边紧跟的代码块内容加上了锁,其实这样理解是不对的, synchronized 实际上是将一个Java对象当做了锁,当某个线程获取到锁时,会将线程信息记录到这个对象的对象头中,当占用结束后,再将锁释放,这样其它线程就可以获取到锁了。

    还是以上边加锁的代码为例

    public synchronized void beAttacked(){
    	this.bossHp --;
    }
    

    当我们创建一个 boss 对象 Boss boss = new Boss("大魔王",10000); ,并且再有 hero1 和 hero2 两个线程调用 beAttacked() 方法时,同步加锁的流程如下:

    hero1 线程hero2 线程
    运行到 synchronized 处,检查 boss 对象的对象头,发现没有被其它线程占用,则在 boss 对象头中记录本线程的线程ID等信息
    进入方法体,执行方法体逻辑运行到 synchronized 处,检查 boss 对象的对象头,发现已经被其它线程占用,等待其它线程释放锁
    方法执行结束,释放锁
    其它线程释放锁,本线程获取到锁,在 boss 对象头中记录本线程的线程ID等信息
    进入方法体,执行方法体逻辑
    方法执行结束,释放锁

    Synchronized 的几种用法

    Synchronized 在使用过程中,可以归纳为有两类用法:

    • 对象锁
      • 修饰一个代码块,如:synchronized(对象) {...}
      • 修饰一个成员方法
    • 类锁
      • 修饰一个代码块,如:synchronized(类.class) {...}
      • 修饰一个静态方法

    这里的对象锁和类锁有什么本质的区别呢?

    在java世界里,一切皆对象。从某种意义上来说,java有两种对象:实例对象和Class对象。每个类的运行时的类型信息就是用Class对象表示的,它包含了与类有关的信息。其实我们的实例对象就通过Class对象来创建的。Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的。

    最关键的一点就是,Class对象是单例的,也就说一个类只会有一个对应的Class对象。

    我们再来通过代码示例来理解一下。

    我们将 Boss 类中 beAttacked 方法去掉 synchronized 关键字,并在方法内加入输出语句。

    @Data
    @AllArgsConstructor
    public class Boss {
    
        private String bossName;
        private int bossHp;
    
        /**
         * 受到攻击
         *
         * 每次受到攻击固定会掉1点血
         */
        public void beAttacked(){
            System.out.println(String.format("%s 对 %s 发起了攻击", Thread.currentThread().getName(), this.getBossName()));
    
            this.bossHp --;
    
            System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
        }
    }
    

    并将客户端 main 方法中 hero1 和 hero2 线程分别起名为 Hero_1 和 Hero_2。

    public static void main(String[] args) {
    
    	// 创建一个有10000点血的 boss 对象
    	Boss boss = new Boss("大魔王",10000);
    
    	// 创建两个英雄,分属不同线程,每个英雄都会对 boss 发起 5000 次攻击
    	Thread hero1 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } }, "Hero_1");
    	Thread hero2 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } }, "Hero_2");
    
    	// 两个英雄同时发起攻击
    	hero1.start();
    	hero2.start();
    
    	// 让主线程等待两个子线程运行结束后,再继续执行后边逻辑
    	try {
    		hero1.join();
    		hero2.join();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    

    运行一下,从控制台输出中,我们很容易看到如下的这种输出

    Hero_1 对 大魔王 发起了攻击
    Hero_2 对 大魔王 发起了攻击
    Hero_1 对 大魔王 攻击结束
    Hero_2 对 大魔王 攻击结束

    从输出中我们就可以看出,两个线程可以并行的执行方法体内容。

    接下来我们在 beAttacked() 方法中加入 synchronized 同步代码块

    public void beAttacked(){
    	synchronized (this){
    		System.out.println(String.format("%s 对 %s 发起了攻击", Thread.currentThread().getName(), this.getBossName()));
    
    		this.bossHp --;
    
    		System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
    	}
    }
    

    再次运行 main 方法后,可以发现同一个线程的两次打印输出之间不会夹杂其它线程的输出,也就是说,一个线程执行完此段代码之前,其它线程无法执行此段代码,如下:

    Hero_1 对 大魔王 发起了攻击
    Hero_1 对 大魔王 攻击结束
    Hero_2 对 大魔王 发起了攻击
    Hero_2 对 大魔王 攻击结束

    但接下来我们改一下 main 方法中的逻辑,之前的逻辑是 hero1 和 hero2 线程会攻击同一个boss,我们改为 hero1 攻击 boss1 , hero2 攻击 boss2 。

    public static void main(String[] args) {
    
    	// 创建一个有10000点血的 boss1 对象
    	Boss boss1 = new Boss("大魔王1",10000);
    	// 创建一个有10000点血的 boss2 对象
    	Boss boss2 = new Boss("大魔王2",10000);
    
    	// 创建两个英雄,分属不同线程,每个英雄都会对 boss 发起 5000 次攻击
    	Thread hero1 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss1.beAttacked(); } }, "Hero_1");
    	Thread hero2 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss2.beAttacked(); } }, "Hero_2");
    
    	// 两个英雄同时发起攻击
    	hero1.start();
    	hero2.start();
    
    	// 让主线程等待两个子线程运行结束后,再继续执行后边逻辑
    	try {
    		hero1.join();
    		hero2.join();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    

    再来运行下,从控制台输出中,我们还是可以看到如下的这种输出

    Hero_2 对 大魔王2 发起了攻击
    Hero_1 对 大魔王1 发起了攻击
    Hero_2 对 大魔王2 攻击结束
    Hero_1 对 大魔王1 攻击结束

    这是为什么呢,我们不是已经在 beAttacked() 方法中加了 synchronized 了么?

    其实,这里也比较容易理解,也就是我们前边说的,锁并不是加在代码块上的,而是加在对象上。

    这里我们用的是 synchronized (this){...} 这种对象锁形式,this 指的是当前对象,hero1 线程中调用 beAttacked() 方法的是 boss1 对象,也就是锁信息记录在了 boss1 对象的对象头中,而hero2 线程中调用 beAttacked() 方法的是 boss2 对象,锁信息记录在了 boss2 对象的对象头中,它们是两把不同的锁,并不会冲突,也就是不会互斥,所以可以同时执行。

    接下来,我们将 synchronized (this){...} 改为 synchronized (类.class){...} 再来试试。

    public void beAttacked(){
    	synchronized (Boss.class){
    		System.out.println(String.format("%s 对 %s 发起了攻击", Thread.currentThread().getName(), this.getBossName()));
    
    		this.bossHp --;
    
    		System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
    	}
    }
    

    再次运行 main 方法后,又会发现同一个线程的两次打印输出之间不会夹杂其它线程的输出了,如下:

    Hero_1 对 大魔王1 发起了攻击
    Hero_1 对 大魔王1 攻击结束
    Hero_2 对 大魔王2 发起了攻击
    Hero_2 对 大魔王2 攻击结束

    这是因为 Boss.class 对象只有一个, hero1 线程和 hero2 线程此时再获取锁时,就会出现互斥的情况了。

    明白了上边的逻辑,那么在成员方法或静态方法上加 synchronized 关键字也就好理解了。

    成员方法是需要有具体对象才可以调用的,所以效果等同于 synchronized (this){...},而静态方法是属于类的,所以效果等同于 synchronized (类.class){...}

    还有,如果同一个对象的两个成员方法上都加有 synchronized 关键字,让一个线程调用这个对象的方法1,另外一个线程调用它的方法2,这种情况也是会出现锁争用的,原理嘛都一样,只要紧抓住锁是加在对象上的就好理解了。

    当然了,虽然我们平常的时候一般都是使用上边的几种写法,但其实 synchronized 可以将任意对象指定为锁,如下边代码:

    @Data
    @AllArgsConstructor
    public class Boss {
    
        private String bossName;
        private int bossHp;
    
        // 创建一个对象,用于对 beAttacked() 方法体加锁
        private static final Object lock = new Object();
    
        /**
         * 受到攻击
         *
         * 每次受到攻击固定会掉1点血
         */
        public void beAttacked(){
            synchronized (lock){
                System.out.println(String.format("%s 对 %s 发起了攻击", Thread.currentThread().getName(), this.getBossName()));
    
                this.bossHp --;
    
                System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
            }
        }
    }
    

    同样的,也可以将其它类的类.class作为锁,如 synchronized (Object.class){...}synchronized (String.class){...} 等等。(虽然这些是被允许的,但我们在实际开发中,最好不要用无关联意义的对象或类对象来作为锁。)

    Synchronized 的几种性质

    可重入性

    可重入的意思是,如果一个线程获取到锁但还没有释放时,它自己还可以继续获取到此锁。

    再来通过个例子来看下,在 Boss 类中加入另外一个同步方法,并在 beAttacked() 方法结束之前调用新方法:

    @Data
    @AllArgsConstructor
    public class Boss {
    
        private String bossName;
        private int bossHp;
    
        /**
         * 受到攻击
         *
         * 每次受到攻击固定会掉1点血
         */
        public void beAttacked(){
            synchronized (this){
                System.out.println(String.format("%s 对 %s 发起了攻击", Thread.currentThread().getName(), this.getBossName()));
    
                this.bossHp --;
                // 触发额外效果
                debuff();
                System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
            }
        }
    
        /**
         * 触发额外效果
         */
        private void debuff(){
            synchronized (this) {
                System.out.println(String.format("%s 对 %s 额外造成了减速效果", Thread.currentThread().getName(), this.getBossName()));
            }
        }
    }
    

    main 方法中还是让 hero1 和 hero2 同时攻击同一个 boss 对象:

    public static void main(String[] args) {
    
    	// 创建一个有10000点血的 boss1 对象
    	Boss boss = new Boss("大魔王",10000);
    
    	// 创建两个英雄,分属不同线程,每个英雄都会对 boss 发起 5000 次攻击
    	Thread hero1 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } }, "Hero_1");
    	Thread hero2 = new Thread(() -> {for(int i = 0; i < 5000; i++){ boss.beAttacked(); } }, "Hero_2");
    
    	// 两个英雄同时发起攻击
    	hero1.start();
    	hero2.start();
    
    	// 让主线程等待两个子线程运行结束后,再继续执行后边逻辑
    	try {
    		hero1.join();
    		hero2.join();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    

    运行后,可以看到如下输出:

    Hero_1 对 大魔王 发起了攻击
    Hero_1 对 大魔王 额外造成了减速效果
    Hero_1 对 大魔王 攻击结束
    Hero_2 对 大魔王 发起了攻击
    Hero_2 对 大魔王 额外造成了减速效果
    Hero_2 对 大魔王 攻击结束

    可以看出,同一个线程在获取到对象锁执行 beAttacked() 方法时,还可以再次获取到同一个对象的锁,并执行 debuff() 方法。

    这是因为每个对象锁关联一个线程持有者和一个计数器,当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而当前持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个 synchronized 方法/块时,计数器会递减,如果计数器减为0则释放该锁。

    代码抛异常后,会自动释放锁

    一般情况下,当线程在执行完synchronized方法/块时,会将锁释放。但如果遇到异常抛出异常时,JVM也会将锁释放。

    再来修改下我们的 Boss 类,beAttacked 方法添加一个 bossHp 参数,表示减少的HP数值,并在方法体内判断如果传进来的 bossHp 数值小于或等于0,就会抛出异常。

    /**
     * 受到攻击
     * @param lossHp 减少的HP数值
     */
    public void beAttacked(int lossHp){
    	synchronized (this){
    		System.out.println(String.format("%s 对 %s 发起了攻击,lossHp:%d", Thread.currentThread().getName(), this.getBossName(), lossHp));
    
    		// 判断如果减少的HP数值小于或等于0,则抛出异常
    		if(lossHp <= 0){
    			throw new RuntimeException("减少的HP数值不能小于或等于0");
    		}
    
    		this.bossHp -= lossHp;
    
    		System.out.println(String.format("%s 对 %s 攻击结束", Thread.currentThread().getName(), this.getBossName()));
    	}
    }
    

    在main方法中,我们将 hero1 对 boss 的攻击减血量写为0, hero2 对 boss 的攻击减血量写为1

    public static void main(String[] args) {
    
    	// 创建一个有10000点血的 boss1 对象
    	Boss boss = new Boss("大魔王",10);
    
    	// 创建两个英雄,分属不同线程,每个英雄都会对 boss 发起 5000 次攻击
    	Thread hero1 = new Thread(() -> boss.beAttacked(0), "Hero_1");
    	Thread hero2 = new Thread(() -> boss.beAttacked(1), "Hero_2");
    
    	// 两个英雄同时发起攻击
    	hero1.start();
    	hero2.start();
    
    	// 让主线程等待两个子线程运行结束后,再继续执行后边逻辑
    	try {
    		hero1.join();
    		hero2.join();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    }
    

    运行后,得到如下输出:

    Hero_1 对 大魔王 发起了攻击,lossHp:0
    Exception in thread “Hero_1” java.lang.RuntimeException: 减少的HP数值不能小于或等于0
      at Boss.beAttacked(Boss.java:22)
      at Test1.lambda$main 0 ( T e s t 1. j a v a : 14 ) a t T e s t 1 0(Test1.java:14) at Test1 0(Test1.java:14)atTest1$Lambda$1/2074407503.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:744)
    Hero_2 对 大魔王 发起了攻击,lossHp:1
    Hero_2 对 大魔王 攻击结束

    从输出中我们可以看出,hero1 没有运行完代码,但是hero2 也可以获取到了锁,这说明 hero1 抛出异常后,也会释放持有的锁。

    Synchronized 的缺陷

    Synchronized 使用起来比较方便,但跟其它同步方式(如 Lock 系列)相比,它还存在如下缺点:

    • 试图获取锁时不能设置超时,也不能中断一个正在试图获取锁的线程,也就是要么获取到锁,要么就一直等。
    • 加锁和释放锁的时机单一。
    • 每个锁只有单一的条件,无法对多种条件进行区分。
    • 无法知道是否成功获取到了锁。

    总结

    Synchronized 是Java多线程高并发的灵魂,要想学好多线程编程,Synchronized 是绕不开的,一篇 Synchronized 基础用法讲解分享给大家,希望能给大家带来帮助!!

    展开全文
  • Synchronized

    2021-04-09 11:49:52
    关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证...
  • java中的synchronized解析

    2019-04-21 18:34:05
    在java编程中,经常需要用到同步,而用得最多的也许是synchronized关键字了,下面看看这个关键字的用法。 一、锁的概念 java的内置锁:每个java对象都可以有一个用于实现同步功能的锁,这些锁成为内置锁。线程进入...
  • Synchronized 底层原理

    2021-10-02 21:30:01
    Synchronized 底层原理 java 对象及对象头 java中的锁 Monitor-重量级锁 Monitor 被翻译为监视器或管程 Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的...
  • synchronized

    2020-09-19 18:33:29
    synchronized可以用来锁方法和锁代码块。 锁方法又可以分成对象锁和类锁,synchronized加在普通方法上就是锁得是当前对象;加在static 的方法上锁的是当前类。 锁代码块也可以分成对象锁和类锁,在方法中使用...
  • synchronized使用的正确姿势

    千次阅读 2019-07-28 11:55:04
    synchronized关键字在java中是用作线程同步的,保障同步区代码的正确执行,同一时间仅有一个线程进入同步区,其原理是使用锁技术,通过竞争,得到锁的线程执行同步区代码,未得到锁的线程自旋、阻塞等待。...
  • Synchronized理解与测试

    2019-07-03 10:38:21
    Synchronized简单测试 1.synchronized关键子说明 对于操作系统进程来说,资源分配是很重要的一个问题,如果操作不当会造成多个进程抢占同一个资源,进而造成死锁,无限等待。线程同样无法避免这个问题,对于资源的...
  • 深入并发-Synchronized

    2021-02-27 10:20:09
    synchronized的使用 在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对 synchronized...(可以自己尝试将synchronizrd去掉,看看结果得到的是不是1000) public
  • 今天看到synchronized关键字的内容,讲到了当子类继承有同步方法的父类时,继承过来的方法是否仍然是同步的?当然,提到方法继承就必须提到方法重写,重写过后的方法是否仍然具有同步性,我找了一些别人的检测代码。...
  • synchronized 线程安全问题的主要原因是: ➢ 存在共享数据(也称临界资源) ➢ 存在多条线程共同操作这些共享数据 解决问题的根本方法: 同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据...
  • 如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢? Java代码 test1 : 4  test2 : 4  test2 : 3  test1 : 3  test1 : 2  test2 : 2  test2 : 1  ...
  • Synchronized实现原理详解一、Synchronized使用场景二、Synchronized实现原理三、锁的优化1、锁升级2、锁粗化3、锁消除 一、Synchronized使用场景 Synchronized是一个同步关键字,在某些多线程场景下,如果不进行...
  • 基础使用基本上Java程序员都简单的了解synchronized的使用: 无非就是用在多线程环境下的同步。 看如下简单的例子:publicclassUnsafeCounter{privateint count=0;publicint getAndIncrement(){returnthis.count++;}...
  • synchronized&volatile&synchronized原理 Synchronized&volatile Synchronized本质上是解决对共享变量的访问顺序问题。 多线程环境下,方法内的变量是线程安全的。 多个线程同时处理一个实例,这...
  • 上一篇文章《Java中synchronized实现对象锁的两种方式及原理解析》中,介绍了方法锁的两种实现方式及Synchronized的底层原理,本文将讲解synchronized的类锁的两种实现方式。 一.类锁的定义 什么是类锁 类锁指...
  • Flutter synchronized lock

    千次阅读 2019-12-05 15:31:16
    涉及到多线程时,必须要考虑锁,数据同步。 找到资料:synchronized:https://pub.dev/packages/synchronized#-readme-tab- 最基本的一个应用: ...我们可以使用 synchronized 来处理就非常容易了。如:...
  • 这篇文章主要介绍了Java synchronized关键字和Lock接口实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下引用当开发过程中,我们遇到并发问题。...
  • synchronized原理

    多人点赞 2021-08-15 15:15:45
    1、synchronized的作用 为了避免临界区的竞态条件发生,有多种手段可以达到目的。 阻塞式的解决方案:synchronized,Lock 非阻塞式的解决方案:原子变量 synchronized,即俗称的【对象锁】,它采用互斥的方式让...
  • 两个线程都有各自的锁,不能形成两个线程竞争一把锁的局势,所以这时,synchronized修饰的方法method()和不用synchronized修饰的效果一样(不信去把synchronized关键字去掉,运行结果一样),所以此时的method()只是...
  • 是否在execute()方法前加上synchronized关键字,这个例子程序的执行结果会有很大的不同。 加上synchronized关键字的运行结果: Hello: 0 Hello: 1 Hello: 2 Hello: 3 Hello: 4 Hello: 5 Hello: 6 Hello: 7 Hello...
  • synchronized并发讲解

    2020-03-16 16:25:11
    syncronized的作用:能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。...在这里先模拟如果不使用synchronized的原始错误,模型:开启两个线程对一个变量i从0开始i+...
  • synchronized的四种用法

    2021-01-27 07:34:10
    一 修饰方法Synchronized修饰一个方法很简单,就是在方法的前面加synchronizedsynchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 40,337
精华内容 16,134
关键字:

去掉synchronized