Java编程基础知识之线程篇(其二)

18 篇文章 0 订阅
11 篇文章 0 订阅

目录

一、创建线程

1.方法一:继承 Thread 类

2.方法二 :实现 Runnable 接口

3.变形

1)匿名内部类

2)使用Lambda表达式

二、多线程增加运行效率

三、Thread常用方法

1.Thread常用构造方法

 2.Thread类的几个常见属性

1) 状态(state)

2)优先级(priority)

3)后台线程(isDaemon)

4)存活(isAlive)

5)中断线程(interrupt)

3.启动线程【start方法】

4.等待线程【join方法】

5.线程休眠【sleep】

6.让出线程【yield】

四、使用jconsole查看线程运行


一、创建线程

1.方法一:继承 Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
public class Thread{
    public void main(String[] args){
        //创建线程对象
        MyThread thread = new MyThread();
        //通过.strat来启动线程
        thread.start();
    }
}

2.方法二 :实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}
public class Thread{
    public void main(String[] args){
        //创建线程对象
        Thread thread = new Thread(MyRunnable());
        //通过.strat来启动线程
        thread.start();
    }
}
对比上面两种方法 :
  • 继承 Thread , 直接使用 this 就表示当前线程对象的引用.
  • 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使Thread.currentThread()

3.变形

1)匿名内部类

创建Thread匿名对象

// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};

创建Runnable匿名内部类

// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});

2)使用Lambda表达式

// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));

二、多线程增加运行效率

serial()方法实现串行,parallel() 方法实现并行,System.nanoTime()记录开始或者结束的时间戳

public class Demo1 {
    public static final int counter = 1_000_000_000;

    public static void main(String[] args) {
        //运行串行
        serial();
        //运行并行
        parallel();
    }
    public static void serial(){
        //记录开始时间
        long begin = System.nanoTime();
        int a = 0;
        int b = 0;
        for(long i = 0 ; i < counter ; i++){
            a++;
        }
        for(long i = 0 ; i < counter ; i++){
            b++;
        }
        //记录结束时间
        long end = System.nanoTime();
        System.out.println("串行耗时:" + (end - begin)*1.0/1000/1000 + "ms");

    }
    public static void parallel(){
        //记录开始时间
        long begin = System.nanoTime();
        //创建线程t1

        Thread t1 = new Thread(() -> {
            int a = 0;
             for(long i = 0 ; i < counter ; i++){
                a++;
             }
         });
        //运行线程
        t1.start();
        //方法自己运行一个循环
        int b = 0;
        for(long i = 0 ; i < counter ; i++){
            b++;
        }
        //此处需要等待线程t1结束才能记录结束时间,因为方法本身可能结束了,但是线程没有结束,需要等待其结束才是真正的时间,使用join()方法(后续介绍)
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("并行耗时:" + (end - begin)*1.0/1000/1000 + "ms");
    }
}

运行结果:

串行耗时:789.0998000000001ms
并行耗时:437.6658ms

        可想而知,后者执行更快,其原因在于,串行是两个循环一个一个来执行,而并行是两个几乎同时运行,充分利用CPU资源,从而提高速度。所以同样是20亿次的循环,并行会快。但是同样在计算量比较大的时候,并行才会快与串行;而小量计算的时候,创建线程本身也需要时间开销,这样就不见的一定比串行快 

//当循环次数为1000的时候
串行耗时:0.0362ms
并行耗时:0.8917ms

这里补充说明一下,main方法也是一个线程 

三、Thread常用方法

        Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。 Thread 类的对象 是用来描述一个线程执行流的, JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

1.Thread常用构造方法

方法        说明
Thread() 创建线程
Thread(Runnable target)使用Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable对象创建线程对象,并命名
【了解】Thread(ThreadGroup group ,Runnable target)线程可以被用来分组管理,分好的组即为线程组,【目前了解即可】
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

 2.Thread类的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况(下面我们会进一步说明)
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题(下面我们进一步说明)
package leaner.Thread;

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还活着");
                    Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());

        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {
        }
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

1) 状态(state)

        在操作系统中,一个线程状态只有就绪状态和阻塞/睡眠状态,在Java中,一个线程的状态被划分的更细致,这些状态使用枚举类在保存在Thread中

  • NEW(新建): 安排了工作, 还未开始行动
  • RUNNABLE(可运行): 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED:(阻塞):获得锁后结束等待
  • WAITING(等待): 等待通知才能结束等待
  • TIMED_WAITING(计时等待): 等待计时结束或收到通知才能结束等待
  • TERMINATED(终止): 工作完成了
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

 状态间转化示意图:

2)优先级(priority)

        每当线程调度有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先高度依赖于系统。当虚拟机依赖于宿主机平台的线程实现时,java线程的优先级会映射到宿主机平台的优先级。平台的线程优先级可能比java内部的10个级别多,也可能少。所以优先级不一定好使。

3)后台线程(isDaemon)

        创建两个线程 t1 和 t2 默认都是前台的线程,即使main方法执行完毕,也得等t1和t2执行完,整个进程才能退出。假如t1 和 t2是后台程序,此时如果main执行完毕,整个进程就直接退出 t1 和 t2就强行终止了

4)存活(isAlive)

        Thread t对象的生命周期和内核中对应的线程,并不完全一致。创建一个t对象后,在调用start之前,系统中是没有对应线程的。在run()方法执行完了之后,系统中的线程就销毁了,但是t这个对象可能还存在。

        通过调用isAlive就能判断当前系统的线程的运行情况,

  • 如果调用start之后,run执行完之前,isAlive就返回true;
  • 如果调用start之前,run执行完之后,isAlive就返回false; 

5)中断线程(interrupt)

对中断处理有两种方法:

  • 使用标记位
  • 使用interrupt()

