2.多线程学习笔记之线程的创建方式

文章目录

2.1.线程的创建方式

第一种:直接使用Thread类使用匿名内部类或者继承的方式,重写run方法[无返回值,不能抛出异常]

把线程和任务合并在了一起

@Slf4j
public class ThreadDemo {
    public static void main(String[] args) {
        // 使用继承的方式:这里没有去继承,使用的是匿名内部类
        Thread thread = new Thread("myThread"){
            @Override
            public void run() {
               log.debug("running...");
            }
        };

        thread.start();
    }
}

第二种:实现Runnable接口,然后重写run方法[无返回值,不能抛出异常]

把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。通过查看源码可以发现,方法二其实到底还是通过方法一执行的!

 public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        // 实现Runnable接口,重写run方法即可
        Thread thread = new Thread(runnable,"myThread");
        thread.start();
    }

第三种:向Future提交Runnable或Callable任务[可以有返回结果,可以抛出异常]

 public static void main(String[] args) throws Exception {
        // 实现Callable接口,实现call方法
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1024;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask(callable);
        new Thread(futureTask,"myThread").start();
        // 阻塞等待call()方法中的代码执行完成,
        Integer result = futureTask.get();
        // 阻塞1s,如果超过1s为获取到结果,就结束阻塞,得到一个空的返回值
        Integer result2 = futureTask.get(1,TimeUnit.SECONDS);
        log.debug("result= "+result);

    } 

2.多线程学习笔记之线程的创建方式

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果,FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况。

2.2.查看线程和进程的方法

windows系统

  • tasklist 查看所有进程
  • tasklist tasklist | findstr “筛选项” 筛选进程
  • taskkill /F /PID pid 杀死进程

linux系统

  • ps -ef 查看所有进程
  • ps -fT -p 查看某个进程(PID)的所有线程
  • kill pid 杀死进程
  • top -H -p 查看某个进程(PID)的所有线程
  • ps -fe 查看所有进程

java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置 需要以如下方式运行你的 java 类

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

如果要认证访问,还需要做如下步骤:

  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名 如果要认证访问,还需要做如下步骤 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

2.3.线程运行原理

3.1虚拟机栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,java虚拟机就会为其分配一块栈内存,栈内存是线程独有的,他们之间互不干扰。栈先进后出,当这个方法执行完就释放该内存,返回到返回地址继续执行。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

2.多线程学习笔记之线程的创建方式

2.多线程学习笔记之线程的创建方式

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

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

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能

2.4.Thread常见方法

方法名 static 功能说明 注意
start() 启动一个新线 程,在新的线程 运行 run 方法 中的代码 start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateExceptio
run() 新线程启动后会 调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结 束,最多等待 n 毫秒 根据程序的具体执行结束时间,可以提前结束等待
getId() 获取线程长整型 的 id 每个线程的id是唯一的
getName() 获取线程名称
setName(String) 设置线程名称
getPriority() 获取线程优先级
setPriority(int) 设置线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断该线程是否被打断 不会清除 打断标记
isAlive( 线程是否还存活(是否运行完毕)
interrupt() 打断线程
interrupted() static 判断当前线程是否被打断
currentThread() static 获取当前正在运行的线程
sleep(long n) static 让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程
yield() static 提示线程调度器 让出当前线程对 CPU的使用 主要是为了测试和调试

4.1 start与run

直接调用run方法是作为普通方法调用,并不会以多线程的方式启动,想run方法中的代码以多线程的方式启动,必须以start的方式进行启动。

4.2 sleep 与 yield

sleep

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

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(没有别的线程需要执行,可能让不出去)

4.3线程优先级

  • 线程优先级 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,只是大概率会分到cpu执行权,不是绝对的会得到cpu执行权。

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

小案例:避免cpu空转,使用率达到100%

sleep 实现 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序

while(true) {
 try {
	 Thread.sleep(50);
 } catch (InterruptedException e) {
	 e.printStackTrace();
 	}
}

可以用 wait 或 条件变量达到类似的效果

不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景

sleep 适用于无需锁同步的场景

4.4 join

让线程同步执行,main线程中调用的 t1.join() ,那就是需要先等t1线程执行完毕后,main线程才能继续向下运行

private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }

