多线程---初阶


目录

🥬什么是进程

🥬进程调度

🌵进程的状态

🌵进程的优先级

🌵进程的上下文

🌵进程的记账信息

🥬进程的虚拟地址空间

🥬线程

🌵线程和进程的不同

🥬创建线程

🥬线程的常用方法

🥬线程的基础知识

🌵中断线程

 🌵线程等待

🥬线程安全

🥬synchronized

🥬volatile

🥬wait和notify

🥬多线程案例

🌵单例模式

🌵阻塞队列

🌵定时器

🌵线程池

🥬小结


在了解多线程之前先来看看进程是什么。

🥬什么是进程

进程就是计算机完成一个工作的过程。

 进程是如何被管理的呢?

管理=描述(PCB)+组织

⭐描述:进程控制块,这是一个C语言的结构体,一个结构体对象代表一个进程。

PCB属性:

1、pid:一个进程的标识符,同一个机器同一时刻不可能有两个进程的pid相同。

2、内存指针:表示进程使用的内存空间的范围。

3、文件描述符表:表示这个进程都打开了哪些文件,

⭐组织:使用一定的数据结构来组织,比较常见的就是用双向链表。查看进程列表,本质上就是遍历操作系统内核中的这个链表,并显示其中的属性;创建一个进程,本质上就是创建一个PCB对象,加入到内核的链表中。销毁一个进程,本质上就是把这个PCB对象从内核链表中删除掉。

(要想让进程跑起来,就得给进程分配一定的系统硬件资源,例如:CPU、内存、磁盘、网络带宽……)

🥬进程调度

一台电脑可能有上百个进程正在运行,但是一台电脑上的CPU只有一个(我的是8核CPU--多核CPU 表示把多个CPU绑在一起,相当于CPU有了8个分身,能同时干活),这时就需要进程调度。

怎么个调度法:

1、并发式执行:CPU进行切换,先运行A进程,然后立马切换去运行B进程……由于CPU运行速度极快,我们坐在电脑前,是感知不到这个过程的。(宏观上是同时执行,微观上其实是顺序执行)

2、并行式执行:由于我们现在的CPU基本上都是多核的,就可以同时运行多个进程。(宏观和微观上都是同时执行)

进程的调度离不开进程的状态、进程优先级、进程的上下文、进程的记账信息。

🌵进程的状态

进程的三个基本状态:R(就绪)、S(休眠)、X(结束)。

R:进程已经准备好了,随时可以运行。

S:某些进程由于某些原因不能立马运行,需要等待。

X:进程正常结束或者由于某些原因中断退出运行。

🌵进程的优先级

优先级决定进程何时运行和接收多少 CPU 时间。

🌵进程的上下文

主要是存储调度出CPU之前,寄存器中的信息(把寄存器信息保存到内存中)等到这个进程下次恢复到CPU上次执行的时候,就把内存保存好的数据恢复到寄存器中。(需要记住上次运行到哪个指令了,方便下次调度的时候继续从这个位置开始)

🌵进程的记账信息

记录一个进程在CPU上运行的时间,用来辅助决定这个进程是继续在CPU上执行,还是要调度出去了。

总结:其实何时进行进程调度,这个事情对于进程本身来说,是感知不到的,执行到进程中的任意指令,都可能会产生这样的调度。此时,这样的调度其实给我们的代码引入了一定的随机性,对于我们代码的正确性来说,就产生了新的挑战。

🥬进程的虚拟地址空间

一个进程要想运行,就需要给它分配一些系统资源,其中内存就是一个最核心的资源。

我们刚开始想的地址分配:直接就依据真实的内存地址划分出空间,分配给每个进程

比如:0x100-0x200   分配给进程1

           0x300-0x400   分配给进程2

           0x500-0x600   分配给进程2

           ………………

但其实不是这样,这些地址都是操作系统抽象出来的虚拟地址,系统会自动的把这个虚拟地址转换成真实的物理地址。如下所示:

0x100-0x200  分配给进程1

0x100-0x200   分配给进程2

0x100-0x600   分配给进程3

那为什么要有虚拟空间地址呢?

