01 Java线程的创建以及线程的基本方法

2 进程与线程

进程:可以视为程序的一个实例,有的程序可以开启多个实例,比如浏览器。有的只能一个实例,比如网易云音乐。

线程:进程包含多个线程,是一个指令流,是JAVA中最小的调度与资源分配单位,在windows中进程是不活动的,只是线程的容器。

资源占用,与通信方法二个角度进行对比,对比进程之间,线程之间,进程与线程之间。

2-1 应用

异步调用

定义:从调用方式来说,调用分为同步调用与异步调用。区别在于是否让主程序等待。

多线程提供程序运行效率

2-2 思考

1.单核情况下,多线程的本质是让不同线程轮流占用cpu,避免一个线程总是占用CPU,其他程序无法干活。

2.多核情况下,多线程能够提高程序运行效率要具体情况具体分析,考虑2点:

  • 计算任务是否能够拆分(阿姆达尔定律)

3.IO操作不占用CPU,但是一般使用阻塞IO:即线程虽然不用CPU,但必须等待IO结束。

  • 阻塞IO没能充分利用CPU,所有需要有非阻塞IO和异步IO提高线程利用率。

3 Java线程基本知识点

3-1 Java中线程的创建方法

方法1 (仅使用Thread类,重写run方法)
package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;    //导入注解
@Slf4j(topic = "c.Test1")
/*
new 类名或接口名(){
    重写方法;
};     //注意分号
//以上就是内部类的格式,其实这整体就相当于是new出来的一个对象
本质:其实是继承该类或者实现接口的子类匿名对象
个人理解:一般我们实现接口或者继承某个对象的时候会另外在定义一个新的有名称的类。匿名内部类由于只使用
一次,就没必要特意的再去重新定义一下。
 */
/*方法1:直接使用Thread*/
public class Test1 {
    public static  void main(String[] args){   //create the thread
        Thread t = new Thread(){
            public void run(){
                log.warn("running");
            }
        };
        t.setName("thread1");
        t.start();                 // start the thread
        log.warn("running");
    }
}
方法2(使用Thread配合Runable接口使用,推荐)

目的:将运行的代码与线程的创建分开

package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;    //导入注解
//方法2: Runable对象配合Thread对象使用
@Slf4j(topic = "c.Test2")
public class Test2 {
    public static  void main(String[] args){   //create the thread

//        Runnable r = new Runnable() {    //定义匿名类实现Runable()接口
//            @Override
//            public void run() {
//                log.warn("running");
//            }
//        };
        // annoyomous class create(lambda style)
        // 定义匿名类实现Runable接口,并使用lamba方式简化
        Runnable r = () -> log.warn("running");
        new Thread(r, "thread1").start();
        log.warn("running");
    }
}
2种方法小结

方法1与方法2的联系:方法2中的Runable对象会作为参数传递给Thread的私有变量target,然后在run方法中被调用。本质上2种方法都是对线程对象中的run()方法进行修改。

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  • 使用方法2方便与高级API线程池进行配合
  • 使用方法2更加灵活,符合JAVA组合优于继承的思想。
  • 可以使用jps查看所有进程,jstack PID查看该进程所有的线程
方法3: FutureTask对象配合Thread对象使用(需要线程返回值)。

Java Runnable与Callable区别

package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//方法2: Runable对象配合Thread对象使用
@Slf4j(topic = "c.Test2")
public class Test3 {
    public static  void main(String[] args) throws InterruptedException, ExecutionException {   //create the thread
        // FutureTask对象能够返回进程的执行结果
        // 源码中FutureTask对象实现了callable接口,因此可以传递给Thread对象
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.warn("Runing");
                Thread.sleep(1000);
                return 100;
            }
        });
        Thread t = new Thread(task,"t1");
        t.start();
        log.warn("{}", task.get());
    }
}

3-2 Java线程内部原理

基本概念:

栈与栈帧:线程执行后,被分配一个栈内存。线程之间的栈内存互不干扰。

  • 栈帧(frames):每个栈内存有许多栈帧组成,一个栈帧对应一次方法调用(利用栈的特性先进后出实现多次方法调用)
  • 活动栈帧:每个线程当前只有一个活动栈帧,即当前线程执行的方法

01 Java线程的创建以及线程的基本方法

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

