当面试中问到关于多线程安全问题时,你还不知道怎么回答嘛?快点进来,我带你多维度深层次来解决这个问题

Posted 小乔不掉发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当面试中问到关于多线程安全问题时,你还不知道怎么回答嘛?快点进来,我带你多维度深层次来解决这个问题相关的知识,希望对你有一定的参考价值。

保证线程安全:

1、synchronized:

synchronized 关键字:线程安全用的,同步关键字

(1)作用:

对象头加锁,同一个对象加锁的线程 同步互斥

(2)语法 / 使用:

(注意:只有对 同一对象 加锁,才会让线程产生同步互斥的作用)
(可重入性:同一个线程可以对同一个对象锁多次申请)

  • 1、同步代码块:( synchronized(某个对象)… )
public class SynchronizedTest 
    public static void increment() 
    
    public static void main(String[] args) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                synchronized (SynchronizedTest.class) 
                    increment();
                
            
        ).start();
    

  • 2、实例同步方法:(synchronized(this))
public class SynchronizedDemo 
    public synchronized void methond() 
    
    public static void main(String[] args)  
        SynchronizedDemo demo = new SynchronizedDemo();
        // 进入方法会锁 demo 指向对象中的锁;出方法会释放 demo 指向的对象中的锁
        demo.method();  
     

  • 3、静态同步方法:(synchronized(当前类.class))
public class SynchronizedDemo  
    public synchronized static void methond() 
    
    public static void main(String[] args)  
    // 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放 SynchronizedDemo.class 指向的对象中的锁
        method();
     

(3)原理 / 底层实现:

1、原理

多个线程间同步互斥(一段代码在任意一个时间点,只有一个线程执行:加锁、释放锁)
( 加锁/释放锁:基于对象

2、底层实现

对象锁(monitor)机制:

  • (1)基于 monitor 对象的监视器:使用 对象头的锁状态 来加锁
  • (2)编译为 字节码指令1个 monitorenter + 2个 monitorexit
    (为什么后面是两个? 多出来的是因为 catch 异常也需要释放对象锁的情况)
  • (3)monitor 存在 计数器 实现 synchronized 的 可重入性:进入+1,退出-1

(4)JVM 对 synchronized 的优化:

JVM 将 synchronized 锁分为4种,级别 从低到高 依次是:无锁、偏向锁、轻量级锁、重量级锁(会根据情况,进行依次升级)

锁只能升级却不能降级,目的是为了提高获得锁和释放锁的效率(基于对象头的锁状态来实现)

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功
  • 偏向锁:同一个对象多次加锁(可重入性)
    偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放
    偏向锁的撤销:需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
    (如果线程处于活动状态,升级为轻量级锁的状态)
  • 轻量级锁:基于 CAS 实现,同一个时间点,经常只有一个线程竞争
    指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能
  • 重量级锁:基于系统的互斥锁 mutex 锁(当有一个线程获得锁后,其他线程阻塞)
    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高

其他优化:

  • 粗优化:是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
public class Test
    private static StringBuffer sb = new StringBuffer();
    public static void main(String[] args) 
        sb.append("a");
        sb.append("b");
        sb.append("c");
   

这里每次调用 stringBuffer.append 方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

  • 锁消除:删除不必要的加锁操作
    根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁
public class Test
    public static void main(String[] args) 
        StringBuffer sb = new StringBuffer();
        sb.append("a").append("b").append("c");
   

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除

2、Lock体系:

(1)为什么会有Lock体系(是为了死锁的解决)

Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了
(SDK:软件开发工具包,开发中是一个工程提供另一个工程接口(JDK 只是他的一个子集))

synchronized 在处理死锁问题时方法非常局限,只能避免资源上锁线性化,除此之外基本别无他法。而下面要介绍的Lock可以有很多种办法来避免死锁的产生:

死锁的产生条件:

  • 1、互斥,共享资源 X 和 Y 只能被一个线程占用
  • 2、占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
  • 3、不可抢占,其他线程不能强行抢占线程 T1 占有的资源
  • 4、循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待

解决死锁: 只要我们破坏其中一个,就可以成功避免死锁的发生

(其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥)

  • 1、对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了
  • 2、对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了
  • 3、对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了

synchronized 无法做到第二点 , 原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源

进一步设计 破坏不可抢占条件:

  • 1、能够 响应中断。给阻塞的线程发送中断信号的时候,能够唤醒它
  • 2、支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误
  • 3、非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回

这三种方法可以全面弥补 synchronized 的问题,体现在 API 上,就是 Lock 接口的三个方法

// 支持中断的 API
void lockInterruptibly() throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

(2)简介:

使用上 : synchronized 是基于对象头加锁来实现。。而 lock 本身就是锁
(语法上都可以相互转变)

原理:Lock 基于 AQS 独占锁实现,多个线程申请锁,基于 CAS 设置线程的同步状态。
成功:获取锁向下执行;失败:线程放下 AQS 队列(阻塞)

(3)AQS

(AbstractQueuedSynchronizer 抽象的队列式的同步器)

同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现

AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法

(4)Reentrantlock:可重入锁,实现Lock

提供了 公平锁 和 非公平锁(两种锁都是基于 AQS 来实现)

  • 公平锁:满足时间上的顺序(队列 FIFO)(排队的方式)
  • 非公平锁:随机性(可能的问题:某个线程运气不好,一直获取不到锁而阻塞)
    但性能更高,无参构造方法默认以非公平锁创建(划拳的方式)

(5)ReentrantReadWriteLock:读写锁

  • readLock(): 返回读锁
  • writeLock():返回写锁

特点:读读并发,写写 / 读写互斥

使用场景:例如:Tomcat多个 http 请求(多个线程),对服务端本地一个文件读写操作,允许读读并发。相比 ReentrantLock 或是 synchronized ,效率更高

以上是关于当面试中问到关于多线程安全问题时,你还不知道怎么回答嘛?快点进来,我带你多维度深层次来解决这个问题的主要内容,如果未能解决你的问题,请参考以下文章

iOS线程锁中你还不知道的内容

码农说面试当面试中遇到redis客户端...

这年头还有问Tomcat调优和JVM参数优化的,你还不知道怎么回答么?那么你一定需要看看这篇文章

关于MindFusion.Diagramming for WinForms这些问题,别说你还不知道!

怎么理解分布式高并发多线程?

都0202年了,你还不知道javascript有几种继承方式?