多线程的概念与创建详解

本文详细介绍了Java多线程的概念,包括进程与线程的区别,创建线程的不同方法(继承Thread、实现Runnable、匿名内部类和Lambda表达式),线程的常见属性(ID、状态、优先级和后台线程),以及线程终止的两种方式(手动标志位和Thread类的中断)。最后讨论了join方法的使用以控制线程执行顺序。
摘要由CSDN通过智能技术生成


前言


一、多线程概念

进程与线程

1.多进程有一个明显的缺点:进程太重了.
1)消耗资源更多
2)速度更慢
一旦需要大规模频繁的创建和销毁进程,开销就比较大了,需要给进程分配资源的。

2.若在创建进程的时候,只分配一个简单的pcb, 而不去分配后续的这些内存硬盘资源
这样就既能够并发的执行任务,又能够提升创建/销段的速度
轻量级进程=>线程(Thread)

3.一个进程可以包含多个线程。这个进程中的多个线程,共同复用了进程中的各种资源(内存,硬盘),但是这些线程各自独立的在cpu上进行调度。因此,线程就可以既能够完成"并发编程”的效果,又可以以比较轻量的方式来运行线程,同样也是通过PCB来描述的。

4.同一个进程中的这些线程共用同一份资源(内存指针+文件描述符表相同)
但是每个线程独立去cpu上调度(状态,上下文,优先级,记账信息等…各自有各自的一份)

5.进程,是操作系统进行资源分配的基本单位。线程,是操作系统进行调度执行的基本单位.

6.多线程的问题:不稳定,多个线程没有彼此独立互不干扰(如多线程冲突;如某一线程异常未处理可能导致整个进程终止)

题:谈谈,进程和线程的区别和联系.

1.进程包含线程.都是为了实现并发编程的方式.线程比进程更轻量~~
⒉.进程是系统分配资源的基本单位.线程是系统调度执行的基本单位.
创建进程的时候把分配资源(虚拟地址空间,文件描述符表)的工作给做了,后续创建线程,直接共用之前的资源即可.

3.进程有独立的地址空间,彼此之间不会相互影响到,进程的独立性=>系统稳定性
多个线程共用这一份地址空间.一个线程一旦抛出异常,就可能会导致整个进程异常结束.=>多个线程之间容易相互影响

二、线程的创建

1.第一个简单线程详解

package Tread;

class MyThread extends Thread{
    @Override
    public void run(){
        while (true) {
            System.out.println("hello01");
        }
    }
}
public class Tread01 {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        myThread.start();//创建出新的进程
        //myTread.run();run只是上面的入口方法(普通的方法)并没有调用系统api,也没有创建出真正的线程来.在主线程内部。

        while (true) {//原来的进程继续执行
            System.out.println("hello02");
        }

    }

}


1.当点击运行程序的时候就会先创建出一个java进程.,这个进程中就包含了至少一个线程.这个线程也叫做主线程,也就是负责执行main方法的线程.
2.start会调动系统api,在系统内核中把线程对应的pcb 啥的给创建出来并管理好,新的线程就参与调度了。
3.使用start的方式执行,此时,这俩循环都在执行。两个线程,分别执行自己的循环.
这两个线程都能参与到cpu的调度中,这俩线程(这俩while循环)并发式的执行
在这里插入图片描述
在这里插入图片描述

2.观察线程的方式

当创建出线程之后。也是可以通过一些方式直观的观察到的一~

1) idea的调试器

在这里插入图片描述

2) jconsole

官方在jdk 中给咱们提供的一个调试工具
jdk所在:
在这里插入图片描述
在这里插入图片描述
双击后,就可以显示所有java线程:
在这里插入图片描述
点连接,然后点确定不安全连接,然后点线程,就能够看见。
在这里插入图片描述

3.创建线程的多种方法

1.创建一个类,继承Thread,重写run方法.

package Tread;

