Java Synchronized关键字学习记录2:性质和原理

本文详细探讨了Java Synchronized关键字的性质,包括可重入和不可中断性,以及加锁、释放锁的原理。介绍了可重入锁的计数器机制,确保线程安全和内存可见性,并分析了其可能导致的效率低和灵活性不足的问题。同时,文章还对比了synchronized与Lock的中断能力差异。
摘要由CSDN通过智能技术生成

【声明】:本篇文章来自本人gitee仓库搬运至CSDN,https://gitee.com/genmers/md-notes

本篇前置:Java Synchronized关键字学习记录1:用法

一、Synchronized关键字的性质和原理

在学习完用法后,我们继续深入学习Synchronied关键字的两个性质和原理

1.1 性质
可重入

Synchronied关键字可重入锁也叫递归锁的概念

不可中断

Synchronied关键字一旦被别人获取就只能等待

1.2 原理
加锁释放锁原理

现象、时机、深入JVM看字节码

可重入原理

加锁次数计数器

保证可见性原理

内存模型

synchronied缺陷

synchronied的三个缺陷


二、说完了用法,我们来看看性质

在之前的学习中,我们知道了一旦有一个线程访问某个对象的synchronied修饰的方法或者代码区域时,就是该线程获得了这个对象的锁,其他线程就不能调用这个对象的synchronied修饰的方法

那么,如果这个线程自己调用这个对象被synchronied修饰的方法时,java时怎么判定的呢,从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。

++这时候就引入了可重入性这个概念++

可重入性:指的是同一线程的外层函数获得锁后,内层函数可以直接再次获得锁

synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的

在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。++原因是Java中线程获得对象锁的操作是以线程为单位的++,而不是以调用为单位的。

总而言之,可重入性质的好处就是可以避免类似一个对象获取了锁又因为不可重入性,需要用到时又还未释放锁而无法申请到而陷入的死锁状况。

补充:可重入的粒度(作用范围)是一整个线程,比如所一个线程拿到了一把锁,又想接着使用这把锁去访问别的方法或者别的类的方法,只要需要的还是手里的这把锁,这时候因为可重入性质,就不需要显式的释放这把锁,就可以直接去完成这件事

三、不可中断

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

相比之下,Lock类可以拥有中断的能力

  • 如果我觉得等待的时间太长了,有权中断现在已经获取到锁的线程执行;
  • 如果为觉得等待时间太长了不想等了,也可以退出。

四、加锁释放锁原理

调用之前的语句

在之前的学习中,我们知道了一旦有一个线程访问某个对象的synchronied修饰的方法或者代码区域时,就是该线程获得了这个对象的锁,其他线程就不能调用这个对象的synchronied修饰的方法,想要调用这个方法就要获取这个锁,如果持有者不释放,因为不可中断性那想要调用的调用者就得等待,++锁的获取和释放都由JVM操作++,我们只需指定对象即可。

以上就是我们看到的现象,我们知道,每个Java对象都可用作实现同步的锁,这个锁被叫做内置锁或者叫做监视器锁(Monitor Lock),线程在进入同步代码块前会获取,退出(方法结束或者出现异常)的时候会自动的释放,获得这个内置锁的唯一途径就是进入这个锁所保护的同步代码块或者方法中

栗子1: 展开synchronied

package demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *@program: 之前清了固态,之前的例子没了
 *@description: 加锁和释放锁的时机
 *@author: Genmer
 *@create: 2020-10-01 15:49
 **/
public class SynchroniedToLock13 {
    Lock lock = new ReentrantLock();
    /**
     * synchronied修饰的方法的加锁释放锁都是JVM自动执行的
     */
    public synchronized void method1(){
        System.out.println("我是synchronied形式的锁");
    }

