Bootstrap

JUC学习02 Java线程

线程

        守护线程

用户线程:用户自己创建的线程(新建Thread线程的操作)
     * 守护线程: 运行在后台,比如垃圾回收(GC)
//创建用户线程和守护线程
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());//false表示他是一个用户线程

            while (true){

            }
        },"aa");
        //将aa设置成守护线程,所以main线程结束后,作为守护线程的aa也结束运行,而用户线程的aa则会继续运行
        t1.setDaemon(true);
        t1.start();
        //此时主线程已经结束,但用户线程还在继续运行
        System.out.println(Thread.currentThread().getName()+" over");
    }

        创建线程的三种方式

/**
     * 创建线程的三种方法
     * Thread Runnable Callable
     */
    static class CreateThread{
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            /**
             * 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
             * 用 Runnable 更容易与线程池等高级 API 配合
             * 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
             *
             * FutureTask可以利用Future接口中的run方法返回结果
             */
            //方法1 Thread类创建
            Thread thread = new Thread("t1"){
                //创建了一个Thread的子类对象,并重写父类的run方法。
                @Override
                public void run() {

                    for (int i = 0; i < 100000; i++) {
                        System.out.println(Thread.currentThread().getName()+":这是Thread1");
                    }
                }
            };
            //方法2 使用Runnable接口配合Thread
            Thread thread2 = new Thread(()->{
                //在源码里,Thread会检查参数Runnable对象是否被赋值,如果没有赋值,那就调用Runnable接口实现的run方法
                for (int i = 0; i < 100000; i++) {
                    System.out.println(Thread.currentThread().getName()+":这是Thread2");
                }
            },"t2");
            //方法三,FutureTask 配合 Thread
            FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("running......");
                    TimeUnit.SECONDS.sleep(2);
                    return "执行完成";
                }
            });
            Thread thread3 = new Thread(task,"FTask1");
            //thread3.start();
            //get方法在call方法执行完成前都会是阻塞状态,只有全部的代码执行完后get才能够返回返回值
            //System.out.println(task.get());
        }

线程运行的原理

        栈帧

/**
 * 线程运行的原理
 * 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
 * 机就会为其分配一块栈内存。
 * 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
 * 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
 */
static class TestFrames{
        /**
         * 线程启动后,Jvm就会分配一块栈内存
         * 每执行一个方法,都会产生一个新的栈帧内存
         * method1,2方法执行完后,内存就会被释放
         * @param args
         */
        public static void main(String[] args) {
            method1(10);
        }
        private static void method1(int x){
            int y = x+3;
            Object m = method2();
            System.out.println(m);
        }
        private static Object method2(){
            Object n = new Object();
            return n;
        }
    }

这个是线程创建后的操作

首先Main主线程会被分配一块独立的栈空间。并为main栈帧中的局部变量args在堆中创建一个newString[]空间。程序计数器运行到一个方法,都会为方法的局部变量开辟一块空间,然后确定方法的返回地址。当方法运行完后便会释放掉栈帧的内存空间。

 多线程下

/**
     * 多线程
     */
    static class TestFramesMulti{
        /**
         * 我们在刚才的运行方式新增一个线程t1
         *
         * 从debug中可以看出,每个线程都会被开辟一块独立的虚拟机栈,有自己的栈帧,它们之间互不干扰。
         * @param args
         */
        public static void main(String[] args) {
            Thread thread = new Thread("t1"){
                @Override
                public void run() {
                    method1(20);
                }
            };
            thread.start();
            method1(10);
        }
        private static void method1(int x){
            int y = x+3;
            Object m = method2();
            System.out.println(m);
        }
        private static Object method2(){
            Object n = new Object();
            return n;
        }
    }

 由图中可以看到,创建新线程的t1倍分配了一块区别于main线程的独立的栈空间。

线程的上下文切换

/**
 * 线程的上下文切换
 * 线程会因为一些原因停止执行而执行另一个线程的代码:
 * 1.时间片用完
 * 2.垃圾回收会暂停所有的工作线程,导致上下文切换
 * 3.被高优先级的线程抢占
 * 4.线程自己调用了sleep,yield,wait,join等方法
 * 当上下文切换发生时,操作系统会保存当前运行的状态并恢复另外一个线程的状态。在Java中就是程序计数器。
 * 它的作用时记住下一条JVM指令的执行地址,是线程私有的。
 * 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
 * Context Switch 频繁发生会影响性能
 */

start和run

start是线程的启动方法,而run只是线程启动后执行的方法

如果只是调用run方法,那就相当于执行了一个普通方法

