今天就来说一说synchronized把。

1.一个生活在JVM层面的Java关键字;

2.那么直接说明一下,它的使用场景

(1)可以修饰类的成员方法,可以使用当前对象的this进行加锁,也可以理解为,当某个类的成员方法添加了synchronized修饰符,那么这个成员方法,在使用的时候,就是只能有一个线程进入该方法,其他均阻塞


public synchronized void method() {

}


 

(2)修饰静态方法,对于当前类的class对象加锁,当然,我们也可以选择对应的静态成员变量,进行锁对象操作,那么如果是静态方法,那么就是对当前类进行锁操作,其他线程访问均会被阻塞


public void method() {
synchronized (Integer.class) {

}
}


 

(3)修饰代码块,自定义选择某个对象进行代码块锁定,那么就是对该对象进行锁住该代码块


public static void method() {
Integer integer = new Integer(1);
synchronized (integer) {

}
}


 

那么可以理解为上述3中操作类别,就是分别为锁住方法、锁住代码块,锁住对象;

开始一个个聊聊,Volatile的几大特性:

1.有序性;

要知道,我们需要保持代码的有序性,这样的话,才能保证业务的正常处理;那么按照之前的理解,我们写的代码,是怎么样的就是怎么样的,为什么还会无序呢?这就是JVM底层对我们的代码进行优化的,时候,会进行指令重排序,这个就是涉及到Volatile了,他就解决了指令重排序。

那么不管是什么样的操作,在我们不知道的情况下,jvm还对我们的代码进行了重排序,那如果存在数据上的严格要求的话,那么就会出现数据上的业务问题了。

Volatile就是通过保持多线程环境下,那么就解决了有序性;

 

  1. 可见性;
    也就是内存可见性,如果我们知道线程间的对于数据的操作是怎么样的,那么就会知道,如果不解决内存可见性的问题的话,那么就会出现多线程异常情况;在我们每个用户线程去访问公共变量的时候,都是将其变量从主内存中拷贝至我们的用户线程中,然后对其进行业务处理,那么这个时候,就会出现不可见问题,如果两个线程间并不知道相互之间的改动,那么如果线程A把变量从0改成了1,那么线程B还是对0进行操作,这个时候就是有问题的。那volatile就解决了这个问题,当线程A将变量0改成了1,那么线程B会立刻接收到最新的变量值。
  2. 原子性:
    原子不可分割;恰巧volatile并不保证数据的原子性操作。但是sync支持啊;

 

 

sync有哪些特性:

1.可重入性:

可以抽象理解成,当你有了你家的大门锁,那你通过这锁的对象,你就可以进入到你家的厕所的锁。那么当你出来的时候,就要解锁厕所,然后出家门的时候,就要解家门锁。这是一个操作上的严格顺序,不可无序。

那么sync本质在jvm层面,就是通过锁的计数来实现获取锁的次数,然后当完成了对应的代码块之后,就会对计数器--,直到计数器清零的时候,别人就可以进入你家的门了。。。

在jvm层面,这些锁操作,解锁操作都帮你封装好了。所以对于程序员来说,只需要确认你锁的对象是什么。直接代码块包括就可以了。个人认为门槛极低;

 

2.不可中断性:

可以理解为,一个线程获取到了锁之后,没人敢把他打断,其他的线程,都得处于阻塞或者等待的过程(等待可以理解为自旋)。除非他自己出来。那么lock是可以被打断的。

 

3.底层的实现方式(简单描述一下)

拷贝一下一个锁对象的方法的反编译文件,看看里面有什么


public class Synchronized {    public synchronized void husband(){        synchronized(new Volatile()){       }   } }


命令: javap -p -v -c Synchronized.class

Classfile /Synchronized.class
Last modified 2020-12-08; size 375 bytes
MD5 checksum 4f5451a229e80c0a6045b29987383d1a
Compiled from "Synchronized.java"
public class juc.Synchronized
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // juc/Synchronized
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Ljuc/Synchronized;
#11 = Utf8 husband
#12 = Utf8 SourceFile
#13 = Utf8 Synchronized.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 juc/Synchronized
#16 = Utf8 java/lang/Object
{
public juc.Synchronized();
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 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljuc/Synchronized;

public synchronized void husband();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 这里
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class juc/Synchronized
2: dup
3: astore_1
4: monitorenter // 这里
5: aload_1
6: monitorexit // 这里
7: goto 15
10: astore_2
11: aload_1
12: monitorexit // 这里
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 10: 0
line 12: 5
line 13: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ljuc/Synchronized;
}
SourceFile: "Synchronized.java"


