一、创建一个线程-start()
start 和 run 的区别:(经典面试题)
- run描述了线程要执行的任务,也可以称为“线程的入口”
- 此处 start会根据不同的系统,分别调用不同的API,来执行系统函数,在系统内核中,创建线程(创建PCB,加入到链表中)
- 创建好的新的线程,再单独来执行 run
- start的执行速度一般是很快的(创建线程,比较轻量),一旦- start执行完毕,新线程就会开始执行,调用- start的线程也会继续执行(- main线程)(开始兵分两路,并发执行)
- 调用 start 不一定非得是 main 线程调用,任何的线程都可以创建其他线程,如果系统资源充裕,就可以任意的创建线程(但线程不是越多越好)
public static void main(String[] args) {  
  
    Thread t = new Thread(() -> {  
        System.out.println("hello t");  
        Thread t2 = new Thread(() -> {  
            System.out.println("hello t2");  
        });        
        t2.start(); //hello t2  
    });    
    t.start();  //hello t
}
注意:
- 先执行的是
main创建的线程,因为main是程序执行入口- 所以先打印
hello t
- 一个 Thread 对象,只能调用一次 start,多次调用就会报线程状态异常  
线程的状态:
- 由于
Java中希望,一个Thread对象,只能对应到一个系统中的线程,因此就会在start中,根据线程状态做出判定。- 如果
Thread对象是还没有调用start的,此时就是一个NEW状态,之后就可以顺利地调用start- 如果
Thread已经调用过start,就会进入其他状态,- 只要不是
NEW状态,接下来执行start都会抛出异常
二、线程的终止(中断)
线程的终止
B 正在执行,A 想让 B 结束
- 其实核心就是 A 要想办法,让 B 的
run方法执行完毕,此时 B 就自然结束了- 而不是说 B 的
run执行一半,A 直接把 B 强制结束了
1. 通过共享的标记来进行沟通
public class Demo3 {  
    public static boolean isQuit = false;  
  
    public static void main(String[] args) throws InterruptedException {  
        boolean isQuit = false;  
        Thread t = new Thread(() -> {  
            while(!isQuit){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }  
            System.out.println("t 线程执行结束");  
        });  
        t.start();  
        Thread.sleep(2000);  
        //修改 isQuit 变量,就能狗影响到 t 线程的结束了  
        System.out.println("main 线程尝试终止 t 线程");  
        isQuit = true;  
    }  
}
通过改变变量,让
t线程跳出了while循环,最终线程终止
变量捕获
- 是
lambda表达式 / 匿名内部类的一个语法规则
isQuit和lambda定义在一个作用域中,此时lambda内部是可以访问到lambda外部(和lambda同一个作用)中的变量- 但是
Java的变量捕获有特殊要求,要求捕获的变量得是final/事实 final(虽然没有final修饰,但没有修改)
public class Demo3 {  
    //public static boolean isQuit = false;  
  
