java并发学习-线程的睡眠与唤醒

准备知识:

对象头

java中每一个对象在运行时都会拥有一个对象头,用于存储对象的一些附加信息。普通的对象头部组成(以64为虚拟机为例)如下图。其中Mark Word主要用来存储对象的运行时数据;Klass用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

Object Header(92 bits)
Mark Word(64 bits)Klass Word(32 bits)

 

 

 

这里我们重点关注Mark Word部分,它的组成如下图。这里我们重点关注状态处于Heavyweight Locked的对象。

Mark Word(64 bits)State
unused:(25 bits) | hashcode:(31 bits) | unused:(1 bit) | age:(4 bits)| biased_lock:0 | 01Normal
thread:(54 bits )| epoch:(2 bits)| unused:(1 bit) | age:(4 bits)| biased_lock:1 | 01Biased
ptr_to_lock_record:(62 bits)    | 00Lightweight Locked
ptr_to_heavyweight_monitor:(62 bits) | 10Heavyweight Locked
           | 11Marked by GC

 

 

 

 

 

 

 

Monitor原理:

Monitor是一个对象,被译为监视器或者管程。当使用synchronized关键字给对象加锁后,对象头中的Mark Word会变为Heavyweight Locked状态,ptr_to_heavyweight_monitor指向一个Monitor对象。Monitor对象的结构可以用下面的图片来表示

  • 刚开始 Monitor 中 Owner 为 null,
  • 当 Thread-2 执行 获取到对象的Monitor锁后,就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  • 当Monitor的Owner已经为Thread-2时,如果 Thread-3,Thread-4来获取Monitor锁就会进入EntryList,并且线程的状态变为 Blocked
  • Thread-2 执行完同步代码块的内容,会唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,在执行过程中调用了wait方法就会进入WaitSet进行等待,线程状态变为Wating
  • 处于WaitSet中的线程会在 Owner 线程调用 notify /notifyAll 时,或者wait时间已过时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

线程的睡眠/唤醒:

1.sleep

sleep(long miilis)方法是Thread类的静态方法,在代码中调用Thread.sleep(timeout)方法,会使操作系统将当前线程挂起,调用Thread.sleep(timeout)方法后会产生下面的变化:

  • 线程的状态变为TIMED_WAITING
  • 调用了sleep方法后,timeout耗尽线程才会重新进入可执行状态
  • 如果在syncronized代码块中执行Thread.sleep(timeout),Monitor锁的Owner并不会发生切换,也就是说线程调用了sleep方法并不会释放锁

2.wait

与sleep方法不同,wait是属于Object类的方法,JDK中常用的两种wait方法,即wait()和wait(long timeout)。从Monitor锁的原理我们不难得出以下结论:

  • wait方法必须在synchronized代码块中执行(或者使用了syncronized关键字修饰的方法)
  • 线程调用wait方法后会释放锁,进入Monitor锁对象的WaitSet
  • 线程调用wait方法后会变成Wating状态
  • 调用了wait()方法的线程会一直处于Waiting状态,直到Monitor对象的Owner调用了notify或者notifyAll方法。notify方法会随机唤醒WaitSet中的一个线程,而notifyAll会唤醒WaitSet中的所有线程
  • wait(long timeout)在等待时间耗尽的情况下也会自动唤醒

3.park/unpark

park/unpark的功能有点类似于wait/notify,都是暂停/唤醒线程。先来看一下关于park和unpark的例子:

先调用park,再调用unpark

package com.huawei.java.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static java.lang.Thread.sleep;

@Slf4j(topic = "TestPark")
public class TestPark {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.info("t1 start...");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("t1 park...");
            LockSupport.park();
            log.info("t1 end...");
        }, "t1");
        thread.start();

        sleep(2000);
        log.info("t1 unpark...");
        LockSupport.unpark(thread);
    }
}



// 运行结果
16:25:45.019 [t1] INFO TestPark - t1 start...
16:25:46.022 [t1] INFO TestPark - t1 park...
16:25:47.018 [main] INFO TestPark - t1 unpark...
16:25:47.018 [t1] INFO TestPark - t1 end...

