并发编程之二:线程创建方法、运行原理、常见方法(sleep,join,interrupt,park,守护线程等)

线程的创建方法

继承Thread

/** Thread */
    public static void test1 () {
        // 创建线程
        Thread thread = new Thread("邢道荣"){
            @Override
            public void run() {
                // 要执行的任务
                System.out.println("说出吾名,吓汝一跳");
                System.out.println("我乃三国第一上将邢道荣");
            }
        };
        // 启动线程
        thread.start();
    }

实现Runnable

/** 将线程与任务分开,让代码更灵活,传递一个对象*/
    public static void test2() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("现如今连无名小辈都有诺大嗓门了");
            }
        };
        // lambda表达式写法
        Runnable runnable2 = () -> System.out.println("现如今连无名小辈都有诺大嗓门了");

        Thread thread = new Thread(runnable);
        thread.start();
    }

注:在java中Tread的start方法才是开启线程的方法,而run方法只是开启线程后线程所要执行的代码。并且,并不是start已开启就会被运行的,这个要看cpu调度时间是否分给了该线程所在的栈帧。
在源码中,Tread的构造参数可以传递一个Runnable对象,Thread里的init方法会将该runnable赋值给Thread的一个成员变量,然后在Thread的run方法里,会判断该runnable对象是否为空,不为空则执行runnable的run方法
我们来看一下源码
runnable里只有一个方法
在这里插入图片描述

Thread的init部分代码
在这里插入图片描述

Thread的run方法
在这里插入图片描述

FutureTask

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况。
它可以将一个线程的结果返回给另一个线程

public static void main(String[] args) throws ExecutionException, InterruptedException {
        // FutrueTask,它可以将一个线程的运行结果返回给另一个线程
        // 泛型为要返回的结果类型
        // 参数中需要一个Callable
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("我是Future的Callable的call方法");
                Thread.sleep(3000);
                return 9527;
            }
        });

        Thread thread = new Thread(futureTask, "从今天起9527就是你的终身代号");
        thread.start();
        // 注意下面的get()方法与上面的thread.start();是并行执行的,只不过下面的get方法阻塞了,一直等待代码的运行,直到返回结果
        // 才会运行下面的代码
        // 得到返回的数据
        Integer integer = futureTask.get();
        System.out.println(integer);
        System.out.println(thread.getName());
    }

线程的运行原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道JVM中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame) 组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 栈帧以线程为单位,每个线程有自己的栈和栈帧它们是相互独立的

线程运行情况

当运行下图中的TestFrames的时候,首先会进行类加载:就是将该类的字节码加载到java的虚拟机jvm中,加载的位置,就是将该类的字节码放到jvm的方法区。类加载完成以后,jvm就会启动一个叫做main的主线程,并且给主线程分配一个线程的栈内存,然后线程就交给任务调度器去执行,当cpu的时间片分给该主线程的时候,就开始执行该线程的代码了。main方法作为一个入口方法,当进入该方法的时候jvm会给它分配一个栈帧。每个线程的栈都有一个程序计数器,它会记录下一条jvm所要执行的指令。每个线程有局部变量表和返回地址,我们今天主要看它俩。
局部变量表:存储方法的局部变量与参数
在这里插入图片描述

在这里插入图片描述

线程的上下文切换(Thread Context Switch)

线程从使用cpu到不使用cpu叫做线程的上下文切换。

因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码

  • 线程的cpu时间片用完
  • 垃圾回收。
  • 有更高优先级的线程需要运行。
  • 线程自己调用了sleep. yield、 wait. join、 park、 synchronized. lock等方法当Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的。
  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等。
  • Context Switch频繁发生会影响性能。

线程中的常见方法