class MyThread extends Thread{
    @Override
    public void run(){
        while (true) {
            System.out.println("hello01");
            //这里只能try-catch,不能throws,因为这里是方法重写,父类没有抛异常。
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Tread01 {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        myThread.start();
        //这里能throws
        while (true) {
            System.out.println("hello02");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

这俩线程在进行sleep 之后,就会进入阻塞状态.
当时间到,系统就会唤醒这俩线程,并且恢复对这俩线程的调度.
当这俩线程都唤醒了之后,谁先调度,谁后调度,可以视为是"随机"的(抢占式执行)

2.创建一个类,实现Runnable,重写run方法.

package Tread;
class MyRunnable implements Runnable{
    @Override
    public void run(){
        while(true) {
            System.out.println("hello1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Tread02 {
    public static void main(String[] args) {
        MyRunnable runnable=new MyRunnable();
        Thread t=new Thread(runnable);
        t.start();
        while(true) {
            System.out.println("hello2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

Thread这里是直接把要完成的工作,放到了Thread 的run方法中.
Runnable这里,则是分开了,把要完成的工作放到Runnable 中,再让Runnable和Thread配合~
把线程要执行的任务和线程本身,进一步的解耦合了

3.继承 Thread,重写run,基于匿名内部类.

package Tread;

public class Tread03 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while (true) {
            System.out.println("hello2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1)创建了一个子类,这个子类继承自Thread,但是,这个子类,是没有名字的!!(匿名),另一方面,这个类的创建,是在Tread3这个类里面.
2)在子类中,重写了run方法.
3)创建了该子类的实例.并且使用t这个引用来指向.

4.实现 Runnable,重写run,基于匿名内部类

package Tread;

public class Tread04 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1)创建了一个 Runnable的子类(类,实现 Runnable)
2)重写了run方法
3)把子类,创建出实例,把这个实例传给Thread的构造方法.

5.使用lambda表达式,表示 run方法的内容

package Tread;

public class Tread05 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true) {
                System.out.println("hello1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("hello2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

lambda表达式,本质上就是一个"匿名函数”。
这样的匿名函数,主要就可以用来作为回调函数来使用.
回调函数,不需要咱们程序猿自己主动调用(要知道文件名)。而是在合适的时机,自动的被调用
此处的这个"回调函数”就是在线程创建成功之后,才真正执行.

给线程重命名

在这里插入图片描述

6.基于Callable interface

也是一种创建线程的方式.
在这里插入图片描述

使用Callable 不能直接作为Thread的构造方法参数.
FutureTask

在这里插入图片描述

线程的创建其他写法还有:基于线程池

4.Thread的几个常见属性

1)ID: getld() 线程的身份标识 (在JVM这里给线程设定的标识)
在这里插入图片描述
2)状态:getState()
3)优先级:getPriority() 设置/获取优先级,作用不是很大(线程的调度,主要还是系统内核来负责的)
4)是否后台线程:isDaemon()
后台线程/守护线程后台线程不影响进程结束;
前台线程前台线程,会影响到进程结束,如果前台线程没执行完,进程是不会结束的。
创建的线程,默认是前台线程。通过setDaemon显式的设置成后台。
(一个进程中所有的前台线程都执行完,退出了,此时即使存在后台线程仍然没执行完,也会随着进程一起退出)

 public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true) {
                System.out.println("hello1");
            }
        });
        t.setDaemon(true);//设置为后台线程了
        t.start();

    }

在这里插入图片描述
立即退出,因为将t设置为后台线程了,只有main线程在,main运行完就结束了。

5)是否存活:isAlive()
Thread 对象,对应的线程(系统内核中)是否存活
Thread 对象的生命周期,并不是和系统中的线程完全一致的!!
一般,都是Thread对象,先创建好,手动调用start,内核才真正创建出线程。消亡的时候,可能是thread对象,先结束了生命周期(没有引用指向这个对象)(如突然掉电),也可能是 thread对象还在,内核中的线程把 run 执行完了,就结束了

5.线程终止

1)程序猿手动设定标志位

通过这个手动设置的标志位,来让run尽快结束~

package Tread;

public class Tread05 {
    public static boolean isQuit=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while (!isQuit) {
                System.out.println("hello1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        isQuit=true;//修改前面的标志位
        System.out.println("线程终止");

    }
}

lambda引入了"变量捕获"这样的机制
lambda内部看起来是在直接访问外部的变量,其实本质上是把外部的变量给复制了一份,到lambda里面.(这样就可以解决:后续真正执行lambda的时候,局部变量可能已经被销毁了)
但是,变量捕获这里有个限制,要求捕获的变量得是final (至少是看起来是final)(这个变量不能被修改)

注意:此处是静态成员变量的使用,不涉及变量捕获。

2)直接Thread类现成的标志位

public class Tread05 {

    public static void main(String[] args) throws InterruptedException {

        Thread t=new Thread(()->{
            //Thread.currentThread()就是t
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        t.interrupt();//修改前面的标志位为true
        System.out.println("线程终止");

    }
}

在这里插入图片描述
在这里插入图片描述

手动设置标志位是没办法唤醒sleep的。
当sleep被唤醒之后,程序猿接下来可以有以下几种操作方式:
1.立即停止循环,立即结束线程
2.继续做点别的事情,过一会再结束线程~catch 中执行别的逻辑,执行完了再break
3.忽略终止的请求,继续循环。不写break

6.等待一个线程join()

操作系统的调度线程的过程,是"随机"的,无法确定,线程执行的先后顺序
等待线程,就是一种规划线程结束顺序的手段。
AB两个线程,希望B先结束,A后结束,此时就可以让A线程中调用B.join()的方法.
此时,B线程还没执行完,A线程就会进入“阻塞”状态.就相当于给B留下了执行的时间.B执行完毕之后,A再从阻塞状态中恢复回来,并且继续往后执行
如果A执行到B.join 的时候,B已经执行完了,A就不必阻塞了,直接往下执行就可以了

public class Tread06 {
    public static void main(String[] args) {

        Thread b=new Thread(()->{
            for (int i = 0; i <5 ; i++) {
                System.out.println("bbbbbb");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("b结束了");
        });
        Thread a=new Thread(()->{
            for (int i = 0; i <3 ; i++) {
                System.out.println("aaaaa");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                b.join();//b如果还没结束,阻塞等待,直到b结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("a结束");

        });
        a.start();
        b.start();
    }
}

阻塞 =>让代码暂时不继续执行了.(该线程暂时不去cpu上参与调度)
sleep 也能让线程阻塞.阻塞是有时间限制的.
join的阻塞,则是"死等"“不见不散”
在这里插入图片描述
更推荐第二种带有更大等待时间的,超过这个时间就不等了。

sleep, join,wait…产生阻塞之后,都是可能被interrupt方法唤醒的~~这几个方法都会在被唤醒之后自动清除标志位(和sleep类似的)

···


  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值