二、多线程详解

作者: 一直想上树的猪 | 来源:发表于2019-09-18 16:47 被阅读0次

    一、线程状态图

    线程状态图

    二、详解

    1.可运行状态

    包括:就绪态运行中两种状态。
    虽然调用了start()方法,这个线程看起来开始运行了,但是不一定会运行,要看cpu有没有给你这个线程分配时间片。如果分配了,当前就是运行中状态;如果没有分配时间片,则处于一个就绪中的状态,只不过是在随时等待着cpu分配时间片。
    查看某个线程的状态

    在jdk文件夹下的-->bin目录下,运行:jps -v命令可以看到当前机器下运行的java程序,接着用jsack [程序号]可以看到指定的程序中所有的线程的状态。

    2.LockSupport

    其中的一个park()是jdk底层的一个专门阻塞线程的方法。

    3.等待状态和等待超时状态

    • 区别在于调用wait方法或者join方法的时候有没有带时间数,如果带了时间数或者调用sleep方法,则进入等待超时状态。如果没有特定地去唤醒这个线程的话,这个线程超过指定的时间就会自动回到可运行状态。
    • 而处于等待状态的线程,必须要专门的唤醒notify()或者notifiAll()方法,才能进入可运行状态。

    4.阻塞

    • 当我们用synchronized关键字时,线程会进入一个阻塞状态。表现上和等待状态很像,只不过它是等待某个内置锁而触发的。当有多个线程共同运行时。
    • 实际开发中我们会用synchronized关键字对程序进行加锁,当有多个线程同时竞争这把锁的时候,在任意时刻,有且仅有一个线程能获取这把锁,其他的线程此时处于一种阻塞状态。当等待锁的线程获取到锁时,重新进入可运行状态。

    5.Thread实现了Runnable接口

    三、run()方法和start()方法

    底层
    start方法在同一线程中只能调用一次!!!
    why?

    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.
             *
             * A zero status value corresponds to state "NEW".
             */
            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. */
            group.add(this);
    
            boolean started = false;
            try {
                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 */
                }
            }
        }
    
    • 可以看到,调用start方法时,会先判断threadStatus 是否为0,如果不为0,则会抛出一个IllegalThreadStateException异常。
    • 同一个线程只能调用start()方法一次,多次调用会抛出java.lang.IllegalThreadStateException。启动一个线程,需要调用start()方法而不是run()方法。此时,当前线程会被添加到线程组中,进入就绪状态,等待线程调度器的调用,若获取到了资源,则能进入运行状态,run()方法只是线程体,即线程执行的内容,若没调用start()方法,run()方法只是一个普通的方法。


      threadStatus

    1.真正意义上的线程

     ThreadRun threadRun = new ThreadRun();
    

    这个new方法只是说我们在jdk的虚拟机中new出了一个Thread类的实例,还没有和操作系统中的线程真正挂钩,只有执行了start方法才真正启动了一个线程。这个start方法不能重复地调用。当线程获取到时间片之后,才会真正执行run方法。run方法只是一个业务方法,它可以被重复地执行,也可以被单独地调用。
    什么意思呢?看如下代码

    run和start
    可以看到run方法本身只是一个实现业务逻辑的地方,真正和线程挂钩的方法还是start方法。

    四、yield()方法

    yield()用的很少,是一个原生方法,只是当前线程让出cpu的执行权,告诉操作系统,CPU我不用了。

     /**
         * A hint to the scheduler that the current thread is willing to yield
         * its current use of a processor. The scheduler is free to ignore this
         * hint.
         *
         * <p> Yield is a heuristic attempt to improve relative progression
         * between threads that would otherwise over-utilise a CPU. Its use
         * should be combined with detailed profiling and benchmarking to
         * ensure that it actually has the desired effect.
         *
         * <p> It is rarely appropriate to use this method. It may be useful
         * for debugging or testing purposes, where it may help to reproduce
         * bugs due to race conditions. It may also be useful when designing
         * concurrency control constructs such as the ones in the
         * {@link java.util.concurrent.locks} package.
         */
        public static native void yield();
    

    ConcurrentHashMap

     /**
         * Initializes table, using the size recorded in sizeCtl.
         */
        private final Node<K,V>[] initTable() {
            Node<K,V>[] tab; int sc;
            while ((tab = table) == null || tab.length == 0) {
                if ((sc = sizeCtl) < 0)
                    Thread.yield(); // lost initialization race; just spin
                else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if ((tab = table) == null || tab.length == 0) {
                            int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = tab = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                    break;
                }
            }
            return tab;
        }
    

    可以看到ConcurrentHashMap用到了yield()方法,在1.8的时候可以允许多个线程对这个结构初始化的,但是在初始化的时候只允许一个线程进行初始化。因此在初始化的时候需要其他的线程让出CPU的时间片交给其他需要执行初始化的线程去执行。
    yield()方法将线程从运行中的状态转化为就绪状态。

    五、join()方法

    join()的作用就是将指定的线程加入到当前线程,将两个交替执行的线程合并为顺序执行。

    • Join方法,当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。
    1.代码实现
    package com.tinner.thread;
    
    import javax.sound.midi.Soundbank;
    
    /**
     * @Author Tinner
     * @create 2019/9/18 16:31
     */
    public class TestJoin {
        static class Goddess implements Runnable{
    
            private Thread thread;
    
            public Goddess(Thread thread) {
                this.thread = thread;
            }
    
            public Goddess() {
            }
    
            @Override
            public void run() {
                System.out.println("Goddess 开始排队打饭");
                try {
                    if (thread != null){
                        thread.join();
                    }
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + "Goddess 打饭完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        static class GoddessBoyFriend implements Runnable{
    
            @Override
            public void run() {
                System.out.println("GoddessBoyFriend 开始排队打饭");
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + " GoddessBoyFriend 打饭完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread lison = Thread.currentThread();
            GoddessBoyFriend goddessBoyFriend = new GoddessBoyFriend();
            Thread gbf = new Thread(goddessBoyFriend);
            Goddess goddess = new Goddess(gbf);
            Thread g = new Thread(goddess);
    
            g.start();
            gbf.start();
            System.out.println("lison开始排队打饭......");
            g.join();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + "lison打饭完成");
        }
    }
    

    以上代码是这样一个故事:lison有一个女神,女神有一个男神,有一天一起去食堂打饭,lison看到了自己女神,于是让女神排在了自己前面,而女神看到了自己的男神,于是又让自己的男神排到了自己前面,最后,男神先打完饭,女神次之,lison反倒成了最后一个打饭的,这就是join的用法


    join例子

    总结

    join方法是将代码执行一半的时候强行插入另一个线程,只有当另一个线程执行完毕之后才执行当前线程后续的代码,让本来交替执行的线程变成顺序执行的状态。

    六、线程优先级

    如果要调整线程的优先级,在每一个线程new出来之后,这个thread有一个setPriority()方法,可以设置一个1~10的数字,如果不指定,默认是5,数字越高优先级越高。设置线程的优先级可以告诉OS我这个线程的优先级比较高,给我多分配点时间片,但是对于频繁操作流的线程,默认的os分配的优先级比较低,对于那种偏于计算之类的线程默认的优先级比较高。线程的优先级在java中基本上可以忽略不计,因为设置了优先级之后,真正映射到os上之后基本没啥用,设置线程的优先级只是一个安慰操作,对于不同的平台和不同的版本内部调度时间片的方式,都不一样。

    七、守护线程

    当我们在代码中new出一个线程之后,称之为用户线程,用户线程和main线程的级别是一样的。守护线程主要做辅助工作,如:后台的调度、资源的回收等操作,Java中典型的:Object中的finalize()方法就是一个守护线程。java程序中只要有一个用户线程存活,整个java程序是不会退出的,但是对于守护线程而,只要java程序中没有用户线程了,哪怕有一万个守护线程,java程序也会退出。
    如何把当前线程设置为守护线程?
    线程类中有一个方法:setDaemon(true),可以将一个线程设置为守护线程

    Thread g = new Thread(goddess);
    g.setDaemon(true);
    

    注意
    如果run方法中有finally代码块,如果当前线程是守护线程,则finally代码块中的代码不一定会执行。因为当程序中所有的用户线程执行完毕之后,JVM会强制地将所有的守护线程关闭,不管守护线程执行到什么情况,这种情况很有可能代码执行一半,CPU时间片不会给你分配,可能就不会让你执行finally从句。一般情况下不在守护线程的finally代码块中执行业务代码。这就是为什么只在守护线程中做一些后台调度或者内存清理的工作。

    image.png

    相关文章

      网友评论

        本文标题:二、多线程详解

        本文链接:https://www.haomeiwen.com/subject/pmhkuctx.html