在这里插入图片描述
![](https://img-blog.csdnimg.cn/20201023113251371.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2phdmFfeHh4eA==,size_16,color_FFFFFF,t_70#pic_center

start与run

在java中Tread的start方法才是开启线程的方法,而run方法只是开启线程后线程所要执行的代码。并且,并不是start已开启就会被运行的,这个要看cpu调度时间是否分给了该线程所在的栈帧。

sleep与yield

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

yield
1.调用yield会让当前线程从Running进入Runnable(就绪状态)状态,然后调度执行其它同优先级的线程。如果这时没
有同优先级的线程,那么不能保证让当前线程暂停的效果
2.具体的实现依赖于操作系统的任务调度器(就是说如果你想把这个线程的cpu时间给让出去,但是这时候没有其它线程,只有这一个线程,那么它可能还是会得到cpu时间继续执行)
yield用法(代码)

public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                int count = 0;
                for (;;) {
                    System.out.println("1-----------------" + count++);
                }
            }
        };

        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                int count = 0;
                for (;;) {
                    Thread.yield();// 线程2让出cpu资源给t1,结果就是t1打印的次数比较多
                    System.out.println("           t2--------------------" + count++);
                }
            }
        };

        Thread thread = new Thread(runnable);
        Thread thread2 = new Thread(runnable2);
        thread.start();
        thread2.start();
    }

sleep与yield的区别

1、任务调度器有可能会把cpu时间分给就绪状态的线程,但是不会分配给阻塞状态的线程。

线程各个状态关系图

在这里插入图片描述
在这里插入图片描述
参考链接:https://www.runoob.com/note/34745

线程优先级

  • 线程优先级会提示(hint) 调度器优先调度该线程,但它仅仅是一 个提示,调度器可以忽略它
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用
  • 不管是yield或者是线程优先级设置,他们仅仅是我们设置,最后的决定权还是在操作系统的任务调度器,它可以完全不按照设置的优先级去执行
public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                int count = 0;
                for (;;) {
                    System.out.println("1-----------------" + count++);
                }
            }
        };

        Runnable runnable2 =() -> {
            int count = 0;
            for (;;) {
                //Thread.yield();// 线程2让出cpu资源给t1,结果就是t1打印的次数比较多
                System.out.println("           t2--------------------" + count++);
            }
        };
        Thread thread = new Thread(runnable);
        Thread thread2 = new Thread(runnable2);
        thread.setPriority(Thread.MIN_PRIORITY);        // 优先级数越大,优先级越高
        thread2.setPriority(Thread.MAX_PRIORITY);       // 直观的结果就是线程2比1运行次数多
        thread.start();
        thread2.start();
    }

join方法(同步等待)

趣味竞猜,看代码,猜答案:下面代码的r输出值为多少?

public static void main(String[] args){
        System.out.println("竹杖芒鞋轻胜马,谁怕?");
        test1();
    }

    public static void test1 () {
        System.out.println("一蓑烟雨任平生");
        Thread thread = new Thread(() -> {
            System.out.println("料峭春风吹酒醒,微冷");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("山头斜照却相迎");
            r = 10;
        }, "t1");
        thread.start();
        System.out.println("回首向来萧瑟处");
        System.out.println(r);
        System.out.println("也无风雨也无晴");
    }

答案为0,
在这里插入图片描述
分析:因为主线程与线程thread是并行执行的,thread需要1s之后才能算出r = 10,而主线程一开始就要打印结果所以只能打印出 r = 0.
但是如果就想在主线程获取r = 10呢?
解决方法1:用sleep?不太行,因为我们不知道线程thread运行完毕到底花费了多少时间
解决方法2:用join,在线程的thread.stare()方法之后即可

join():等待线程运行结束:哪个线程调用join,就等哪个线程运行结束

 public static void main(String[] args) throws InterruptedException {
        System.out.println("竹杖芒鞋轻胜马,谁怕?");
        test1();
    }

    public static void test1 () throws InterruptedException {
        System.out.println("一蓑烟雨任平生");
        Thread thread = new Thread(() -> {
            System.out.println("料峭春风吹酒醒,微冷");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("山头斜照却相迎");
            r = 10;
        }, "t1");
        thread.start();
        thread.join();
        System.out.println("回首向来萧瑟处");
        System.out.println(r);
        System.out.println("也无风雨也无晴");
    }

