多线程系列2


前言

前篇文章,我们已经学习过了,进程与线程的相关内容。那么在Java中如何实现多线程编程。原来对于线程的操作,是使用操作系统提供的 API,由于 java 是个跨平台的语言,很多操作系统提供的功能,都被 JVM 封装好了,咱们这里不需要学习操作系统原生态的 API(基于C语言实现的),只需要学习 java 提供的 API 即可。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Thread 类

Java 中操作多线程,最核心的类就是 Thread。

1.1 创建 Thread

在 Java 中创建线程的写法有很多种,下面是一些创建线程常用的写法!!!

1.1.1 继承 Thread 并重写 run 方法

// 继承 Thread, 并重写 run 方法
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello,thread");
    }
}

public class Thread1{
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

注意事项:
1.start() 方法的内部并没有调用 run() 方法,start 的工作是创建了一个新的线程,然后由这个创建好的线程去执行run方法的任务。
2.系统调度线程,是 抢占性执行,具体哪个线程先上,哪个线程后上,不确定,取决于操作系统调度器具体实现策略,虽然多线程间具有优先级,但在应用层面无法修改。

1.1.2 实现 Runnable 接口

这种实现方式较第一种相比,达到了解耦合:目的是为了让 线程 和 线程要执行的任务 之间分开,对于未来的代码改动更加方便。

// 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}

public class Thread2 {
    public static void main(String[] args) {
        // 描述了线程要执行的任务
        Runnable runnable = new MyRunnable();
        // 创建线程
        Thread t = new Thread(runnable);
        // 启动线程
        t.start();

1.1.3 匿名内部类继承Thread

public class Thread3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello,thread");
            }
        };
        t.start();
    }
}

1.1.4 匿名内部类,实现 Runnable 接口

public class Thread4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread");
            }
        });
        t.start();
    }
}

1.1.5 Lambda 表达式

使用 lambda 表达式最简单,更推荐使用。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        });
        t.start();
    }
}

1.2 Thread 的常见属性

Thread 常见的属性基本包括有:ID、名称、状态、优先级、是否为后台线程、是否存活、是否被中断。

1.2.1 ID

使用 getID 来获取线程标识符。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        });
        t.start();
        // 获取 t 线程的标识符ID
        System.out.println(t.getId());
    }
}

在这里插入图片描述

1.2.2 名称

使用 getName 来获取线程名称。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        t.start();
        // 获取 t 线程的名称
        System.out.println(t.getName());
    }
}

在这里插入图片描述
也可以通过 jdk 中的 jconsole 查看线程信息,如下图:
在这里插入图片描述

1.2.3 状态

使用 getState 获取线程状态。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        t.start();
        // 获取 t 线程的状态
        System.out.println(t.getState());
    }
}

在这里插入图片描述

1.2.4 优先级

使用 getPriority 设置线程优先级,虽然是可以设置优先级,但是设置了用处也不大,这里就不作演示。

1.2.5 是否为后台线程

使用 isDaemon 查看线程是否为后台线程 (守护线程)。

前台线程:前台线程会阻止进程结束,前台线程没完成,整个进程没法结束。
后台线程:后台线程不会组织进程结束。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        t.start();
        // 查看 t 是否为后台线程
        System.out.println(t.isDaemon());
    }
}

在这里插入图片描述
另外,可以把线程设置为 后台线程,此时进程的结束,就跟这个线程没有关系了。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        // 查看 t 是否为后台线程
        System.out.println(t.isDaemon()); // false
        t.setDaemon(true);
        System.out.println(t.isDaemon()); // true
        t.start();
    }
}

1.2.6 是否存活

使用 isAlive 查看线程是否存活。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        t.start();
        // 查看 t 是否存活
        System.out.println(t.isAlive());
    }
}

1.2.7 是否被中断

使用 isInterrupted 查看线程时候被中断。

public class Thread5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello lambda thread");
        },"Thread5");
        t.start();
        // 查看 t 是否被中断
        System.out.println(t.isInterrupted());
    }
}

在这里插入图片描述

二、中断一个线程

要理解,这里的 中断 的意思不是让线程立即停止,而是给线程发了一个通知,告诉线程应该要结束了,但是否真的结束,取决于线程这块的代码具体怎么编写的。

2.1 自定义标志位

可以使用一个标志位来控制是否要中断线程。

public class Thread6 {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (flag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread");
        });
        t.start();

        Thread.sleep(300);
        // 在主线程这里,就能通过 flag 标志位来操作 t 线程是否要结束
        flag = true;
    }
}

在这个程序中,之所以修改 flag,能结束线程,完全取决于 t 线程内部的代码,代码中通过 flag 控制循环。因此,这里只是告诉这个线程要结束,线程是否真的结束,要看线程内部执行的代码。

2.2 自带标志位

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
           while (!Thread.currentThread().isInterrupted()) {
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("thread");
           }
        });
        t.start();

        Thread.sleep(3000);
        t.interrupt();
    }

上述代码中:
Thread.currentThread() 是在 t.run() 中调用的,此处获取的线程就是 t 线程。
currentThread 是 Thread 类的静态方法,通过这个方法,可以截取到当前线程,哪个线程调用这个方法,就得到哪个线程的引用。
t.interrupt 就可以终止线程了

在这里插入图片描述
调用 t.interrupt() 中断线程,此时 interrupt 要做两件事情:

1.把线程内部的标志位(boolean)设置成 true。
2.如果线程在 sleep,会触发异常,把 sleep 唤醒。

