Synchronize关键字详解

前言

synchronized关键字,多线程并发编程最常用关键字

作用

对于synchronized关键字的作用,简单来说一句话就可以概况:

在同一时刻,最多只有一个线程能执行该段代码。

以串行化的方式执行代码,自然不存在线程安全问题。

用法

synchronized关键字的用法大致可以分为两类:对象锁和类锁。当然,Java中一切皆对象,类也是对象。所以这里的对象锁中的对象可以认为是狭义上的对象。

  • 对象锁:锁住的是一个对象,具体用法是利用synchronized关键字修饰非静态方法或者synchronized代码块中的参数传入对象实例(比如:this)
  • 类锁:锁住的是整个类(或者说是java.lang.Class的对象),具体用法是用synchronized修饰静态方法或者synchronized代码块中的参数传入xxx.class
对象锁
  • synchronized关键字修饰

    非静态方法

    public class ObjectLockDemo implements Runnable {
    
        private static int count = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            ObjectLockDemo instance = new ObjectLockDemo();
    
            Thread thread1 = new Thread(instance);
            Thread thread2 = new Thread(instance);
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(count);
        }
    
        @Override
        public void run() {
            increase();
        }
    
        public synchronized void increase() {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
        }
    }
    
  • synchronized代码块中的参数传入对象实例

    public class ObjectLockDemo implements Runnable {
    
       private static int count = 0;
    
       public static void main(String[] args) throws InterruptedException {
    
           ObjectLockDemo instance = new ObjectLockDemo();
    
           Thread thread1 = new Thread(instance);
           Thread thread2 = new Thread(instance);
    
           thread1.start();
           thread2.start();
    
           thread1.join();
           thread2.join();
    
           System.out.println(count);
       }
    
       @Override
       public void run() {
           increase();
       }
    
       public void increase() {
           synchronized (this) {
               for (int i = 0; i < 100000; i++) {
                   count++;
               }
           }
       }
    }
    

两个实例都是实例锁,即当thread1和thread2传入同一个instance时,才能起到锁的效果。想要看到不正确的写法,thread1和thread2中传入不同的实例即可。

// 其他代码不变,只改下这两行代码
Thread thread1 = new Thread(new ObjectLockDemo());
Thread thread2 = new Thread(new ObjectLockDemo());
类锁
  • synchronized修饰静态方法

    public class ClassLockDemo implements Runnable {
    
        private static int count = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new ClassLockDemo());
            Thread thread2 = new Thread(new ClassLockDemo());
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(count);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                increase();
            }
        }
    
        public synchronized static void increase() {
            count++;
        }
    }
    
  • synchronized代码块中的参数传入xxx.class

    public class ClassLockDemo implements Runnable {
    
        private static int count = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new ClassLockDemo());
            Thread thread2 = new Thread(new ClassLockDemo());
    
            thread1.start();
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(count);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                increase();
            }
        }
    
        public static void increase() {
            synchronized (ClassLockDemo.class) {
                count++;
            }
        }
    }
    

类锁锁住的是整个类,或者说是Class类的对象,只要传入的都是同一个类的实例,就能起到互斥的作用。
以上是synchronized关键字的两种基本用法:对象锁和类锁。深入理解这两种用法,是正确使用synchronized的关键。

synchronized实现原理

synchronized实现原理同样分成两种情况:修饰代码块、修饰方法。

修饰代码块
public class ObjectLockDemo {

    public void increase() {
        synchronized (this) {
            System.out.println("increase()");
        }
    }

}

代码编译后,使用反编译命令javap查看字节码
运行javap -v -p ObjectLockDemo.class命令后会看到如下字节码,因为synchronized 关键字在increase()方法中,所以只需要看increase()相关的字节码

  public void increase();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter              // 获取锁
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String increase()
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit               // 释放锁
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit               // 释放锁
        20: aload_2
        21: athrow
        22: return

可以清晰的看到,synchronized关键字修饰代码块时,是通过monitorenter指令和monitorexit指令来时实现的,因为正常执行完成和抛出异常都会释放锁,所以这个方法的字节码会有两个monitorexit指令,分别处于正常分支和异常分支。

修饰方法
public class ObjectLockDemo {

    public synchronized void increase() {
        System.out.println("increase()");
    }
    
}

运行javap -v -p ObjectLockDemo.class命令后会看到如下字节码,因为synchronized 关键字修饰increase()方法,所以只需要看increase()相关的字节码

  public synchronized void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 锁标识
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String increase()
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8

