Java并发编程(4)
锁1、synchronized用过吗?怎么用?
synchronized是常用来保证代码的原子性的。
//1.修饰实例方法
// 有两个对象obj1和obj2,线程A调用Object.test(),线程B调用obj2.test(),不会互斥
// 但A和B如果都调用obj1.test(),会互斥<br>//场景:一个银行账户对象,不同线程操作同一个账户时要排队
public synchronized void test(){
}
//2.修饰静态方法
//线程A调用obj1.test(),线程B调用obj2.test(),仍然会互斥,因为锁是类锁,不是实例锁<br>//场景:当”类的所有实例共享资源“需要保护时,如修改全局配置、写日志文件、统计类的静态计数器
public static synchronized void test(){
}
//3.修饰代码块<br>//场景:只要锁住”关键区域代码“而不是整个方法。如转账时,只锁定两个账户,避免锁范围太大影响性能
public void test() {
synchronized(this){ //不同实例对象可以同时
//synchronized(Example.class),不管有多少个对象实例,同时只能有一个线程进入
//临界区代码
}
}
[*]实例方法锁:给一间“卧室”上锁(对象自己的房门),别人想进要拿这间房的钥匙。
[*]静态方法锁:给“整栋楼的大门”上锁(类锁),不管哪间房都进不去,大家都排队。
[*]代码块锁:只给“卧室里的保险柜”上锁(对象里的某个资源),你可以自由走动,但柜子只能一个人开。
2、synchronized的实现原理
[*] 怎么加锁?
[*]修饰代码块:JVM采用monitorenter、monitorexit两个指令,一个开始一个结束
[*]修饰方法:JVM在方法表里打上一个标志位ACC_SYNCHRONIZED。在执行时,JVM会检查:如果这个方法有这个标志,调用它时就会自动获取对应对象的Monitor锁-->执行完自动释放锁,不需要显式写lock/unLock。
2. synchronized锁住的是什么?
每个对象都有一个隐含的锁机制(Monitor),java虚拟机里用ObjectMonitor实现。当线程执行synchronized时,本质就是在尝试获取对象对应的ObjectMonitor。
在ObjectMonitor里,有几个核心变量:
[*]_owner:指向当前持有锁的线程
[*]_count:重入次数(一个线程重复进入同一锁时+1)
[*]_EntryList:保存所有正在“争抢锁”的线程
[*]_WaitSet:保存调用了wait()方法的线程(等待被唤醒)
工作机制:
[*]加锁:
[*]线程尝试获取Monitor
[*]如果没人持有,设置_owner = 当前线程,执行成功。
[*]若有人持有,就把自己假如_EntryList,进入阻塞状态
[*]解锁:
[*]当前线程执行完同步代码块,_count -1
[*]若_count == 0,说明完全释放锁,把_owner置空
[*]然后从_EntryList里挑一个线程来获取锁
[*]等待/唤醒
[*]wait():当前持锁线程把自己放入_WatiSet,同时释放锁(_Owner置空)
[*]notify():随机从_WaitSet里唤醒一个线程,他会回到_EntryList,等待重新竞争锁
3、除了原子性,synchronized可见性、有序性、可重入性怎么实现?
1. 可见性(加锁更新完后其他线程可见)
[*]线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
[*]加锁后,其他现场无法获取哦主内存中的共享变量
[*]线程解锁前,必须把共享变量的最新值刷新到主内存中。
2. 有序性(最终结果一致)
[*]synchronized同步的代码块,具有排他性,一次只能被一个线程拥有,所以能保证同一时刻,代码时单线程执行的。
[*]as-if-serial:单线程的程序保证最终结果是有序的,但不保证不会指令重排
[*]所以synchronized保证的有序是执行结果的有序,而不是指令重排的有序性。
3. 可重入性
[*]意思:允许一个线程二次请求自己持有对象锁的临界资源。
[*]synchronized锁对象的时候会有个计数器,记录下同一线程获取锁的次数,在执行完对应的代码块之后,计数器会-1,直到计数器清零,就释放锁了,别的线程才有机会获取。
4、锁升级?synchronized优化了解吗
在HotSpot里,每个对象都有一个对象头,其中有一块区域叫Mark Word,里面会存储对象的一些运行时数据(如对象的哈希码、GC分代年龄、锁标志位(2bit)、是否偏向锁标志(1bit)、锁的指针)。JMV就靠着64bit来标记对象当前的锁状态。
① 无锁:默认情况下,对象是无锁状态。
② 偏向锁:若一个对象总是被同一个线程获取锁,JVM就会偏向它,把Mark Word里直接记录这个线程ID。下次这个线程再来,就不用做CAS了,直接认定它已经持有锁-->提高性能
偏向锁获取:
1)检查对象是否可偏向(看偏向锁标志位是否为01、偏向标志)
2)检查是不是自己(看线程ID是否等于自己的-->等于则直接执行代码,不用CAS)
3)不是自己,就尝试CAS:若成功则拿到锁,把线程ID改为自己;若失败,则说明有竞争。
4)竞争失败:JVM在安全点停下来,把偏向锁升级为轻量级锁,然后按照轻量级锁的逻辑来竞争
5)执行同步代码。
偏向锁的释放:(与普通锁不同,不会主动释放,只有其他线程来竞争的时候,才会撤销)
1)有人来抢-->检查原来的持有线程是否还在用
[*]不用了:撤销偏向锁(回到无锁)
[*]还在用:升级为轻量级锁
2)太频繁就批量优化(批量重偏向/批量撤销)
③ 轻量级锁:当另一个线程也来竞争时,偏向锁就失效了,JVM会升级为轻量级锁。JVM在当前线程的栈帧里创建一个叫Lock Record的结果,会保存【该对象当前的Mark Word副本,线程自己“占有锁”标记】。然后JVM会尝试用CAS把对象头里的Mark Word改为指向这个Lock Record指针。若成功了,说明这个线程抢到了锁。(不再存放哈希码;只涉及用户态,性能高)
④ 重量级锁:多个线程同时竞争,CAS不断失败,会把对象头里的Mark Word改成指向一个Monitor对象的指针,里面有Owner、EntryList和Waitset,此时抢不到锁的线程会先进入“自旋”(默认10次),若达到等待次数后还未获取到锁会被挂起(阻塞),等锁释放后再唤醒。
[*]【注意】这个过程时不可逆的。
synchronized做了哪些优化?
[*]JDK1.6之前的:直接调用ObjectMonitor.enter()和ObjectMonitor.exit(),这种就是重量级锁,一旦线程竞争,就会发生操作系统层面的阻塞/唤醒,性能差
[*]JDK6后引入优化策略:
[*]偏向锁:不做CAS,直接改偏向锁标记
[*]轻量级锁:每个线程在自己的栈里创建一个Lock Record,用CAS操作尝试获取锁。
[*]自旋锁:轻量级锁升级到重量级时,若发现锁被占用,先别急着阻塞,先在CPU上循环“自旋“一段时间(避免不必要的操作系统上下文切换)
[*]锁粗化:连续出现多次synchronized,JVM将连续加锁/解锁合并成一个大范围的锁
[*]锁消除:JIT编译器发现加锁没有必要(数据没有共享),直接把锁去掉。
5、synchronized和ReentrantLock的区别
[*] 锁的实现:
[*]synchronized是Java语言的关键字,基于JVM实现(JVM编译时会自动管理)。
[*]ReentrantLock是基于JDK的API层面(要自己写代码)实现的(lock和Unlock方法配合try/finnaly语句块来完成)
[*]性能:
[*]在JDK1.6锁优化之前,synchronized的性能比ReentrantLock差很多。但JDK6开始优化后,性能就差不多了。
[*]功能特点:
[*]synchronized中若一个线程在等待获取锁,只能一直等下去,不能被中断。ReentrantLock提供lockInterruptibly()方法,若线程在等待锁的过程中被interrupt,它会立刻响应中断,放弃等待。
[*]synchronized永远是非公平锁(JVM不保证等待时间最长的线程一定先拿到锁)。ReentrantLock可以在构造时指定非公平锁还是公平锁。
[*]synchronized配合wait()、notify()、notifyAll()来实现线程间通信。这些方法是Object的方法,比较原始。ReentrantLock提供Condition对象。这个对象可以创建多个Condition队列,精确控制哪个线程被唤醒,但synchronized的wait/notify只有一个等待队列,无法细分。
[*]synchronized的锁的获取和释放是JVM自动管理(进入同步块时加锁,退出时自动释放)。ReentrantLock必须手动lock和unclok,虽然灵活但容易因为忘记unlock出现死锁。
6、AQS了解多少
(1)AQS(AbstractQueueSynchronizer),是JDK并发包(java.util.concurrent)里的一个抽象类。是一个通用的同步器框架,帮你处理”线程竞争资源-->排队等待-->成功后唤醒的逻辑。
(2)核心组件
[*]同步状态:private volatile int state,state就是一份资源计数。如ReentrantLock:0表示没被占用,1表示已加锁。修改state用CAS来保证原子性,volatile保证可见性。
[*]等待队列:若线程抢锁失败,会被封装成一个Node节点,放进队列。这个队列是FIFO双向链表。队列里的线程会挂起(park()),等前驱节点释放锁时被唤醒(unpark())
[*]独占 / 共享模式:独占(一个线程持有锁如ReentrantLock);共享(多个线程能同时获取资源(比如Semaphore允许N个线程同时进入)
(3)工作流程
[*]尝试获取锁:调用acquire()-->内部会调用tryAcquire()。若state修改成功,当前线程获取锁。若失败,入队等待。
[*]等待/挂起:队列里的线程会挂起,节省CPU
[*]释放锁:调用release-->内部调用tryRelease(),成功释放后,会唤醒队列中的下一个线程。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]