当面试中问到关于多线程安全问题时,你还不知道怎么回答嘛?快点进来,我带你多维度深层次来解决这个问题
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 ,效率更高
以上是关于当面试中问到关于多线程安全问题时,你还不知道怎么回答嘛?快点进来,我带你多维度深层次来解决这个问题的主要内容,如果未能解决你的问题,请参考以下文章
这年头还有问Tomcat调优和JVM参数优化的,你还不知道怎么回答么?那么你一定需要看看这篇文章