注意!!! 但是在唤醒的时候,还会再做一件事,把刚刚设置的标志位,再设置回 flase 。(清空了标志位) 这就导致,当异常被 catch 完了之后,循环继续往下执行。
所以,这里可以设置三种情况:

2.2.1 忽略中断请求

 Thread t = new Thread(() -> {
           while (!Thread.currentThread().isInterrupted()) {
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("thread");
           }
        });
        t.start();

        Thread.sleep(1000);
        t.interrupt();

2.2.2 立即响应中断请求

Thread t = new Thread(() -> {
           while (!Thread.currentThread().isInterrupted()) {
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   break;
               }
               System.out.println("thread");
           }
        });
        t.start();

        Thread.sleep(1000);
        t.interrupt();

在这里插入图片描述
当 catch 捕捉完异常之后,使用 break 立即退出循环,那么 t 线程就不会继续往下执行。

2.2.3 稍后中断

Thread t = new Thread(() -> {
           while (!Thread.currentThread().isInterrupted()) {
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();

                   // 可以在此处处理一些工作
                   try {
                       Thread.sleep(3000);
                   } catch (InterruptedException ex) {
                       ex.printStackTrace();
                   }
                   
                   break;
               }
               System.out.println("thread");
           }
        });
        t.start();

        Thread.sleep(1000);
        t.interrupt();

可以在 beak 跳出循环之前处理一些工作,之后再中断线程。

三、等待一个线程

使用 jon
线程是随机调度执行的,所谓等待一个线程,其实是处理两个线程之间的执行结束顺序。

下面代码,本来执行完 start 之后,t 线程和 main 线程就并发执行了。当主线程碰到了 join,main 线程发生了阻塞,一直阻塞到 t 线程结束,main 线程才继续往下执行。

public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("你好,thread");
            }
        });
        t.start();
        // 此处的 join 就是让主线程 main 等待 t 线程执行完毕,再继续执行 main 线程。
        t.join();

        System.out.println("main 主线程");
    }

另外,注意事项:
1.当主线程碰到 join 的时候,如果 t 线程已经执行完了,那么 main 线程就不会发生阻塞。

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread");
            }
        });
        t.start();
        Thread.sleep(5000);
        System.out.println("join 之前");
        t.join();
        System.out.println("join之后");
    }
  1. join 还提供了带参数的版本。可以在参数部分指定一个超时时间。(最长等待时间)
Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("你好,thread");
            }
        });
        t.start();
        // 此处的 join 就是让主线程 main 等待 t 线程执行完毕,再继续执行 main 线程。
        // 无参数,main 线程相当于 死等。
        // 有参数,1000 - 指定了 main 线程的最大等待时间
        t.join(1000);

        System.out.println("main 主线程");

四、获取线程引用

通过 Thread.currentThread 来获取线程的引用,哪个线程调用,获取的就是哪个线程对应的引用。

    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            System.out.println("thread");
            System.out.println(Thread.currentThread());
        });
        t.start();
        t.join();
        System.out.println(Thread.currentThread());
    }

五、休眠一个线程

使用 sleep 来休眠一个线程。
本质上就是让这个线程不去 CPU 上执行,不去参与 CPU 调度。

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                // 让 t 线程休眠 1000 ms
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread");
        });
        t.start();
    }

前面我们讲到 线程的就绪状态阻塞状态
在操作系统中,就绪状态是 PCB 在链表中都是 随叫随到 的。
在这里插入图片描述
当 线程A 调用了 sleep, A 就会进入休眠状态,把 A 从就绪链表中拿出来,放到另一个链表中,另一个链表中的 PCB 都是阻塞状态,暂时不参与 CPU 的调度和执行。
而操作系统每次需要调度,就直接去 就绪链表中 拿就好了。
在这里插入图片描述

注意
1.前面所说的,PCB 是使用链表来组织的,这里并不准确,而是使用一系列简单的链表,以这一系列链表为核心的数据结构。
2.一但线程进入了阻塞状态,就是对应的 PCB 进入阻塞队列中,此时无法参与调度。

这里有个问题:加入一个线程 sleep(1000),对应的 PCB 就要在阻塞队列中待 1000ms 这么久,当这个 PCB 回到了就绪队列,会被立即调度吗?
答案肯定不是的,虽然这里是 sleep(1000),但是实际上考虑是调度的开销,对应的线程是无法被唤醒之后就立即执行的,实际上的时间大海率是 大于1000ms 的。

六、线程的状态

Java 对线程的状态进行了细化。

6.1 NEW

new 代表的是,创建了 thread 类,还没有调用 start 方法。(内核里还没创建线程)

6.2 TIMED_WAITING

timed_waiting 表示的是,线程执行完毕了,Thread对象还在。

6.3 RUNNABLE

runnable 表示的是,可运行的,这里有两种含义;
1.线程正在 CPU 上参与调度执行。
2.线程对应 PCB 在就就绪队列中,随时都可以去 CPU 上执行。

6.4 WAIT、TIMED_WAITING、BLOCKED

wait,timed_waiting,blocked 这个三个表示的都是,线程处于阻塞阻塞状态,只是阻塞的原因不同。

6.5 线程状态转换

在这里插入图片描述

比如以下程序:

在这里插入图片描述


七、总结

以上就是今天要讲的内容,下一篇会继续续分享我对于多线程的其他认识,如果小伙伴们有喜欢作者写的内容或者对你有些帮助,希望能点个关注,支持一下小编,我也会继续努力,大家一起进步!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值