在这里插入图片描述
由上得出,在主线程先运行thread.start方法,然后再运行的join的时候,陷入等待,当线程thread得到cpu时间,然后运行thread中的代码即:r=10,运行完毕,thread线程终止,于是接着运行主线程中的代码,于是就能在主线程中得到r = 10的结果了。
在这里插入图片描述

join运用场景:线程的同步

以调用方角度来讲

  • 不需要等待结果返回,就能继续运行就是异步
  • 需要等待结果返回就是同步

猜猜下面代码打印的时间是2s还是3s呢

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("t1开始join");
        System.out.println("t2开始join");
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

如上代码共耗时2s多,因为线程t1与线程t2几乎是同时start的,然后两者开始休眠,然后之后再开始join的于是共同花费的时间大约是睡眠时间最长的那个多一点。
在这里插入图片描述
但是如果是在t1start后join后再进将t2进行start的,然后t2进行join的,答案可想而知,是3s多

	    long start = System.currentTimeMillis();
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        System.out.println("t1开始join");
        System.out.println("t2开始join");
        long end = System.currentTimeMillis();
        System.out.println(end - start);

在这里插入图片描述
因为线程1开始start,然后线程1开始join,也就是说在线程1为运行完毕之前是不会执行下面的代码的,是线程同步。于是线程1运行完(休眠1s),此时join方法结束,然后再运行线程2的start方法,然后join于是再休眠2s,最后结果就是3s多了。

有时效的join

join方法里可以传递一个时间参数,单位毫秒,它表示只等待这么长时间,即使过了这么长时间,线程还没有运行完,但是线程依旧会从join中走出来,就是这么人性。
但是如果传入了30s,但是线程在运行了1s就结束了,它也会从join中走出来,就是这么任性,就是这么快。

在这里插入图片描述

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        long start = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join(1000);
        t2.join(1000);
        System.out.println("t1开始join");
        System.out.println("t2开始join");
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

t2虽然睡眠5s,但是join方法在1秒后就结束了,所以总时间是2s
在这里插入图片描述

interrupt(打断阻塞线程、正在运行的线程)

它除了打断sleep,join,wait这些睡眠线程之外,其他的线程也可以 用interrupt去打断。
它可以打断处于阻塞状态的线程、正在运行中的线程。
现象1、:并且打断后会有一个打断标记,表示你这个线程是不是被其他的线程打断过,或者干扰过。这个打断标记是个bool值,如果被打断过bool值就是真true,否则取值就为false假,但是阻塞中的线程被打断后会清空打断标记,重置为false。
现象2:线程被打断后并不会停下来,它只是知道了有其他线程去干扰它打断它,并不会影响线程的正常执行。
用处:打断标记可以用来判断这个线程被打断后是继续运行还是被终止。
1、正在运行的线程可通过打断标记是否为true(打断)来判断,线程是否被打断
2、阻塞中的线程可以通过是否抛出interrupted异常(打断)来判断是否被打断。

  • 处于阻塞状态的线程(这种线程操作系统的调度器不会去考虑它们,就是不会主动的把时间片给他们用),一般我们调用sleep,join,wait这些方法都会使线程进入阻塞状态。

1、打断处于阻塞状态的线程:会清空打断状态

以打断sleep为例子。
打断这些阻塞线程的时候会抛出异常:InterruptedException。在Interrupt在打断阻塞线程的时候会将打断标记清空,就是重置为false。

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("大梦谁先觉,平生我自知");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("草堂春睡足,窗外日迟迟");
            }
        });
        t1.start();
        // 这里开启t1后让主线程睡1s,确保t1进入睡眠,否则还没t1线程还没睡眠就被打断,那样就是打断正在运行的线程
        Thread.sleep(1000);
        System.out.println("张飞烧草堂,打断了梦境");
        t1.interrupt();
        System.out.println("打断标记 :" + t1.isInterrupted());
    }
}