先调用unpark,再调用park

package com.huawei.java.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static java.lang.Thread.sleep;

@Slf4j(topic = "TestPark")
public class TestPark {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.info("t1 start...");
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("t1 park...");
            LockSupport.park();
            log.info("t1 end...");
        }, "t1");
        thread.start();

        sleep(1000);
        log.info("t1 unpark...");
        LockSupport.unpark(thread);
    }
}

// 执行结果
16:24:47.641 [t1] INFO TestPark - t1 start...
16:24:48.639 [main] INFO TestPark - t1 unpark...
16:24:49.643 [t1] INFO TestPark - t1 park...
16:24:49.643 [t1] INFO TestPark - t1 end...

关于用法,park/unpark是LockSupport的静态方法,类似于wait,LockSupport.park()用于暂停当前线程;LockSupport.unpark(Thread t)用于唤醒线程t。

park/unpark的原理:每一个线程对象都会关联一个Parker对象,如下图。counter的取值只能为0或者1,0表示当前对象需要暂停,1表示当前线程可以继续执行,初始值为0。cond为条件变量。

对于先调用park,再调用unpark的场景:

  • 线程t1正常执行,调用LockSupport.park()方法,程序会去检查counter的值
  • counter = 0,cond条件变量阻塞,线程t1挂起
  • 程序设置counter = 0
  • 过了1s后,主线程调用LockSupport.unpark(t1)方法, 将counter值设置为1
  • cond条件变量变为非阻塞,唤醒线程t1,将counter值设置为0

对于先调用unpark,再调用park的场景:

  • 主线程先调用了LockSupport.unpark(t1)方法,将counter值设置为1,此时线程t1处于运行状态,无需进行唤醒
  • 过了1s后,线程t1调用LockSupport.park()方法,发现counter = 1,线程无需进行阻塞,继续运行
  • 程序设置counter = 0

相比于wait&notify:

  • LockSupport.unpark唤醒某一个具体的线程
  • wait&notify必须配合Monitor对象锁使用
  • 可以先调用unpark然后再调用park,但是只能先调用wait再调用notify方法

4.join

join()是Thread类的成员方法,先来看一个简单的例子:

package com.huawei.java.test;

import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "TestJoin")
public class TestJoin {

    private static int num = 1;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                log.info("running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                num++;
            }
        };
        t1.setName("t1");
        t1.start();
        t1.join();
        log.info("num = " + num);
    }
}

输出结果:

13:01:07.599 [t1] INFO TestJoin - running
13:01:08.602 [main] INFO TestJoin - num = 2

Process finished with exit code 0

 上面的代码中,在主线程中调用了调用了线程t1的join方法,主线程就会等待t1执行完成后在往下执行。当然JDK还提供了带参数的join(long timeout),如果主线程调用了t1的join(long timeout)方法,那么主线程会等待t1执行结束或者到达了timeout就会继续往下执行。

关于join方法的原理,我们可以研究一下join方法的源码:


    /**
     * 不带参数的join方法
     */
    public final void join() throws InterruptedException {
        join(0);
    }

    /**
     * 带有超时时间的join方法
     */
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

    /**
     * Tests if this thread is alive. A thread is alive if it has
     * been started and has not yet died.
     *
     * @return  <code>true</code> if this thread is alive;
     *          <code>false</code> otherwise.
     */
    public final native boolean isAlive();

以上面的demo为例,分析一下join的原理:

  • 当线程终止时,会调用线程自身的notifyAll()方法,通知所有等待在该线程对象上的线程
  • 主线程调用了t1线程的join方法,首先获取到t1对象的Monitor锁
  • 如果timeout设置为0或者直接调用join()方法,那么主线程会进入t1线程的Monitor 锁对象的waitSet,t1运行结束调用notifyAll()方法,唤醒主线程
  • 对于设置了timeout > 0的join方法,主线程会进入t1线程的Monitor 锁对象的waitSet,与上一点不同的是,当timeout耗尽时,主线程也会自动唤醒

5.yield

同sleep方法一样,yield方法是Thread的静态方法。调用Thread.yield()会让当前线程从Running状态进入Runnable状态。

 

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值