何时发生?

  • 垃圾回收
  • 时间片用完
  • 更高优先级线程
  • 线程主动sleep,yield,wait,join,park,synchronized,lock

3-3 线程中常用方法

thread中部分方法:
方法名
start()|run() 创建线程并让线程处于就绪态(runable)|执行线程代码,不会创建线程
sleep() 让线程由 running 变为 Time waiting(一定时间内无法被调度)
yield() 屈服,让步; 放弃; 让路; 让线程由 running 变为 runable(仍然可被调度)
join() 等待线程运行结束(线程同步,需要运行的结果)
interrupt() 打断线程(包括阻塞的与正在运行的)
下面3个方法是过时方法 容易破坏同步代码块,造成死锁。
stop() 使用2阶段终止模式进行替代
suspend()
resume()

sleep():可以配合interrupt使用,interrupt可以wake up线程

yield():放弃CPU的使用权,依赖于CPU的调度器,会出现线程yield之后,任务调度器仍然调度该线程的情况

interrupt():

  • 中断线程的阻塞状态(采用sleep(),join(),wait()方法后的线程)
  • 中断正常的线程,会使得线程中断标记为True
    • 结合中断标记去优雅的停止线程
package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//方法2: Runable对象配合Thread对象使用
@Slf4j(topic = "c.Test2")
public class Test4 {
    public static  void main(String[] args) throws InterruptedException, ExecutionException {   //create the thread
        Thread t1 = new Thread(() -> {
            while(true){
                if(Thread.currentThread().isInterrupted()){  //当前线程是否被中断过
//                    System.out.println("break the loop");
                    log.warn("break the loop");
                    break;
                }
            }
        },"t1");
        t1.start();
        Thread.sleep(1000);  // main线程休眠1s
        log.warn("interrupt t1");
        t1.interrupt();            // 中断线程t1
    }
}

运行结果:

[main] WARN c.Test2 - interrupt t1
[t1] WARN c.Test2 - break the loop

Process finished with exit code 0
interrput 与park结合使用
package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.LockSupport;
//方法2: Runable对象配合Thread对象使用
@Slf4j(topic = "c.Test7")
public class Test7 {
    public static  void main(String[] args) throws InterruptedException{   //create the thread
        log.warn("the main thread!");
        parktest();
    }
    // park处于调度的考虑让当前线程不执行,等待获取许可证,除非被当前线程unpark方法调用。
    private static void parktest() throws InterruptedException{
        Runnable r = ()->{
            // 定义匿名类实现Runable接口,由于Runable是函数式接口,因此可以通过lamba进行简化
            log.warn("park...");
            LockSupport.park();
            log.warn("unpark...");

            // interrupted()    方法会查看中断状态,将中断标记重置为假。
            // isinterrupted()  方法仅查看中断状态
            // park之后,用interrupt恢复后,必须恢复中断标记为假,否则再次park会失效。
            log.debug("interrupt state: {}", Thread.interrupted());
        };
        Thread t1 = new Thread(r,"t1");
        t1.start();
        t1.sleep(1000);
        t1.interrupt();      // 通过interrupt可以恢复park的进程,并将中断标志变为True
    }
}

执行结果:

[main] WARN c.Test7 - the main thread!
[t1] WARN c.Test7 - park...
[t1] WARN c.Test7 - unpark...

注意点:

  • park方法能够让线程停止运行
  • park之后,可以用interrupt恢复运行,但必须手动恢复中断标记为假,否则再次park会失效。
  • 区分2个方法interrupt以及isinterrupted
二阶段终止模式代码示例

01 Java线程的创建以及线程的基本方法

package chapter2;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
@Slf4j(topic = "c.Test6")
public class Test6 {
    public static  void main(String[] args) throws InterruptedException, ExecutionException {
        TwoStageTermination tmp = new TwoStageTermination();
        tmp.start();
        Thread.sleep(5000);
        tmp.stop();
    }
}

/*
* 每隔2s运行并检查,可通过外部线程杀死该线程
* */