    /**
     * method2是method1的展开形式
     */
    public void method2(){
        // 获取锁
        lock.lock();
        try {
            System.out.println("我是lock形式的锁");
        } finally {
            //这里演示异常后释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SynchroniedToLock13 s =new SynchroniedToLock13();
        s.method1();
        s.method2();
    }
}

栗子2: 反编译看同步代码块

package demo;
/**
 *@program:
 *@description: 反编译字节码
 *@author: Genmer
 *@create: 2020-10-03 02:10
 **/
public class Decompilation14 {
    private Object object = new Object();

    public void insert(Thread thread){
        //反编译查看同步代码块
        synchronized (object){

        }
    }
}

  1. 现在来到控制台
  2. cd到.java文件所在的地方
  3. javac java文件名 编译出class文件
  4. javap -verbose,反编译class成字节码
 ~ : cd /Users/genmersmbp/IdeaProjects/concurrency_practice/synchronied/src/demo/
 
 ~/IdeaProjects/concurrency_practice/synchronied/src/demo  javap -verbose Decompilation14

编译结果:截取方法部分

警告: 二进制文件Decompilation14包含demo.Decompilation14
//...
  public void insert(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: aload_2
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit
        15: aload_3
        16: athrow
        17: return
  //...
SourceFile: "Decompilation14.java"

我们可以看到6、8、14行的 monitorenter、monitorexit就类似我们操作系统里的PV操作,用来控制多线程对访问临界区资源访问的

由于释放锁的时机除了方法结束,还有抛出异常等其他情况,所以一般会有一个monitorenter对应多个monitorexit的情况

每一个对象都和一个monitor lock相关联,而一个monitor lock同一时间只能被一个线程获得,一个线程在获取与这个对象相关联monitor lock的时候,只会发生以下3种情况之一:

monitorenter: 获取锁

  • 如果monitor计数器为0,目前还没被获得,这个线程会立即被获取,计数器+1
  • 如果monitor已经获得了所有权,又重入了,计数器会累加
  • 如果monitor已经被其他线程持有了,只能得到阻塞的状态,monitor计数器为0,才会去获取

monitorexit: 释放锁(前提锁已拥有)

  • 过程就是monitor计数器-1
  • 如果计数器为0,说明当前线程已不再拥有这个锁,当前阻塞的线程会再次尝试获取
  • 如果计数器不为0,说明锁可重入进来的
五、可重入原理
关于可重入,就是一个线程获取了锁,想再次使用可以不用申请直接再次获得

由于各个原理关系密切,前面已经有所提及,++原理就是一个加锁次数计数器++

  1. 首先每个对象都自动的会含有一把锁,JVM负责跟踪对象被加锁的次数
  2. 线程第一次给对象加锁的时候,计数器变为1。每当相同线程在这个对象上再次获得锁时,计数器会递增
  3. 每当任务离开时,计数递减,当计数为0的时候,锁被完全释放
保证可见性的原理

在讲可见性原理之前,我们先看看Java内存模型,便于理解可见性的实现

首先先复习一下内存模型的概念:

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范 定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JVM程序运行的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
image
简单总结一句就是:线程之间的通信靠的是通过更新在主内存的共享变量的值,而这个过程就是JMM控制的

在了解了内存模型之后,我们来了解了Synchronied的同步时如何实现的

一旦一个方法或者代码块被synchronied关键字修饰,那么执行完毕之后被锁住的对象所做的任何修改,都要在释放锁之前,从内存写回到主内存中,也就是说:不会存在线程内存和主内存内容不一致的情况。同样的道理,他在进入到代码块得到锁之后,被锁定对象的数据,也是直接从主内存读取

就是这样synchronied关键字为可见性提供了保障

synchronied缺陷

讲完了synchronied的优点之后,我们也来总结下他的缺陷

  • 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
  • 不够灵活(读写锁更灵活,读的时候不加锁,因为没风险):加锁和释放锁时机单一,每个锁仅有单一的条件(某个对象),可能少不够的
  • 无法知道是否成功获取到锁

Java Synchronized关键字学习记录扩展:ReentrantLock

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值