Java面试题系列——JavaSE面试题(线程六)

1、yield() 与 join() 方法的区别?

yield()方法yield()让当前线程状态改为可运行状态,和其它线程一起竞争资源。join()方法:join()方法执行当前线程,并使得其他线程处于阻塞状态join(long millis)方法执行当前线程millis后,再和其它线程一起竞争资源。

2、什么是Volatile,它的作用是什么?

        具体想弄明白volatile的作用,还需要了解下java的内存模型。在Java语言编写的程序中,有时为了提高程序的运行效率,编译器会自动对其进行优化,把经常被访问的变量缓存起来,程序在读取这个变量时有可能会直接从缓存(例如寄存器)中来读取这个值,而不是去内存中读取。这样做的一个好处是提高了程序的运行效率,但当遇到多线程编程时,变量的值可能因为别的线程而改变了,而缓存中的值不会改变,从而造成应用程序读取的值和实际的值不一致。

        volatile是一个类型修饰符,它用来修饰被不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到它时都是直接从对应的内存当中提取,而不会利用缓存。在使用了volatile修饰成员变量后,所有线程在任何时候所看到的变量都是相同的。

        使用示例:

public  class MyThread implements Runnable{
    private  volatile Boolean flag;
    public void stop(){
        flag = false;
    }
    public void run(){
        while (flag);// do something
    }

          以上代码示例是用来停止线程最常用的一种方法,如果 boolean 类型的变量 flag 没有被声
明为 volatile,那么,当这个线程的 nun 方法在判断 flag 值时,使用的有可能是缓存中的值,此
时就不能及时地获取其他线程对 flag 所做的操作,因此会导致线程不能及时地停止。

        注意事项:volatile不能保证操作的原子性,因此一般情况下volatile不能代替synchronized。此外,使用volatile会阻止编译器对代码的优化(禁止指令重排序),因此会降低程序的执行效率。所以除非迫不得已,不建议使用volatile。

3、什么是AtomicInteger?

        AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

4、JDK1.8 针对synchronized,都做过哪些优化?

synchronized 核心优化方案主要包含以下 4 个:

(1)锁膨胀

我们先来回顾一下锁膨胀对 synchronized 性能的影响,所谓的锁膨胀是指 synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。

 synchronized 锁常用的四种形态及其优缺点:

         (1)原因是大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

        偏向锁的升级:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,以后线程1再次获取锁时比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么查看Java对象头中记录线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

        (2)轻量级锁:考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

        轻量级锁什么时候升级为重量级锁?线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

        (

(2)锁消除

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append () 方法,或 Vector 的 add () 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

以上代码经过编译之后的字节码如下:

 从上述结果可以看出,之前我们写的线程安全的加锁的 StringBuffer 对象,在生成字节码之后就被替换成了不加锁不安全的 StringBuilder 对象了,原因是 StringBuffer 的变量属于一个局部变量,并且不会从该方法中逃逸出去,所以此时我们就可以使用锁消除(不加锁)来加速程序的运行。

(3)锁粗化

锁粗化是指,将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁

我只听说锁 “细化” 可以提高程序的执行效率,也就是将锁的范围尽可能缩小,这样在锁竞争时,等待获取锁的线程才能更早的获取锁,从而提高程序的运行效率,但锁粗化是如何提高性能的呢?

没错,锁细化的观点在大多数情况下都是成立了,但是一系列连续加锁和解锁的操作,也会导致不必要的性能开销,从而影响程序的执行效率,比如这段代码:

public String method() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10; i++) {
        // 伪代码:加锁操作
        sb.append("i:" + i);
        // 伪代码:解锁操作
    }
    return sb.toString();
}

 这里我们不考虑编译器优化的情况,如果在 for 循环中定义锁,那么锁的范围很小,但每次 for 循环都需要进行加锁和释放锁的操作,性能是很低的;但如果我们直接在 for 循环的外层加一把锁,那么对于同一个对象操作这段代码的性能就会提高很多,如下伪代码所示:

