Java并发编程-线程(三)

导读:本篇文章讲解 Java并发编程-线程(三),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

方法一,直接使用Thread

public class TestThread {

    public static void main(String[] args) {
        Thread thread = new Thread("t1") {
            @Override
            public void run() {
                System.out.println("test thread " + Thread.currentThread().getName());
            }
        };

        thread.start();
        System.out.println("main begin");
    }
}

执行结果,可以看到主线程先执行,子线程后执行。

main begin
test thread t1

方法二,使用Runnable 配合Thread

该方法较为灵活,推荐使用Runnable的方式创建线程。

@Test
    public void testRunnable() throws InterruptedException {
        // 使用lambda表达生成一个Runnable对象
        Runnable runnalbe = () -> {
            System.out.println("test Runnalbe");
        };
        new Thread(runnalbe).start();
        System.out.println("main exe");
        Thread.sleep(100);
    }

执行结果

main exe
test Runnalbe

原理之Thread与Runnable的关系

  • 方法1是把线程和任务合并在一起,方法2是把线程(thread)和任务(runnable)分开。
  • 用Runnable更容易与线程池等高级API配合。
  • 用Runnable让任务类脱落了Thread继承体系,更灵活。

方法三,FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况。
FutureTask实际上就是一个Runnable,因此可以作为Thread的参数。

@Test
    public void testFutureTask() throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(
        		// Callable接口的Lambda实现
                () -> {
                    System.out.println("future task test");
                    return 100;
                }
        );
        new Thread(futureTask, "t2").start();
        // 阻塞等待返回值
        System.out.println(futureTask.get());
    }

执行结果可以看到,先执行子线程,在输出子线程的返回值,因为主线程要获取子线程的值,所以会等待子线程执行完毕再执行。

future task test
100

线程查看

  • linux下,通过top命令可以动态的查看进程信息。
  • top -H -p processId 可以动态的查看某进程下的线程信息。
  1. 执行top命令
    在这里插入图片描述
  2. 查询某进程下的线程信息
[root@localhost ~]# jps
2527 Jps
[root@localhost ~]# top -H -p 4262

在这里插入图片描述
3. 通过jstack查询执行jstack时刻的某进程下的所有线程快照信息
在这里插入图片描述

线程上下文切换

线程上下文切换原因

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

  • 线程的cpu时间片用完。
  • 垃圾回收。
  • 有更高优先级的线程需要执行。
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。

线程上下文切换需要做什么

保存当前线程状态,并恢复另一线程状态。
线程状态包括哪些:

  1. 当Context Switch发生时,需要由操作系统保持当前线程的状态,并恢复另一线程的状态,Java中对应的概念就是程序计数器(program counter register),它的作用是记住下一条Jvm执行的执行地址,是线程私有的。
  2. 除了程序计数器,还有虚拟机栈中每个栈帧(方法)的信息(如果局部变量、操作数栈、返回地址等)。
  3. 频繁的上下文切换会影响性能。
    比如main线程的时间片用完了,需要保存main线程的程序计数器的下一次执行地址和栈中的每个栈帧信息。
    在这里插入图片描述

线程常见方法

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

线程方法详解

join方法

以调用角度来讲,如果:

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

interrupt方法

打断sleep,wait,join的线程

阻塞状态的线程,抛出InterruptedException异常并清除打断标记,因此打断标记为false。
示例:

    @Test
    public void testInterrupt() throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.debug("t1 begin");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.debug("t1 interrupted");
                e.printStackTrace();
            }
        }, "t1");
        thread.start();

        Thread.sleep(1000);
        thread.interrupt();
        log.debug("main thread end, t1 interrupt status:{}", thread.isInterrupted());
    }

执行结果:

21:46:55 [t1] juc.test - t1 begin
21:46:56 [t1] juc.test - t1 interrupted
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.study.concurrentprogramming.ConcurrentprogrammingApplicationTests.lambda$testInterrupt$0(ConcurrentprogrammingApplicationTests.java:21)
	at java.lang.Thread.run(Thread.java:748)
21:46:56 [main] juc.test - main thread end, t1 interrupt status:false

打断正常运行的线程

