Java:java学习笔记之Synchronized的简单理解和使用

Posted JMW1407

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java:java学习笔记之Synchronized的简单理解和使用相关的知识,希望对你有一定的参考价值。

Java Synchronized的简单理解和使用

Synchronized

0、背景


如上图所示,比如在王者荣耀程序中,我们队有二个线程分别统计后裔和安琪拉的经济,

  • A线程从内存中read 当前队伍总经济加载到线程的本地栈,进行 +100 操作之后
  • 这时候B线程也从内存中取出经济值 + 200,将200写回内存
  • B线程刚执行完,后脚A线程将100 写回到内存中

就出问题了,我们队的经济应该是300, 但是内存中存的却是100。

synchronized 怎么解决这个问题的?

二个线程,A线程让队伍经济 +1 ,B线程让经济 + 2,分别执行一千次,正确的结果应该是3000,结果得到的却是 2845。


这个就是加锁之后的代码和控制台的输出。

1、synchronized锁是什么?

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

    public synchronized void test() 
        // 关注公众号Java3y
        // doSomething
    
  • synchronized是一种互斥锁
    • 一次只能允许一个线程进入被锁住的代码块
  • synchronized是一种内置锁/监视器锁
    • Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!

另外,在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。为什么呢?

因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

2、特点

  • 首先需要明确的一点是:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。我们常听到的类锁其实也是对象锁。
  • Java类只有一个Class对象(可以有多个实例对象,多个实例共享这个Class对象),而Class对象也是特殊的Java对象。所以我们常说的类锁,其实就是Class对象的锁。
  • synchronized是⼀种互斥锁,⼀次只能允许⼀个线程进⼊被锁住的代码块
  • synchronized是⼀种内置锁监视器锁,Java中每个对象都有⼀个内置锁(监视器,也可以理解成锁标记),⽽synchronized就是使⽤对象的内置锁(监视器)来将代码块(⽅法)锁定的!(锁的是对象,但我们同步的是⽅法/代码块)

3、优点

4、使用场景

Java中Synchronized的使用

synchronized一般我们用来修饰三种东西:

  • 1、Synchronized修饰普通同步方法:锁对象当前实例对象;
  • 2、Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
  • 3、Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class);
public class SynchronizedSample 
 
    private final Object lock = new Object();
 
    private static int money = 0;
		//非静态方法
    public synchronized void noStaticMethod()
        money++;
    
		//静态方法
    public static synchronized void staticMethod()
        money++;
    
		
    public void codeBlock()
      	//代码块
        synchronized (lock)
            money++;
        
    


4.1.修饰实例方法:

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

    @Override
    public synchronized void run() 
        Date startDate = DateUtil.date();
        for (int i = 0; i < 5; i++) 
            try 
                System.out.println("线程 :" + Thread.currentThread().getName() + " 当前计数器 :" + (counter++));
                System.out.println("开始时间 :" + startDate + " 当前时间 :" + DateUtil.date());
                System.out.println();
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

4.2.修饰静态方法:

也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。

  • 因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。

所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

package com.example.lock.syn.demo02;

import cn.hutool.core.date.DateUtil;
import java.util.Date;

/**
 * synchrosnized 关键字测试
 * 同步-静态方法
 */
public class SynchronizedDemo3 implements Runnable 
    private static int counter = 1;
    
    /**
     * 静态的同步方法
     */
    public synchronized static void method() 
        Date startDate = DateUtil.date();
        for (int i = 0; i < 5; i++) 
            try 
                System.out.println("线程 :" + Thread.currentThread().getName() + " 当前计数器 :" + (counter++));
                System.out.println("开始时间 :" + startDate + " 当前时间 :" + DateUtil.date());
                System.out.println();
                Thread.sleep(1000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    
    @Override
    public void run() 
        method();
    


    public static void main(String[] args) 
        SynchronizedDemo3 syncThread1 = new SynchronizedDemo3();
        SynchronizedDemo3 syncThread2 = new SynchronizedDemo3();
        Thread thread1 = new Thread(syncThread1, "sync-thread-1");
        Thread thread2 = new Thread(syncThread1, "sync-thread-2");
        thread1.start();
        thread2.start();
    



结果说明

  • syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。
  • 这是因为run中调用了静态方法method,而静态方法是属于同一类的,所以syncThread1和syncThread2相当于用了同一把锁。

4.3.修饰代码块:

指定加锁对象,对给定对象/类加锁。synchronized(this | object) 表示进入同步代码库前要获得给定对象的锁。

  • synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
/**
 * synchrosnized 关键字测试
 * 同步代码块
 */
public class SynchronizedDemo1 implements Runnable 
    /**
     * 全局变量
     * 创建一个计数器
     */
    private static int counter = 1;

    @Override
    public void run() 
        Date startDate = DateUtil.date();
        synchronized (this) 
            for (int i = 0; i < 5; i++) 
                try 
                    System.out.println("线程 :" + Thread.currentThread().getName() + " 当前计数器 :" + (counter++));
                    System.out.println("开始时间 :" + startDate + " 当前时间 :" + DateUtil.date());
                    System.out.println();
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        
    
    public static void main(String[] args) 
        SynchronizedDemo1 syncThread = new SynchronizedDemo1();
        Thread thread1 = new Thread(syncThread, "sync-thread-1");
        Thread thread2 = new Thread(syncThread, "sync-thread-2");
        thread1.start();
        thread2.start();
    



结果说明,当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码时:

  • 在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。
  • Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

稍加改动

public static void main(String[] args) 
        SynchronizedDemo1 syncThread1 = new SynchronizedDemo1();
        SynchronizedDemo1 syncThread2 = new SynchronizedDemo1();
        Thread thread1 = new Thread(syncThread1, "sync-thread-1");
        Thread thread2 = new Thread(syncThread2, "sync-thread-2");
        thread1.start();
        thread2.start();
    


从图上可以看出来,两个线程都是新建一个对象去执行的,所以锁也是两个,所以执行方式是同时执行了。

4.4、注意

  • 1、使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。
  • 2、使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。
  • 3、使用使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。
  • 4、线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

4.5、双重校验锁实现对象单例(线程安全)

public class Singleton 

    private volatile static Singleton uniqueInstance;

    private Singleton() 
    

    public  static Singleton getUniqueInstance() 
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) 
            //类对象加锁
            synchronized (Singleton.class) 
                if (uniqueInstance == null) 
                    uniqueInstance = new Singleton();
                
            
        
        return uniqueInstance;
    