public String method() {
    StringBuilder sb = new StringBuilder();
    // 伪代码:加锁操作
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    // 伪代码:解锁操作
    return sb.toString();
}

锁粗化的作用:如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。

(4)自适应自旋锁

自旋锁是指通过自身循环,尝试获取锁的一种方式,伪代码实现如下:

// 尝试获取锁
while(!isLock()){
    
}

自旋锁优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。然而对于 synchronized 关键字来说,它的自旋锁更加的 “智能”,synchronized 中的自旋锁是自适应自旋锁,这就好比之前一直开的手动挡的三轮车,而经过了 JDK 1.6 的优化之后,我们的这部 “车”,一下子变成自动挡的兰博基尼了。

自适应自旋锁是指,线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。


5、java中常见的锁

(1)乐观锁和悲观锁

        乐观锁就是认为其他线程在对数据进行访问时不会修改数据,所以其实并没有对数据进行加锁,因为大家都不会修改数据也就不存在数据的同步问题了;只是在需要更新数据的时候去判断有没有其他线程更新了这个数据,如果没有更新那么就写入数据,如果数据被其他线程修改,则加锁/更新失败。

        悲观锁则认为对数据进行访问时,其他线程一定会修改数据,因此在访问数据的时候就会先加锁,确保了在访问的数据整个过程中其他线程都不会对数据进行修改。

        总结:乐观锁更适合多读的场景,因为读操作没有加锁;悲观锁更适合多写的场景,独占数据的读写权限,确保数据的读取和更新都是准确的;

(2)偏向锁、轻量锁、重量锁

        这三种锁指的是synchronized的三种锁状态,在了解这三个锁之前。我们先来了解几个概念:“Java对象头”,“Monitor”。

        我们都了解Java对象是在堆内存中存放的,一个Java对象除了其成员属性要占用空间之外,还有一个“对象头”信息也要占用堆内存的空间,对象头主要包括两部分:

        Mark Word:主要存储锁信息(锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)、HashCode、GC分代信息。

锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。

biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。

分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。

对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。

偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。

epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。

ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

        Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

        Monitor:每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。不加 synchronized 的对象不会关联Monitor。

管程的组成:

        偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价(偏向某个线程)。当初次执行到synchronized代码块儿时,锁对象通过CAS操作修改Mark Word锁标志位变为偏向锁,执行完同步代码块儿后线程不会主动释放偏向锁,当第二次执行synchronized代码块儿时线程会判断对比锁对象头内的ThreadId是否为当前线程,如果是那么无需加锁可以直接执行。偏向锁只有遇到其他线程尝试竞争偏向锁时,才会释放锁,线程是不会主动释放锁。

        使用原因:在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

        轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁。锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋操作,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

        重量级锁:当轻量锁自旋超过一定次数(默认循环10此,参数可更改),则会升级为重量级锁,通过CAS修改Mard word锁标记位00→10,再次自旋获取锁时发现已经变为了重量级锁,则会将自己挂起休眠。

整体的锁状态升级流程如下:

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

(3)可重入锁(递归锁)和不可重入锁

不可重入锁指当同一个线程对在已加锁范围内代码中再次进行加锁操作,由于第二次加锁时需要等待上次锁释放才可以加锁造成锁的互相等待,也就是常说的死锁。

public class LockTest {

    public synchronized void doSomething(){
        System.out.println("我要进屋拿身份证");
        doOther();
    }

    public synchronized void doOther(){
        System.out.println("没身份证不准进屋");
    }
}

可重入锁见明知意,指当前线程在一个方法外获取锁之后又在内层方法尝试获取锁,不会因为之前的锁未释放而阻塞,synchronized和ReentrantLock都是可重入锁。

(4)读写锁

ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

(5)公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,同样会依赖AQS队列,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

ReentrantLock内公平锁和非公平锁源码:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }








 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