为了一定程度的减少内存访问越界,带来的后果。这样做,也让进程与进程之间相互影响的可能性减少了,隔离性增加了,进程也就更稳定了。

      假设现在进程1的内存范围0x100 - 0x200,如果我的代码尝试修改0x201的地址的数据,这个操作就是越界访问(错误的操作),如果这是一个真实的物理地址,这个修改就真的把0x201给改了,如果这个0x201正好是进程2要使用的内存,此时进程2可能就出错误了,就直接崩溃了。
      如果进程访问的是虚拟地址,也尝试修改0x201,此时系统就要针对0x201来查询页表,找到对应的物理地址。由于0x201已经是非法地址,在页表中查不到,系统就明白了,你这是在越界访问,于是就直接让这个进程出现崩溃(系统会给进程发送一个SEGMENT_ FAULT信号,这个信号通常会导致进程崩溃),防止影响到其他的进程。

虽然虚拟空间地址让进程与进程之间的相互独立性提高了,整个系统也更加稳定了,但是这也导致进程与进程之前很难相互访问对方的内存,如果需要相互沟通交流就需要借助一定的特殊手段(借助某个双方都能访问的中间量来进行访问),例如,文件,管道(是内核中提供的一个队列),消息队列,信号量,信号, socket。

🥬线程

进程是可以"并发"编程,提高效率,但是创建进程需要分配资源,销毁进程需要释放资源,如果频繁创建、销毁进程,开销属实有点大。于是就有了线程,线程也称"轻量级进程"。

轻量体现在:

创建线程比创建进程更高效;

销毁线程比销毁进程更高效;

线程调度比进程调度更高效。

为什么更高效呢?

因为创建线程不用去申请资源,销毁线程也不需要去释放资源。让线程产生于进程内部,同一个进程可以产生一个或多个线程,这些线程可以和进程共用一样的资源。进程和线程是包含关系。这样子就让线程比进程更高效了。

这里共用一样的资源指的是:

1、内存,线程1与线程共用同一份内存(同一个变量,相当于说两个线程可以访问同一个变量)。

2、文件,线程1打开的文件,线程2也能使用。

 上述就是对线程的描述,从操作系统内核角度而言,线程就是用PCB来描述的,不是说有几个PCB就有几个进程,如果几个PCB中的pid相同,说明他们是属于同一个进程。

虽然说线程的出现解决了资源开销大的问题,但也不是说同一个进程中线程越多越好,如果线程过多,可能会造成线程不安全。

线程过多可能出现的问题:

1、线程过多,线程之间频繁的调度,调度的开销就无法忽略。

2、线程过多,如果两个线程修改同一份内存的数据,就会产生冲突。

3、线程过多,如果某个线程抛出异常,没有catch住,可能会导致整个进程都异常退出。

🌵线程和进程的不同

进程和线程之间的区别和联系: (经典面试题)
1、进程是包含线程的,一个进程里可以有一个线程,也可以有多个线程。
2、每个进程都有独立的内存空间(虚拟地址空间),同一个进程的多个线程之间,共用这个虚拟地址空间。
3、进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位。

🥬创建线程

在Java中,使用Thread这个类的对象来表示一个操作系统中的线程。PCB是在操作系统内核中,描述线程的。Thread类是在Java代码中描述线程的。

使用java如何创建线程?

a)继承Thread重写run方法

//继承Thread重写run
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello Thread");
    }
}
public class ThredDemo1 {
        public static void main(String[] args) {
            Thread t = new MyThread();
            // start 方法,就会在操作系统中真的创建一个线程出来。(内核中创建一个PCB, 加入到双线链表中)
            // 这个新的线程, 就会执行 run 中所描述的代码。
            t.start();
            //t.run();
        }
}

start 方法会在操作系统中真的创建一个线程出来。(内核中创建一个PCB, 加入到双线链表中)这个新的线程, 就会执行 run 中所描述的代码。

其实我们直接引用run方法也能执行run中的代码

 那为什么我们要使用start呢?接下来再来看一个代码:

class MyThread2 extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
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) {
                e.printStackTrace();
            }

        }

    }
}

运行结果:

b)实现Runnable重写run

//实现Runnable重写run
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello Thread");
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t=new Thread(new MyRunnable());
        t.start();
    }
}

c)继承Thread,重写run,使用匿名内部类

public class ThreadDemo4 {
    public static void main(String[] args) {
        // 创建了一个匿名的类, 这个类继承了 Thread,
        // 此处new 的实例, 其实是 new 了这个新的子类的实例
        Thread t=new Thread(){
            @Override
            public void run() {
                System.out.println("hello Thread");
            }
        };
        t.start();
    }
}

d)实现Runnable重写run使用匿名内部类

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello Thread");
            }
        });
        t.start();
    }
}

e) lambda

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello Thread");
        });
        t.start();
    }
}

🥬线程的常用方法

1、线程ID

getId();

2、线程名字

getName()

3、线程当前状态

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值