结果,按理说线程被打断后打断标记是为true的,但是像这种sleep,join,wait等等阻塞线程在被打断后会抛出异常,并将打断标记置为空false。这个打断标记可以用来判断这个线程被打断后是继续运行还是被终止。
在这里插入图片描述

由以上结果我们得知在线程睡眠的时间被打断但是线程中睡眠后的“草堂春睡足,窗外日迟迟”还是被打印了出来
:阻塞的线程被打断后虽然抛出了异常,但是并不会停下来,它只是知道了有其他线程去干扰它打断它,并不会影响线程的正常执行。正在运行的线程不会抛异常,更不会被打断。

2、打断正在运行的线程

不会抛出异常,且打断后的标记会被置为真(true)

public class InterruptTest2 {
    public static void main(String[] args) throws InterruptedException{
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               while (true) {
                   boolean interrupted = Thread.currentThread().isInterrupted();
                   if (interrupted) {
                       System.out.println("白袍甘道夫的睡眠被萨鲁曼打断");
                       break;
                   }
               }
           }
       });
       t1.start();
       // 主线程睡1s,这样确保在主线程打断t1后,再在t1中获取中获取打断标记
       Thread.sleep(1000);
       t1.interrupt();
       System.out.println(t1.isInterrupted());
    }
}

在这里插入图片描述
打断标记可以用户让线程知道被打断,有人想让你停下来,然后线程的代码里可以进行一些善后工作。可以停下来,也可以继续执行。

3、两阶段终止模式:一个用到interrupt的多线程设计模式

两阶段终止模式(Two Phase Termination)
在一个线程T1中如何“优雅”终止线程T2?这里的【优雅】指的是给T2一个料理后事的机会。
1.错误思路

  • 使用线程对象的stop()方法停止线程(已被废弃,不推荐使用)
    stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
  • 使用System.exit(int)方法停止线程
    ·目的仅是停止一个线程,但这种做法会让线程所在的整个java程序都停止。

应用场景:比如做一个系统的监控程序,监控cpu的使用率,内存使用情况等,这个时候写一个监控线程,它里面是一个while(true)循环一直在执行。比如以后我不想监控了,我点击一个按钮就可以让它停下来,那么这个时候就可以用到两阶段终止模式。
目的:为了在线程被打断后能够正确的释放资源。
在这里插入图片描述

在这里插入图片描述
两阶段终止模式运行流程分析:系统进入while(true),正常运行,通过打断标记判断有没有被打断(是真还是假)?
被打断(标记为真):料理后事
没被打断(标记为假):这里的睡眠是指,即使是一个监控程序也不可能一直执行,每次间隔2s再次执行。所以在睡眠的时候也有可能被打断。在睡眠的时候(阻塞)被打断是会抛异常的。无异常说明没被打断,然后监控程序,继续执行它的监控任务比如打个cpu使用率啥的,然后进入下次循环。但是有异常抛出说明被打断,这个时候要设置打断标记,然后进入下一次循环,然后再通过打断标记判断是否被打断。
代码:

public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(4000);
        tpt.stop();
    }
}
class TwoPhaseTermination {
    private Thread monitor;

    /** 启动监控线程 */
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread current = Thread.currentThread();
                    if (current.isInterrupted()) {
                        System.out.println("线程被打断,释放线程资源,料理后事");
                        break;
                    }
                    try {
                        // 情况1:睡眠或者阻塞中被打断抛出异常
                        Thread.sleep(1000);
                        // 情况2 :线程正常运行,被打断后打断标记为ture
                        // 到执行while循环的下次的时候打断标记为true,直接判断即可
                        System.out.println("继续执行正常监控记录。。。");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        // 重新设置打断标记,设置为true,那么当进入while的下次循环的时候,
                        // 打断标记为true,刚好进入料理后的逻辑里
                        System.out.println("线程状态" + monitor.getState());
                        current.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }

