多线程之一(进程理解、线程理解与创建、Thread类、线程状态)

目录

1. 进程理解

1.1 进程管理

1.2 PCB属性

 1.3 虚拟地址空间&进程间通信

2.多线程理解(Thread)

2.1 线程概念

2.2 线程的作用

2.3 进程和线程的区别(*)

2.4 多进程需要注意的问题

3. 线程创建

3.1 创建线程(继承Thread类)

 3.2 Jconsole工具查看线程

 3.3 创建线程其他方法

3.3.1 实现Runnable接口,重写run方法

3.3.2 使用匿名内部类,实现创建Thread子类的方式

3.3.3 使用匿名内部类,实现Runnable接口的方法

3.3.4 lambda表达式

4. Thread类的常见方法

4.1 Thread常见构造方法 

 4.2 Thread的常见属性

 4.3 线程启动(start和run的区别*)

4.4 线程中断(isInterrupted)

4.5 线程等待(join)

4.6 获取当前线程引用

5. 线程状态


1. 进程理解

进程是操作系统对一个正在运行的程序的一种抽象,通俗的说,进程就是运行起来的程序;
同一时刻系统中的进程,有很多,而操作系统就得给他们安排好,
也就是 进程是操作系统进行资源分配的基本单位。

1.1 进程管理

进程管理 = 描述 + 组织

描述:详细的表示一个进程中有哪些属性/信息,而结构体就是用来描述的;

一个结构体对象对应一个进程,结构体也叫PCB(进程控制块)

组织:通过一定的数据结构,将若干个用来描述的实体组织到一起,进行增删改查;

系统通常会使用 双向链表 这样的结构来将PCB组织到一起。

创建一个进程,本质上就是创建PCB,给进程分配资源赋值到PCB中,并且加入到链表上

销毁一个进程,本质上就是从链表上删除对应的PCB结点,是否PCB持有的资源

查看任务管理器的进程列表,本质上就是在遍历这个链表

一个进程也可以是多个PCB,系统管理PCB的链表也可能是多个

1.2 PCB属性

(1)PID:进程的身份表示

一个主机,同一时刻,进程的pid是唯一的(相当于身份证号),可以通过pid区分进程

(2)内存指针:描述了这个进程使用的内存空间是哪个范围(分配空间范围)

(3)文件描述符:描述了这个进程打开了哪些文件(文件就是存储在硬盘上的数据)

下面几点都是和 “进程调度” 有关的

进程调度就是通过 “并行” 和 “并发” 的方式,让计算机可以同时执行多个进程

并行执行:每个CPU核心上,都可以独立运行一个进程

                  多个CPU核心上,就可以独立运行多个进程

并发执行:一个CPU核心,先运行进程1,再运行进程2,再运行进程3...

                宏观上,同时运行多个程序;微观上,同一时刻运行一个程序

(4)进程状态: R(可执行状态)  S(可中断睡眠状态) X(退出状态)  D    T     Z

阻塞状态的进程,无法被调度到CPU上执行

就绪状态的进程,才可以在CPU上执行

(5)进程优先级:优先给谁安排先执行

系统调度的时候,就会根据优先级,来给进程安排执行时间

创建进程时,也可以通过系统调用来干预优先级(相对的)

(6)进程上下文:主要存储调度出CPU之前,寄存器的信息(把寄存器的信息保存到内存中)等到这个进程下次恢复到CPU上次执行时,将内存数据恢复到寄存器中

通俗的说就是“读档”和“存档”

进程在CPU上执行,要切换到别的进程,就需要保存当前运行结果的中间结果(存档),下次再到他执行时,就恢复之前的中间结果(读档),然后继续执行。

(7)进程的记账信息:记录进程在CPU上执行时长

再来辅助决定,进程是在CPU上继续执行,还是调度出去 

 1.3 虚拟地址空间&进程间通信

为了不出现进程间互相影响,指针越界操作等情况,就需要让每个进程都有各自的内存空间,不让这些进程的活动范围重叠,给每个进程划分的内存空间,就叫做 “虚拟地址空间”(不是真实的物理内存地址),通过专门的设备MMU来完成虚拟地址,到物理之前的映射

使用虚拟地址空间,就让进程间存在了 “隔离性”

(一个进程不能直接访问另一个进程的内存数据,防止干扰操作,提高系统稳定性)

但这样就会导致进程之间很难进行交互,为了解决这个问题又有了“进程间通信”

进程间通信:核心原理就是,找多个进程都可以访问到的公共资源,然后基于公共资源进行数据交换。

