线程相关概念
-
程序
- 为完成特点的任务,用某种语言编写的一组指令的集合,简单来说就是我们的代码
-
进程
- 定义:进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间,当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
- 内存分配:进程有自己独立的地址空间。在一个进程内,任何内存的改变(如变量的更改)不会影响到其他进程。
- 资源开销:创建、销毁进程的开销较大,因为操作系统需要为进程分配和回收资源。
-
通信:进程间通信(IPC)比较复杂,常用的IPC机制包括管道、消息队列、共享内存、信号等。
-
执行方式:进程可以包含多个线程,进程的每个实例独立执行。
程序是运行的代码,进程是程序运行之后可以动的活起来的东西
-
线程
- 定义:线程是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源。
-
内存分配:同一进程中的线程共享地址空间、全局变量和文件描述符等资源。这意味着一个线程对某些变量的更改会影响到同一进程中的其他线程。
-
资源开销:创建、销毁线程的开销较小,因为线程之间共享进程的资源,不需要像进程那样分配独立的资源。
-
通信:线程之间的通信更简单,因为它们共享相同的内存空间,可以通过共享变量直接进行通信。
-
执行方式:线程是并发执行的,可以同时执行多个线程,以提高程序的并发性和性能。
线程是由进程创建的,是进程的一个实体,一个进程可以拥有多个线程,如下图。
下载的程序运行作为进程,进程里的三个下载对应三个线程。
每个进程拥有独立的内存空间,内存空间内变量不相互影响。
一个进程可以拥有多个线程,线程共享地址空间,线程之间相互影响。
-
并发:
- 同一个时刻,多个任务交替进行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发
-
并行:
- 同一时刻,多个任务同时进行,多核cpu可以实现并行。
线程的基本使用
- 创建线程的两种方式
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法。
继承Thread类重写run方法
//1.当一个类继承了Thread类,该类就可以当做线程使用 //2.我们会重写Thread方法,写上自己的业务代码 //3.run Thread类 实现了 Runnable接口的run方法
class Cat extends Thread{
int times=0;
@Override
public void run() {
while (true) {
//该线程每隔一秒,在控制台输出“喵喵,我是小猫咪”
System.out.println("喵喵,我是小猫咪"+(++times));
//让该线程休眠一秒 ctrl+alt+t
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(times==80){
break;
}
}
}
}
我们设计一个猫猫类继承Thread,并且重写run方法写上自己的业务逻辑,实现功能,通过While(true)一直调用,直到times==80退出调用
接下来我们在main方法中调用并且创建该线程启动进程(start启动线程)
public class Thread01 {
public static void main(String[] args) {
//创建Cat对象,可以当做线程使用
Cat cat=new Cat();
cat.start();
}
}
我们来看结果
我们进一步讨论一下线程的运行
多线程机制
启动进程之后在main里面先开启线程,通过start方法又开启了一个线程
只有最后一个线程结束,你的进程才会结束,而非主方法结束,进程才会结束
比如主线程里面打印60次,Thread线程里面打印80次,还是等80次打印完才退出进程。
为什么是Start方法
我们看我们在主线程里面调用线程的代码
public static void main(String[] args) {
//创建Cat对象,可以当做线程使用
Cat cat=new Cat();
cat.start();
}
我们明明在写线程方法的时候用的是run,为什么在调用的时候用的是start呢
- run方法就是一个普通的方法,没有真正的启动一个线程,就会把run方案执行完毕,才向下执行
- 当main线程启动一个子线程Thread-0,主线程不会阻塞,会继续执行
- 主线程与子线程交替执行
如图所示
在start()方法中,真正实现多线程效果的是start0方法,这是一个native方法(本地方法),由JVM调用。
实现Runnable接口,重写run方法。
在Runnable接口中没有Thread类的start方法,所以我们不能直接调用。
而是
创建了Thread对象,把dog对象(实现Runnable),放入Thread
代码示例
/通过实现Runnable类来开发线程
public class Thread02 {
public static void main(String[] args) {
Dog dog=new Dog();
//dog.start();这里不能调用start
//创建了Thread对象,把dog对象(实现Runnable),放入Thread
Thread t1=new Thread(dog);
t1.start();
}
}
class Dog implements Runnable {//通过实现runnable接口,开发线程
int count=0;
@Override
public void run() {
while (true){
System.out.println("小狗汪汪叫。。hi"+(++count)+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(count==10){
break;
}
}
}
}
运算结果
当然,关于Runnable的使用涉及到代理模式,我们接着展开说明
Runnable代理模式
先模拟一个线程代理类,里面有一个属性Runnable。
在构造器里面接收一个实现了Runnable接口的对象target
Thread里面的start方法里面调用的start0方法是真正实现多线程的方法。
class ThreadProxy implements Runnable{
private Runnable target=null;
@Override
public void run() {
if(target==null){
target.run();
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start(){
start0();
}
public void start0(){
run();
}
}
多个子线程案例
我们通过Runnable接口进行实现,我们直接来看代马
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread t3 = new Thread(t1);
Thread t4 = new Thread(t2);
t3.start();
t4.start();
}
}
class T1 implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
//每隔一秒输出“hello,world”,输出10次
System.out.println("hello,world" + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 10) {
break;
}
}
}
}
class T2 implements Runnable {
int count = 0;
@Override
public void run() {
while (true) {
//每隔一秒输出“hi”,输出5次
System.out.println("hi " + (++count));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (count == 5) {
break;
}
}
}
}
在main线程里面又创建了两个线程,不难理解。
如果main线程先退出,不影响子线程1,2的进行
继承Threadvs实现Runnable的区别
我们来通过具体案例去理解这个
public static void main(String[] args) {
T1 t1 = new T1();
T2 t2 = new T2();
Thread t3 = new Thread(t1);
Thread t4 = new Thread(t1);
在通过实现Runnable 所创建的类的实例化可以将它的实例化对象用于多个线程,适合多个线程共享一个资源的情况
我们再来看一个题目
使用多线程,同时模拟三个窗口同时售票100张
我们直接看代码
通过继承Thread类实现
public class SellTicket {
public static void main(String[] args) {
//测试
SellTicket01 sellTicket01=new SellTicket01();
SellTicket01 sellTicket02=new SellTicket01();
SellTicket01 sellTicket03=new SellTicket01();
sellTicket01.start();//启动收票线程
sellTicket02.start();
sellTicket03.start();
}
}
class SellTicket01 extends Thread{
private static int ticketNUm = 100;
@Override
public void run() {
while(true){
if(ticketNUm <=0){
System.out.println("售票结束");
break;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口"+Thread.currentThread().getName()+"售出了一张票"+"剩余票数"+(--ticketNUm));
}
}
}
接着我们运行一下
实现Runnable方法
//实现接口方法
class SellTicket02 implements Runnable{
private static int ticketNUm = 100;
@Override
public void run() {
while(true){
if(ticketNUm <=0){
System.out.println("售票结束");
break;
}
//休眠50毫秒
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("窗口"+Thread.currentThread().getName()+"售出了一张票"+"剩余票数"+(--ticketNUm));
}
}
}
调用
System.out.println("===使用实现接口方式来售票===");
SellTicket02 sellTicket02=new SellTicket02();
new Thread(sellTicket02).start();//第一个线程——窗口
new Thread(sellTicket02).start();//第二个线程——窗口
new Thread(sellTicket02).start();//第三个线程——窗口
为什么会出现票数为负数的情况呢
我们假设票数还剩两张,此时t1进入线程,但是票数还没有完成减少的时候,t2,t3也进入了线程,所以这个时候票数减少的操作还会执行,出现负数
通知线程终止
在我们学习了之前线程的运行的时候我们使用While(true)使线程不进行终止,那我们如果需要终止线程的思路就是将While(true)的true进行变换,我们可以再main线程里面进行控制
我们先来写继承Threa类实现线程
我们来看代码
class T extends Thread{
int count=0;
//设置一个控制变量
private boolean loop=true;
@Override
public void run(){
while (loop){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("T 运行中......"+(++count));
}
}
public void setLoop(boolean loop){
this.loop = loop;
}
}
在上述代码中,我们将While中的布尔值设置成可以进行赋值的loop,并且写了set方法去实现loop的赋值,我们回到主线程去看如何进行控制
public class ThreadExit
{
public static void main(String[] args) {
T t = new T();
t.start();
//如果希望main线程去控制t1线程的终止,必须可以修改Loop
//让t1退出run方法,从而终止t1线程->通知方式
//让主线程休眠10秒,再通知退出
System.out.println("main线程休眠10秒");
try {
Thread.sleep(10*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t.setLoop(false);
}
}
我们通过让计算机10秒钟之后休眠,写了Thread的sleep方法,并且将setloop写在里面,实现了线程的终止。
我们来看运算结果。
在运行了10秒之后,退出子线程。
线程的常用方法
我们来应用上述的这些方法
public class ThreadMethod01 {
public static void main(String[] args) throws InterruptedException {
//测试相关方法
Threaddelete t = new Threaddelete();
t.setName("徐子昂");
t.setPriority(Thread.MIN_PRIORITY);
t.start();//启动子线程
//主线程打印5 hi,然后中断子线程的休眠
for(int i = 0; i < 5; i++){
Thread.sleep(1000);
System.out.println("hi"+i);
}
t.interrupt();//当执行到这里,就会中断t线程的休眠
}
}
class Threaddelete extends Thread {//自定义线程类
@Override
public void run() {
while (true) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "吃包子~~~~~~~" + i);
}
try {
System.out.println(Thread.currentThread().getName() + "休眠中~~~~~");
Thread.sleep(5000);
} catch (InterruptedException e) {
//当该线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
System.out.println(Thread.currentThread().getName() + "被 interrupt了");
}
}
}
}
在该线程中,我们使用了interrupt方法使线程在休眠的过程中被中断休眠,因为当main线程的i=5的时候会中断休眠的线程(被catch方法捕捉到)
我们来看第二组常用方法
我们通过主线程和子线程吃包子的例子来给大家演示说明。
我们先创建一个子线程(牢大),功能是循环吃20个包子,吃一次休眠一会。我们再写 一个主线程,里面调用子线程,并且写自己吃包子的线程,分别模拟两个人
join方法
join方法可以理解为小弟给牢大让包子吃,小弟先和牢大一起吃,在吃到5个包子的时候小弟给牢大让包子,此时牢大调用join方法,当牢大线程执行完毕之后小弟继续吃包子,我们来看代码。
public class ThreadMethod2 {
public static void main(String[] args) throws InterruptedException {
T7 t7 = new T7();
t7.start();
for (int i = 1; i <=20; i++) {
Thread.sleep(1000);
System.out.println("主线程(小弟)吃了"+i+"包子");
if(i==5) {
System.out.println("主线程(小弟)让牢大先吃");
t7.join();//这里相当于让t7线程执行完毕
System.out.println("牢大吃完了,小弟接着吃");
}
}
}
}
class T7 extends Thread {
@Override
public void run() {
for (int i = 1; i <=20; i++) {
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程(牢大)吃了"+i+"包子");
}
}
}
我们来看结果
yield方法
yield方法也有插队的作用,但是系统会更具实际情况进行分析要不要插队,如果可以满足两个进程的运行那就不需要进程插队,比如有一万个包子,那小弟和牢大一起吃就行,不需要让包子。
我们来看调用
public static void main(String[] args) throws InterruptedException {
T7 t7 = new T7();
t7.start();
for (int i = 1; i <=20; i++) {
Thread.sleep(1000);
System.out.println("主线程(小弟)吃了"+i+"包子");
if(i==5) {
System.out.println("主线程(小弟)让牢大先吃");
//t7.join();//这里相当于让t7线程执行完毕
Thread.yield();//不一定成功
System.out.println("牢大吃完了,小弟接着吃");
}
}
}
}
运行结果
此时牢大和小弟交替吃包子,礼让失败。