目录
5、使用lambda表达式来表示要执行的任务,这种方法是不用写run方法的。
一、为什么要引入线程?
答:因为随然进程实现了并发式执行,提高了效率,但是频繁地创建销毁,开销还是会非常大,为了解决这个问题,程序员发明了线程。
二、关于线程的说明
创建线程不用去申请资源,销毁线程也不用去释放资源,因为线程产生在进程内部,用的是创建进程分配到的资源。
三、线程与进程的关系
线程于进程是包含关系,一个进程包含一个线程或多个线程;
进程创建好后,分配到了一定的系统资源,然后在这个进程里面创建线程,此时,线程用的是之前创建好的进程分配好的资源。
四、线程和代码有什么关联?
可以认为,一个线程就是代码中的一个执行流。
执行流:按照一定的顺序来执行一组指令。
五、进程和线程之间的区别和联系【经典面试题】
答:1.进程是包含线程的,一个进程可以有一个线程,也可以有多个线程;
2.进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位;
3.每个进程都有自己独立的虚拟地址空间,同一个进程的多个线程之间共用这个虚拟地址空间。
六、使用Java来操作线程
在Java中,使用Thread这个类的对象来表示操作系统中的线程。
PCB在操作系统内核中描述线程,Thread类在Java代码中描述线程。
/Thread是java标准库中描述的一个关于线程的类
//常见的方式就是自己定义一个类继承Thread
//重写Thread中的run方法,run方法就表示线程要执行的具体任务
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello word");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t = new MyThread();
//执行start方法,会在操作系统中真的创建一个线程出来(内核中创建一个PCB,加入到双向链表中)
//这个新的线程,就会执行run中所写的代码
t.start();
}
}
start和run的区别【经典面试题】
首先它俩的执行结果一样,但是他们在内核的执行过程却是天差地别。
start:在点击程序运行后,主函数会在操作系统内核中创建一个PCB,它里面就是main方法,接下来执行t.start(),会在操作系统内核中再创建一个PCB,并执行run方法中的具体任务,此时这两个PCB属于同一个进程;
run:在点击程序运行后,主函数会在操作系统内核中创建一个PCB,它里面也是main方法,接下来直接执行t.run(),执行的是run方法中的具体任务,并没有重新创建一个新的PCB。
以下代码能更好的说明start和run之间的区别
class MyThread2 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t=new MyThread2();
//t.start();
//t.run();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
什么是休眠?
答:即sleep,它的作用是让线程暂时放弃CPU,等待一段时间后再重新参与调度。
七、线程创建的多种方式【经典面试题】
1、通过继承Thread,重写run(上面写的第一个代码)
2、实现Runnable接口,重写run
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.start();
}
}
3、继承Thread,重写run,使用匿名内部类的方式
匿名内部类的意思是没有类名的内部类,在一个类中被创建出实例来,适用于只用一个实例的情况。
public class ThreadDemo4 {
public static void main(String[] args) {
//()后面加上{},这种语法就是匿名内部类
//相当于是创建了一个匿名的类,这个类继承了Thread
//此处new的实例其实是new了这个新的子类的实例
Thread t=new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
}
}
这种写法本质上和创建一个类,然后再继承Thread没区别,只不过写法更简单了。
4、实现Runnable接口,重写run,使用匿名内部类
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
}
}
这种写法和第三种写法有些相似,别搞混了
5、使用lambda表达式来表示要执行的任务,这种方法是不用写run方法的。
很多语言都有lambda表达式,lambda表达式本质上就是一匿名函数,它是通过函数式接口的方式实现的。
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
注意这三种写法的区别
以上五种创建线程的方式,其实本质上都相同,都是借助Thread类,在操作系统内核创建新的PCB,加入到操作系统内核的双向链表中,区别仅仅是Java语法层面的区别。
八、如何更清晰明了的看见Java中新创建的线程?
在点击运行时,打开JDK内置的jconsole.exe,就可以看到正在运行的线程了。
如果打开jconsole.exe,看不到正在运行的线程,可以以管理员方式运行,这样就可以看到了。
main就是那个main方法,Thread-0就是我们新建好的线程。
并且我们还可以在图的右侧看到代码的相关信息。
如果写的程序挂了,我们可以通过这种查看线程的方式找出问题所在。
九、多线程的优势
这里要注意一个点:当写非常大的数字的时候,要用下划线来做出分割,要不然不容易数清。
我们可以举一个例子来说明多线程的优势
假设要求a自增十亿次,b自增十亿次,分别通过串行和并发(多线程)的方式执行
public class ThreadDemo7 {
private static final long count=10_0000_0000L;
//利用串行的方式对a和b进行自增
public static void serial(){
//获取当前系统的毫秒级时间戳
long beg=System.currentTimeMillis();
long a=0;
for (long i = 0; i <= count; i++) {
a++;
}
long b=0;
for(long i=0; i<=count; i++){
b++;
}
long end=System.currentTimeMillis();
System.out.println("共用时间为:"+(end-beg));
}
//利用并发的方式对a和b进行自增
public static void concurrency() throws InterruptedException {
//获取当前系统的毫秒级时间戳
long beg=System.currentTimeMillis();
Thread t1=new Thread(){
@Override
public void run() {
long a=0;
for (long i = 0; i < count; i++) {
a++;
}
}
};
t1.start();
Thread t2=new Thread(){
@Override
public void run() {
long b=0;
for (long i = 0; i < count; i++) {
b++;
}
}
};
t2.start();
//此时t1、t2、serial()是并发执行的,因此serial()执行完就结束了,并不会等t1、t2结束
//为了解决这个问题我们需要引入join()方法
//这个方法的作用是保证t1和t2都执行完了之后再结束计时,然后让serial执行完
//准确地讲是join等待对应的线程结束
//即t1和t2没执行完之前,join方法就阻塞等待
t1.join();
t2.join();
long end=System.currentTimeMillis();
System.out.println("共用时间为:"+(end-beg));
}
public static void main(String[] args) throws InterruptedException {
serial();
concurrency();
}
}
我们可以清楚地看到时间的差异
那么问题来了,为什么不是整整减少一半时间呢?
答:串行执行一个线程执行了二十亿次,中间调度了若干次,并发执行两个线程各自执行十亿次,中间也调度了若干次,这两种执行方式中间调度的若干次是不确定的,因此时间只是减少了,但不是减少一半。
这里我们也需要注意同一个进程里面的多个线程共享这个进程分配到的资源,这个共享资源指两方面:
1.内存:线程1和线程2公用同一份内存(其实指的是同一个变量)
2.文件:线程1打开的文件,线程2也能去使用
对于进程来说,进程1就不能访问进程2的变量
十、Thread类的具体用法(所包含的一些属性和方法)
Thread的name,就是为了方便调试
public class ThreadDemo8 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"myThread");
t.start();
}
}
在JDK内置的jconsole.exe中可以看到
myThread就是自己起的名字,图中右边的11代表正在执行代码的行数
我们这里又要注意的 一点是写代码的时候一定要关注程序是不是方便调试
getID()
getState()
它说的是线程的状态,和“进程的状态”类似,存在的意义都是为了辅助线程调度
getPriority()
它说的是线程的优先级,和“进程的优先级”类似
要注意,此处的状态和优先级和内核PCB中的状态、优先级并不完全一致。
关于后台线程
需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
展开来说就是创建的一个线程,默认不是后台线程,此时如果main方法结束了,线程还没结束,JVM进程不会结束;如果当前线程是后台线程,此时如果main方法结束了,线程还没结束,JVM进程会直接结束,同时这个后台进程也就结束了。
isAlive
意思是是否存活,表示内核中的PCB是不是销毁了,也可以说系统中的线程是不是销毁了。
isInterrupted
是否被中断
isDaemon
是否是后台线程
下面的代码是上面各属性的实际应用
public class ThreadDemo9 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while(true){
//打印当前线程的名字
//Thread.currentThread()这个静态方法,可以获取到当前的线程实例
//哪个线程调用这个方法,哪个线程就能获取到对应的实例
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
},"myThread");
t.start();
//接下来打印这个线程的各个属性
System.out.println("id为:"+t.getId());
System.out.println("名字为:"+t.getName());
System.out.println("状态为:"+t.getState());
System.out.println("优先级为:"+t.getPriority());
System.out.println("后台:"+t.isDaemon());
System.out.println("中断:"+t.isInterrupted());
System.out.println("存活:"+t.isAlive());
}
}
执行结果为
start()方法
在前面已经说过,可在前面查看
中断一个线程
让一个线程结束,即让内核中的PCB被销毁
中断一个线程的关键在于让线程对应的入口方法执行完,这里的入口方法指的是继承Thread类,重写的run方法,实现Runnable接口,重写的run方法,lambda里面写的方法
像这个代码,run方法执行完就结束了
但很多情况下,线程不一定能这么快执行完run()方法
比如run()方法里面有个死循环
实际开发中我们能够控制线程随时结束的方法有
1.使用一个Boolean变量来作为循环结束标记
public class ThreadDemo10 {
private static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
while(flag){
System.out.println("线程正在运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程结束运行");
}
};
t.start();
//主循环中等待3秒
Thread.sleep(3000);
//3秒之后把flag改为false
flag=false;
}
}
执行结果为
2.刚才是使用自己定义的变量作为循环标记,其实还可以使用标准库里内置的标记
获取线程内置的标记位:线程的isInterrupted()判定当前线程是不是要结束循环
修改线程内置的标记位:t.interrupt()来修改这个标记位
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
//默认情况下isInterrupted()值位false
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程正在运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
Thread.sleep(3000);
//这个操作把Thread.currentThread().isInterrupted()设为true
t.interrupt();
}
}
结果出现了异常
原因是这里的interrupt方法可能有两种行为:
1.如果当前线程正在运行中,此时就会修改Thread.currentThread.isInterrupted()为true
2.如果当前线程正在sleep/wait/等待锁...此时就会触发InterruptedException
应该在run方法的循环中加break
public class ThreadDemo11 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
//默认情况下isInterrupted()值位false
while(!Thread.currentThread().isInterrupted()){
System.out.println("线程正在运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();//在新版本中这里的异常变了,应该改为这个,否则break加不了
//在这里加个break循环就可以结束了
break;
}
}
}
};
t.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这个操作把Thread.currentThread().isInterrupted()设为true
t.interrupt();
}
}
isInterrupted()是Thread的实例方法,与它类似的还有interrupted(),这个是Thread的类方法(加static),这二者的区别是使用这个静态的方法,会自动清除标记位,例如调用interrupt()方法,把标记位设为true,就应该结束循环,当调用 interrupted()来判定标记位的时候,就会返回true,同时把标记位再改回false,下次调用interrupted()的时候就返回false;当调用 isInterrupted()来判定标记位的时候,也会返回true,但是不会对标记位进行修改,后面调用interrupted()的时候仍然是返回true。
线程等待
由于多个线程的调度顺序不是我们程序员可以控制的,所以我们为了实现顺序可控,就引入了线程等待,它的作用是控制线程结束的先后顺序
举个例子
Thread t=new Thread();
t.join();//程序执行到这个代码,调用这个代码的线程就会阻塞等待
阻塞等待的意思是代码将停在这里,操作系统短时间内不会把这个线程放在CPU上执行了。
其实Java基础语法中的输入也是阻塞等待
Scanner scanner=new Scanner(System.in);
scanner.nextInt();//从键盘读入数据
读入的操作是非常不确定的,也许现在,也需要几天后,因此这也是一种阻塞等待
举例说明
public class ThreadDemo12 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程正在运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
try {
System.out.println("线程开始执行");
t.join();
System.out.println("线程执行结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
再代码中出现了t.start()和t.join()
t.start()的意思是在操作系统内核中新建了一个PCB,并执行其中的run方法
t.join()的意思是它是在main()方法中的一个线程,这个代码使得它对应的PCB必须阻塞等待t.start()新建的PCB,等到t.start()新建的PCB执行完后,t.join()对应的PCB才开始执行。
最后执行结果为
假设执行到join方法之前,t.start()创建好的线程已经结束了会怎么样呢?
执行代码为
public class ThreadDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程正在运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
Thread.sleep(7000);
try {
System.out.println("线程开始执行");
t.join();
System.out.println("线程执行结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
结果为
join有两个版本,一个是有参数版本,它的意思是有最大等待时间,只要超了这个时间,立刻就走,一秒都不多呆;还有一个是无参数版本,这个就是死等,等不到绝不罢休。
在实际开发中,往往使用有参数版本,使用无参数版本是不明智的。
获取当前线程引用
使用currentThread
public class ThreadDemo13 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getId());
System.out.println(this.getId());
}
};
t.start();
}
}
代码中看起来this和Thread.currentThread()没有区别,但实际上,没区别的前提是使用继承Thread,重写run方法的方式创建线程,才是没区别。
如果当前是通过实现Runnable接口,重写run方法或者是用lambda的方式创建线程,就不行了。
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId());
System.out.println(this.getId());
}
});
t.start();
}
在这个代码中,this指向的是Runnable实例,而不是Thread实例。此时也就没有getId()这样的方法了。
所以以后为了不必要的错误,都是用Thread.currentThread()方法就好了。
休眠当前进程
当某个进程调用sleep/join/wait/等待锁...就会把对应的PCB放到阻塞队列中。
线程的状态
用于辅助系统对于线程进行调度这样的属性
NEW: Thread对象创建出来了,但是内核的PCB还没创建出来
RUNNABLE: 当前的PCB也创建出来了,同时这个PCB随时待命(就绪),这个线程可能是 正在CPU上运行,也可能在就绪队列中排队。
TIMED_WAITING:表示当前的PCB在阻塞队列中等待,这里的等待是带有结束时间的等待,
Thread.sleep(1000);这个操作就会触发这个状态。
WAITING: 线程调用了wait方法,正在阻塞等待,此时处在WAITING状态(死等),除非 是其他的线程唤醒了该线程。
BLOCKED: 线程中尝试进行加锁,发现锁已经被其他线程占用了,此时该线程也会阻塞等 待,这个等待会在其他线程释放锁之后被唤醒。
TERMINATED: 表示当前PCB已经结束了,但Thread对象还在,此时调用获取状态得到的就是 这个状态。
TIMED_WAITING、WAITING、BLOCKED都表示线程处于阻塞等待,不过结束等待的条件不一样。
上述状态具体表现代码为
public class ThreadDemo14 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
System.out.println(t.getId()+":"+t.getState());
t.start();
System.out.println(t.getId()+":"+t.getState());
Thread.sleep(2000);
System.out.println(t.getId()+":"+t.getState());
t.interrupt();
Thread.sleep(1000);//这行代码是为了给系统反应时间
System.out.println(t.getId()+":"+t.getState());
}
}
结果为
理解线程状态最大的意义在于未来调试一些多线程的程序。
需要注意的是,当前这几个状态都是Java的Thread类的状态,和操作系统内部PCB里面的状态并不是完全一致的。这里主要是考虑到Java的跨平台。
总结
创建线程的目的是为了更好的提高效率,但线程也不是越多越好,如果线程太多,就会造成CPU资源紧缺,反而可能会造成效率下降。
可能造成线程安全问题。
如果一个进程里面某个线程抛出了异常,并且没有合理的catch,可能会导致整个进程都异常退出。因此对多线程程序的编写也就提出了更高要求。