线程的打断标记不会被清除。
执行interrupt()方法只是告诉线程被打断了,设置打断标记,但是线程自己是否结束,需要线程自己决策。
示例:

    @Test
    public void testInterruptNormal() throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.debug("t1 begin");
            while (true) {
                if(Thread.currentThread().isInterrupted()) {
                    log.debug("t1 interrupted");
                    break;
                }
            }
        }, "t1");
        thread.start();

        Thread.sleep(1000);
        thread.interrupt();
        log.debug("main thread end, t1 interrupt status:{}", thread.isInterrupted());
    }

执行结果:

21:54:57 [t1] juc.test - t1 begin
21:54:58 [t1] juc.test - t1 interrupted
21:54:58 [main] juc.test - main thread end, t1 interrupt status:true

两阶段终止模式

two phase termination
在一个线程T1中如何“优雅”终止线程T2? 这里的【优雅】指的是给T2一个料理后事的机会。

错误思路

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

两阶段终止设计示例

一个后台监控线程。
在这里插入图片描述
代码示例:
在这里插入图片描述

打断park线程

park线程,是指执行了LockSupport.park()方法导致暂停的线程。
注意如果park线程的打断标记为真,则park方法无法使线程暂停,可以使用Thread.interrupted()方法读取打断状态。
代码示例:

    @Test
    public void testPark() throws InterruptedException {
        Thread thread = new Thread(() -> {
            log.debug("t1 park");
            LockSupport.park();
            log.debug("t1 unPark");
            log.debug("t1 interrupt status:{}", Thread.interrupted());
        }, "t1");

        thread.start();
        sleep(2000);
        thread.interrupt();
    }

执行结果:

22:40:39.575 [t1] juc.test - t1 park
22:40:41.583 [t1] juc.test - t1 unPark
22:40:41.583 [t1] juc.test - t1 interrupt status:true

主线程与守护线程

默认情况下,java进程需要等待所以线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
在这里插入图片描述

注意:

  1. 垃圾回收器线程就是一种守护线程。
  2. Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat结束到shutdown命令后,不会等待这两个守护线程处理完当前请求,就是直接结束了。

线程状态

从操作系统层面来描述有5种状态

在这里插入图片描述

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

从Java API层面来描述6种状态

根据Thread.State枚举,分为六种状态
在这里插入图片描述

  • New线程刚被创建,但是还没有调用start()方法。
  • RUNNABLE当调用了start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行的)。
  • BLOCKED(阻塞),WAITING(无限等待),TIMED_WAITING(超时等待)都是Java API层面对【阻塞状态】的细分,都无法被调度分配时间片执行,后面会在状态转换已经详述;BLOCKED(获取不到锁,线程被阻塞),WAITING(比如有Join的线程加入,当前线程处于该状态),TIMED_WAITING(当前线程执行sleep时处于该状态)。
  • TERMINATED 当线程代码运行结束。

线程的六种状态演示

代码示例

 @Test
    public void testThreadState() throws InterruptedException {
        Boolean lock = false;


        // 创建线程,但是没有与操作系统线程产生联系,处于NEW 状态
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("thread 1");
            }
        };
        t1.setName("t1");

        // 线程启动运行,处于RUNNABLE状态
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                while (true) {

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

        // 线程获取不到锁,处于BLOCKED状态
        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t3 start");
            }
        }, "t3");
        t3.start();

        // 线程执行中加入了其他线程,当前线程处于WAITING状态
        Thread t4 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t4");
        t4.start();

        // 线程sleep,线程处于TIMED_WAITING状态
        Thread t5 = new Thread(() -> {
            try {
                sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t5");
        t5.start();

        // 线程执行结束,线程处于TERMINATED
        Thread t6 = new Thread(() -> {
            System.out.println("t6 end");
        }, "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());
    }

执行结果:

t6 end
10:56:03.544 [main] juc.test - t1 state NEW
10:56:03.554 [main] juc.test - t2 state RUNNABLE
10:56:03.554 [main] juc.test - t3 state BLOCKED
10:56:03.554 [main] juc.test - t4 state WAITING
10:56:03.554 [main] juc.test - t5 state TIMED_WAITING
10:56:03.554 [main] juc.test - t6 state TERMINATED

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100304.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!