使用标记位:(volatile为关键字,将在后续锁的文章中介绍)

public class Demo3 {
    public volatile static boolean flag = false;

    public static void main(String[] args) {
        //创建线程
        Thread t1 = new Thread(() -> {
            //标志位没有修改将循环
            while (!flag) {
                //每次打印线程的名字,currenrThread方法为静态方法,获得当前线程的引用,由于使用的是Runnable接口,
                //所以无法使用this来进行引用,如果是创建Thread的匿名对象,则可以使用this来说进行指引
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //启动线程
        t1.start();
        try {
            //main线程休眠5s
            Thread.sleep(5000);
            //main线程醒来就将标记修改
            flag = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

        只不过这种标记位中断存在缺陷, 一个循环体内部如果没有一直去判断是否应该中断程序,那么即使循环体内部运行的时候,已经被标记为中断也只能等到下一个轮回才能检查出来

使用interrupt方法发出中断信号中断程序

        Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

        中断线程三个方法

方法作用
public void interrupt()
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
public static boolean interrupted()
判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()
判断对象关联的线程的标志位是否设置,调用后不清除标志位
public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        try {
            Thread.sleep(5000);
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果:

        这里报了一个异常之后,程序接着循环了,很明显不符合我们的预期....下面通过两个方面解释这一问题

        第一个原因:这是因为sleep(wait、join,这类方法签名上有抛出异常)会让程序进入轻量级阻塞状态(WAITING、TIME_WAITING)会抛出异常 ,处在这个状态的线程是没有上CPU运行的,本身没有在运行,谈不上中断,对于运行线程来说才有中断这一说法,所以sleep既然没有在运行,然后有被发出中断信号,自然就报了一个异常出来

         第二个原因:在TIME_WAITING时收到中断信号,捕获到异常,然后线程被强制唤醒,执行catch语句块内的代码,但是这个代码只是做了简单的打印异常之后就没处理了,所以循环又接着进行了........解决办法是在catch里面添加break跳出循环即可。

        如果线程处在BLOCKED状态(即是否等待获得锁状态)则不会抛出InterruptedException,如果线程处在运行状态,也不会抛出中断异常

        所以interrupt是无法中断synchronized代码块/方法的。

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            Object object = new Object();
            synchronized(object){
                while(true){
                    System.out.println("线程正在运行......");
                }
            }
        });
        t1.start();
        t1.interrupt();
    }

运行结果

线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
.........

         但是可以通过检查线程的中断状态来判断它是否收到过中断信号。

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            Object object = new Object();
            synchronized(object){
                while(true){
                    System.out.println("线程正在运行......");
                    if(Thread.currentThread().isInterrupted()){
                        break;
                    }
                }
            }
        });
        t1.start();
        try {
            Thread.sleep(10);
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

         使用interrupted方法和isInterrupted方法来判断线程的中断标记位,二者区别如下

  • 前者为静态方法,直接对类使用
    • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • 后者为实例方法,需要获取到当前线程的引用
    • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

         细节补充:

        interrupted()是一个静态方法,而Thread内部的标记位是一个实例变量,正常手段不应该能被一个静态变量修改到,其实这个方法内部手段其实和isInterrupted差不多,都使用到了currentThread()静态方法,来获取对象引用,然后对这个这个对象内部的中断标记当前的标记位进行保存,然后判断是否为true来进行修改,然后修改完后有调用了clearInterruptEvent()方法来清除标记位。

public static boolean interrupted() {
        Thread t = currentThread();
        boolean interrupted = t.interrupted;
        // We may have been interrupted the moment after we read the field,
        // so only clear the field if we saw that it was set and will return
        // true; otherwise we could lose an interrupt.
        if (interrupted) {
            t.interrupted = false;
            clearInterruptEvent();
        }
        return interrupted;
    }

         相比之下,isInterrupted方法朴实无华,直接将对象的中断标记进行返回

public boolean isInterrupted() {
        return interrupted;
    }

3.启动线程【start方法】

        之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味线程就开始运行了。
  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 而调用 start() 方法才真的在操作系统的底层创建出一个新线程,并且运行run方法,新旧线程为并发关系

4.等待线程【join方法】

        有时候需要等待一个线程结束之后才能进行后续工作,这就需要join来进行帮助,这是一个实例方法,哪个线程对象调用它后续代码只有这个线程执行完之后才能运作。

        注意这个方法也是抛出一个InterruptedException

public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for(int i = 0 ; i < 1_000_000_000 ; i++){
                int a = 0;
            }
        });
        t1.start();
        try {
            System.out.println("t1线程状态"+ t1.getState());
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1线程状态"+ t1.getState());
    }

        【join】的“同形异构”方法 

方法 说明
public void join()
等待线程结束
public void join(long millis)
等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)
同理,但可以更高精度

5.线程休眠【sleep】

方法说明
public static void sleep(long millis) throws InterruptedException

 

休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws
InterruptedException
可以更高精度的休眠
        事实上,睡眠的时间不是说睡眠时间一过就立即上CPU,而是说这段睡眠时间是肯定不上CPU,时间过了能不能上CPU也知道
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
   }
}

6.让出线程【yield】

        yield大公无私,让出CPU

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("张三");
            // 先注释掉, 再放开
            // Thread.yield();
       }
   }
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("李四");
       }
   }
}, "t2");
t2.start();

​
可以看到 :
        1. 不使用 yield 的时候 , 张三李四大概五五开
        2. 使用 yield , 张三的数量远远少于李四
结论 :
        yield 不改变线程的状态 , 但是会重新去排队 .

 

四、使用jconsole查看线程运行

        这个应用是Java jdk自己带的,在jdk的并目录下,它只能识别出Java程序 

 

 

声明:个人笔记使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

摸鱼儿hzj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值