【Java并发】深入浅出 synchronized关键词原理-上

发布时间:2024年01月03日

一个问题的思考

建设我们有两个线程,一个进行5000次的相加操作,另一个进行5000次的减操作。那么最终结果是多少

package com.jia.syn;

import java.util.concurrent.TimeUnit;

/**
 * @author qxlx
 * @date 2024/1/2 10:08 PM
 */
public class SynTest {

    private Integer tickets = 0;

    public void sell() {
        tickets++;
    }

    public void sell2() {
        tickets--;
    }


    public static void main(String[] args) throws InterruptedException {
        SynTest synTest = new SynTest();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synTest.sell();
            }
        });

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synTest.sell2();
            }
        });

        thread1.start();
        thread.start();

        thread.join();
        thread1.join();


        TimeUnit.SECONDS.sleep(3);
        System.out.println("总共卖出多少票" + synTest.tickets);
    }

}

执行上述代码之后,发现结果却不是0,为什么

 7 getfield #3 <com/jia/syn/SynTest.tickets : Ljava/lang/Integer;>
10 invokevirtual #4 <java/lang/Integer.intValue : ()I>
13 iconst_1
14 iadd
15 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
18 dup_x1

通过分析字节码变量,可以看出其实i++, i–操作其实是三个步骤,也就是先获取i的值,对i+1操作,然后在对i赋值。那么这样就可以解释为什么执行的最终结果不是期望的0值。

程序在执行的时候,不同的两个线程执行。比如会出现线程2获取到i的值是10,对i-1操作9,但是想要将i=9赋值操作的时候,发现CPU执行权被线程1获取,此时线程1获取到i的值是10,对i+1操作,然后复制给i=11。但是紧接着就是线程2对i=9赋值。所以最终出现的结果就是9,而不是 原来的11。将线程1的值进行覆盖更新了。
在这里插入图片描述

临界区

上述其实是多个线程对于共享资源进行读写操作,导致出现数据不一致。如果是只读,那没有问题,但是有写操作,就会出现乱序问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
上述中的

    public void sell() { // 临界区
        tickets++;
    }

    public void sell2() { //临界区
        tickets--;
    }

为了解决上述的问题,那么可以使用多种手段进行解决。

  • 阻塞式:synchronized、lock
  • 非阻塞式:原子变量

Synchronized

在这里插入图片描述

    private Object obj = new Object();
    
    // 方法
    //静态方法-锁住的类对象
    public static synchronized void test1() {
    }
    
    //普通方法-锁住的对象实例
    public synchronized void test2(){
    }
    
    
    //代码块
    public void test3 (){
        //代码块-锁住的是该类对象
        synchronized (SynTest2.class) {
            
        }
    }

    //代码块
    public void test4 (){
        //代码块-锁住的是该对象实例
        synchronized (this) {

        }
    }

    //代码块
    public void test5 (){
        //代码块-锁住的是该obj对象实例
        synchronized (obj) {

        }
    }

所以解决上述的问题,就可以加syn锁。

原理

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语
Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的 优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操 作的开销,内置锁的并发性能已经基本与Lock持平。

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥 原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态 之间来回切换,对性能有较大影响。

同步方法

在这里插入图片描述

同步代码块

 0 aload_0
 1 dup
 2 astore_1
 3 monitorenter //进入
 4 aload_1
 5 monitorexit //退出
 6 goto 14 (+8)
 9 astore_2
10 aload_1
11 monitorexit //异常退出
12 aload_2
13 athrow
14 return

管程

管程,其实是管理共享变量以及对共享变量操作过程。英文是Monitor,也叫监视器。
管程有三种不同的管程模型,Hasen模型、Hoare模型、MESA模型。目前主要用的后者。

并发编程中,互斥解决的是对于共享资源同时只能有一个线程访问,同步是线程之间如何通信、协作的问题。管程都可以解决。
在这里插入图片描述
条件变量等待队列解决的是同步问题,入口等待队列解决的是互斥问题。

java中对管程的实现进行了精简,只有一个条件变量等待队列。
在这里插入图片描述

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖
于 ObjectMonitor 实现,这是 JVM 内部基于 C++ 实现的一套机制。 ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp)

ObjectMonitor(){
2 _header = NULL; //对象头 markOop
3 _count = 0;
4 _waiters = 0,
5 _recursions = 0; // 锁的重入次数
6 _object = NULL; //存储锁对象
7 _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
8 _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
9 _WaitSetLock = 0 ;
10 _Responsible = NULL ;
11 _succ = NULL ;
12 _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
13 FreeNext = NULL ;
14 _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失
败的线程)
15 _SpinFreq = 0 ;
16 _SpinClock = 0 ;
17 OwnerIsThread = 0 ;
18 _previous_owner_tid = 0;
19 }

我们主要关注的其实是waitSet,cxq 、EntryList。

1.多个线程竞争获取锁
多个线程同时请求获取Monitor锁时,会通过CAS操作,设置_owner字段。谁设置成功,就获取锁。

2.没有获取锁的线程排队等待获取锁
多个线程获取锁,获取到锁的线程就去执行任务,没有获取到锁的线程会进入到_cxq队列中等待获取锁。

3.获取到锁之后通知排队等待锁的线程去竞争锁
当执行完线程的释放锁的时候,会从_EntryLitst取出一个线程,去通过CAS竞争锁,之所以不让这个线程获取锁而去竞争锁,是因为同时可能有别的线程可能获取到锁。

如果_EntryList队列为空的话,那么将_cxq所有线程全部搬移到_EntryList中。在中_EntryList中获取线程。

在这里插入图片描述
另外就是当调用 Object.wait() 会进入 _WaitSet 队列,只要被唤醒时,才会重新进入 EntryList 中去增强锁。

在这里插入图片描述

总结

本篇主要通过一个案例讲解了线程安全问题,以及介绍了syn代码块和方法底层实现的区别,以及介绍了管程、java中实现管程的方式。下一篇文章,开始介绍syn的锁升级。

文章来源:https://blog.csdn.net/jia970426/article/details/135351492
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。