Synchronized 用法和底层原理

目录

一、对象锁和类锁

1. 对象锁

2. 类锁

二、应用举例

三、使用总结

四、实际应用

五、底层原理

理解Java对象头与Monitor


一、对象锁和类锁

1. 对象锁

在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

2. 类锁

在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

二、应用举例

synchronized用到不同地方对代码产生的影响:
1. synchronized关键字修饰方法
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都能够调用他们。

public synchronized void method(){
    // 
}


这也就是同步方法,那这synchronized锁定的是调用这个同步方法对象(this)。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,他们之间会形成互斥,达到同步的效果。同时如果该对象中有多个同步方法,则当一个线程获执行对象中的一个synchronized方法,则该对象中其它同步方法也不允许别的线程执行。但是这个对象所属的Class所产生的另一对象P2却能够任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:

public void method()   {  
    synchronized (this)     
    {  
       //..  
    }  
}  


此次就是一个P1对象的对象锁,哪个拿到了P1对象锁的线程,才能够调用P1的同步方法,而对P2而言,P1这个锁和他毫不相干,程式也可能在这种情形下摆脱同步机制的控制,造成数据混乱。
2.同步块,示例代码如下:

public void method(SomeObject so) {  
   synchronized(so)  
   {  
        //..  
   }  
} 


这时,锁就是so这个对象,每个对象对应一个唯一的锁,所以哪个线程拿到这个对象锁谁就能够运行他所控制的那段代码。当有一个明确的对象作为锁时,就能够这样写程式,但当没有明确的对象作为锁,只是想让一段代码同步时,能够创建一个特别的instance变量(他得是个对象)来充当锁:

  private byte[] lock = new byte[0]; 

    Public void method(){  
           synchronized(lock)
           {
              //同步块里的代码
              //释放锁
              lock.wait()
           }

        //后面的代码
    }  


PS:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

在B线程先占用so对象后,A线程只能等A执行完synchronized同步代码块释放锁之后,才能执行synchronized代码块,而不会跳过synchronized代码块而去执行后面的代码。


3.将synchronized作用于static 函数,示例代码如下:
 

Class Foo  
{  
    public synchronized static void method1()   

    {  
        //.  
    }  
    public void method2()  
    {  
        synchronized(Foo.class)  
        //
    }  

    public synchronized void method3()  
    {  
           //.  
    }  
} 

method1和method2 的锁都是Foo.class 构成同步
method3的锁是this对象和method1,method2不构成同步

下面是一个经典问题:

 当一个线程进入一个对象的一个synchronized 方法后,其它线程是否可
入此对象的其它方法?
几种情况:
   1.   其他方法前是否加了 synchronized 关键字,如果没加,则能。
   2.   如果这个方法内部调用了 wait ,则可以进入其他 synchronized 方法。
   3.   如果其他个方法都加了 synchronized 关键字,并且内部没有调用 wait ,则不能。
   4.   如果其他方法是 static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是 this 。
 

三、使用总结

1、对于静态方法,由于此时对象还未生成,所以只能采用类锁;
2、只要采用类锁,就会拦截所有线程,只能让一个线程访问。
3、对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。
4、如果对象锁跟访问的对象没有关系,那么就会都同时访问。

四、实际应用

public class Main {
    /**
     * 当前打印到的数字
     */
    private static volatile int curNum = 0;
    /**
     * 打印到的最大数字
     */
    private static final int MAX_NUM = 100;
    /**
     * 线程数
     */
    private static final int THREAD_NUM = 10;
    /**
     * 共享的锁
     */
    private static final Object lock = new Object();
 
    public static void main(String[] args) {
        for (int i = 1; i <= THREAD_NUM; i++) {
            new Thread(new Turn(i), "线程" + i).start();
        }
    }
 
    private static class Turn implements Runnable {
        private int index;
 
        public Turn(int index) {
            this.index = index;
        }
 
        @Override
        public void run() {
            while (curNum <= MAX_NUM) {
                synchronized (lock) {
                    if ((curNum % THREAD_NUM + 1) != index) {
                        try {
                            //释放锁
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        System.out.println(Thread.currentThread().getName() + ":" + curNum++);
                        lock.notifyAll();
                    }
                }
            }
        }
    }
}
 
/*
线程1:0
线程2:1
线程3:2
线程4:3
线程5:4
线程6:5
线程7:6
线程8:7
线程9:8
线程10:9
线程1:10
线程2:11
线程3:12
线程4:13
线程5:14
线程6:15
线程7:16
线程8:17
线程9:18
线程10:19
线程1:20
线程2:21
线程3:22
线程4:23
线程5:24
线程6:25
线程7:26
线程8:27
线程9:28
线程10:29
线程1:30
线程2:31
线程3:32
线程4:33
线程5:34
线程6:35
线程7:36
线程8:37
线程9:38
线程10:39
线程1:40
线程2:41
线程3:42
线程4:43
线程5:44
线程6:45
线程7:46
线程8:47
线程9:48
线程10:49
线程1:50
线程2:51
线程3:52
线程4:53
线程5:54
线程6:55
线程7:56
线程8:57
线程9:58
线程10:59
线程1:60
线程2:61
线程3:62
线程4:63
线程5:64
线程6:65
线程7:66
线程8:67
线程9:68
线程10:69
线程1:70
线程2:71
线程3:72
线程4:73
线程5:74
线程6:75
线程7:76
线程8:77
线程9:78
线程10:79
线程1:80
线程2:81
线程3:82
线程4:83
线程5:84
线程6:85
线程7:86
线程8:87
线程9:88
线程10:89
线程1:90
线程2:91
线程3:92
线程4:93
线程5:94
线程6:95
线程7:96
线程8:97
线程9:98
线程10:99
线程1:100
 */

五、底层原理

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}
反编译结果

关于这两条指令的作用,我们直接参考JVM规范中描述:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

理解Java对象头与Monitor

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

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

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

monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因
 

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

虚拟机位数头对象结构说明
32/64bit       Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit  Class Metadata Address     类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

  

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

锁状态   25bit  4bit    1bit是否是偏向锁    2bit 锁标志位
无锁状态    对象HashCode    对象分代年龄    0   01

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


 

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ReentrantLock和synchronized都是用于实现并发编程中的同步机制,但它们的底层原理和使用方式有所不同。 1. synchronized底层原理synchronized是Java中的关键字,它基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步。在Java对象头中,有一个标志位用于表示对象是否被锁定。当线程进入synchronized代码块时,它会尝试获取对象的锁,如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。当线程退出synchronized代码块时,它会释放对象的锁,使其他线程可以获取锁并执行相应的代码。 2. ReentrantLock的底层原理: ReentrantLock是Java中的一个类,它使用了一种称为CAS(Compare and Swap)的机制来实现同步。CAS是一种无锁的同步机制,它利用了CPU的原子指令来实现对共享变量的原子操作。ReentrantLock内部维护了一个同步状态变量,通过CAS操作来获取和释放锁。当一个线程尝试获取锁时,如果锁已经被其他线程持有,则该线程会进入等待状态,直到锁被释放。与synchronized不同,ReentrantLock提供了更灵活的锁获取和释放方式,例如可以实现公平锁和可重入锁。 总结: - synchronized是Java中的关键字,基于进入和退出监视器对象来实现同步,而ReentrantLock是一个类,使用CAS机制来实现同步。 - synchronized是隐式锁,不需要手动获取和释放锁,而ReentrantLock是显式锁,需要手动调用lock()方法获取锁,unlock()方法释放锁。 - ReentrantLock相比synchronized更灵活,可以实现公平锁和可重入锁等特性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据与后端架构提升之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值