4.5 interrupt

打断 sleep,wait,join 的线程 这几个方法都会让线程进入阻塞状态 打断 sleep 的线程, 会清空打断状态 ,以 sleep 为例

private static void test1() throws InterruptedException {
 Thread t1 = new Thread(()->{
 	sleep(1);
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
 log.debug(" 打断状态: {}", t1.isInterrupted()); // false
}

打断正常运行的线程 打断正常运行的线程, 不会清空打断状态

interrupt()方法会让该线程的打断标记设置为true,正在运行的线程可以获取到打断标记,是否有别的线程想让他停止,如果有,那么正在运行的这个线程可以自己决定要不要停止下来,或者是做一些资源的释放在进行关闭掉线程。

private static void test2() throws InterruptedException {
        Thread t2 = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if (interrupted) {
                    log.debug(" 打断状态: {}", interrupted); // true
                    break;
                }
            }
        }, "t2");
        t2.start();
        sleep(0.5);
        t2.interrupt();
    }

4.6 合理的终止线程

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。

如下所示:那么线程的 isInterrupted() 方法可以取得线程的打断标记,如果线程在睡眠 sleep 期间被打断,打断标记是不会变的,为false,但是 sleep 期间被打断会抛出异常,我们据此手动设置打断标记为 true;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事。

2.多线程学习笔记之线程的创建方式

@Slf4j
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination termination = new TwoPhaseTermination();
        // 启动监视器 3s后关闭
        termination.start();
        Thread.sleep(3000);
        termination.stop();
    }
}
@Slf4j
class TwoPhaseTermination{
    // 监视器
    private Thread monitor;

    public void start(){
        monitor=new Thread(()->{
            while (true){
                // 获取到当前线程对象
                Thread current= Thread.currentThread();
                // 判断是否被设置打断标记
                if (current.isInterrupted()){
                    log.debug("释放资源...");
                    break;
                }

                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.debug("执行监控任务...");
                } catch (InterruptedException e) {
                    // 在睡眠中被打断,重新设置打断标记
                    current.interrupt();
                    log.debug("在睡眠中被打断...[InterruptedException: sleep interrupted]");
                }



            }
        },"monitor");
        monitor.start();
    }

    public void stop(){
        //  设置打断标记
        monitor.interrupt();
    }
}

升级版: 使用 vloatile 关键字实现

@Slf4j
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination termination = new TwoPhaseTermination();
        // 启动监视器 3s后关闭
        termination.start();
        Thread.sleep(3000);
        log.debug("停止监控...");
        termination.stop();

    }
}
@Slf4j
class TwoPhaseTermination{
    // 监视器
    private Thread monitor;
    private volatile boolean stop = false;
    public void start(){
        monitor=new Thread(()->{
            while (true){

                if (stop){
                    log.debug("释放资源...");
                    break;
                }

                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.debug("执行监控任务...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }



            }
        },"monitor");
        monitor.start();
    }

    public void stop(){
        monitor.interrupt();
        stop=true;
    }
}

打断 park 线程, 不会清空打断状态

@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {
        parkDemo();
    }

    public static void parkDemo() throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.debug("park...");
            // 让线程进入阻塞状态,不能向下继续运行,如果打断标记为true则park()方法会失效
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断标记:{}",Thread.currentThread().isInterrupted());
        }, "myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        
        // 如果此处需要再次进行 LockSupport.park(); 则不会生效,代码还是会向下运行
        //如果要想生效 需要将isInterrupted()换成interrupted() 
        // interrupted() 方法获取完打断标记后,会清除掉打断标记,设置为false
    }
}

4.7 不推荐使用的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名 static 功能说明
stop() 停止线程运行
supend() 挂起(暂停)线程运行
resume() 恢复线程运行

2.5.守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程 可以调用thread.setDaemon(true)方法变成守护线程

  • 注意 垃圾回收器线程就是一种守护线程

  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求

