Q:synchronized关键字的底层原理?
synchronized
概念
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有 【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。
案例
class BankAccount {
private int balance = 0;
// synchronized方法来保证线程安全
public synchronized void deposit(int amount) {
synchronized (this) {
balance += amount;
System.out.println(Thread.currentThread().getName() + " deposited " + amount + ", balance is now " + balance);
}
}
public int getBalance() {
return balance;
}
}
class DepositTask implements Runnable {
private BankAccount account;
private int amount;
public DepositTask(BankAccount account, int amount) {
this.account = account;
this.amount = amount;
}
@Override
public void run() {
account.deposit(amount);
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 创建多个线程同时存款
Thread t1 = new Thread(new DepositTask(account, 100), "Thread-1");
Thread t2 = new Thread(new DepositTask(account, 200), "Thread-2");
Thread t3 = new Thread(new DepositTask(account, 300), "Thread-3");
// 启动线程
t1.start();
t2.start();
t3.start();
// 等待所有线程完成
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 最后输出账户余额
System.out.println("Final balance: " + account.getBalance());
}
}
Monitor
概念
- Synchronized由monitor实现的,monitor是jvm级别的对象( C++实现)。
- 线程获得锁需要使用对象(锁)关联monitor,在monitor内部有三个属性,分别是owner、entrylist、waitset。其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程。
具体流程
- 代码进入synchorized代码块,先获取对象关联的monitor,然后判断 Owner是否有线程持有 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功。
- 如果有线程持有,则让当前线程进入entryList进行阻塞。
- 如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)。
- 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待。
Q:synchronized关键字的底层原理-进阶?
- Monitor实现的锁属于重量级锁,因为涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
对象在JVM中的内存结构(以什么形式存储的)
MarkWord
轻量级锁
加锁过程:
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程:
- 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
- 如果Lock Record的Mark Word不为null,则利用CAS指令将对象头的mark word 恢复成为无锁状态。
- 如果失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后不用重新 CAS。以后只要不发生竞争, 这个对象就归该线程所有。
加锁过程:
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作, 性能相对轻量级锁更好一些。
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
三种锁实现机制的区别
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次 修改都是CAS操作,保证原子性。
偏向锁:如果一段很长的时间内都只被一个线程使用锁,就可以使用偏向锁,在第一 次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断 mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。
Q: JMM(Java 内存模型)是什么?
JMM(Java Memory Model)是Java内存模型,是java虚拟机规范中所定义的一种内存模型。
特点:
- 所有的共享变量都存储于主内存中,这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程存储自己的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
Q:CAS 是什么?
概述
CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想, 在无锁情况下保证线程操作共享数据的原子性。
在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作,(AQS框架)、 AtomicXXX类。
实现机制
对于一个线程,目前持有:当前内存值V、旧的预期值A、即将更新的值B。当且仅当旧的预期值A 和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回 false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
底层实现
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。
乐观锁与悲观锁
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量, 就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改 共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
Q:请谈谈你对 volatile 的理解?
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那 么就具备了两层语义:
保证线程间的可见性
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。
- 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
- 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上
Q:什么是AQS?
概述
AQS全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架, 它是构建锁或者其他同步组件的基础框架。
AQS与Synchronized的区别:
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
工作机制
线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功。 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持 有锁,它们都会到FIFO队列中进行等待, FIFO是一个双向队列,head属性表示头结点,tail表示尾结点。
AQS是公平锁吗,还是非公平锁?
- 新的线程与队列中的线程共同来抢资源,是非公平锁。
- 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁。
Q:ReentrantLock的实现原理?
基于CAS+AQS实现,默认是非公平锁,同时也可以构造公平锁。
相比于synchronized锁有以下特点:
可中断、可以设置超时时间、可以设置公平锁、支持多个条件变量、与synchronized一样,都支持重入
Q:synchronized和Lock有什么区别 ?
语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现。
- Lock 是接口,源码由 jdk 提供,用 java 语言实现。
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时, 需要手动调用unlock 方法释放锁。
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量。
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock。
性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖。
- 在竞争激烈时,Lock 的实现通常会提供更好的性能。
Q:死锁产生的条件是什么?
Q:如何进行死锁诊断?
当程序出现了死锁现象,可以使用jdk自带的工具:jps和 jstack,进行检测。
Q:ConcurrentHashMap?
采用 CAS + Synchronized来保证并发安全进行
- CAS控制数组节点的添加。
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不 会产生并发的问题 , 效率得到提升。
Q:导致并发程序出现问题的根本原因是什么?
Java并发编程三大特性:原子性 可见性 有序性。