对比代码可知,公平和非公平锁方法唯一区别在于公平锁再获取同步状态多了一个if判断:hasQueuedPredecessors()。

查看hasQueuedPredecessors()方法。

 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

该方法主要作用时判断当前线程是否在AQS队列中的首位,如果时返回true,如果不是返回false,从而实现“公平竞争”特性,而且非公平锁则直接争夺,所以存在后申请却先得锁的情况。

6、什么是守护线程,如何做到?

线程分为两种,用户线程和守护线程。用户线程就是我们使用普通方法创建的线程。其实守护线程和用户线程区别不大,可以理解为特殊的用户线程。特殊就特殊在如果程序中所有的用户线程都退出了,那么所有的守护线程就都会被杀死,很好理解,没有被守护的对象了,也不需要守护线程了。一下便是启动守护线程最常用的方法:

Thread daemon = new Thread(()
  -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

7、线程是如何开始的?

(1)java线程创建

(1.1 )start()

有一些初学者在学习线程的时候会比较疑惑,启动一个线程为什么是调用 start()方法,而不是 run 方法,在此做一个简单的分析,我们先简单看一下 start()方法的定义 :

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * start方法将导致this thread开始执行。由JVM调用this thread的run方法
     *
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     *
     * 结果是 调用start方法的当前线程 和 执行run方法的另一个线程 同时运行。
     *
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *多次启动线程永远不合法。 特别是,线程一旦完成执行就不会重新启动
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void `start()` {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *对于由VM创建/设置的main方法线程或“system”组线程,不会调用此方法。未来添加到此方法的任何新功能可能也必须添加到VM中
         *
         * A zero status value corresponds to state "NEW".
         *status=0 代表是 status 是 "NEW"。
         */
        //1、判断线程的转态
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        //2、通知组该线程即将启动,以便将其添加到线程组的列表中;并且减少线程组的未启动线程数递减
        group.add(this);

        boolean started = false;
        try {
            //3、调用native方法,底层开启异步线程,并调用run方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
//                忽略异常。 如果start0抛出一个Throwable,它将被传递给调用堆栈
            }
        }
    }
    //native方法,JVM创建并启动线程,并调用run方法
    private native void start0();

 通过代码需要注意一下几点:

1)start方法用synchronized修饰,为同步方法;
2)虽然为同步方法,但不能避免多次调用问题,用threadStatus来记录线程状态,如果线程被多次    start会抛出异常;threadStatus的状态由JVM控制;
3)使用Runnable时,主线程无法捕获子线程中的异常状态。线程的异常,应在线程内部解决。


(1.2 )start0()

 接着,我们从源码看到调用 start 方法实际上是调用一个 native 方法start0() 来启动一个线程,首先 start0() 这个方法是在Thread 的静态块中来注册的,代码如下 :

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

register Natives 的本地方法的定义在文件 Thread.c,它定义了各个操作系统平台要用的关于线程的公共数据和操作,以下是 Thread.c 的全部内容。链接地址:jdk8/jdk8/jdk: 00cd9dc3c2b5 src/share/native/java/lang/Thread.c

static JNINativeMethod methods[] = {
    #线程启动调用start0()
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};

#undef THD
#undef OBJ
#undef STE

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

从 这 段代码可以看出 , start0() 实 际 会 执 行 JVM_StartThread 方法,这个方法是干吗的呢?从名字上来看,似乎是在 JVM 层面去启动一个线程,如果真的是这样,那么在 JVM 层面,一定会调用 Java 中定义的 run 方法。那接下来继续去找找答案。


(2)jvm创建线程

先找到 jvm.cpp 这个文件,这个文件可以在 hotspot 的源码中找到,如果愿意深究源码的,可以参考背景知识中,Hotspot源码下载地址,进行源码下载。我们接着看下 jvm.cpp中JVM_StartThread方法的定义。

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
......省略........ 
 //1、创建本地线程
 native_thread = new JavaThread(&thread_entry, sz);
