多线程编程(基础一)
文章目录
创建线程
通过代码认识线程
- 即使是最简单的main方法在运行的时候,也涉及到线程了,一运行程序,先产生了一个java进程,这个进程里会有一个线程调用main方法,调用main方法的线程也叫主线程。
- 上述代码一运行,会创建多个线程,除了调用main方法的主线程,还有其他线程,比如有关垃圾回收的线程等,所以这么简单的程序也是一个多线程程序。
创建一个线程
一个创建线程的方法:写一个子类,继承Thread类,并重写父类的run()方法
- 这里重写run()方法,是为了明确新创建的出来的线程的任务。
- myThread.start()是真正的开始创建线程(创建了线程也不一定立刻上CPU执行,得等操作系统调度),如果将myThread.start()改为myThread.run(),则不会创建新的线程,而是在原来的线程里调用run()方法
我们从代码中看到的是先创建了线程,后打印了"hello main",但是执行结果却恰恰相反,这是为什么呢?
- 其实并不是先执行完我们创建的这个线程,再接着执行调用main方法的主线程。因为实际上每个线程都是一个独立的执行流,两个线程是并发执行的,所以哪个线程先执行完,这个是不确定的,是随机的。到底哪个先执行完和线程本身以及操作系统的调度有关。
- 这两个线程可能被分配到CPU的同一核心上(通过时间片轮转的方式执行),也可能被分配到CPU的不同核心上,这个也是不确定的。
- 这里看到的先打印main后打印thread,大概率是我们写的这个线程的创建需要一定的时间开销,而与此同时主线程会继续执行,所以可能先打印出了main,后打印的thread
- Process finished with exit code 0 代表进程结束了,退出码是0。操作系统中用进程的退出码来表示进程的执行结果。0代表执行完毕,结果正确,非0代表执行完毕,结果不正确,还有一种情况是进程中途崩溃了,返回的很可能是个随机值。
进程A创建了进程B,那么就说进程A是进程B的父进程,但是线程之间没有这种关系(即使一个线程在执行的过程中创建了另一个线程),我们认为线程之间是平等的。 👈
查看线程
先给代码加上一个死循环,让进程一直不结束,然后运行bin目录下的jconsloe.exe就可以看到当前进程中的线程了
- 这里我们也可以看到当前进程中有很多线程,main是调用main方法的主线程,Thread-0是我们自己创建的线程,其他线程是辅助线程。
创建线程的几种方式:
- 第一种就是我们上面写的,写一个类继承Thread并且成重写run()方法
- 创建一个类,实现Runable()接口,重写run()方法
- 这种写法相比与第一种是把任务和线程分离开了,Runable代表要执行的任务,Thread实例代表一个线程,start()代表线程开始创建并执行。
- 使用匿名内部类继承Thread,实现继承,重写run()方法,new内部类实例三部一体
- 使用匿名内部类实现Runnable接口,让实现接口,重写方法,new内部类对象三部一体
使用了内部类就会比前两种代码更为简洁。
- 使用lambda表达式
单多线程速率对比
单线程方式:
public class Test {
public static final long COUNT = 20_0000_0000;
//单线程
public static void func1(){
long a = 0;
long beg = System.currentTimeMillis();
for (long i = 0; i < COUNT; i++) {
a++;
}
a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
long end = System.currentTimeMillis();
System.out.println((end-beg)+"ms");
}
//多线程
public static void func2(){
long beg = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
long a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2 = new Thread(() -> {
long a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println((end-beg)+"ms");
}
public static void main(String[] args) {
func2();
//func1();
}
}
- 由时间来看,多线程确实比单线程快了不少
- 但是我们看到,时间上并不是两个线程并发执行,时间上并不是砍半原因是:
- 线程的创建也需要时间开销,
- 两个线程在执行上不一定是一直并行执行,也可能是一部分时间并行执行,一部分时间并发执行(因为线程多了之后,在一段时间内,各个线程上CPU执行的时间就会相应减少)
- 线程的调度也需要时间开销
多线程使用场景:
- CPU密集型场景(代码中CPU运算较多),使用多线程可以更好的CPU资源
- IO密集型场景,IO操作几乎不消耗CPU,这时候就可以 给CPU找点活干,使用多线程,避免闲置,
Thread类及常见方法
构造方法
- Thread()在第一种创建线程的方式中用到了
- Thread()在第二种方式中用到了
- Thread(Runnable target,String name) 是给创建的线程命名
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("haha");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t = new Thread(runnable,"我的线程");
t.start();
}
在用jconsole命令打开的窗口中就可以看到这个自己命名的线程
属性
- 当创建一个线程时,就会随之为这个线程创建这些属性(在Thread类中)
- ID : 线程的身份标识,线程的身份标识有好几个,操作系统内核的PCB中有身份标识,用户态线程库里有个身份标识(pthread,操作系统提供的线程库),JVM里有个身份标识(JVM Thread类底层也是调用的phread库)
- 名称: 线程的名字,默认是Thread0,Thread1,也可以自己指定名字
- 状态:JVM中的线程的的状态(JVM中有自己的状态体系,比操作系统内核中的状态更丰富),不是内核中PCB中的状态
- 优先级:获取到优先级
- isDaemon() ,daemon称为守护线程,也叫后台线程,线程分前台线程和后台线程,一个线程创建出来默认是前台线程,只有前台线程执行完了,进程才会结束,进程结束时不会管后台线程是否结束。
package thread;
public class Demo1 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable,"我的线程");
thread.start();
System.out.println(thread.getId());
System.out.println(thread.getName());
System.out.println(thread.getState());
System.out.println(thread.getPriority());
System.out.println(thread.isDaemon());
}
}
注意:上面这些操作都是获取到该线程某一时刻的信息(比如某一时刻的状态),所以每次运行获取到的线程的状态都是不确定的
启动线程
启动线程用start()方法,调用start()方法是线程真正创建执行,run()方法是描述一个任务,创建一个Thread实例是准备好线程
中断线程
有的线程任务是比较复杂的,执行时间比较长,有时候我们想让线程提前结束,就需要中断线程,中断线程就是在一个线程中控制另一个线程结束
中断线程有两种方式:
第一种方式:自己设立一个标志位:
package thread;
public class Demo2 {
private static boolean flg = false;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while(!flg){
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行结束");
}
};
Thread thread = new Thread(runnable,"我的线程");
thread.start();
Thread.sleep(5000);
System.out.println("控制线程结束");
flg = true;
}
}
- 通过自己设立的标志位,来控制线程结束
第二种方式:使用Thread类中的标志位
package thread;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//break; //立即退出
/*try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;*/ //稍后退出
}
}
}
};
Thread t = new Thread(runnable,"我的线程");
t.start();
Thread.sleep(5000);
t.interrupt();
}
}
- Thread.currentThread() 会返回当前线程对应的Thread对象(哪个线程中调用了这个方法就会返回这个线程对应的Thread对象),用这个对象调用isInterrupted()会返回当前对象中的标志位,默认是false
- interrupt() :如果t线程当前没有处在阻塞状态,就会修改标志位为true;如果t线程正在处在阻塞状态,就会让t线程内部产生阻塞的方法,如sleep()抛出异常,但是并不会修改标志位
- 从控制台中可以看到捕获并打印了异常,说明调用interrupt() 时t进程正处于阻塞状态。但是打印完异常,然后t进程还是接着执行,并没有中断线程,因为我们在catch中只是打印了异常,并没有做处理。也就是说当线程处于阻塞状态时,会触发异常,但是到底是否中断,何时中断,是由我们程序员自己控制的(也就是改变catch中的实现逻辑),
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread t = new Thread(runnable,"我的线程");
t.start();
Thread.sleep(5000);
t.interrupt();
注意: t线程大部分时间是处于阻塞状态,很小一部分时间是处于正常运行状态,(始终在阻塞和正常运行状态切换的)所以我们调用interrupt() 时,大概率会产生异常,也有很小的概率会修改标志位。如果我们把Thread(5000)注释掉,则大概离会修改标志位
==总结:==interrupt() 并不会立马让t进程中断,当调用interrupt()时,如果t线程是非阻塞状态,则修改标志位为true, 然后等t 线程再次运行到类似while(!Thread.currentThread().isInterrupted()) 的判定条件时,则不进入循环;如果t线程是阻塞状态,则会立即在让t线程产生阻塞状态的方法处抛出异常,然后我们可以在catch() 中实现相应的逻辑。
==思考:==为什么线程阻塞状态下要走异常?
因为线程阻塞时,如果不走异常就要等sleep()完,然后走到while()中的判定条件才能让线程中断,假如极端情况下,sleep()要等一小时,那得一小时后才能执行到判断条件让线程中断,这样就不太合理,==我们期望一调用interrupt() ,该线程就能立即做出反应,而不是过了好大一会才做出反应,==对于阻塞中的线程,它不能上CPU,也就不能做出反应,所以让异常把sleep()唤醒,然后执行catch()中的逻辑,做出相应的反应。
等待线程
有时候需要等另一个线程执行完,才能执行这个线程,这时候就需要用到join(),当一个线程因为调用join()而进入阻塞状态时,对应的的是WAITING状态。
package thread;
public class Demo4 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
int i = 3;
while(i>0){
System.out.println("哈哈");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i--;
}
}
};
Thread t1 = new Thread(runnable);
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("嘻嘻");
}
}
上面代码中,我们在主线程中调用了t1.join(),意思就是让主线程等待t1执行完(让主线程处于阻塞状态),主线程再接着往下执行。如果主线程执行到t1.join()时,t1已经执行完毕了,那主线程不会阻塞,接着往下执行就行了。(这是join()的两种行为)
调用join,使某个线程等待,然后再interrupt让某个线程中断,也会抛出异常
使用join(long millis)还可以设定最大等待时间
获取当前线程的引用
Thread.currentThread();
在哪个线程中调用这个静态方法,就可以返回哪个线程的Thread实例
休眠线程
- 休眠线程用的是sleep()方法,那sleep() 具体是干什么的,我们看下面的图
- 在操作系统内核中有很多PCB(一个PCB对应一个线程),这些PCB是通过双向链表连接起来的(有多个链表),大致分为就绪队列(操作系统调度时就会从就绪队列中选线程上CPU执行)和阻塞队列,就绪队列中的PCB对应的线程是已经准备好的,随时可以上CPU执行;阻塞队列中PCB对应的线程是因为一些原因还没有准备好上CPU执行的,所以不能上CPU执行的。当我们在正常运行的线程中调用sleep()时,该线程对应的PCB就会从就绪队列转移到阻塞队列,当sleep()结束后,PCB就会从阻塞队列转移到就绪队列中。
- 假如sleep(1000),1000ms之后PCB就会从阻塞队列转移到就绪队列,虽然转移到就绪队列中了,但是什么时候上CPU执行还得看调度器的行为,所以sleep(1000)真实的休眠时间是不少于1000ms的
- 同一进程中的线程对应的PCB其实还是有一些关联关系的,PCB中还有一个进程组id,这些id是一样的;另外,这些PCB中的内存指针和文件描述符表是一样的
- 操作系统只认PCB,不认进程或者线程,只不过一些PCB中有关联关系(内存指针和文件描述符表是一样的)我们把这几个有关联关系的PCB对应的线程的总和称为进程
- 在Linux中没有TCB(线程控制块),只有PCB表示线程,其他系统中可能有TCB
还有一个yield()方法其实和sleep(0)是一样的,就是让当前线程短暂放弃CPU,排到就绪队伍的后面位置,所以和sleep()效果是一样的。
问题:当线程上CPU执行时,其PCB还在就绪队列中吗?
线程的状态
线程的状态是一个枚举类型Thread.State,可以通过以下这种方式获取到线程的状态。下面这几种状态是JVM中的状态,并不是操作系统中的状态,JVM中的状态比操作系统中的状态更复杂一些
NEW
- NEW状态代表创建了Thread实例,但是还没有start(),也就是系统内核中还没有线程。创建Thread实例到创建线程之间状态为NEW
package thread;
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
System.out.println(thread.getState());
thread.start();
}
}
就绪状态
RUNNABLE
- 就绪状态,就绪状态其实包含了两种状态:一种是正在CPU上执行,一种是已经准备好了(对应的PCB在就绪队列中),准备上CPU执行,这俩种状态都是RUNNABLE状态
package thread;
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
System.out.println(thread.getState());
}
}
- 现在拿到的状态是RUNNABLE状态,但是也有可能拿到TIMED_WAITING状态,因为获取线程状态时,线程可能在运行,也可能在sleep()
阻塞状态
阻塞状态包含了三种状态:BOLCKED WAITING TIMED_WAITING (这三种状态都是阻塞状态,只不过导致阻塞的原因不相同)。当线程进入这三种状态时,线程对应的PCB都会进入内核中相应的阻塞队列,但是这几种状态下的线程对应的PCB是进入一个阻塞队列还是不同的阻塞队列,这个不确定,得看系统的具体实现。操作系统内核是不区分这三种状态的,这三种状态时JVM里的状态,操作系统衡量线程的状态时另外的方式,没有JVM这么细。这三种状态的进入方式不同,唤醒方式也不同。
BOLCKED
当两个线程针对同一个对象竞争锁时,其中没有竞争到锁的线程就会进入BLOCKED状态,然后等线程释放锁之后,由操作系统来把BLOCKED状态的线程唤醒。
WAITING
当一个线程中调用wait()方法后,该线程就会释放锁,然后进入WAITING状态,等待另一个线程中调用notify()方法后,该线程才有可能被唤醒,
TIMED_WAITING
当在线程中调用sleep()方法时,线程就是TIMED_WAITING状态,等sleep的时间过了之后,操作系统就会把该线程唤醒。
package thread;
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
System.out.println(thread.getState());
}
}
- 那当然这里也有可能是RUNNABLE状态,因为俩线程并发执行
TERMINATED
- TERMINATED代表线程执行结束了
package thread;
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
thread.join();
System.out.println(thread.getState());
}
}
线程状态转换
主干道是:NEW -> RUNNABLE -> TERMINATED
RUNNABLE时会根据特定任务进入支线,这些支线都是阻塞状态
这三种阻塞状态进入方式不同,阻塞时间不同,被唤醒方式也不同
之前学的isAlive()方法可以认为除了NEW和TERMINATED状态其他都是活着的