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

Posted 野生java研究僧

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);

     

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)组成,对应着每次方法调用时所占用的内存

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

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。处理好这两种情况那我们就可以放心地来料理后事。

@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.线程的状态

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

  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 枚举,分为六种状态

  • 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()<

以上是关于2.多线程学习笔记之线程的创建方式的主要内容,如果未能解决你的问题,请参考以下文章

多线程Java多线程学习笔记 | 多线程基础知识

java笔记之线程方式1启动线程

3.多线程学习笔记之共享模型之管程

多线程 Thread 线程同步 synchronized

学习笔记之多线程笔记

3.多线程学习笔记之共享模型之管程