照顾没有多线程基础的朋友,补一下线程的基本知识,jdk1.8以前不支持lambada表达式写法,举例多线程原始写法
@Slf4j(topic = "a")
public class Test {
static Lock customerLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new myThread());
thread.start();
}
}
@Slf4j(topic = "a")
class myThread implements Runnable{
@Override
public void run() {
log.debug("这是一个自定义线程任务");
}
}
synchronized(lock)//锁的实际上是对象,这个lock一定要全局变量,不能是局部变量。
如果一定要用局部变量,编译也能通过。当lock是个局部变量时,jvm会开启锁的优化(锁消除),那么锁局部变量相当于无锁,没用意义。 sync主要优化手段 锁消除和锁粗化
什么是对象?对象的组成部分?
对象就是堆中的一块内存,如图
synchronized 给对象加锁其实是改变了对象头中的某些值,那么对象头是什么?由什么组成呢?
hotspot openjdk文档中明确指明对象头的组成部分
- Consists of two words 对象头由两个字组成
- Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code
包含了堆中对象基本信息、类型、GC状态、锁状态、身份散列码 对象类型 --- 通过kclass pointer当中的地址指向类的首地址对象头 = markword(8字节) + klass pointer(4字节) jdk1.8默认开启指针压缩因而klass pointer是4字节,没用开启指针压缩是 8 字节,只规定了2部分组成没有明确规定字的大小
klass pointer 存的是一个地址---------指向当前对象的类在元空间首地址markword 不是固定的结构,对象初始化时时无锁状态
-XX:BiasedLockingStartupDelay=0 关闭延迟开启偏向锁
-XX:-UseBiasedLocking 禁止偏向锁
-XX:+UseBiasedLocking 启用偏向锁
无锁状态下25位未使用;31位存储hashcode;ud1代表未使用;age存储的是对象年龄,因此对象年龄最大为2<<4=15
bl1标识偏向锁标识;lock锁状态标识只有两位最多标识4种状态 由于无锁分为可偏向不可偏向 4位无法表示,因而bl专门用于表示偏向锁是否偏向。
因特尔操作系统采用的是小端存储方式,如图
小端存储方式是低位在前,高位在后
锁不可偏向的三种情况
- jvm关闭了偏向锁,或者延迟了偏向(jvm默认,因为偏向有缺点)
- 对象被计算了hash code 没有地方存储线程信息
- 对象膨胀之后(如升级为轻量锁),膨胀过程不可逆,极端情况下可逆 偏向锁首先看锁标志位是否为 01 如果是 01 说明是偏向锁。看对象头是否存储了线程ID,如果存储了线程ID说明已偏向
开发过程中尽量不要使用new Thread 的方式创建线程任务,尽量使用线程池。当线程t1 与线程t2 通过join方式顺序执行,t2的线程ID可能与t1相同, 由于线程ID相同 那么 t2执行时它依然是偏向锁而不会膨胀升级为轻量锁。
总结: 当一个对象锁第一次被线程持有时是偏向锁;如果持有线程来加锁还是偏向锁,如果其他线程来加锁但是时顺序执行没有竞争则膨胀为轻量锁。当多个线程竞争加锁不是顺序执行时膨胀升级为重量锁
wait 方法的原理
Object提供了wait方法,该方法要求调用的Object是被sync加锁的对象,否则抛出异常。(没有加锁的状态对象头无法关联ObjectMonitor)
当一个锁对象调用了wait方法会升级为重量锁
wait方法和sleep方法的区别(二者都会让当前线程阻塞)
wait方法会释放锁,适用于做线程之间的交互。sleep不会释放锁。
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
boolean hasGirl = false;
Thread t1 = new Thread(()->{
log.debug("加班");
synchronized (obj){
if(!hasGirl){
log.debug("coding ...");
}else {
log.debug("no girl ...");
try {
//线程睡眠时由于sleep并不会释放锁,下面的循环代码不会执行
//将这里修改为 marx.wait(),当前线程释放锁其他代码可以继续执行,当前线程阻塞直至被唤醒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"marx");
marx.start();
TimeUnit.SECONDS.sleep(1);
for (int i = 5;i<5; i++){
new Thread(()->{
synchronized (obj){
log.debug("coding -------");
}
},"t"+i).start();
}
TimeUnit.SECONDS.sleep(4);
}
上面的代码案例中,即使最后marx线程被其他线程唤醒 依然不会进行coding工作 因为 else没有执行结束。 所以 wait一般使用场景是与 while一起 而不是 if else;将if else代码修改为
while (!hasGirl){
try {
log.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("marx 被唤醒 coding....");
}
wait方法释放锁后可以通过 notify唤醒线程。但如果同时有多个线程wait时,调用notify方法是随机唤醒一个。
可以调用notifyAll将同时wait的所有线程一起唤醒。但是,无法实现精确唤醒;那么可能会导致其他业务逻辑非正确执行。
sync关键字当中 wait 和 notify|notifyAll 无法实现精确唤醒满足条件部分线程。
需要注意的是,一旦调用wait方法就会膨胀为重量锁,wait调用后会关联指向 ObjectMonitor(62),并将当前线程对象放入ObjectMonitor当中的 WaitSet 队列。
ReentrantLock实现精确唤醒代码案例(await + asignal || asignalAll)
static boolean isw = false;
static boolean ism = false;
static ReentrantLock lock = new ReentrantLock();
static Condition conditionw = lock.newCondition();//解决线程释放锁无法精确唤醒的问题
static Condition conditionm = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread marx = new Thread(()->{
lock.lock();
while (!isw){
log.debug("条件未满足 wait");
}
try {
conditionw.await();//ReentrantLock允许在当前线程创建一个Condition
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("marx醒来了 开始工作");
lock.unlock();
},"marx");
marx.start();
Thread rose = new Thread(()->{
lock.lock();
while (!ism){
log.debug("没有钱 wait");
}
try {
conditionm.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("rose醒来了 开始工作");
lock.unlock();
},"rose");
rose.start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.lock();
log.debug("发加班费了");
ism = true;
conditionm.signalAll();//唤醒指定线程 conditionm 条件的所有线程 signal()只唤醒头部线程
lock.unlock();
},"boss").start();
}
ReentrantLock使用尾插法维护了一个entrySet阻塞队列,可以保证线程的启动顺序、执行顺序一致。
Synchronized 使用的是头插法维护了一个entryList阻塞队列,因此线程启动顺序与执行顺序相反。