    /** 停止监控线程*/
    public void stop() {
        monitor.interrupt();
    }
}

在这里插入图片描述
因为如果睡眠中被打断,则打断标记会被重置为false,所以我们重新设置打断标记,上图中可以看到,在睡眠中被打断时获取的线程为runnable(运行状态非阻塞状态)此时再次打断则打断标记就为true了,就能在运行while的下次循环的时候判断了。

java中的线程有六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
在这里插入图片描述
javaAPI
在这里插入图片描述
interrupt不会清楚打断标记(阻塞线程除外),interrupted在返回打断标记之后会将打断标记设置为false.

4、interrupt打断park线程

park线程:LockSupport的方法。
作用:让当前线程停下来。
现象1:正常情况下park后的程序不会被执行,但是被interrupt打断后,会继续执行。但是如果线程被interrupt打断后再次执行park方法,则park方法不起作用。不起作用的原因是因为如果线程打断标记为true,则park方法不会执行,所以我们只要将打断标记设置为false,park方法就会再次执行了,怎么才能让他自动执行呢,有一个方法interrupted,在取出打断标记之后,会把打断标记设置为false,于是park就会再去执行了。
现象2:当线程park后再执行interrupt方法后打断标记为true。

mport java.util.concurrent.locks.LockSupport;
public class ParkTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("park....");
                LockSupport.park();
                System.out.println("unpark...");
                System.out.println("正常情况下,park后的程序不会被执行");
            }
        });
        t1.start();
        Thread.sleep(1000);
        //t1.interrupt();
    }
}

park后的代码不会被执行 见下图
在这里插入图片描述

在执行park方法后被打断之后,park后的代码会被继续执行(park方法就失效了) 见下图
在这里插入图片描述
在执行park方法,然后被interrupt打断后再次执行park方法,则park方法不起作用 见下图
在这里插入图片描述
interrupted方法在取出打断标记之后会将打断标记设置为false,如果线程的打断标记为true则park方法不会执行,所以我们这里将打断标记设置为false,park就会再执行了,即线程就停止了。 见下图

在这里插入图片描述

不推荐使用的方法stop,suspend,resume

下面的方法会破会同步代码块,使线程的锁得不到释放,造成死锁等问题。
stop:可以用两阶段终止模式来代替stop方法。
suspend:用wait方法替代。
resume:notify方法替代。
在这里插入图片描述

主线程与守护线程

定义:默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
使用场景:
1、垃圾回收器线程就是一种守护线程。(我们创建的对象在堆内存中存储,当没有其他对象引用这些对象的时候,这些没有被引用的对象就会定期的被垃圾回收器回收。垃圾回收期线程是守护线程,如果你的程序停止了,那么垃圾回收器线程也会被强制停止)
2、Tomcat中的Acceptor和Poller线程都是守护线程。这两个线程是tomcat用来接受请求和分发请求的的线程。所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求,它们就会自动结束,因为它们是守护线程,当tomcat停止后,除了守护线程其他线程都结束了,那么这些守护线程就会强制停止。
下面这段代码,线程t1会无限循环的执行

public class DaemonTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                    System.out.println("恐怖游轮式无限循环");
                }
            }
        }, "t1");
        // 将t1线程设置为守护线程
        //t1.setDaemon(true);
        t1.start();
        Thread.sleep(1000);
        System.out.println("game over");
    }
}

结果:
在这里插入图片描述

将t1设置为守护线程,当主线程执行完毕,无论守护线程的代码(这里是死循环)是否执行完毕守护线程都会强制将它自己结束。如下代码,在主线程睡眠1s后,守护线程也会自动结束运行。
在这里插入图片描述
个人学习笔记,不喜勿喷,行百里者半九十,加油,坚持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值