static class TestCurrentMethod{
        public static void main(String[] args) {
            Thread t1 = new Thread("t1") {
                @Override
                public void run() {
                    System.out.println("执行run方法的线程:"+Thread.currentThread().getName());
                    //这里可以证明一下上面的打印其实是main方法打印的
                    try {
                        TimeUnit.SECONDS.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            //如果只是调用run方法,那其实就是调取了一个普通方法
            t1.run();
            /**
             * 执行结果:
             * 执行run方法的线程:main
             * 结束运行
             */
            System.out.println("结束运行");
        }
    }

start不能被调用多次,否则会报IllegalThreadException

sleep和yield

sleep

  • 1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • 2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 3. 睡眠结束后的线程未必会立刻得到执行
  • 4. 建议用 TimeUnit sleep 代替 Thread sleep 来获得更好的可读性

yield

  • 1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  • 2. 具体的实现依赖于操作系统的任务调度器

调用Sleep后进程的状态(对应sleep.1)

public static void TestSleep(){
            Thread t1 = new Thread("t1"){
                @Override
                public void run() {
                    System.out.println("1.t1的状态是:"+Thread.currentThread().getState());
                    try {
                        Thread.sleep(3000);
                        System.out.println("2.t1的状态是:"+Thread.currentThread().getState());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            System.out.println("3.t1的状态是:"+t1.getState());
            t1.start();
            System.out.println("4.t1的状态是:"+t1.getState());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("5.t1的状态是:"+t1.getState());
        }
3.t1的状态是:NEW
4.t1的状态是:RUNNABLE
1.t1的状态是:RUNNABLE
5.t1的状态是:TIMED_WAITING
2.t1的状态是:RUNNABLE

sleep.2

public static void TestInterrupter(){
            Thread t1 = new Thread("t1"){
                @Override
                public void run() {
                    System.out.println("进入睡眠咯");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        System.out.println("起床气了起床气了");
                        e.printStackTrace();
                    }
                }
            };
            t1.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
            System.out.println("现在准备打断t1");
            t1.interrupt();
        }
进入睡眠咯
现在准备打断t1
起床气了起床气了
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at hwh.JUC.Day01.Note01$TestSleepAndYield$2.run(Note01.java:238)

Process finished with exit code 0

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
public static void TestYield(){
            Runnable task1 = () -> {
                int count = 0;
                for (;;) {
                    System.out.println("---->1 " + count++);
                }
            };
            Runnable task2 = () -> {
                int count = 0;
                for (;;) {
                    Thread.yield();
                    System.out.println(" ---->2 " + count++);
                }
            };
            Thread t1 = new Thread(task1, "t1");

            Thread t2 = new Thread(task2, "t2");
             t1.setPriority(Thread.MIN_PRIORITY);
             t2.setPriority(Thread.MAX_PRIORITY);
            t1.start();
            t2.start();
        }

实际执行上,t1打印的数字更高。

join 方法详解

      

为什么需要 join

下面的代码执行,打印 r 是什么?
static int r = 0;
            public static void main (String[]args) throws InterruptedException {
                test1();
            }
            private static void test1 () throws InterruptedException {
                log.debug("开始");
                Thread t1 = new Thread(() -> {
                    log.debug("开始");
                    sleep(1);
                    log.debug("结束");
                    r = 10;
                });
                t1.start();
                log.debug("结果为:{}", r);
                log.debug("结束");
            }

分析

因为主线程和线程 t1 是并行执行的, t1 线程需要 1 秒之后才能算出 r=10
而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
sleep 行不行?为什么?
join ,加在 t1.start() 之后即可

主线程里调用t1的join方法,主线程就不会立刻结束而是等待t1线程结束。所以会打印出r=10

等待多个结果

  • 以调用方角度来讲,如果需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

 调用join后实现了main和t1的线程同步

private static void test2() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                r1 = 10;
            });
            Thread t2 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                r2 = 20;
            });
            long start = System.currentTimeMillis();
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            long end = System.currentTimeMillis();
            System.out.println("r1 = "+r1+"||r2="+r2+"||time-"+(end-start));
        }

在这种复杂的调用里,多个线程通过join方法实现了线程同步

 也可以给join加上参数,使其只等待参数值的时间。

Interrupt方法详解

Interrupt可以用来打断处于阻塞状态的线程(sleep,join,wait)

        private static void testInterrupt(){
            Thread t1 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                r1 = 10;
            });
            t1.start();
            System.out.println("Interrupter start");
            t1.interrupt();
            try {
                //线程处于sleep,wait,join 当线程被打断后,打断标记会被置为false
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("isInterrupted="+t1.isInterrupted());
        }

打断正在运行的线程

        private static void testRunningInterrupt(){
            Thread t1 = new Thread(() -> {
                while (true){
                    //可以利用打断状态来主动停止线程
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("被打断了");
                        break;
                    }

                }
            });
            t1.start();
            System.out.println("Interrupter start");
            t1.interrupt();
            try {
                //线程处于sleep,wait,join 当线程被打断后,打断标记会被置为false
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //如果只是这样打断,isInterrupted的值会是真,但t1线程 不会停止运行
            System.out.println("isInterrupted="+t1.isInterrupted());
        }

两阶段终止模式

/**
 * 两阶段终止模式
 * 在一个多线程案例中,如何让t1线程优雅的停止t2,这个优雅指的是给t2一个料理后事的机会.
 * 
 * 错误思路:利用stop方法停止运行
 *         如果强制杀死线程,如果该线程锁住了共享资源,那么整个线程就没有机会再释放锁。
 */
/**
     * 两阶段终止模式
     * 在一个多线程案例中,如何让t1线程优雅的停止t2,这个优雅指的是给t2一个料理后事的机会.
     *
     * 错误思路:利用stop方法停止运行
     *         如果强制杀死线程,如果该线程锁住了共享资源,那么整个线程就没有机会再释放锁。
     */
    static class Test3{
        public static void main(String[] args) throws InterruptedException {
            TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
            twoPhaseTermination.start();
            Thread.sleep(3500);
            twoPhaseTermination.stop();
        }
        static class TwoPhaseTermination{
            private Thread monitor;
            //启动监控线程
            public void start(){
                monitor = new Thread(()->{
                    while (true){
                        Thread current = Thread.currentThread();
                        if (current.isInterrupted()){
                            System.out.println("料理后事咯");
                            break;
                        }else {
                            try {
                                TimeUnit.SECONDS.sleep(1); //被打断情况1
                                System.out.println("执行监控操作"); //被打断情况2
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                System.out.println("正在Sleep中被打断");
                                current.interrupt();
                            }
                        }
                    }
                });
                monitor.start();
            }
            //停止监控线程
            public void stop(){
                monitor.interrupt();
            }
        }
    }

后续会有voliate关键字来优化这段代码

线程的五种状态(操作系统层面)

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程的六种状态(JavaAPI层面)

 

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
  • 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED WAITING TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束

小结

;