......省略........
  //2、启动线程
  Thread::start(native_thread);

JVM_END

JVM_ENTRY 是用来定义JVM_Start Thread函数的,在这个函数里面创建了一个真正和平台有关的本地线程。创建本地线程调用的是native_thread = new JavaThread(&thread_entry, sz);,这里的参数thread_entry我们也来看一下:

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  JavaCalls::call_virtual(&result,
                          obj,
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          vmSymbols::run_method_name(),//**重点关注**
                          vmSymbols::void_method_signature(),
                          THREAD);
}

其实就是通过回调方法调用Java的线程中定义的run方法,此处是个宏定义,在vmSymbols.hpp文件中可以找到如下代码:

#define VM_SYMBOLS_DO(template, do_alias)   
template(run_method_name,"run")   //回调的是run方法

这里的接着看new 出来的JavaThread是什么?

(2.2 )JavaThread

从thread.cpp中找到JavaThread的定义:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  //创建线程    
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
}

此方法有两个参数:(1)entry_point:表示函数名称,线程创建成功之后会根据这个函数名称调用对应的函数,也就是run()方法;(2)stack_sz:表示当前进程内已经有的线程数量。

下面,我们重点关注与一下 os::create_thread这个方法,它实际就是调用平台创建线程的方法,它会根据不同的操作系统去创建线程。
(2.3)os::create_thread

上一步的时候,我们说过了,它会根据不同的操作系统调用不同的创建线程的方式,本文就以Linux操作系统为例,打开os_linux.cpp可以找到os::create_thread方法

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
  assert(thread->osthread() == NULL, "caller responsible");

  // Allocate the OSThread object
  OSThread* osthread = new OSThread(NULL, NULL);

 ........................

    pthread_t tid;
    //java_start方法重点看
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
}

(2.4) java_start

接着看下·java_start方法:

static void *java_start(Thread *thread) {
  。。。。。。。。。。。。。。
  // handshaking with parent thread
  {
    MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);

    // notify parent thread
     //1、设置初始化状态
    osthread->set_state(INITIALIZED);
      //2、唤醒所有线程
    sync->notify_all();

    // wait until os::start_thread()
    //3、不停的查看线程的当前状态是不是Initialized, 如果是的话,调用了sync->wait()的方法等待。
    while (osthread->get_state() == INITIALIZED) {
      sync->wait(Mutex::_no_safepoint_check_flag);
    }
  }

  // call one more level start routine
    //4、被唤醒后执行run方法
  thread->run();

  return 0;
}

此方法主要包含几个流程:1、jvm先设置了当前线程的状态是Initialized;2、用notify_all()唤醒所有的线程;3、查看当前线程是不是Initialized状态,如果是的话,调用sync->wait()的方法进行等待;4、thread->run()要等到被唤醒才能执行。

(3)启动线程

(3.1)Thread::start

根据2.1节中JVM_StartThread方法定义的,在线程创建之后,就会执行Thread::start(native_thread),用来启动线程,启动线程会调用 Thread.cpp 文件中的Thread::start(Thread* thread)方法。如下:

void Thread::start(Thread* thread) {
  trace("start", thread);
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
      // Initialize the thread state to RUNNABLE before starting this thread.
      // Can not set it after the thread started because we do not know the
      // exact thread state at that time. It could be in MONITOR_WAIT or
      // in SLEEPING or some other state.
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
  //根据不同的操作系统进行线程的启动
    os::start_thread(thread);
  }
}

start 方法中会 先判断是否为Java线程,如果是java线程会将线程的状态设置为RUNNABLE,接着调用os::start_thread(thread),调用平台启动线程的方法。

(3.2)os::start_thread

在os.cpp中,可以找到os::start_thread方法,

void os::start_thread(Thread* thread) {
  // guard suspend/resume
  MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE);
  pd_start_thread(thread);
}

该方法设置了线程的状态为RUNNABLE,但没有notify线程,然后又调用了os_linux.cpp中的pd_start_thread(thread)