4.6、总结

  • synchronized 关键字加到 static 静态方法synchronized(class) 代码块上都是是给 Class类上锁。
  • synchronized 关键字加到实例方法上是给对象实例上锁。
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

5、底层原理

Synchronized解析——如果你愿意一层一层剥开我的心

5.1、对象头

Java对象保存在内存中时,由以下三部分组成:

1,对象头

2,实例数据: 对象的实例数据就是在java代码中能看到的属性和他们的值

3,对齐填充字节: 因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数,没有特别的功能。

java的对象头由以下三部分组成:

1,Mark Word

2,指向类的指针:Java对象的类数据保存在方法区

3,数组长度(只有数组对象才有): 该数据在32位和64位JVM中长度都是32bit

5.1.1、Mark Word

Java的对象头和对象组成详解

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

  • Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
  • Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

    其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

JVM一般是这样使用锁和Mark Word的:

  • 1、没有锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0
  • 2、当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
    • 线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,MarkWord中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
  • 3、当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤4。
  • 4、偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤5。
  • 5、轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤6。
  • 6、自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

5.2、Monitor

java monitor是什么意思,Java面试常见问题:Monitor对象是什么?

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁

  • Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。
  • 每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
  • 如果使用synchronized给对象加锁(重量级)之后,该对象的Mark Word就被设置指向了Monitor对象的指针

对象是如何跟monitor关联的呢?直接先看图:

Monitor结构:

Java Monitor 的工作机理如图所示:


为了形象生动一点,举个例子:

  synchronized(this)  //进入_EntryList队列
            doSth();
            this.wait();  //进入_WaitSet队列
        

5.3、synchronized 同步代码块

public class SynchronizedDemo 
	public void method() 
		synchronized (this) 
			System.out.println("synchronized 代码块");
		
	

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:

  • 首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,
  • 然后执行javap -c -s -v -l SynchronizedDemo.class

    从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 字节码指令,

monitorenter 指令指向同步代码块的开始位置
monitorexit 指令则指明同步代码块的结束位置
JVM要保证monitorentry和monitorexit都是成对出现的

  • 1、当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
  • 2、在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加1。
  • 3、在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
  • 4、如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

5.4、synchronized 修饰方法

public class SynchronizedDemo2 
	public synchronized void method() 
		System.out.println("synchronized 方法");
	



synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

  • JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

5.5、monitorenter、monitorexit、ACC_SYNCHRONIZED说明

Synchronized解析——如果你愿意一层一层剥开我的心

5.6、总结

1、synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令:

  • 其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

2、synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

3、不过两者的本质都是对对象监视器 monitor 的获取

6、锁的优化

Java并发——Synchronized实现原理详解

7、synchronized 和 ReentrantLock 的区别

8、Synchronized与Volatile区别

  • 1、性能上:volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好
  • 2、修饰范围上:volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
  • 3、特性上:volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • 4、作用上:volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

参考

1、Java锁机制了解一下
2、Java线程安全与锁机制

以上是关于Java:java学习笔记之Synchronized的简单理解和使用的主要内容,如果未能解决你的问题,请参考以下文章

JAVA多线程之Synchronize 关键字原理

java多线程中synchronize锁的使用和学习,Thread多线程学习

Java学习笔记之:Java简介

Java学习笔记之:java引用数据类型之字符串

Java学习笔记之:Java数组

Java学习笔记之:java的数据类型