日常记录——多线程与高并发—synchronized概念、原理、锁升级、用法、特性、注意、优化

一、概念

Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

二、原理

首先要明白,锁锁住的是什么:是对象,那怎么知道一个对象是否被锁住了呢,那就要从对象说起。在HotSpot JVM(虚拟机可自己重新实现,这里只谈oracle自带的)实现中,创一个对象在堆内存中,分为三块区域:对象头,实例数据,填充数据。
实例数据:存放类的属性数据信息,包括父类的属性信息。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是虚拟机用来通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,如:哈希码、GC分代年龄、锁状态标志等。Mark Word才是对象锁的关键。
对象头里有两位是锁标志位,一位是偏向锁标志位;

锁标志位 偏向锁标志位 偏向锁标志位
01 0 无锁
01 1 偏向锁
00 轻量级锁
10 重量级锁
11 GC标志

根据标志位来判断当前对象的锁的状态。
如果无锁。一切好说,来个线程把资源给你。
如果是偏向锁,将线程id存放在自己的对象头中。
如果是轻量级锁,2个线程公平竞争,谁获取到锁,谁执行,Mark Word记录线程栈指针的锁记录,另一个自旋,当前线程执行完,另一个再执行。
如果是重量级锁,多个线程竞争,Mark Word记录监视器对象,监视器里有_EntryList队列,记录和管理竞争线程(锁池)。
每个对象都有对象监视器,其结构为(C实现):

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet(等待池)
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表(锁池)
 }

结合线程的状态来说明监视器工作流程:
当多个线程同时访问synchronized方法,那么这些线程会先被放进_EntryList队列(锁池),此时线程处于阻塞状态。
当一个线程获取到了实例对象的监视器对象,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取。
当running状态的线程调用wait()方法,那么当前线程释放监视器对象,进入阻塞状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列(等待池),直到有线程调用notify()方法唤醒该线程,则该线程放入_EntryLis(锁池)t重新竞争资源。
如果当前线程执行完毕,那么就释放监视器对象,ObjectMonitor对象的_owner变为null,_count减1。
以上为对象锁(synchronized)的原理。

三、锁升级

JDK1.5后锁的升级过程:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
1无锁:无线程竞争。
2.偏向锁:当有一个线程获取锁,升级为偏向锁,只有一个线程访问对象,并认为下一个访问线程还是该线程,记录线程id,如果下次线程访问还是该线程,直接执行逻辑。
3.轻量级锁:当前有两个线程竞争锁,一个线程t1获取到锁资源,另一个线程t2自旋等待t1线程执行解锁释放锁资源,自旋10次以上,还未等到资源,升级为重量级锁,交给os管理,因为自旋是消耗cpu资源的。
4.重量级锁:多个线程访问锁资源,交给os管理,不占用cpu资源。
轻量级锁使用场景:并发数少,代码执行速度快。
重量级锁使用场景:并发数高,代码执行速度慢。

重量级锁不一定比轻量级锁效率低

四、用法

1.锁定代码块:锁定(obj)中对象。访问锁定obj代码块阻塞。

//锁定obj对象
    public void addObj(){
        synchronized (obj){
            count++;
            System.out.println(Thread.currentThread().getName()+"---"+count);
        }
    }

2.锁定普通方法:锁定当前对象,同类不同对象不产生阻塞,相同对象产生阻塞。

//锁当前对象
    public synchronized void  addThis1(){
            count++;
            System.out.println(Thread.currentThread().getName()+"---"+count);
    }

2.锁定静态方法:锁定当前类Class,static synchronized方法和synchronized方法不阻塞,前者是锁定类对象,后者是锁定实例对象。

//锁类
    public static synchronized void addClass1(){
        count++;
        System.out.println(Thread.currentThread().getName()+"---"+count);
    }

五、特性

1.同步和非同步方法可以同时调用。
2.可重入性:同一线程调用多个加锁方法,但是锁资源是同一个资源,只是在锁对象的recursions数量加1,无资源竞争。
3.抛出异常自动释放锁,但这种情况可能造成数据不一致,需注意处理。

六、注意

1.不能用null对象作为锁对象,通过上文,获取锁是通过对象的Monitor监视器,如果为空会抛出出NPE。
2.String、Integer、Long等封装类不可作为锁对象,String在常量池,可能其他线程也在使用该对象,但获取不到锁。Integer等封装类数值更换一次就会生成新对象,加锁对象就丢失了。

七、优化

1.锁细化:将同步代码尽量减少,降低锁资源竞争时间。
2.锁粗化:过多的小锁会增加CPU的资源切换消耗,不如用一个锁替换大量小锁。

猜你喜欢

转载自blog.csdn.net/weixin_43001336/article/details/106975481