void os::pd_start_thread(Thread* thread) {
  OSThread * osthread = thread->osthread();
  assert(osthread->get_state() != INITIALIZED, "just checking");
  Monitor* sync_with_child = osthread->startThread_lock();
  MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);
  sync_with_child->notify();
}

此时notify了线程,因为这时候的线程的状态是RUNNABLE, 线程被唤醒后,2.4小节中的Java_start方法继续往下执行,于是调用了thread->run()的方法。

(4)线程回调

(4.1) JavaThread::run()

接着来看一下 Thread.cpp 文件中的 JavaThread::run()方法 。

// The first routine called by a new Java thread
void JavaThread::run() {
  // initialize thread-local alloc buffer related fields
  this->initialize_tlab();

。。。。。。。。。。。。

  // We call another function to do the rest so we are sure that the stack addresses used
  // from there will be lower than the stack base just computed
  thread_main_inner();

}

这个方法中主要是做一系列的初始化操作,最后调用了thread_main_inner方法:

void JavaThread::thread_main_inner() {
  assert(JavaThread::current() == this, "sanity check");
  assert(this->threadObj() != NULL, "just checking");
 .............
     //
    this->entry_point()(this, this);
  }

  DTRACE_THREAD_PROBE(stop, this);

  this->exit(false);
  delete this;
}

方法中调用this->entry_point()(this, this), 在2.2小节中说过entry_point 是一个函数名,线程创建成功后会调用这个函数,这个函数就是在2.1节中定义的run()方法。

#define VM_SYMBOLS_DO(template, do_alias)   
template(run_method_name,"run")   //回调的是run方法

这也就是为什么在线程启动后,会回调线程里复写的run()方法。

(5)小结

线程创建流程图:

 以上通过Hotspot源码对线程的创建和启动做了详细的讲解,我们可以清楚的了解到了线程创建和启动的流程和原理,下面对以上过程做一个简单的总结:(1)使用new Thread()创建一个线程,然后调用start()方法进行java层面的线程启动;(2)调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;(3)调用new JavaThread(&thread_entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_thread方法进行线程创建;(4)新创建的线程状态为Initialized,调用了sync->wait()的方法进行等待,等到被唤醒才继续执行thread->run();;(5)调用Thread::start(native_thread);方法进行线程启动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式;(6)线程启动之后状态设置为RUNNABLE, 并唤醒第4步中等待的线程,接着执行thread->run()的方法;(7)JavaThread::run()方法会回调第1步new Thread中复写的run()方法。到此,整个线程的创建和启动流程就完成了。

持续更新中,敬请期待!

参考文章:

yield与join的区别_静-修的博客-CSDN博客_yield和join

volatile有什么作用?_zhao_miao的博客-CSDN博客_volatile 有什么作

https://blog.csdn.net/zhao_miao/article/details/90752209volatile有什么作用_煎饼灬果子的博客-CSDN博客_修饰符volatile的作用是https://blog.csdn.net/zhao_miao/article/details/90752209

 AtomicInteger是什么_仰望月亮一刻钟的博客-CSDN博客_atomicinteger是什么

AtomicInteger简介_golden_lion的博客-CSDN博客_atomicinteger是什么

synchronized 中的 4 个优化,你知道几个? - Java中文社群_老王的个人空间 - OSCHINA - 中文开源技术交流社区

jdk1.8对synchronized锁的优化_常敲代码手不生的博客-CSDN博客_jdk对synchronized的优化

 Java中的锁 - 知乎

Java中常见的几种锁-Java基础-PHP中文网

Java中常见的锁_Youngk-01的博客-CSDN博客_java常见的锁

 终于搞懂线程的启动流程了_henry_2016的博客-CSDN博客_线程唤醒后从哪里开始执行

Mark Word 详解_duanmy0687的博客-CSDN博客_markword

百度安全验证

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小海海不怕困难

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值