主流操作系统提供的进程通信机制有:文件、Socket(网卡) 、管道、消息队列、磁盘、信号量、信号

2.多线程理解(Thread)

2.1 线程概念

一个线程就是一个执行流(按一定顺序执行的一组代码),
每个线程之间都可以按照顺序执行自己的代码,多个线程之间“同时”执行多份代码

同一个进程中的这些线程,共用一份系统资源(内存+文件)

进程是 资源分配 的基本单位

线程是 调度执行 的基本单位

2.2 线程的作用

首先是,因为“并发编程”可以更充分利用CPU

其次,多进程虽然也可以实现并发编程,但线程比进程更轻量

(创建、销毁、调度线程都比进程更快)

所以,使用多线程:

1.能够充分利用多核CPU,能够提高效率

2.只是创建第一个线程时,需要申请资源,后续再创建新的进程,都是共用一份资源

销毁线程时,也是销毁到最后一个的时候,才真正释放资源,前面的线程销毁,都不必真的释放资源

最后,又有了更好的方法,“线程池”和“协程”

2.3 进程和线程的区别(*)

1.进程包含线程(多个)

2.线程比进程更轻量(创建、销毁、调度更快)

3.同一个进程的多个线程之间共用同一份内存/文件资源,

   进程和进程之间,则是独立的内存/文件资源

4.进程是操作系统资源分配的基本单位

   线程是操作系统调度执行的基本单位

2.4 多进程需要注意的问题

1. 多线程,一定程度下,线程数目越多,效率越高;但CPU核心数是有限的,当线程数目多到一定程度的时候,CPU核心就被占满了,线程调度开销太大,反而会使效率变慢

2. 当两个线程去争夺同一个资源是,此时两个线程就出现了问题,这就会使“线程不安全”

3. 如果某个线程出现异常,并且异常没有处理好,此时就可能会使整个进程崩溃,此时其他线程也会运行出现问题


3. 线程创建

3.1 创建线程(继承Thread类)

继承Thread类,重写run方法

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("thread!");
    }
}

标准库中,提供了一个Thread类,使用的时候就可以继承这个类

Thread类,相当于是对操作系统中的线程进行的封装

重写run方法,run是Thread父类中已有的方法,但是这里要重写

run中的逻辑,就是这个线程要执行的工作


创建子类,重写run方法,相当于“安排任务”

public class Demo01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

创建MyThread实例,并不会在系统中真的创建一个线程

而是,调用start方法的时候,才可以真正创建线程

新的线程才会执行run方法中的逻辑,直到run中逻辑执行完,新的线程才会运行结束

class MyThread extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

抢占式执行

 3.2 Jconsole工具查看线程

(1)找到自己电脑上JDK安装路径中的bin—》jconsole.exe打开

 (2)点击本地进程,选择当前项目连接,连接成功后,就可以查看当前项目中的线程

 (3)选择查看线程信息

 3.3 创建线程其他方法

3.3.1 实现Runnable接口,重写run方法

 创建了Runnable实例,将runnable实例作为参数传给线程

将线程要干的工作 和 线程本身 分开了,使用Runnable来专门表示“线程要完成的工作”

这种方法的好处是,降低了耦合性,如果以后要修改代码,改动相对较少

只需要将修改后的Runnable传给其他的实体就可以了

并且如果是多个线程干一样的工作,Runnable也更适合

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        while (true) {
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

3.3.2 使用匿名内部类,实现创建Thread子类的方式

创建Thread的子类,同时实例化出一个对象

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

3.3.3 使用匿名内部类,实现Runnable接口的方法

匿名内部类的实例,作为构造方法的参数

public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
    }
}

3.3.4 lambda表达式

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

4. Thread类的常见方法

4.1 Thread常见构造方法 

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用Runnable对象创建线程对象,并命名

public class demo01 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"这是线程名");
        thread.start();
    }
}

打开Jconsole.exe,可以看到线程的名字,就是我们自己命名出来的

 4.2 Thread的常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

(1)getId() 这个是java中给Thread对象安排的身份标识,和操作系统内核中PCB的pid,以及操作系统提供的线程API的线程id都不是一个

(2)setDaemon() 判断是否是守护线程。默认创建的线程是“前台线程”,前台线程会阻止进程退出,如果main运行完,前台线程还没完,进程不会退出;如果是“后台线程”,后台线程不会阻止进程退出,如果main等其他的前台进程执行完,此时即使后台线程没执行完,也会退出。