@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true){
                if (Thread.currentThread().isInterrupted()){
                    break;
                }
                log.debug("myThread正在运行");
            }
            log.debug("myThread结束");
        }, "myThread");

        thread.setDaemon(true);
        thread.start();

        Thread.sleep(1000);
        log.debug("main线程结束");


    }

}

2.6.线程的状态

从操作系统层面进行分析:

2.多线程学习笔记之线程的创建方式

  1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
  3. 运行状态,指线程获取了CPU时间片,正在运行
    1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
    2. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    3. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
  5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。 Thread.State 枚举,分为六种状态

2.多线程学习笔记之线程的创建方式

  • NEW 跟五种状态里的初始状态是一个意思
  • RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {

        // new[新建状态]
        Thread t1 = new Thread(() -> {
            log.debug("running...");
        }, "t1");


        // runnable[运行状态]
        Thread t2 = new Thread(() -> {
            while (true){

            }
        }, "t2");
        t2.start();

        //terminated [终止状态]
        Thread t3 = new Thread(() -> {
            log.debug("running...");
        }, "t3");
        t3.start();

        // timed_waiting [定时等待]
        Thread t4 = new Thread(() -> {
        synchronized (Test.class){
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        }, "t4");
        t4.start();
        // waiting [等待]
        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t5");
        t5.start();

        // blocked[阻塞状态]
        Thread t6 = new Thread(() -> {
            synchronized (Test.class){
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t6");
        t6.start();

        Thread.sleep(1000);

        log.debug("t1 state: {}",t1.getState());
        log.debug("t2 state: {}",t2.getState());
        log.debug("t3 state: {}",t3.getState());
        log.debug("t4 state: {}",t4.getState());
        log.debug("t5 state: {}",t5.getState());
        log.debug("t6 state: {}",t6.getState());


    }

}

2.多线程学习笔记之线程的创建方式

习题[统筹规划]

阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示

  • 参考图二,用两个线程(两个人协作)模拟烧水泡茶过程

  • 文中办法乙、丙都相当于任务串行

  • 而图一相当于启动了 4 个线程,有点浪费

  • 用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间

附:华罗庚《统筹方法》 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复 杂的科研项目的组织与管理中,都可以应用。 怎样应用呢?主要是把工序安排好。

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么 办?

  • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝
  • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡 茶喝。
  • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡 茶喝。

哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都浪费了一定的空余时间。

这是小事,但这是引子,可以引出生产管理等方面有用的方法来。

水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而 这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:

2.多线程学习笔记之线程的创建方式

  • 从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作 效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大 可利用“等水开”的时间来做。

  • 是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但 稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这 么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱 备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关 键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。

  • 洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:

2.多线程学习笔记之线程的创建方式

  • 看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。 这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接 解决所有问题,但是,我们利用这种方法来考虑问题,也是不无裨益的。
@Slf4j
public class Test1 {
   public static void main(String[] args) {
       Thread t1 = new Thread(() -> {
           // 洗水壶花费1秒
           log.debug("洗水壶");
           sleep(1);
           // 烧开水5秒
           log.debug("烧开水");
           sleep(5);

       }, "小王");

       Thread t2 = new Thread(() -> {
           // 洗茶壶1秒
           log.debug("洗茶壶");
           sleep(1);

           // 洗茶杯 2秒
           log.debug("洗茶杯");

           // 拿茶叶 1秒
           log.debug("拿茶叶");
           sleep(1);
           // 开水烧开后可以去泡茶
           try {
               t1.join();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           log.debug("泡茶");
       }, "老王");

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

2.7.本章小结

本章的重点在于掌握

  • 线程创建方式

  • 线程重要 api,如 start,run,sleep,join,interrupt 等

  • 线程状态

  • 应用方面

    • 异步调用:主线程执行期间,其它线程异步执行耗时操作

    • 提高效率:并行计算,缩短运算时间

    • 同步等待:join

    • 统筹规划:合理使用线程,得到最优效果

  • 原理方面

    • 线程运行流程:栈、栈帧、上下文切换、程序计数器

    • Thread 两种创建方式 的源码

  • 模式方面

    • 终止模式之两阶段终止

黑马程序员juc:学习地址

上一篇:并发编程笔记


下一篇:12.5