synchronized
1、作用
1)作用于方法时,锁住的是对象的实例(this);
2)当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
3)作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
2、原理
4、对象头
Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。
Mark Word 平时存储这个对象的 哈希码 、分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、 重量级锁指针 、 线程 ID 等内容。
对象头上的标记位替换,类似于火车硬卧换票,等解锁(下车)时,再把栈帧中的hash码等还原回来。
5、偏向锁–轻量级锁–重量级锁(锁膨胀)
详细参考
1)场景:
a、偏向锁:
偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁
意义:通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
b、轻量级锁
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
1)线程利用object对象加锁,内部创建锁记录(Lock Record)对象,record对象地址和markword交换,并将对象引用指向锁对象
2)注意是交替运行,如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有
竞争)
3)进行锁膨胀,将轻量级锁变为重量级锁。
创建Monitor,并将markword指向它。
将自己放入EntryList,将线程0设为Owner。
4)线程0退出时按照重量级锁流程解锁,唤醒阻塞线程
C、重量级锁
依赖于操作系统Mutex Lock所实现阻塞。
2)比喻
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
小南要使用房间保证计算不被其它人干扰(原子性),最初用的是防盗锁,当上下文切换时,锁住门。是安全的。
每次上锁太麻烦了,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。于是,小南干脆在门上刻上了自己的名字,下次来用房间时,只要名字还在,那么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦掉,升级为挂书包的方式。
ReentrantLock
1 特点
可重入:同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
可中断
可以设置超时时间
可以设置为公平锁
支持多个条件变量
2 条件变量
synchronized 是那些不满足条件的线程都在一间休息室等消息
ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
3 应用
互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
4原理
NonfairSync 继承自 AQS;
锁重入时state++
条件变量实现原理:每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
synchronized和lock有什么区别?
1. 原始构成
-
synchronized时关键字属于jvm
monitorenter,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步或方法中才能掉wait/notify等方法
monitorexit
-
Lock是具体类,是api层面的锁(java.util.)
2. 使用方法
- sychronized不需要用户取手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
- ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成
3. 等待是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成
- ReentrantLock可中断,设置超时方法tryLock(long timeout, TimeUnit unit),或者lockInterruptibly()放代码块中,调用interrupt()方法可中断。
4. 加锁是否公平
- synchronized非公平锁
- ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
5. 锁绑定多个条件Condition
- synchronized没有
- ReentrantLock用来实现分组唤醒需要要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
6、代码示例
package com.jian8.juc.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized和lock区别
* <p===lock可绑定多个条件===
* 对线程之间按顺序调用,实现A>B>C三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 紧接着
* AA打印5次,BB打印10次,CC打印15次
* 。。。。
* 来十轮
*/
public class SyncAndReentrantLockDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareData.print5();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareData.print10();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
shareData.print15();
}
}, "C").start();
}
}
class ShareData {
private int number = 1;//A:1 B:2 C:3
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
//判断
while (number != 1) {
condition1.await();
}
//干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
//判断
while (number != 2) {
condition2.await();
}
//干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
//判断
while (number != 3) {
condition3.await();
}
//干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//通知
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
锁优化
1. 减少上锁时间
同步代码块中尽量短
2. 减少锁的粒度
将一个锁拆分为多个锁提高并发度,例如:
1)LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
2)concurrenthashmap 分段锁,Segment 数组长度为 16不可扩容
3. 锁粗化
多次循环进入同步块不如同步块内多次循环 另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)
4. 锁消除
,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。
5. 读写分离
CopyOnWriteArrayList ConyOnWriteSet
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