“只要有一个前台(main也是一个前台),进程就还活着,前台没了,后台也会没”

设置操作得在start之前,如果线程启动,就不能修改 

public class demo01 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"这是线程名");
        //使用setDaemon设置为后台线程
        //设置操作得在start之前,如果线程启动,就不能改
        thread.setDaemon(true);
        thread.start();
        System.out.println("main线程执行结束!");
    }
}

(3)isAlive() 判断内核中线程是否存活。Thread对象,虽然和内核中的线程,是一一对应关系,但是生命周期并非完全相同;Thread对象存在了,内核里的线程不一定有,调用start方法,内核线程才存在。当内核里的线程执行完(run运行完),内核的线程就销毁了,但是Thread对象还在

(4)写一段代码看看Thread属性 

public class Demo02 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"我的线程");
        thread.start();
        System.out.println(thread.getId());
        System.out.println(thread.getName());
        System.out.println(thread.getPriority());
        System.out.println(thread.getState());
        System.out.println(thread.isDaemon());
        System.out.println(thread.isAlive());
        System.out.println(thread.isInterrupted());
    }
}

 4.3 线程启动(start和run的区别*)

调用start才会真正创建线程,不调用start就没创建线程(在内核里创建PCB)

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}
public class Demo03 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//创建一个新线程,调用run
        //myThread.run();//在当前线程调用run
    }
}

start 和run的区别(*)

直接调用run,并没有创建线程,只是在原来的线程中运行代码

调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)

作用功能不同:

  1. run方法的作用是描述线程具体要执行的任务;
  2. start方法的作用是真正的去申请系统线程

运行结果不同:

  1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
  2. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

4.4 线程中断(isInterrupted)

线程中断,本质上是让run方法尽快结束,而不是run执行一半,强制结束

有两种方法

(1)直接自己定义一个标志位,作为线程是否结束的标记

public class Demo04 {
    private static boolean isQuit = false;

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello tread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread线程执行完了!");
        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = true;
        System.out.println("设置让thread线程结束!");
    }
}

 (2)使用标准库中自带的标记位interrupt

public class Demo05 {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("hello tread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("thread线程执行完了!");
            });
            thread.start();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread.interrupt();
            System.out.println("设置让thread线程结束!");
        }
}

 运行程序,发现出现了异常,这是因为

interrupt方法的行为,有两种情况

1.thread线程在运行状态会设置,标志位为true

 2.thread线程在阻塞状态(sleep)不会设置标志位,而是触发一个InterruptedException

这个异常会把sleep提前唤醒

所以,要想顺利结束循环,由线程自身的代码进行判定处理

线程自身可以(1)立即结束线程(break)

(2)啥都不干

(3)稍后处理(sleep,break

 

4.5 线程等待(join)

线程之间的调度顺序,是不确定的,可以通过join来控制线程之间的结束顺序

方法 说明
join() 等待线程结束
join(long millis) 等待线程结束,最多等millios毫秒
join(long millis, int nanos) 也可以等待更高精度的时间 毫秒+纳秒

 在main中,调用join效果就是,让main线程阻塞等待,等到thread执行完,main才继续执行

public class Demo06 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        System.out.println("main线程join之前");
        thread.join();
        System.out.println("main线程join之后");
    }
}

4.6 获取当前线程引用

currentThread();   获取到当前这个线程对应的Thread对象的引用

5. 线程状态

NEW: 安排了工作 , 还未开始行动(Thread对象创建出来了,但是内核的PCB还没创建,(还没真正创建线程))
RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作(就绪状态(正在CPU上运行+在就绪队列中排队))
BLOCKED: 这几个都表示排队等着其他事情(等待锁的时候进入阻塞状态)
WAITING: 这几个都表示排队等着其他事情(特殊的阻塞状态,调用wait)
TIMED_WAITING: 这几个都表示排队等着其他事情(按照一定的时间,进行阻塞sleep)
TERMINATED: 工作完成了 (内核的PCB销毁了,但是Thread对象还在)

 

public class Demo07 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
        //tread开始之前,得到的就是 NEW
        System.out.println(thread.getState());
        thread.start();

        Thread.sleep(50);
        //tread正在工作,得到的是 RUNNABLE
        System.out.println(thread.getState());

        thread.join();
        //tread结束之后,得到的状态是 TERMINATED
        System.out.println(thread.getState());
    }
}

猜你喜欢

转载自blog.csdn.net/m0_58761900/article/details/126423041