Thread类
什么是Thread类??
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
基于上述API:
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装. 也就是这个Thread实例和操作系统的线程是一 一对应的。(有一种类似引用类型的效果,通过这个东西我能操作到另一个东西)
java的跨平台性:JVM封装操作系统很多功能,不同的操作系统对应不同的JVM,java提供很多API让我们去学习。
main线程和t线程执行演示
创建Thread实例并不创建线程,还得start启动一下才是真正在操作系统创建线程PCB,由新线程执行run方法;run方法执行完,这个新的线程就自然销毁。但是Thread实例并不能做到同步销毁,得等到它指向为空才会gc回收。
创建过程:(了解即可)
Java用start()启动线程,start()调用start0()本地方法(通过JNI调用),继而调用Linux中pthread_create()创建线程(这里调用操作系统API。内核创建PCB,并把要执行指令交给这个PCB),Java线程中的run()作为参数传入pthread_create() (当PCB调度到CPU上执行的时候,也就执行到run的代码)。
class mythread extends Thread{
@Override
public void run() {
System.out.println("hello world");
}
}
public class tesssss{//创建线程的方法1
public static void main(String[] args) {//主线程
Thread t=new mythread();
t.start();
}
}
这个创建的线程打印hello world和直接用打印hello world是用不同区别。。直接打印它是使用java进程里一个主线程
我们用Thread.sleep休眠1秒分别用main线程和t线程打印试试看
class mythread extends Thread{
@Override
public void run() {
while (true) {
System.out.print("2");
try {
Thread.sleep(1000);//让当前线程休眠1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class tesssss{//创建线程的方法1
public static void main(String[] args) throws InterruptedException {//主线程
Thread t=new mythread();
t.start();
while (true){
System.out.print("1");
Thread.sleep(1000);
}
}
}
执行结果:谁先谁后是说不准的
我们的操作系统调度线程是抢占式执行,哪个线程先调度是不确定,这取决于操作系统调度器。。这里的执行顺序是“随机的”;虽然这里能设置优先级,在内核里并非是随机的。但是干扰因素太多,到我们看到的效果就只能认为是随机的。所以我们的更改线程优先级的方法作用并不常用。
查看线程
C:\Program Files\Java\jdk1.8.0_192\bin jdk找到我们bin目录,打开jconsole.exe.
选择创建的tesssss进程,然后连接—不安全连接
小疑问:main线程结束销毁后,Thread-0会销毁吗?这里涉及到守护线程与非守护线程,后面细说.
创建线程的方法
1:继承Thread 重写run
class mythread extends Thread{
@Override
public void run() {
//
}
}
public class tesssss{
public static void main(String[] args) throws InterruptedException {//主线程
Thread t=new mythread();
t.start();
2:实现 Runnable 接口;重写run方法
//2:实现 Runnable 接口
class Myrunable implements Runnable {
@Override
public void run() {
System.out.println("1");
}
}
public class test0 {
public static void main(String[] args) {
Runnable r1=new Myrunable(); //描述了这个任务
Thread t1=new Thread(r1); //把任务交给线程执行
t1.start();
}
}
3:使用Lambda 表达式
本质:实现了Runnable接口的匿名类的实例,并将其作为参数传递给Thread对象。
public class test {
public static void main(String[] args) {
Thread t=new Thread(()-> {
System.out.println(123);
});
t.start();
}
}
4:Callable 接口
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果
Runnable的run方法描述的任务返回值是void;而使用Callable的call方法描述任务的返回值是泛型参数:有时候我们需要线程计算一个结果返回;需要使用Callable创建线程比较合适。
代码示例:Runnable的run方法
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
}
};
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}
这个代码很繁琐;得先阻塞main线程;等待t1线程通知回来执行得到结果。
代码示例:Callable的call方法
1:创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
2:把 callable 实例使用 FutureTask 包装一下.
3:创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
call 方法, 完成计算.;计算结果就放到了 FutureTask 对象中
4:在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
为什么不能直接把call实例直接传入到Thread里呢?
Thread的构造方法签名接受的是Runnable;为了能接收Callable类型的任务,需要借助ExecutorService接口以及Future和FutureTask等类。FutureTask实现了RunnableFuture接口,而RunnableFuture同时实现了Runnable和Future接口,从而可以将其传递给Thread的构造方法。
为什么Callable一般搭配FutureTask来使用
搭配FutureTask来使用:Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定;FutureTask 就可以负责这个等待结果出来的工作
Thread类用法
名字对你调试有帮助,这个名字是系统的线程名。线程默认的名字是thread-0,1,2,3等
Thread常见的属性
1:线程ID
1:ID :getid() 线程唯一标识,不同线程不会重复
2:线程名称
2:名称 ; 获取名字getname,创建名字
3:线程状态
3:状态:java的线程状态比操作系统要丰富很多,后面细说
4:线程优先级
4:优先级:作用并不大,,具体到操作系统很多影响因素;把控还是不稳
5:前后台线程
5:是否后台线程
//JVM会在一个进程所有的前台线程结束后才结束运行,手动创建和main都是默认前台,JVM自带的是后台。用setDaemon设置为后台线程
//前台线程:会阻止进程结束,前台线程的工作没做完,进程是不会结束的
//后台线程:不会阻止进程结束,后台线程工作没做完,进程是可以结束的
setDaemon(false) 改前台线程。(怎么记这个方法其它和后台是false还是true:你就想前台;是不是有个人在站着收银;F是不是就站着的)
setDaemon (true) 改后台线程 当把除了main都改成后台时,main结束后,进程就结束。就像下面的代码,打印了几个1后main结束,进程就直接结束,不会等到你t线程的run方法执行完
6:是否存活isAlive
6:是否存活;也就是run方法是否执行完
判断线程是否在内核干活,干完活就消失,重新变为false,没创建的时候也是false。。内核把run执行完,线程销毁,pcb释放,Thread t这个对象不一定被释放。t这个对象得到它引用不指向任何对象,才回收。
7:线程中断
7:中断:(让线程干一半活就不干了);不是让线程立即停止,而是通知线程你该停止,是否真的要停止取决于具体的代码写法。为什么不设置一终止就立即停止呢,而是使用标志位呢? 其实两个线程是并发执行,mian调用t终止,main并不知道t执行到哪了。这样终止达不到我们想要的效果。
1:使用标志位来控制线程是否要停止
public class test1 {
public static Boolean flag=true;
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (flag) {
System.out.print("a");
count++;
}
});
t.start();
Thread.sleep(1000);
flag=false;
System.out.println(count);
}
}
//我在打印1秒的a后我修改flag让它停止
2:但是第一种的方式,可能会不能及时响应;为什么说不及时呢?因为如果线程在等待、睡眠就没法检测到你这个自定义的标志位的状态变了。使用Thread自带的标志位进行判定
public class test2 {
public static int count =0;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
// while (!Thread.currentThread().isInterrupted())
// //Thread.currentThread()是Thread类的静态方法,获取到当前调用这个方法线程的引用
// //isInterrupted(),true表示被终止,false表示未被终止,还要继续
// System.out.print(1);
int i=0 ;
while (i<1000){
System.out.print(Thread.currentThread().isInterrupted());
i++;
count++;
}
});
t.start();
Thread.sleep(1);
t.interrupt();//终止t线程,main调用,
//虽然我们调用这个方法阻止了线程但是并没有真真正正的阻止线程,
// 而是把Thread.currentThread().isInterrupted()变成ture,线程还在执行。
// 如果我们把这个条件换在我们要阻止的语句上使run方法直接结束,就终止了(注释掉的写法)
System.out.println(count);
}
}
运行的结果是这样得注意;我们在t.start立马就执行221次循环,然后休眠的1毫秒过程,我们的主线程直接往下执行大概率比调度t现在执行打印速度快,所以就会先打印221,因为这个执行速度是非常快的,但是我们代码并没有设置能用这个标志终止的写法,所以还会继续执行下去。
interrupt这个方法还能唤醒sleep,如果线程在休眠(比如我在t线程休眠10秒;然后在main线程调用这个方法),此时调用这个方法会把线程唤醒,让sleep提前返回,在我们抛异常的触发异常结束。sleep唤醒还会清空标志位。
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
唤醒sleep,标志被清除,还会继续往下执行;在catch加个break就能解决,当抛异常就结束循环,不给你清除标记又继续循环的机会。这样子写也能说是标志位起作用了;让休眠的代码异常结束;走catch的代码进而结束循环。
等待线程:t.join
等待线程:因为我们的线程是随机调度,所以谁先执行完和谁后执行完是不确定的,等待线程能让你这个线程等另一个执行完再继续往下执行。
class myrunnable implements Runnable{//测试等待线程,Runnable接口写法
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("123");
}
}
public class test3 {
public static void main(String[] args) throws InterruptedException {
Runnable r1=new myrunnable();
Thread t=new Thread(r1);
t.start();
t.join();//在谁那里调用这个方法,就是谁进入阻塞、这里main线程里面调用;所以main线程先阻塞直到这个t线程执行完
System.out.println("abc");
}
}
如果开始执行join时,我的线程已经结束了是什么情况呢,比如我在t.start后面休眠个5秒,t线程结束两秒后才进行t.join。
那么join不会阻塞,直接返回。
join方法:无参版本,等到你执行完
有参版本,你可以设置一个时间,指定最大等待时间时间
设置一个等待时间,就像等不到的人还会再等吗?当然把等待的时间放在学习上(main线程利用时间继续执行下面的代码)
class myrunnable implements Runnable{//测试等待线程,Runnable接口写法
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("123");
}
}
public class test3 {
public static void main(String[] args) throws InterruptedException {
Runnable r1=new myrunnable();
Thread t=new Thread(r1);
t.start();
t.join(2000);
System.out.println("abc");//如果abc先执行,等两秒我就不等了,先打印abc后,然后你t线程再自己慢慢执行
//多线程之间执行的顺序有变数,但是单个线程之间,顺还是固定的,从上到下
}
}
获取当前线程引用 currentThread
currentThread()方法;获取当前线程引用;返回Thread类型,静态的类方法(public static Thread currentThread();)在哪个线程调用,就能获得哪个线程的。
public class test6 {
public static void main(String[] args) {
Thread t=new Thread(()->{
Thread t2=Thread.currentThread();
System.out.println(t2.getName());
},"liao");
Thread t1=Thread.currentThread();
System.out.println(t1.getName());
t.start();
// System.out.println(Thread.currentThread());//直接打印得到的结果是toString方法写的结果,引用类型
}
}
输出结果是main liao
休眠方法sleep
本质让这个线程不参与调度,不去cpu上执行。从就绪队列加入到阻塞队列进入阻塞状态。
就绪队列:执行的时候,cpu在这里选一个去调度执行
阻塞队列:休眠就是加入到这里来。这里的队列并不是数据结构的队列,只是一个名称,pcb底层是一个以一系列列表为核心的数据结构,不是简单的链表,可能是链表或者红黑树。。。
sleep(1000)实际上会大于1000毫秒,因为你在唤醒之后并不是就立即被执行,还等等候cpu的调度执行。
线程的状态
java对线程状态进行细划;
1.NEW 创建Thread对象,但是还没调用start(还没在操作系统创建对应线程 PCB)
2.RUNNABLE 可运行的;两种情况(操作系统区分不了到底是哪两种,即便我们知道是哪一种,你也做不了什么改变,这里就不关你什么事)
a情况:正在cpu上执行
b情况:在就绪队列,随时可以去cpu上执行
阻塞:但是阻塞,下面345因为不同原因的阻塞
3.WAITING
4.TIMED_WAITING:当你在线程中加个sleep,并且刚好执行到休眠的时候就是这个状态
5.BLOCKED
6.TERMINATED 操作系统的PCB已经创建完成,Thread也还在。(当这个线程PCB销毁了,代码的t对象虽然还没立即被回收,因为做不到java和操作系统线程同步回收,但是这个t也是失去它的意义和作用,就需要这样一个状态标识这个t无效)
获取线程状态:t.getState();
线程状态转换
代码:可以看到结束之后的状态是TERMNATED ,约定一个线程只能start一次,也没法再通过多线程完成其它任务。
但是你还能利用这个对象去调用它一些方法属性
public class test6 {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
},"liao");
System.out.println("启动之前状态" +t1.getState());
//启动之前状态NEW
t1.start();
System.out.println("启动中"+t1.getState());
//启动中RUNNABLE
t1.join();
System.out.println("启动之后状态" +t1.getState());
//启动之后状态TERMINATED
}
}
感受一下多线程与单线程的执行效率区别
有cpu密集(大量加减乘除等算术运算)和io密集(读写文件、控制台、网络),先体验一下cpu密集。体验一下在大量运算下两者区别:
两个变量自增1000w次 (为了更好体现;使用currentTimeMillis 获取到当前系统的 ms 级时间戳.)
a方案:一个线程先对a自增,然后对b自增
b方案:两个线程,分别对a和b自增
public class test7 {//测试在大量的运算下多线程与单线程的差别,1000万次不够
static int a1=0;
static int b1=0;
static int a2=0;
static int b2=0;
public static void main(String[] args) {
long start1=System.currentTimeMillis();
test1();
long end1=System.currentTimeMillis();//获取系统时间的毫秒数
System.out.print(end1-start1+"毫秒");
long start2=System.currentTimeMillis();
test2();
long end2=System.currentTimeMillis();
System.out.print(end2-start2+"毫秒");
}
public static void test1(){
for (long i = 0; i <1000_000_0000L ; i++) {
//100亿次,得用long,数值得后面加个大写或者小写L
a1++;
}
for (long i = 0; i <1000_0000000L ; i++) {
b1++;
}
}
public static void test2(){
Thread t1=new Thread(()->{
for (long i = 0; i <1000_000_0000L ; i++) {
a2++;
}
});
Thread t2=new Thread(()->{
for (long i = 0; i <1000_000_0000L ; i++) {
b2++;
}
});
}
}
可以看到光在时间上就相差如此之巨大。其实这里有点问题,所以这里两个线程时间并不是正确的,因为我们的线程是并发执行,可能你在执行两个线程的运算时,我main线程就趁这段时间把后面的start和end的时间戳计算完。所以这里两个线程的实现代码不能这么写,得等两个线程执行完才计算时间戳
public class test7 {//测试在大量的运算下多线程与单线程的差别,1000万次不够
static int a1=0;
static int b1=0;
static int a2=0;
static int b2=0;
public static void main(String[] args) throws InterruptedException {
long start1=System.currentTimeMillis();
test1();
long end1=System.currentTimeMillis();
System.out.println(end1-start1+"毫秒");
test2();
}
public static void test1(){
for (long i = 0; i <1000_000_0000L ; i++) {
//100亿次,得用long,数值得后面加个大写或者小写L
a1++;
}
for (long i = 0; i <1000_0000000L ; i++) {
b1++;
}
}
public static void test2() throws InterruptedException {
long start2=System.currentTimeMillis();
Thread t1=new Thread(()->{
for (long i = 0; i <1000_000_0000L ; i++) {
a2++;
}
});
Thread t2=new Thread(()->{
for (long i = 0; i <1000_000_0000L ; i++) {
b2++;
}
});
t1.start();
t2.start();
t1.join();//main方法等待他们线程的执行完再记时间戳
t2.join();
long end2=System.currentTimeMillis();
System.out.print(end2-start2+"毫秒");
}
}
修改后的结果,计时得在方法里面计,还得等待线程执行完,不然main线程不老实,老是会先把时间戳先计算完了。两线程并不意味着时间缩短一半,因为调度可能是并行(在两个核心上执行)也可能是并发(在一个核心上执行)。我们join两个等待,它会两个线程一起执行,如果t2先执行完,还会等待t1执行完再返回。t1,t2谁先谁后执行完是不知道,但是main一定是等待他们两执行完再继续开始后面的语句。
IO密集上:程序进行了一些耗时的 IO 操作,阻塞了界面的响应这种情况下使用多线程也是可以有效改善的.(一个线程负责 IO,另一个线程用来响应用户的操作)