多线程(一)
1、synchronized
synchronized加锁,synchronized 是最常⽤的线程同步⼿段之⼀,CAS是乐观锁的实现,
synchronized就是悲观锁了。
前言Java对象的构成
在 JVM 中,对象在内存中分为三块区域:
-
对象头
Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复⽤⾃⼰的存储空间,也就是说在运⾏期间Mark Word⾥存储的数据会随着锁标志 位的变化⽽变化。Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对 象是哪个类的实例。
-
实例数据
这部分主要是存放类的数据信息,⽗类的信息。 -
对其填充
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字 节对⻬。
Tip:不知道⼤家有没有被问过⼀个空对象占多少个字节?就是8个字节,是因为对⻬填充的关
系哈,不到8个字节对其填充会帮我们⾃动补⻬。
有序性、可见性、原子性,synchronized又是怎么做到的呢?
1)synchronized锁升级过程
jdk1.6 中为了减少获得锁和释放锁带来的性能消耗⽽引⼊的偏向锁和轻量级锁。
针对 synchronized 获取锁的⽅式,JVM 使⽤了锁升级的优化⽅式,就是先使⽤偏向锁优先同⼀线程然
后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂⾃旋,防⽌线程被系统挂起。
最后如果以上都失败就升级为重量级锁。
偏向锁:
锁争夺也就是对象头指向的Monitor对象的争夺,⼀旦有线程持有了这个对象,标志位修改为1,就进⼊偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。
这个过程是采⽤了CAS乐观锁操作的,每次同⼀线程进⼊,虚拟机就不进⾏任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
轻量级锁:
还是跟Mark Work 相关,如果这个对象是⽆锁的,jvm就会在当前线程的栈帧中建⽴⼀个叫锁记录(Lock Record)的空间,⽤来存储锁对象的Mark Word 拷⻉,然后把Lock Record中的owner指向当前对象。
JVM接下来会利⽤CAS尝试把对象原本的Mark Word 更新会Lock Record的指针,成功就说明加锁成功,改变锁标志位,执⾏相关同步操作。
如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻
塞。
⾃旋锁:
Linux系统的⽤户态和内核态的切换很耗资源,其实就是线程的等待唤起过程,那怎么才能减少这种消耗呢?
⾃旋,过来的现在就不断⾃旋,防⽌线程被挂起,⼀旦可以获取资源,就直接尝试成功,直到超出阈值,⾃旋锁的默认⼤⼩是10次,-XX:PreBlockSpin可以修改。
⾃旋都失败了,那就升级为重量级的锁,像1.5的⼀样,等待唤起咯。
详细版:
2)它是如何保证同一时刻只有⼀个线程可以进入临界区呢?
synchronized,代表这个⽅法加锁,相当于不管哪⼀个线程(例如线程A),运⾏到这个⽅法时,都要检
查有没有其它线程B(或者C、 D等)正在⽤这个⽅法(或者该类的其他同步⽅法),有的话要等正在使⽤
synchronized⽅法的线程B(或者C 、D)运⾏完这个⽅法后再运⾏此线程A,没有的话,锁定调⽤者,
然后直接运⾏。
1、synchronized 应⽤在实例⽅法上时,对当前实例对象this加锁
在字节码中是通过⽅法的 ACC_SYNCHRONIZED 标志来实现的。反正其他线程进这个⽅法就看看是否有这个标志位,有就代表有别的线程拥有了他,你就别碰了。
public class Synchronized {
public synchronized void husband(){
}
}
2、修饰静态⽅法,对当前类的Class对象加锁
public class Synchronized {
public void husband(){
synchronized(Synchronized.class){
}
}
}
3、synchronized 应⽤在同步块上时
public class Synchronized {
public void husband(){
synchronized(new test()){
}
}
}
在字节码中是通过 monitorenter 和 monitorexit 实现的。
每个对象都会与⼀个monitor相关联,当某个monitor被拥有之后对象就会被锁住,当线程执⾏到
monitorenter指令时,就会去尝试获得对应的monitor。
步骤如下:
- 每个monitor维护着⼀个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当⼀个线程获得monitor(执⾏monitorenter)后,该计数器⾃增变为 1 。
当同⼀个线程再次获得该monitor的时候,计数器再次⾃增;
当不同线程想要获得该monitor的时候,就会被阻塞。 - 当同⼀个线程释放 monitor(执⾏monitorexit指令)的时候,计数器再⾃减。
当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor。
3)还有其他的同步手段么?
ReentrantLock。
比较方面 | SynChronized | ReentrantLock(实现了 Lock接口) |
---|---|---|
原始构成 | 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 | 它是JDK 1.5之后提供的API层面的互斥锁类 |
实现 | 通过JVM加锁解锁 | api层面的加锁解锁,需要手动释放锁。 |
代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成, |
灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock只能锁住代码块,Lock因为是方法调用,可以跨方法,灵活性更大 |
等待可中断 | 不可中断,除非抛出异常(释放锁方式:1.代码执行完,正常释放锁;2.抛出异常,由JVM退出等待) | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行) |
是否公平锁 | 非公平锁 | 两者都可以,默认非公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁, |
条件Condition | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能(可以指定去唤醒绑定到Condition身上的线程) |
2、公平锁与非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进⼊队列去排队,永远都是队列的第⼀位才
能得到锁。
优点: 所有的线程都能得到资源,不会饿死在队列中。
缺点: 吞吐量会下降很多,队列⾥⾯除了第⼀个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很⼤。
⾮公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进⼊等待队列,如果能获取到,就直接获取到锁。
优点: 可以减少CPU唤醒线程的开销,整体的吞吐效率会⾼点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点: 这样可能导致队列中间的线程⼀直获取不到锁或者⻓时间获取不到锁,导致饿死。
3、ReentrantLock
1)ReentrantLock的底层实现
ReentrantLock 就是基于 AQS 实现的
AQS:也就是队列同步器,这是实现 ReentrantLock 的基础。
AQS 有⼀个 state 标记位,值为1 时表示有线程占⽤,其他线程需要进⼊到同步队列等待,同步队列是
⼀个双向链表。
当获得锁的线程需要等待某个条件时,会进⼊ condition 的等待队列,等待队列可以有多个。
当 condition 条件满⾜时,线程会从等待队列重新进⼊同步队列进⾏获取锁的竞争。
2)ReentrantLock中公平锁,非公平锁的实现
ReentrantLock他本身有⼀个内部类Sync类,他继承了AbstractQueuedSynchronizer,我们在操作锁的⼤部分操作,都是Sync本身去实现的。
Sync呢⼜分别有两个⼦类:FairSync和NofairSync
他们⼦类的名字就可以⻅名知意了,公平和不公平那⼜是怎么在代码层⾯体现的呢?
公平锁:
他加了⼀个hasQueuedPredecessors的判断,那他判断⾥⾯有些什么玩意呢?
⼤概意思也是判断当前的线程是不是位于同步队列的⾸位,是就是返回true,否就返回false。
线A现在想要获得锁,先去判断下state,发现也是0,去看了看队列,⾃⼰居然是第⼀位,果断修改了持有线程为⾃⼰。
线程b过来了,去判断⼀下state,嗯哼?居然是state=1,那cas就失败了呀,所以只能乖乖去排队了。
线程A暖男来了,持有没多久就释放了,改掉了所有的状态就去唤醒线程B了,这个时候线程C进来了,但是他先判断了下state发现是0,以为有戏,然后去看了看队列,发现前⾯有⼈了,作为新时代的良好市⺠,果断排队去了。
线程B得到A的召唤,去判断state了,发现值为0,⾃⼰也是队列的第⼀位,那很⾹呀,可以得到了。
非公平锁:
A线程准备进去获取锁,⾸先判断了⼀下state状态,发现是0,所以可以CAS成功,并且修改了当前持有锁的线程为⾃⼰。
这个时候B线程也过来了,也是⼀上来先去判断了⼀下state状态,发现是1,那就CAS失败了,真晦⽓,只能乖乖去等待队列,等着唤醒了,先去睡⼀觉吧。
A持有久了,也有点腻了,准备释放掉锁,给别的仔⼀个机会,所以改了state状态,抹掉了持有锁线程
的痕迹,准备去叫醒B。
这个时候有个带绿帽⼦的仔C过来了,发现state怎么是0啊,果断CAS修改为1,还修改了当前持有锁的线程为⾃⼰。B线程被A叫醒准备去获取锁,发现state居然是1,CAS就失败了,只能失落的继续回去等待队列。