同步代码

可以看到,其中有一个关键本地方法“monitorenter” “monitorexit”

就可以理解为这个方法就是获取当前对象的锁,当进入锁的时候,就会对monitor计数+1,当前当前线程就是持有者,当处理完成后,就会执行monitorexit,对monitor计数-1;直到为0的时候,才会被其他线程所竞争;

 

 

那么上图中其实还对方法进行上锁了,可以关注到 ACC_SYNCHRONIZED 方法的一个修饰关键,那么同理,当前执行到这个方法的时候,会判断该方法会不会有 ACC_SYNCHRONIZED 标识,有的话,也会执行上述锁 代码块的一个 操作流程,也就是 “monitorenter” “monitorexit”,只不过没直观表现;那么本质来说都是一个对象 “monitor” 进行抢占操作,那么这个对象可以看看他的数据结构是怎么样的:(C++)


ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}


 

那么在观察上述源码的时候,可以看到一些原子操作。

这里就需要引出 “重量级锁”了, 很多 Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核的方法,那么可以理解为就是park() upark().

这个就是涉及到用户态和内核态的一个转换了。如果说,线程阻塞,和短期的自旋活跃,谁更消耗资源,那么就是前者了。(这里还可以提出密集型IO)

 

用户态:所有的程序执行都是在用户控件中运行。

内核态:I/O操作就是进入内核操作

此处描述一个用户态到内核态的转换流程:

1.用户态将一些数据放到寄存器中,那么创建了一些堆栈。这个时候就告诉操作系统,我需要你提供服务了。

2.用户态执行系统调用

3.CUP切换到内核态,去到对应的内存指定的位置执行指令。

4.系统调用处理器去读取我们执行放到内存的数据参数,执行程序的请求。

5.调用完成后,操作系统会将CUP转换为用户态,返回结果,然后执行下个指令。所以说,在1.6之前,就是重量级的锁,这也是他重量的本质原因。在ObjectMonitor调用的过程中,有内核的运行机制决定,大量的系统资源的消耗,导致执行效率低下。

 

1.偏向锁:很多情况下,虽然添加sync,保证了多线程环境的安全性,但是在多数情况下,只有会一个线程执行,没有竞争情况。那么这个执行操作,去获取锁,解锁,是没有必要的,那么就有了偏向锁的概念,偏向锁是在第一个线程第一次访问的时候,就记录他的线程ID,那么线程判断还是该线程的话,那么就不执行上锁、解锁过程了。但是当有新的线程ID进入,那么就升级为轻量级锁。

 

2.轻量级锁:由偏向锁升级而来,那么此时的线程就是需要被加锁的代码片段,一旦存在多个线程间的竞争关系后,那么就会开启自旋锁。

 

3.自旋锁:由多个线程ID竞争升级;短暂死循环,一直获取锁;循环次数超出后,将进入重量锁。

 

4.重量级锁:重量级锁就是上面说到的monitor对象实现,也就是sync底层c++实现对象,当一段代码上锁后,该代码只能允许单个线程运行,如果有其他线程也想进入,就会被阻塞挂起;然后第一个线程执行完成后,就会唤醒 第二个线程,如此线程的切换,在内核态、用户态转换是相当耗资源的,所以不希望到这个级别,更多的,自旋锁,通过已CPU的小消耗,解决资源消耗,是可行的。

 

转换流程图

 

 

今天就来说一说synchronized把。_java

 

上面锁很多,但是还是需要总结一下对比Lock

1.synchroized是Java关键字,是一个作用在JVM底层的一个操作;Lock是一个Java接口,有很多锁的实现类,可以理解为是在JDK层面的一个操作;

2.synchroized不需要程序员维护建立锁,释放锁,jvm帮你维护;Lock是需要程序员对其上锁、解锁操作,并且需要控制数量对齐,顺序有序;

3.syhchroized是不能中断的,jvm层面的东西,你想中断都难。那么Lock是可能会被中断的,也可以不被(之前所述的finally就是一个意思)。

4.通过Lock,我们可以得到获取锁的结果,返回boolean;但是synchroized没有结果;

5.Lock有很多丰富的API,其中读锁,可以在多线程环境下保持共享。synchroized就比较强,直接将所有代码锁定,不可灵活使用;

6.synchroized是非公平锁(线程先后顺序并不重要,关键看谁抢到锁就是王道);Lock中有个可重入锁(ReentrantLock)可以灵活选择公平、非公平;