前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java锁全总结

Java锁全总结

原创
作者头像
花落花相惜
修改2021-11-24 14:13:35
1700
修改2021-11-24 14:13:35
举报
文章被收录于专栏:花落的技术专栏
对象的几个部分的作用:

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字节是为了减少堆内存的碎片空间(不一定准确)。

Java对象的状态主要靠Mark Word来标记,主要有5种,大部分与线程有关。

Mark Word包含了信息如:哈希码(HashCode)、GC分代年龄、锁状态标识、持有锁的线程、偏向线程ID、偏向时间戳等等

sychronized修饰普工方法和静态方法的区别:
代码语言:txt
复制
class SimpleOper {
代码语言:txt
复制
    //修饰普工方法:锁当前类对象
代码语言:txt
复制
    public synchronized void count() {
代码语言:txt
复制
    }
代码语言:txt
复制
    //修饰静态方法:锁当前类的class对象,一个类只有一个class对象
代码语言:txt
复制
    public static synchronized void count() {
代码语言:txt
复制
    }
代码语言:txt
复制
}

synchronized修饰普通方法或代码块获取的是对象锁。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized

的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!

锁有哪些分类:
各个类型的锁:

悲观锁与乐观锁:悲观锁意味着,觉得自已在使用共享资源的时候,其他线程将会修改数据,因此为了避免篡改,将其他要进入临界区的线程阻塞住,直到完成使用。乐观锁与之相反,认为自己在使用共享资源的时候,其他线程不会修改数据,因此,乐观锁只进行锁竞争,而不会将其他线程阻塞住。

独占锁与共享锁:取决于是否允许多个线程同时拿到锁,访问共享资源。如果能预料到接下来的时间不会发生写操作,无疑允许多方访问更能提高并发效率。虽然大家都进入了临界区,但是没有产生竞争条件,毕竟,读操作不会改变任何数据。

公平锁与不公平锁:区分于是否按照申请锁的顺序,为线程分配锁,即是否允许插队。如果允许插队,将可能使一些线程饥饿,迟迟不能分配到锁继续运行下去;如果不允许插队,那么一些需要紧急处理的线程任务,则被延迟处理,毕竟生活中,我们也需要VIP通道。

可重入锁与不可重入锁:区分获取锁的线程,还没释放锁之前,能否重复地获取锁。如果一个递归方法需要上锁,而当这个锁是不可重入锁时,是无法递归的。以生活例子举例,能理解为加塞行为是否允许。当在餐厅进食中,发现需要加菜,餐厅是允许这样的加塞行为,也是合意的。可如果你挂了号看医生,在诊断完后,你对医生说“帮我朋友也看一下呗”,你看医生理不理你,如此,这样的加塞行为是不允许的。

可中断锁与不可中断锁:是否允许在迟迟申请不到锁,或线程发生中断时能进行响应。可中断锁赋予了线程决定自己等待锁的时间,以及对中断的响应。

Lock(ReentrantLock)的底层实现主要是Volatile +

CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。

双重校验锁 为什么要用volatile修饰

因为如果不用volatile修饰的话,在 single=new Single(),会发生指令重排序,

Single的代码可以分解为三个部分

1.分配对象的内存空间

2.初始化对象

3.设置single指向内存空间

在JIT里,可能会将2与3进行重排序,在单线程里这里并不会发生什么问题,但是在多线程情况下,会出现下面的问题:

A2与A3重排序后,会让线程B在B1处判断出single不为null,线程B接下来将访问的single引用的对象是一个未初始化的对象。

所以用volatile修饰 single来就是禁止2与3的重排序,来保证线程安全的延迟初始化。

CAS机制:

见我的另一篇CAS机制

Retrantlock/AQS机制:

见我的另一篇Retrantlock使用

volatile:

volatile关键字——最轻量的同步

关于加锁和解锁的对象:

synchronized代码块 :同步代码块,作用范围是整个代码块,作用对象是调用这个代码块的对象。

synchronized方法 :同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。

synchronized静态方法 :同步静态方法,作用范围是整个静态方法,作用对象是调用这个类的所有对象。

synchronized(this):作用范围是该对象中所有被synchronized标记的变量、方法或代码块,作用对象是对象本身。

