java多线程进阶同步锁

Posted 烟锁迷城

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程进阶同步锁相关的知识,希望对你有一定的参考价值。

目录

1、原子性问题

2、锁

2.1、实例锁

2.2、类锁

2.3、代码块

 3、锁的存储

4、锁的类型

4.1、乐观锁与悲观锁

4.1.1、乐观锁

4.1.2、悲观锁

4.2、偏向锁

4.3、轻量级锁

4.4、重量级锁

5、synchronized的优化


1、原子性问题

一千个线程循环计数,最终结果总是小于等于1000的随机数。.

public class Count 

    public static int count = 0;

    public static void incr() 
        try 
            Thread.sleep(1);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        count++;
    

    public static void main(String[] args) throws InterruptedException 
        for (int i = 0; i < 1000; i++) 
            new Thread(() -> Count.incr()).start();
        
        Thread.sleep(2000);
        System.out.println("结果:" + count);
    

2、锁

在java中,加锁需要使用synchronized关键字,锁的本质就是对于共享资源访问的一个限制,它让同一时间内只有一个线程能访问这个共享资源,以此确保多线程并发的原子性操作,因此对于synchronized而言,加锁是有作用范围的,范围就是共享资源的使用范围。

2.1、实例锁

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁,只针对于当前对象实例有效。

public class SynchronizedDemo 

    synchronized void method1() 
        
    

    void method2() 
        synchronized (this) 

        
    

2.2、类锁

静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,针对所有对象都互斥,因为静态方法是唯一的,所以在静态方法上加锁也是类锁。

public class SynchronizedDemo 

    synchronized static void method3() 

    

    void method4() 
        synchronized (SynchronizedDemo.class) 

        
    

2.3、代码块

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁,如单例模式给HashMap加锁。

public class SynchronizedDemo 
    Object object = new Object();
    
    void method5() 
        synchronized (object) 
            
        
    

 3、锁的存储

synchronize关键字是对某一个对象进行加锁,那么肯定会在某个位置上具有标记,线程可以根据标记判断是否加锁,那么可以确定的就是,标记应该在加锁对象上。

在java中对某一个对象加锁之后,在这个对象的JVM层面的存储结构中,对象头保存有关于锁的信息。

下表是32位存储的内容,64位与32位的存储是几乎没有差别的,可以看到在表中,除了无锁状态之外,还有三种锁,那么一个synchronized具有三种锁类型吗?

4、锁的类型

当两个线程抢占资源的时候,会经历三种类型的锁:偏向锁,轻量级锁,重量级锁,它们是同一把锁的三种状态。
一定要意识到,锁本身就意味着额外的性能消耗,因此为了提升性能,最好的办法是无锁,因此java对synchronized做出了一些优化,三种锁类型就是优化结果。

4.1、乐观锁与悲观锁

4.1.1、乐观锁

乐观锁的预期是乐观的,它默认不会有人修改数据,但是它又无法避免真的可能会有人修改数据,所以它将会比较预期数据和原始数据是否一致,如果一致就修改,不一致就修改失败,下文中CAS就是乐观锁的思想,意为比较并替换。

乐观锁会出现ABA的情况,即数据A被改为B,又被改为A,乐观锁无法发现数据被修改,为解决此问题,可以使用版本迭代的方式来检测是否被篡改过数据,在进行修改时版本上升,在进行比较时同时比较数据和版本

CAS在许多的实现下,其操作必须是原子性的,因为其使用在多线程情况下,进行的本质是两个步骤,比较并替换,这个步骤必须被合成一个原子性操作以保证线程安全。在JAVA实现中,常采用四个参数,object,offset,A,B。offset是偏移量,和Object组合获得内存中的lock_flag,A是预期值,B是要更新的数值,以此做到原子操作。

在CAS上,依旧会有lock,这个lock并非JAVA的锁,而是类似总线锁的操作,以确保多CPU下的安全,此与可见性问题有关。

4.1.2、悲观锁

悲观锁的预期是悲观的,它默认会有人修改数据,所以它会先加锁,然后修改数据。

4.2、偏向锁

偏向锁属于乐观锁,会在加锁但没有产生竞争的情况下使用,进行一次CAS。

偏向锁的存储内容包括线程ID,在单线程的情况下,调用锁资源的线程ID将会被存储在偏向锁中。当一个新的线程开始获取资源时,偏向锁会把新线程ID与保存的线程ID进行对比,如果一致就可以直接获得,不一致将导致该线程的锁膨胀,成为轻量级锁

偏向锁默认关闭,加锁基本就是为了进行多线程竞争处理,所以默认就是会有多个线程,每次比较都会造成性能的消耗,所以默认关闭。

4.3、轻量级锁

轻量级锁属于乐观锁,会在共享资源被抢占的时候使用,进行多次CAS,因为线程调用时间很短,所以相比于阻塞等待,再次进行尝试资源获取的方式会消耗更少的时间。当线程发现锁已经被抢占,就会在间隔一段时间后再次尝试获取,这个过程就叫自旋,自旋的次数是有限制的,通常是10次,以此避免对CPU资源的过多消耗。如果10次之后依然无法获取到资源,将导致该线程的锁膨胀,成为重量级锁。

在1.6之后,优化出自适应自旋,可以自行调整自旋次数

4.4、重量级锁

重量级锁属于悲观锁,非公平锁(允许插队),会在自旋全部失败的时候启用,线程进入阻塞状态。每一个java对象都有一个monitor,每一个线程都有一个监视器Monitor Record,当线程想要获取一个加锁资源时就必须获取到它的monitor,然后将所有权据为己有,直到线程运行完毕才会释放所有权,唤醒被阻塞的线程抢占该资源。准备抢占锁的线程会进入同步队列,没有抢占到资源的线程将进入阻塞状态,随后从同步队列进入到等待队列(wait),等待资源释放后被唤醒(notify),重新进入同步队列,准备进行抢占。

5、synchronized的优化

jdk1.6中对锁的实现引入了大量的优化

  • 锁粗化:将紧紧连接在一起的lock指令合成一个
  • 锁消除:清除掉没有竞争资源的锁
  • 增加轻量级锁和自旋
  • 自适应自旋:从原本的10次自旋尝试变成了自适应,即上次等待时间长的将会缩短时间,上次时间短的可以放宽时间。

以上是关于java多线程进阶同步锁的主要内容,如果未能解决你的问题,请参考以下文章

java多线程知识总结

Java多线程核心技术演进ConcurrentHashMap—Java进阶

java多线程进阶同步锁

java---1续

Python使多线程中的每个锁都成为可能

java同步锁的正确使用