    public static void main(String[] args) throws InterruptedException {  
        boolean isQuit = false;  
        Thread t = new Thread(() -> {  
            while(!isQuit){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }  
            System.out.println("t 线程执行结束");  
        });  
        t.start();  
        Thread.sleep(2000);  
        //修改 isQuit 变量,就能影响到 t 线程的结束了  
        System.out.println("main 线程尝试终止 t 线程");  
        isQuit = true;  //会报错
    }  
}
将
isQuit定义在main内部后
- 由于
isQuit变量前后由false被改为true了,所以会报错- 若将最后一行注释掉,此时
isQuit就是一个事实final(虽然没有final修饰,但没更改) ,isQuit变量就会被捕获到,程序就不会报错
- 上面写成成员变量,就可以正常修改访问,因为走的语法是“内部类访问外部类的成员”,和“变量捕获”无关
lambda表达式本质上是一个“函数式接口”产生的“匿名内部类”,外面写的 “class Demo3“ 就是外部类
2. 调用 interrupt 方法来通知
- 初始情况下 Thread类里面有一个成员,boolean类型的interrupted
- 初始情况下,这个变量是 false,未被终止,但一旦外面的其他线程,调用一个interrupt()方法,就会设置上述标志位
public class Demo4 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t = new Thread(() -> {  
            //先获取到线程的引用  
            Thread currentThread = Thread.currentThread();  
            while(!currentThread.isInterrupted()){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }        
        });        
        
        t.start();  
        
        Thread.sleep(1000);  
        //在主线程中,控制 t 线程被终止,设置上述标志位  
        //将其由 false 设为 true,意味着循环就结束了,线程也就结束了  
        t.interrupt();  
    }
}
currentThread()是Thread类的静态方法,可以获取到调用这个方法的线程的实例,哪个线程调用,返回的引用就只想哪个线程的实例,类似this- lambda 的代码在编译器眼里,出现在
Thread t上方,此时t还没有被定义,所以不能直接用t调用isInterrupted()方法
- 由于判定
isInterrupted()和执行打印,这两个操作太快了,因此整个循环,主要的时间都是花在sleep上
main调用Interrupt的时候,大概率t线程正处于sleep状态中,此处Interrupt不仅仅能设置标志位,还能把刚才这个sleep操作给唤醒- 比如,当还在
sleep的时候,此时Interrupt被调用了,此时sleep就会被直接唤醒,并且抛出InterruptedException异常- 由于
catch中的默认代再次抛出异常,但没人catch,最终就到了JVM这一层,进程就直接异常终止了
综上: interrupt 不仅能设置标志位,还能唤醒 sleep
-  只不过由于 catch中生成的默认代码影响了执行的结果,导致我们看起来像整个进程都结束了
-  为了排除干扰,我们可以把 catch中的throw给干掉,换成一个打印“执行到catch操作”
public class Demo4 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t = new Thread(() -> {  
            //现货区到线程的引用  
            Thread currentThread = Thread.currentThread();  
            while(!currentThread.isInterrupted()){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    //throw new RuntimeException(e);   //屏蔽掉生成的默认代码干扰
                    System.out.println("执行到catch操作");  
                }            
            }        
        });  
        t.start();  
			  
        Thread.sleep(1000);  
        t.interrupt();  
    }
}
- 从运行结果可以看到,此时调用
interrupt把sleep唤醒了,触发异常,被catch住了。但是虽然catch住了,但是循环还在执行,看起来就像标志位没被设置一样
注意:
- 首先,
Interrupt肯定会设置这个标志位的- 其次,当
sleep等阻塞的函数被唤醒后,就会清空刚才设置的Interrupted标志位,下一轮循环判定的时候,就会认为标志位没有被设置,于是循环就会继续执行
因此,如果确实想要结束循环,结束线程,就需要在catch中加上return/break
清除标志位
- 清除标志位这个操作可以让编写 B 线程的程序员有更大的操作空间
- A 希望 B 线程终止,B 收到这样的请求之后,B 需要自行决定,是否要终止或立即执行还是稍后执行 ~~~ (B 线程内部的代码决定,与其他线程无关)
- 如果 B 想无视 A,就直接
catch,啥都不做,B 线程仍然会继续执行。sleep清除标志位,就可以使 B 做出这种选择,如果 sleep 不清除标志位的话,B 就势必会结束,无法写出让线程急速执行代码了- 如果 B 线程想立即结束,就直接在
catch中写上return/break,此时 B 线程就会立即结束- 如果 B 线程想要稍后再结束,就可以在
catch中写上一些其他的逻辑(比如释放资源、清理一些数据、提交一些结果… 收尾工作)。这些逻辑完成之后,再进行return/break
- 这是 JVM 内部的逻辑,需要结合 JVM 的源码才能看到这个操作
三、等待一个线程-join()
操作系统针对多个线程的执行,是一个“随机调度,抢占式执行的过程”。因为我们写代码的时候不希望是随机,希望是确定,所以期望通过一些变成手段来对这里的随机进行干预
- 线程等待就是在确定两个线程的“结束顺序”。因为我们无法确定两个线程调度执行的顺序,但是可以控制结束的先后顺序
- 让后结束的线程等待先结束的线程即可
- 此时后结束的线程就会进入阻塞,一直到先结束的进程真的结束了,阻塞才解除
- 比如现在有两个线程
A和B- 在
A线程中调用B.join(),意思就是让A线程等B线程先结束,然后A再继续执行- 此处的
B是被等待的一方
public class Demo5 {  
    public static void main(String[] args){  
        Thread t = new Thread(() -> {  
            for (int i = 0; i < 3; i++) {  
                System.out.println("这是线程 t");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("线程 t 结束");  
        });        
        t.start();  
        //让主线程等待 t 线程  
        System.out.println("main 线程开始等待");  
        try {  
            t.join();   //执行到这里,main线程阻塞等待
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }   
         
    	//当 t 线程执行结束后,join才会返回,main才会继续执    
        System.out.println("main 线程等待结束");  
    }
}
 //t 线程先结束,main 线程后结束
- 上述代码是 main先等待,然后t执行了半天才结束,此时main在阻塞。
- 如果调整一下,让 t先结束,然后main才开是join,这个时候也不会出现阻塞,因为t线程已经结束了,而join就是用来确保被等待的线程先结束,若已经结束了,join就不必再等待了
- 任何的线程之间都是可以相互等待的,不是说必须得 main线程等待别人
- 任何线程,也不一定是两个线程之间,一个线程可以同时等待多个别的线程,或者若干线程之间也能相互等待
public class Demo6 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 2; i++) {  
                System.out.println("hello t1");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("t1 结束");  
        });  
        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 2; i++) {  
                System.out.println("hello t2");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("t2 结束");  
        });        
        t1.start();  
        t2.start();  
        System.out.println("main 开始等待");  
        t1.join();  
        t2.join();  
        System.out.println("main 结束");  
    }
}
- 此时两个
join都是写在main里面的,所以都是main线程等待t1和t2,t1和t2之间没有等待关系- 若在
t2中写t1.join,则t2需要等待t1线程先执行完
- 上述 join 都是无参的,意思是“死等”,“不见不散”,被等待的线程,只要不执行完,就会持续阻塞
- 上述死等操作,其实不是一个好的选择,因为一旦被等待的线程代码出现了一些 bug 了,就可能使这个线程迟迟无法结束,从而使等待线程一直阻塞而无法进行其他操作
- 所以在实际开发中,都会设置一个“超时时间”,最多等多久,就是 join 的参数 
这个精度和操作系统相关, 像 Windows, Linux 系统,线程调度开销比较大;计算机中还有一类系统——“实时操作系统”,就能把调度开销尽可能降低,开销小于一定的误差要求,从而可以做到更精确。一般用于航空航天,军事… 不过实时操作系统是舍弃了很多功能换来的实时性
四、获取当前线程引用
想在某个线程中,获取到自身的 Thread 对象的引用,就可以通过 currentThread 来获取到
public class Demo7 {  
    public static void main(String[] args) throws InterruptedException {  
        
        Thread mainThread = Thread.currentThread();  
        
        Thread t = new Thread(()->{  
            //需要在 t 中调用主线程.join  
            System.out.println("t 线程开始等待");  
            try {  
                mainThread.join();  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }            
            System.out.println("t 线程结束");  
        });        
        t.start();  
        Thread.sleep(1000);  
        System.out.println("main 线程结束");  
    }
}
//打印顺序:
//t 线程开始等待
//main 线程结束
//t 线程结束
- 任何线程中,都可以通过这样的操作,拿到线程的引用
- 任何需要的时候,都可以通过这个方法来获取到
五、休眠当前线程
- Thread.sleep可以让调用的线程阻塞等待,是有一定时间的
- 线程执行 sleep,就会使这个线程不参与CPU调度,从而把CPU资源让出来给别人使用
- 也把 sleep这种操作,称为“放权”,放弃使用CPU的权利
- 有的开发场景中,发现某个线程的 CPU占用率过高,就可以通过sleep来进行放权改善
- 虽然 CPU就是给程序用的,但是有的程序可能包含很多线程,这些线程之间是有“轻重缓急”的
- 虽然线程的优先级就可以产生影响,但是优先级的影响是比较有限的,也可以通过 sleep来更明显的影响到这里的CPU占用






















