文章目录
- 一、一个简单的线程程序及运行
- 二、线程的创建
- 三、线程类——Thread详解
- 常见构造方法
- 常见几个属性
- 线程的启动——start()
- 线程的中断
- 线程的等待——join()
- 线程引用的获取
- 线程的休眠
- 四、多线程编程效率举例
一、一个简单的线程程序及运行
在写这样一个代码之前,我们需要对Thread类有一个简单/感性的认识
我们已经知道进程是OS进行资源分配的基本单位,线程是OS进行调度的基本单位。也就是说其实进程和线程都是基于操作系统的概念。又因为app的运行需要OS进行协助,所以,app中必须要有针对进程和线程的处理,而OS中也必须有关于对app发送过来的这些信息进程接收和加工,从而启动硬件,完成我们的任务。
简而言之,就是说OS和app之间需要有一个类似API一样的东西进行联系。但其实并不是一个就可以。因为我们经常会有这样的需求:不同的OS上运行相同的app;相同的OS上运行不同的app。
因此,实际上,我们的OS会提供一套这样的类库,每个app又会提供一套用于连接OS的类库,不同的OS上对应的app类库不同。
举个例子:
Thread 类是 JVM 用来管理线程的一个类。每个线程都有一个唯一的 Thread 对象与之关 联。
另外,Thread类是一个实现了Runnable接口的类,其中这个线程跑起来就是靠的这个接口中run方法。但是这个跑是在后台跑,我们并不能看到明显的效果,因此为了看到效果,我们需要重写run方法。这里我们采用的是继承Thread类重写run方法。
方法被调用了线程才能跑起来,怎么调用呢?这里用到的了Thread类对象的start方法。具体我会在后边讨论。也可以说只有start方法被调用了,这个线程才算创建成功了。
复盘一下,怎么样才算创建好一个线程呢?
①MyThread类继承Thread类,重写run方法②调用start方法
下边我们就来,写一下程序。
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("子线程:hello world");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t1=new MyThread();
Thread t2=new MyThread();
t1.start();
t2.start();
for(int i=0;i<100;i++){
System.out.println("主线程:hello");
}
}
}
(不完全运行截图)
除了程序运行结果,我们还可以通过jdk提供的线程的观察工具——jconsole.exe,观察线程。
具体使用方法是:
当然除了上述方法我们还可以通过下边这种方式进行观察:
此时,我们的一个简单的线程程序就写好了。
下边,我们来讨论几个问题:
1.主线程&子线程
1.什么是主线程?什么是子线程?
当一个程序启动时,就有一个进程被OS创建,同时进程中的某个线程也立即运行。这个线程就叫做主线程。
子线程就是由其他线程所创建的线程。包括主线程但不仅限于主线程。
2.主线程的重要性
(1)是产生其他子线程的线程
(2)它通常需要最后完成执行
补充:
main方法执行完了,主线程也就完了;同理,run方法完了,子线程也就完了。
这里边的t1、t2、t3都是子进程、main是主进程。
2.主线程和子线程的执行顺序
由于线程抢占式执行的特点,使得线程调度也具有随机性。即使代码时固定的。
因此,不同线程的执行顺序是不可预估的,具有随机性。
这是由OS内核实现决定的。
3.多线程程序和普通程序有什么区别
- 每个线程都是一个独立的执行流
- 多个线程之间是“并发”执行的。
- 代码固定,执行顺序不一定固定
虽然我们这里说是创建一个线程,但其实这个程序整体本质上还是一个多线程程序,main是一个主线程,创建的子线程包裹在主线程下。
二、线程的创建
上边我们已经写了一个简单的多线程程序,但线程的创建方法远不止一种。基本上有三种,另外还有两种基于此的拓展。
方法一:继承Thread类
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("子线程:hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t1=new MyThread();
t1.start();
for(int i=0;i<100;i++){
System.out.println("主线程:hello");
}
}
}
方法二:实现Runnable接口
public class ThreadDemo2 {
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("hello world");
}
}
public static void main(String[] args) {
Thread t=new thread.MyThread();
t.start();
System.out.println("hello");
}
}
方法三:匿名内部类创建Thread子类对象
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
System.out.println("hello world");
}
};
t.start();
System.out.println("hello");
}
}
方法四:匿名内部类创建Runnable子类对象
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
t.start();
System.out.println("hello");
}
}
方法五:lambda表达式创建Runnable子类对象【用的比较多】
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t=new Thread(()-> System.out.println("hello world"));
t.start();
System.out.println("hello");
}
}
三、线程类——Thread详解
每个执行流,需要有一个对象来描述,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
是java.lang包下的==>不需要导包
常见构造方法
方法 | 说明 | |
---|---|---|
Thread() | 创建对象 | |
Thread(Runnable target) | 使用Runnable对象创建线程对象 | |
Thread(String name) | 创建线程对象并命名 | |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 | |
Thread(ThreadGroup group,Runnable target) | 创建到指定线程组下 |
假如我们起名字mythread,那么通过jconsole观察到的线程名就是mythread,而我们在代码中的t是代码中的变量名
常见几个属性
常见属性有id、名称、状态、优先级,但是由于都是私有类型的,所以必须要有对应方法才能访问到,除此以外还需判断几个特性比如是否存在,是否是后台线程,是否被中断。
属性 | 获取方法 |
---|---|
id | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否是后台程序 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted |
其中:
-
id,线程的唯一标识,不同线程id不同。只能get,不能set.
-
名称,就是我们在构造方法中起的名字,在各种调试工具会用到。可set可get。get通过getName()方法得到,set通过Thread构造方法设置。
-
状态,表示当前线程所处的状态,下一篇会详细讨论(java中线程的状态会比OS原生状态更丰富一些)
-
优先级高的线程理论上 更容易被调度到。但基本上没啥用,影响因素太多。
-
后台进程:也就是守护线程。JVM会在一个进程的所有非后台进程结束后,才会结束运行
我们这里结合前台线程解释后台线程。
前台线程,会组织进程结束,前台线程没做完,进程完不了;
后台线程,不会组织进程结束,后台线程工作没做完,进程也可以结束。
易错:线程(都是死循环)调度过程中的交替打印和线程是前台和后台没有关系。是前台和是后台只跟setDaemon的设置有关。
思考:JVM进程什么退出?
1.所有的前台线程全部退出
2.与主线程无关,主线程的退出不影响
-
是否存活:判断OS中线程是否存活也就是run方法是否运行结束
t的回收:没有被引用,就会被GC回收。
GC:(垃圾回收( Garbage Collection )是一种自动管理内存的机制)
-
线程中断问题,下边会详细讨论
线程的启动——start()
调用start方法,才会真正在OS底层创建出一个线程。
run方法和start方法功能对比:
run方法:描述了线程要做的工作
start方法:真正在OS内核中创建了一个线程,并且让这个线程调用run方法
线程的中断
此处中断的含义:让当前线程停止执行。【不要和OS中的中断弄混】
注意:中断的意思不是让线程立即就停止,而是给线程发送一个通知,你要停止了,但是否真的停止,取决于线程具体的实现。
线程的中断常见的有两种方式:
(1)通过共享的标记来进行沟通
public class ThreadDemo6 {
public static boolean flag=true;
public static void main(String[] args) {
Thread t=new Thread(()-> {
while(flag){
System.out.println("hello world");
}
});
t.start();
//在主线程中可以随时通过flag变量的取值,来操作t线程是否结束
flag=false;
}
}
(2)调用interrupt方法进行通知【用的更多些】
这里是用的Thread自带的标志位进行中断。这个东西可以唤醒上边的sleep
public class ThreadDemo7 {
public static boolean flag=true;
public static void main(String[] args) {
Thread t=new Thread(()-> {
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello world");
}
});
t.start();
t.interrupt();
}
}
currentThread方法:是Thread类的静态方法,通过这个方法可以获得当前线程,哪个线程调用这个方法,就得到哪个线程的引用,类似于this引用。
isInterrupted方法:是在t.run中被调用的,这里获取的线程就是t线程。结果为true就是表示被终止,为false就是表示没有被终止(需要继续执行)。
interrupt方法:就是终止线程。这里t.interrupt()就是终止t线程。这里是在main方法中,也就是主线程通知子线程,它需要中断了,
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞/sleep,就以异常方式通知,否则设置标志位,变成true |
public static boolean interrupted()(少) | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isinterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清楚标志位 |
思考:“如果线程正在阻塞,就以异常方式通知,否则设置标志位,变成true”这句话怎么理解呢?
- 此时interrupt触发sleep内部的异常,导致sleep提前返回。
-
sleep被唤醒之后,又会把标志位设回false
public class ThreadDemo8 { public static void main(String[] args) throws InterruptedException { Thread t=new Thread(()-> { while(!Thread.currentThread().isInterrupted()){ System.out.println("hello world"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); Thread.sleep(1000); t.interrupt(); } }
特别注意:sleep存在时的线程中断
interrupt做的事情:
- 线程内部的标志位会设置成true
- 线程若在sleep,会出发异常,把sleep唤醒
sleep被唤醒之后做的事情:
把刚刚设置的标志位,再设回false(清空标志位)
效果:当sleep的异常被catch完后,循环还要继续执行
这也就说明了,我们上边为什么说是通知它终止,而不是它一定终止。除此以外,sleep被唤醒之后,还有其他处理方式。下边我们来总结一下。
(1)忽略中断请求
(2)立即响应中断请求【加了break】
(3)稍后进行中断【中间加了其他代码(任何)】
也就是说我们可以通过清除标志位之后的操作来决定线程t如何对待我们的中断请求。
与之类似的是,像wait、join等造成代码“暂停”的方法都会有类似的清除标志位的设定。
线程的等待——join()
线程的等待就是等待一个线程结束。
由于抢占式执行,我们无法判定两个线程谁先开始,但是我们可以通过调用join方法决定谁先结束。
方法 | 说明 |
---|---|
public void join() | 等待线程结束。如果仍未结束,那么就等待;反之立即返回 |
public void join(long miles) | 等待线程结束,最多等miles秒 |
public void join(long miles,int nanos) | 第二个的plus版本,体现在精度更高上 |
其中这里的nanos是纳秒单位
下边我们针对第一个join方法来举个例子:
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t=new Thread(()-> {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
System.out.println("join之前");
try {
//此时的join就是让当前的main线程等待t线程执行完再,继续执行
//此时main线程走到这里就停止了,我们称为它被阻塞了
//此时一定达到一个目的————t线程先于main线程结束
System.out.println();
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("join之后");
}
}
上述情况是,执行join时,t仍未结束,那么main线程就会阻塞;
但如果join不阻塞,main线程不会阻塞,会立即返回。
相对于第一种,第二种使用的更多。第一种没有参数,相当于“死等”。第二种是指定一个最长等待时间,超过一定时间,不再等。
而第三种,是第二种的plus版,其中nano是纳的意思,也就是说多少毫秒多少纳秒更加精准罢了。
线程引用的获取
方法 | 说明 |
---|---|
public static Thread currentThread() | 返回当前线程的引用 |
类似this引用,比较“智能”。哪个线程调用,就是哪个线程的引用。
线程的休眠
方法 | 说明 |
---|---|
public static void sleep(long miles) throws InterruptedException | 休眠当前线程miles毫秒 |
public static void sleep(long miles,int nanos) throws InterruptedException | 可以更高精度休眠 |
四、多线程编程效率举例
一般而言,程序分成使用cpu密集即需要大量运算和io密集即读写文件密集。
我们之前一直在说多线程编程优于多进程编程,但是那只是理论上,我们并没有直观的感受。
下边我们通过单线程和多线程实现对a和b分别自增100w次效率对比 ,来直观感受一下。
public class ThreadDemo9 {
public static void main(String[] args) {
//serial();//串行耗时
concurrency();//并行耗时
}
public static void serial(){
long start=System.currentTimeMillis();
long a=0;
long b=0;
for (long i = 0; i <100_0000_0000L; i++) {
a++;
}
for (long i = 0; i <100_0000_0000L; i++) {
b++;
}
long end=System.currentTimeMillis();
System.out.println("执行时间:"+(end-start)+"ms");
}
public static void concurrency(){
Thread t1=new Thread(()-> {
long a=0;
for (long i = 0; i <100_0000_0000L; i++) {
a++;
}
});
Thread t2=new Thread(()-> {
long b=0;
for (long i = 0; i <100_0000_0000L; i++) {
b++;
}
});
long start=System.currentTimeMillis();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long end=System.currentTimeMillis();
System.out.println("执行时间:"+(end-start)+"ms");
}
}
接下来,我们讨论几个问题:
-
为什么两个线程的结果不是一个线程耗时的一半?
线程的切换需要时间、不能保证两个线程总是并行执行,即使是cpu是多核的。
-
为什么要join?
线程调度是随机的,如果不join,有可能a和b都还没有完成全部的自增任务,就已经结束了。
-
start和end的定义时机特别注意