@Slf4j(topic = "c.TwoStageTermination")
class TwoStageTermination
{
    private Thread monitor;
    public void stop(){       /* 正常状态停止(中断)监控进程*/
            monitor.interrupt();    /* 设置中断标记为true*/
        }
    public void start(){
        monitor = new Thread(() -> {
            while(true){
                Thread current = Thread.currentThread();
                if(current.isInterrupted()){
                    log.warn("stop the thread of monitor!");
                    break;
                }
                try{
                    current.sleep(2000);
                    log.warn("sleep 2 seconds !");
                    log.warn("check the status...");  
                }catch(Exception e){
                    e.printStackTrace();
                    current.interrupt();    /* 设置中断标记为true*/
                }
            }
        },"monitor_thread");
        monitor.start();
    }
}

运行结果:

[monitor_thread] WARN c.TwoStageTermination - sleep 2 seconds !
[monitor_thread] WARN c.TwoStageTermination - check the status...
[monitor_thread] WARN c.TwoStageTermination - sleep 2 seconds !
[monitor_thread] WARN c.TwoStageTermination - check the status...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at chapter2.TwoStageTermination.lambda$start$0(Test6.java:36)
	at java.lang.Thread.run(Thread.java:748)
[monitor_thread] WARN c.TwoStageTermination - stop the thread of monitor!
  • 注意点:
    • 线程在sleep状态被interrupt会触发异常,中断标记为false
    • 线程运行状态被interrupt,中断标记为true。
    • 二阶段终止模式可以用于过时的stop方法
守护线程(daemon)

知识点:

  • 通常Java进程会在主线程以及其他线程运行结束后才会结束。
  • 但对于守护线程而言,进程结束无需考虑守护线程是否已经停止。
  • 守护进程的实际例子:
    • Java中的垃圾回收线程
    • Tomcat中的Acceptor以及Poller线程都是守护线程

3-4 线程的状态

从操作系统的角度

可以划分为:开始,可执行(就绪),执行,阻塞,结束

从Java程序的角度

在Java的源代码Thread.java文件中定义了6种状态,分别是

NEW:线程刚刚被创建时,还没有start()的状态

RUNABLE: Java中的RUNABLE包含了操作系统层面的运行,阻塞,可运行状态。

BLOCKED,WAITING,TIMED_WAITINGJava API层面的阻塞

  • TIMED_WAITING:使用sleep方法可能会出现
  • WAITING: 使用join方法后可能会出现
  • BLOCKED:使用synchronize方法可能会出现

TERMINATED:程序终止状态

01 Java线程的创建以及线程的基本方法

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

3-5 多线程的简单实例-烧水泡茶

场景:

01 Java线程的创建以及线程的基本方法

封装好的Sleeper类

package chapter2;
import java.util.concurrent.TimeUnit;
public class Sleeper {
    public static void sleep(int i){
        try{
            TimeUnit.SECONDS.sleep(i);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

主程序

package chapter2;
//这个程序要运行必须在IDEA中装好lombok插件,并有lombok和slf4j-simple包
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
//方法2: Runable对象配合Thread对象使用
@Slf4j(topic = "c.Test8")
public class Test8 {
    public static  void main(String[] args){   //create the thread
        Thread t1 = new Thread(()->{
            log.warn("洗水壶");
            Sleeper.sleep(1);
            log.warn("烧开水");
            Sleeper.sleep(15);
            log.warn("完成洗茶壶和烧开水");
        },"老王");

        Thread t2 = new Thread(()->{
            log.warn("洗茶壶");
            Sleeper.sleep(1);
            log.warn("洗茶杯");
            Sleeper.sleep(2);
            log.warn("拿茶叶");
            Sleeper.sleep(1);
            log.warn("完成洗茶壶和茶杯,拿茶叶");
            try{
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.warn("小王泡茶");
        },"小王");

        t1.start();
        t2.start();
    }
}

执行结果:

[小王] WARN c.Test8 - 洗茶壶
[老王] WARN c.Test8 - 洗水壶
[老王] WARN c.Test8 - 烧开水
[小王] WARN c.Test8 - 洗茶杯
[小王] WARN c.Test8 - 拿茶叶
[小王] WARN c.Test8 - 完成洗茶壶和茶杯,拿茶叶
[老王] WARN c.Test8 - 完成洗茶壶和烧开水
[小王] WARN c.Test8 - 小王泡茶

总结: 使用2个线程进行了统筹规划,利用join完成最后的同步。

主要参考资料

并发编程课程


2021.2.10