synchronized(ClassName.class) :作用范围是静态的方法或者静态变量,作用对象是Class对象。

synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类锁,它们的区别如下:

使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。

线程A访问实例对象的非static synchronized方法时,线程B也可以同时访问实例对象的static

synchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。

对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

类锁:对象锁是用来控制实例方法之间的同步,类锁是来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有个互斥锁,而类的静态方法是需要Class对象。所以所谓类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。类锁和对象锁不是同一个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:一个线程访问静态sychronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,为他们需要的锁是不同的。

wait、notify、notifyAll与锁池、等待池

锁池:某个对象的锁已被线程A拥有,其他线程要执行该对象的 synchronized 方法获取锁时就会进入该对象的锁池,锁池中的线程回去竞争该对象的锁

等待池:某个线程调用了某个对象的 wait 方法,该线程就会释放该对象的锁,进入该对象的等待池,等待池中的线程不会去竞争该对象的锁

调用 notify 会随机唤醒等待池中的一个线程,唤醒后会进入到锁池

调用 notifyAll 会唤醒等待池中的所有线程,唤醒后会都进入到锁池

怎么安全停止一个线程任务?原理是什么?线程池里有类似机制吗?

1、使用violate boolean变量退出标志,使线程正常退出,也就是当run方法完成后线程终止。(推荐)

2、使用interrupt()方法中断线程,但是线程不一定会终止。

3、使用stop方法强行终止线程。不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。

  • synchronized 的原理?

可以看到被synchronized同步的代码块,会在前后分别加上monitorenter和monitorexit,这两个字节码都需要指定加锁和解锁的对象。

每个对象都有Monitor(监视器锁),当monitor被占用的时候,对象进入锁定状态。

monitorenter:如果该线程已经占有了monitor,只是重新进入,则进入数+1。

monitorexit:指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个

monitor 的所有权。

  • 什么是锁升级,降级?

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)。

所谓的锁升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM

监测到不同的竞争状况是,会自动切换到不同的锁实现。这种切换就是锁的升级、降级。

偏向锁

因为经过大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

在程序的一开始,处于无锁状态。紧接着,有一个线程申请锁,此时通过CAS竞争锁(CAS保证了此竞争行为的原子性),获取锁成功,Mark Word

将标记为偏向锁。当同样的线程再次到来,发现是锁的持有者并且是偏向锁,直接进入临界区。

因此,偏向锁意味着,不会发生竞争条件,因为只有一个线程。

轻量级锁

随着程序的运行,有新的线程要进入临界区,通过CAS竞争锁失败。Mark

Word立即将偏向锁标记锁为轻量级锁,因为已经发生了竞争条件。紧接着,会反复同通过CAS为线程获取锁,如果占有锁的线程在临界区待的时间很短,那么申请锁的线程将很快拿到锁。

因此,轻量级锁意味着,有竞争条件,但是大家能很快地被分配到锁。

重量级锁

当然,申请锁的线程并不总是能很快地获取到锁,与其反复地CAS重试而浪费CPU时间,不如直接将线程阻塞住。那么,在轻量级锁的情况下,如果有线程超过一定次数的重试还是获取不到锁,Mark

Word立即将轻量级锁标记为重量级锁,此后所有获取不到锁的线程将被阻塞,需要Monitor的参与。

因此,重量级锁意味着,在有竞争条件的情况下,线程不能很快地被分配到锁。

Synchronized的锁只能膨胀,不能收缩。偏向锁和轻量锁为乐观锁,重量级锁为悲观锁。

Synchronized的好处在于,它的优化、锁申请释放、锁的分配都是自动的,开发者能快速地使用。

轻量级锁什么时候升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 对象的几个部分的作用:
  • sychronized修饰普工方法和静态方法的区别:
  • 锁有哪些分类:
    • 各个类型的锁:
    • 双重校验锁 为什么要用volatile修饰
    • CAS机制:
    • Retrantlock/AQS机制:
    • volatile:
    • 关于加锁和解锁的对象:
    • wait、notify、notifyAll与锁池、等待池
    • 怎么安全停止一个线程任务?原理是什么?线程池里有类似机制吗?
      • 偏向锁
        • 轻量级锁
          • 重量级锁
            • 轻量级锁什么时候升级为重量级锁?
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档