通过字节码可以看到,synchronized修饰方法时,并不是通过monitorentermonitorexit指令实现的,而是在flags中加入了ACC_SYNCHRONIZED标识。
也就是说synchronized修饰代码块时,通过monitorentermonitorexit指令实现同步锁;修饰方法时,通过flags中ACC_SYNCHRONIZED实现同步锁。monitorentermonitorexitACC_SYNCHRONIZED,都是基于Monitor(监视器)来实现的。

Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象

在HotSpot虚拟机中,Monitor是通过ObjectMonitor类来实现的,主要属性如下:

ObjectMonitor() {
  _header       = NULL;
  _count        = 0;       // 线程获取锁的次数
  _waiters      = 0,
  _recursions   = 0;       // 锁重入的次数
  _object       = NULL;
  _owner        = NULL;    // 指向持有monitorObject对象的线程
  _WaitSet      = NULL;    // 等待池
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;   // 锁池
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
}

Java中每个对象都与一个监视器相关联,一次只有一个线程可以锁定监视器。
当多个线程同时访问同一段同步代码时,首先包装成ObjectWaiter对象进入到_EntryList集合里面,当线程获取到对象的Monitor后,会进入_object区域,并把_owner指向当前线程,_count变量加1。
如果线程调用wait()方法,会释放当前持有的Monitor,_owner恢复成NULL,_count减1。线程进入_WaitSet集合中等待被唤醒。如果当前线程执行完毕,也会释放Monitor锁,并且复位对应变量的值。
在这里插入图片描述

synchronized特性

从不同的角度来划分的话,synchronized是独占锁、非公平锁、悲观锁、可重入锁、不可中断锁。
这里重点介绍下synchronized的可重入和不可中断性。

可重入性

若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

通俗点说就是:线程获取锁进入一个synchronized方法/代码块,又调用了一个synchronized方法/代码块,在进入第二个synchronized方法/代码块时,不需要先释放进入第一个synchronized时获取的锁,也不需要再次争抢锁,而是直接进入。所以可重入锁也叫递归锁
synchronized的可重入性不需要两个synchronized方法是同一个方法、也不需要是同一个类中的方法,仅仅要求是同一个线程。下面演示可重入性

  • 同一个方法

    public class ObjectLockDemo {
    
        private static int count = 0;
    
        public static void main(String[] args) {
            new ObjectLockDemo().display();
        }
    
        public synchronized void display() {
            System.out.println("display():count=" + count);
            if (count++ == 0) {
                display();
            }
        }
    }
    

    执行结果:

    display():count=0
    display():count=1
    

    从输出结果可以看出,获得锁的线程两次进入同一个synchronized方法并不会产生死锁,因为synchronized具有可重入性。

  • 不同的方法

    public class ObjectLockDemo {
        
        public static void main(String[] args) {
            new ObjectLockDemo().displayA();
        }
    
        public synchronized void displayA() {
            System.out.println("displayA()");
            displayB();
        }
    
        public synchronized void displayB() {
            System.out.println("displayB()");
        }
    }
    

    执行结果

    displayA()
    displayB()
    

    从输出结果可以看出,获得锁的线程两次进入不同的synchronized方法并不会产生死锁,因为synchronized具有可重入性。

  • 不同的类

    public class ObjectLockDemo {
    
        public synchronized void display() {
            System.out.println("ObjectLockDemo.display()");
        }
    }
    
    public class ReentryDemo {
    
        public static void main(String[] args) {
            new ReentryDemo().display();
        }
    
        public synchronized void display() {
            ObjectLockDemo lockDemo = new ObjectLockDemo();
            System.out.println("ReentryDemo.display()");
            lockDemo.display();
        }
    }
    

    执行结果

    ReentryDemo.display()
    ObjectLockDemo.display()
    

    从输出结果可以看出,获得锁的线程两次进入不同的类中synchronized方法并不会产生死锁,因为synchronized具有可重入性。

可重入性的优点:避免死锁和提高封装性。避免死锁在上面三个例子中已经有了很好的体现。可重入性避免了使用了同一把锁时,反复的加锁解锁,提高了封装性,简化了锁的使用难度。

不可中断

一旦锁被某个线程获得了,别的线程想要获得锁,只能选择等待或者阻塞,直到那个线程释放了这个锁。如果那个线程永远不释放,其他线程只能永远等下去。

synchronized缺点

  • 试图获得锁时,不能设置超时时间,不能中断正在等待获取锁的线程
  • 加锁和解锁条件单一,加锁仅仅有单一条件(对象或者类)
  • 早期版本效率低下(重量级锁)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值