今天就来说一说synchronized把。
1.一个生活在JVM层面的Java关键字;
2.那么直接说明一下,它的使用场景
(1)可以修饰类的成员方法,可以使用当前对象的this进行加锁,也可以理解为,当某个类的成员方法添加了synchronized修饰符,那么这个成员方法,在使用的时候,就是只能有一个线程进入该方法,其他均阻塞
(2)修饰静态方法,对于当前类的class对象加锁,当然,我们也可以选择对应的静态成员变量,进行锁对象操作,那么如果是静态方法,那么就是对当前类进行锁操作,其他线程访问均会被阻塞
(3)修饰代码块,自定义选择某个对象进行代码块锁定,那么就是对该对象进行锁住该代码块
那么可以理解为上述3中操作类别,就是分别为锁住方法、锁住代码块,锁住对象;
开始一个个聊聊,Volatile的几大特性:
1.有序性;
要知道,我们需要保持代码的有序性,这样的话,才能保证业务的正常处理;那么按照之前的理解,我们写的代码,是怎么样的就是怎么样的,为什么还会无序呢?这就是JVM底层对我们的代码进行优化的,时候,会进行指令重排序,这个就是涉及到Volatile了,他就解决了指令重排序。
那么不管是什么样的操作,在我们不知道的情况下,jvm还对我们的代码进行了重排序,那如果存在数据上的严格要求的话,那么就会出现数据上的业务问题了。
Volatile就是通过保持多线程环境下,那么就解决了有序性;
- 可见性;
也就是内存可见性,如果我们知道线程间的对于数据的操作是怎么样的,那么就会知道,如果不解决内存可见性的问题的话,那么就会出现多线程异常情况;在我们每个用户线程去访问公共变量的时候,都是将其变量从主内存中拷贝至我们的用户线程中,然后对其进行业务处理,那么这个时候,就会出现不可见问题,如果两个线程间并不知道相互之间的改动,那么如果线程A把变量从0改成了1,那么线程B还是对0进行操作,这个时候就是有问题的。那volatile就解决了这个问题,当线程A将变量0改成了1,那么线程B会立刻接收到最新的变量值。 - 原子性:
原子不可分割;恰巧volatile并不保证数据的原子性操作。但是sync支持啊;
sync有哪些特性:
1.可重入性:
可以抽象理解成,当你有了你家的大门锁,那你通过这锁的对象,你就可以进入到你家的厕所的锁。那么当你出来的时候,就要解锁厕所,然后出家门的时候,就要解家门锁。这是一个操作上的严格顺序,不可无序。
那么sync本质在jvm层面,就是通过锁的计数来实现获取锁的次数,然后当完成了对应的代码块之后,就会对计数器--,直到计数器清零的时候,别人就可以进入你家的门了。。。
在jvm层面,这些锁操作,解锁操作都帮你封装好了。所以对于程序员来说,只需要确认你锁的对象是什么。直接代码块包括就可以了。个人认为门槛极低;
2.不可中断性:
可以理解为,一个线程获取到了锁之后,没人敢把他打断,其他的线程,都得处于阻塞或者等待的过程(等待可以理解为自旋)。除非他自己出来。那么lock是可以被打断的。
3.底层的实现方式(简单描述一下)
拷贝一下一个锁对象的方法的反编译文件,看看里面有什么
public class Synchronized { public synchronized void husband(){ synchronized(new Volatile()){ } } }
同步代码
可以看到,其中有一个关键本地方法“monitorenter” “monitorexit”
就可以理解为这个方法就是获取当前对象的锁,当进入锁的时候,就会对monitor计数+1,当前当前线程就是持有者,当处理完成后,就会执行monitorexit,对monitor计数-1;直到为0的时候,才会被其他线程所竞争;
那么上图中其实还对方法进行上锁了,可以关注到 ACC_SYNCHRONIZED 方法的一个修饰关键,那么同理,当前执行到这个方法的时候,会判断该方法会不会有 ACC_SYNCHRONIZED 标识,有的话,也会执行上述锁 代码块的一个 操作流程,也就是 “monitorenter” “monitorexit”,只不过没直观表现;那么本质来说都是一个对象 “monitor” 进行抢占操作,那么这个对象可以看看他的数据结构是怎么样的:(C++)
那么在观察上述源码的时候,可以看到一些原子操作。
这里就需要引出 “重量级锁”了, 很多 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的小消耗,解决资源消耗,是可行的。
转换流程图
上面锁很多,但是还是需要总结一下对比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)可以灵活选择公平、非公平;