• java并发编程 ||深入理解synchronized,锁的升级机制


    上一章我们说了多线程编程所带来的好处( java并发编程 ||Thread生命周期详解),但是既然有那么多好处,肯定也会带来一些问题,这一章我们就来看看它带来的问题以及解决的办法。

    多线程所带来的问题?

    线程不安全

    1.首先我们举一个例子来证明线程的不安全

    我们对一个数自增1000次,并且用多线程来实现。

    1. /**
    2. * @Author Dark traveler
    3. * @Note 我心净处,何处不是西天。
    4. * @Descrption
    5. * @E-Mail : 1029149772@qq.com
    6. * @Date : Created in 9:02 2020-3-18
    7. */
    8. public class NoSafeThread {
    9. private static int sum = 0;
    10. public static void incr(){
    11. try {
    12. Thread.sleep(1);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. sum++;
    17. }
    18. public static void main(String[] args) {
    19. for(int i=0,len=1000;i<len;i++){
    20. new Thread(()->{
    21. incr();
    22. }).start();
    23. }
    24. //确保所有程序都运行完成了
    25. TimeUnit.SECONDS.sleep(2);
    26. System.out.println(sum);
    27. }
    28. }

    我们会发现,我们一直得不到我们想要的答案1000,始终是一个小于1000的随机值,但是从逻辑上来说,分明应该是1000才对啊? 

    这是为什么呢,对,这就代表了线程是不安全的,我们现在new了1000个线程来调用这个方法,很多线程可能会同时进入这个方法,也就意味着可能会有两个线程同时拿到sum = 0的初始值,然后同时进入方法incr(),并且线程1自增以后把sum赋值为1,但是线程2此时并没有拿到这个1,因为它们两个都进入了这个incr()方法,所以线程2又把0自增成为1,并且把sum再次赋值为1,这样就成为了1次重复操作,当很多次这种情况出现的时候,就出现了上面那种情况。

    2.怎么解决线程不安全

    既然出现了线程不安全,会让n个线程同时进入一个方法,那我们只要想办法同时只有一个线程进入这个方法,那就没问题了吧。所以java就引入了锁的概念,我们来看看怎么用锁把上面那个答案变回1000。

    1. /**
    2. * @Author Dark traveler
    3. * @Note 我心净处,何处不是西天。
    4. * @Descrption
    5. * @E-Mail : 1029149772@qq.com
    6. * @Date : Created in 9:02 2020-3-18
    7. */
    8. public class NoSafeThread {
    9. private static int sum = 0;
    10. private static Object lock = new Object();
    11. public synchronized static void incr(){
    12. try {
    13. Thread.sleep(1);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. //锁定代码块
    18. /* synchronized (lock){
    19. sum++;
    20. }*/
    21. sum++;
    22. }
    23. public static void main(String[] args) throws InterruptedException {
    24. for(int i=0,len=1000;i<len;i++){
    25. new Thread(()->{
    26. incr();
    27. }).start();
    28. }
    29. //确保上面线程都运行完毕了
    30. TimeUnit.SECONDS.sleep(2);
    31. System.out.println(sum);
    32. }
    33. }

    我们使用了关键字synchronized来完成对方法的上锁,使得每一时刻只有一个线程进入这个方法,这样就能确保线程安全了,那我们来看看synchronized关键字的用法。

    3.synchronized关键字(重量级锁)

    synchronized的三种使用方法,代码如下

     *修饰实例方法:demo()

     *修饰静态方法:demo3()

     *修饰代码块:demo2()

    1. /**
    2. * @Author Dark traveler
    3. * @Note 我心净处,何处不是西天。
    4. * @Descrption
    5. * @E-Mail : 1029149772@qq.com
    6. * @Date : Created in 11:03 2020-3-18
    7. */
    8. public class LockThread {
    9. //2种表现形式,放在方法层面和代码块层面来控制
    10. //2种作用范围,对象锁还是类锁 区别:是否跨对象跨线程被保护
    11. //修饰方法层面,对象锁
    12. public synchronized void demo(){
    13. }
    14. //也是对象锁,比较灵活
    15. public void demo2(){
    16. //todo
    17. synchronized (this){
    18. //保护存在线程安全的变量
    19. }
    20. }
    21. //加了static,全局锁
    22. public synchronized static void demo3(){
    23. }
    24. //也是全局锁
    25. public void demo4(){
    26. synchronized (LockThread.class){
    27. }
    28. }
    29. public static void main(String[] args) {
    30. LockThread lockThread1 = new LockThread();
    31. LockThread lockThread2 = new LockThread();
    32. //不是同一个对象,不存在互斥特性,不排队
    33. new Thread(()->lockThread1.demo()).start();
    34. new Thread(()->lockThread2.demo()).start();
    35. //存在互斥性,要排队
    36. new Thread(()->lockThread1.demo3()).start();
    37. new Thread(()->lockThread2.demo3()).start();
    38. }
    39. }

    总结:

    1.锁的共享性和互斥性,只有共享项相同的锁才会有互斥作用。

    2.锁的两种表现形式,锁整个方法和锁相应的代码块。

    3.锁的两种作用范围,锁单个对象实例和锁整个类。

    synchronized为什么能起到锁的作用?锁在内存中是怎么存储的。

    我们启用synchronized关键字的时候,会去取得这个锁的对象,我们先来看看一个对象在内存中的内存布局。

    查看hotspot源码,我们可以看到对象头中具体放了哪些东西。

     

    所以我们发现,锁其实都是放到对象头中的,如果我们使用 synchronized关键字的时候,它会去取得这个对象的头中的锁信息,通过这个锁中的信息来判断是否加锁。

    4. 1.6jdk以后锁的升级

    虽然使用synchronized关键字可以保证线程的安全,但是降低了效率,那既想保证线程安全,又想保证性能怎么办呢?所以jdk1.6以后,想到了一种锁升级的情况,只有到重量级锁的时候才阻塞,之前的锁都不阻塞。

    无锁 -》 偏向锁 -》轻量级锁 -》重量级锁

    首先来说下锁的升级流程,假设有两个线程ThreadA/ThreadB访问同步代码块

    1.只有ThreadA去访问(大部分情况属于这种)-》引入偏向锁标记(ThreadA的ThreadId,偏向锁的标志)

    2.ThreadA和ThreadB交替访问-》轻量级锁(自旋锁)

    3.多个线程同时访问-》申请重量级锁,阻塞

    无锁-》偏向锁:

    当线程A去访问同步代码块的时候,先通过CAS来比较,实现原子性,检查对象头中是否存储了线程1,如果没有存储就通过cas来替换,把线程A的id加入锁对象头中,并且加上偏向锁的标志,获得偏向锁布局就变成下图。

    注:

    CAS 乐观锁,compare and swap(value,expect,update),就相当于判断下当前的值是不是最新的。我们通过下面的图来简单说明下cas的作用原理,假设我们当前内存中有个值为 int i =0,当下面的CAS开始时,先读取当前的值E = 0,然后计算这个值(比如计算过程为++),那么计算值  V = 1,接下来并不是直接赋值,而是再比较一次当前值E和现在内存中这个 i的值,现在叫N,是不是相等,如果是相等,就把计算值更新为V,CAS结束。

    当然上面是最完美的状态,在这个比较当前值E和内存中的最新值N的时候,也会出现不相等的状态,那么当前CAS就会失败,不会去更新新值。

    除了失败以外,还有一种可能,就是有其它线程对当前值进行了操作,比如另一个线程把这个值从0改成1,又从1改成了0,其它线程是操作过这个值的,但是在CAS中的比较却是能成立的,这就是CAS中的ABA问题,那么这种问题怎么解决呢,或者说怎么感知到呢,那就是加一个标志,可以是版本号,也可以是一个布尔值,当读取这个内存中的i的时候,把它的版本号一起取过来,只有有线程对它进行过操作,就升级一下版本号,这样就能解决ABA问题了。

    偏向锁-》轻量级锁(自旋锁):

    此时当线程B来访问同步代码块,它也会通过CAS来比较对象头中的锁,线程A和B的id当然不相同,所以这个比较一定会失败,然后它就会去把线程A暂停,并且把它的偏向锁给撤销,把这个锁对象的锁标志和线程id给情况,这里又会分为两种情况,如果线程A已经做完了同步代码块中的指令,A就直接把锁对象释放,由线程B来把线程id放进锁对象,访问同步代码块;如果线程A并没有做完同步代码块中的指令,那么就会升级成轻量级锁,然后两者以轻量级锁的方式来竞争这个锁对象。

    轻量级锁-》重量级锁

    当升级成轻量级锁以后,线程B会启动自旋,就是在一个循环中反复通过cas来获取锁。(因为绝大部分线程在获得锁以后会在非常短的时间内释放锁,所以这种情况阻塞损耗性能不值得)因为自旋也会占用cpu资源,所以在自旋到指定次数以后,还没有获得轻量级锁,锁就会膨胀成重量级锁,然后直接阻塞。

    所以自旋就有两种形式:

    1.设置自旋次数 preBliockSpin,jvm设置

    2.自适应自旋,推荐,根据线程获得锁的自旋次数来调整,虚拟机知道你上一次自旋锁是成功的,那么它会觉得你下一次也很可能成功,所以它会自动调整自旋锁的次数。

    注:

    自旋锁,一个循环

    for(;;){

        if(cas){

             return ;//获得锁成功

         }

    }

    重量级锁

    升级到重量级锁以后,没有获取到锁的线程会被阻塞,blocked状态,我们来看看升级到重量级锁做了什么。

    使用重量级锁后,我们发现对象里面有一个配对监视器,3和5,表示拿到锁和释放锁,11是异常也释放锁,这是一个对象监视器,ObjectMonitor。

     看hotspot,我们会发现每个对象都存在一个ObjectMonitor,这也是重量级锁的核心。

    monitor -》MutexLock,把用户态改变成内核态,基于操作系统底层来实现互斥,所以这也是比较耗费性能的原因。

    也就是说重量级锁后,一个线程拿到对象监视器,成功后调用指令monitorenter,然后操作同步代码块,其它线程就被放到一个同步队列中,直到完成后调用指令monitorexit来唤醒同步队列中的一个。

    总结:

    偏向锁-》轻量级锁:一个是通过cas,原子替换;一个是自旋来不断尝试,不会阻塞线程,所以性能高。

    重量级锁:通过阻塞来加锁,性能较低。

    synchroized关键字把这些都封装到了jvm中,所以越简单的使用,底层的实现其实越复杂。

    5、wait、notify、notifyall

    线程的通信机制

    我们用个代码演示一下:

    1. /**
    2. * @Author Dark traveler
    3. * @Note 我心净处,何处不是西天。
    4. * @Descrption
    5. * @E-Mail : 1029149772@qq.com
    6. * @Date : Created in 15:22 2020-3-18
    7. */
    8. public class ThreadA extends Thread{
    9. private Object lock;
    10. public ThreadA(Object lock){
    11. this.lock = lock;
    12. }
    13. @Override
    14. public void run() {
    15. synchronized (lock){
    16. System.out.println("start ThreadA");
    17. try {
    18. lock.wait();//实现线程的阻塞
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. System.out.println("end ThreadA");
    23. }
    24. }
    25. }
    26. /**
    27. * @Author Dark traveler
    28. * @Note 我心净处,何处不是西天。
    29. * @Descrption
    30. * @E-Mail : 1029149772@qq.com
    31. * @Date : Created in 15:22 2020-3-18
    32. */
    33. public class ThreadB extends Thread{
    34. private Object lock;
    35. public ThreadB(Object lock){
    36. this.lock = lock;
    37. }
    38. @Override
    39. public void run() {
    40. synchronized (lock){
    41. System.out.println("start ThreadB");
    42. lock.notify();//实现线程的唤醒
    43. System.out.println("end ThreadB");
    44. }
    45. }
    46. }
    47. /**
    48. * @Author Dark traveler
    49. * @Note 我心净处,何处不是西天。
    50. * @Descrption
    51. * @E-Mail : 1029149772@qq.com
    52. * @Date : Created in 15:26 2020-3-18
    53. */
    54. public class waitNotifyDemo {
    55. public static void main(String[] args) {
    56. Object lock = new Object();
    57. ThreadA threadA = new ThreadA(lock);
    58. threadA.start();
    59. ThreadB threadB = new ThreadB(lock);
    60. threadB.start();
    61. }
    62. }

    wait:实现线程的阻塞,并且释放当前的同步锁

    motify/notifyall:唤醒被阻塞的单个线程(全部线程)

    用图来表示一下刚刚的流程

  • 相关阅读:
    征战蓝桥 —— 2013年第四届 —— C/C++A组第7题——错误票据
    征战蓝桥 —— 2013年第四届 —— C/C++A组第5题——前缀判断
    征战蓝桥 —— 2013年第四届 —— C/C++A组第5题——前缀判断
    征战蓝桥 —— 2013年第四届 —— C/C++A组第5题——前缀判断
    征战蓝桥 —— 2013年第四届 —— C/C++A组第4题——颠倒的价牌
    征战蓝桥 —— 2013年第四届 —— C/C++A组第4题——颠倒的价牌
    征战蓝桥 —— 2013年第四届 —— C/C++A组第4题——颠倒的价牌
    单例模式中的多线程安全问题分析
    单例模式中的多线程安全问题分析
    每个程序员都应该警惕的文件上传漏洞!!
  • 原文地址:https://www.cnblogs.com/huangwentian/p/14691427.html
Copyright © 2020-2023  润新知