[Java Web]想学会线程?看这里


内存管理:空间上划分

【内核使用的内存】【分配给普通进程使用的内存】【空闲空间】

【进程A】【进程B】 空间划分不保证是连续的


1. Java 应用程序员眼中的内存

JVM 的内存空间分为 栈区、堆区、方法区...(这是属于 Java 应用和 JVM 之间的概念)

JVM 作为 OS(操作系统)眼中的普通进程

2.线性地址(虚拟地址) VS 物理地址

物理地址:真实的内存中的地址

线性地址:物理地址被操作系统进行转换后的一个地址

 这个图说明了,我们在线性地址上可能地址是一样的,但是映射到物理内存上,就是不同的。

没有线性地址之前,程序中,同一个程序多次运行,会生成不同的进程,不一定能保证同一个进程,一定被放到内存的同一个位置

所以,引入线性地址后,我们就不需要考虑这些问题了,这个就交给MMU来管理了。

OS分配出来的空间只是线性地址空间,实际的物理内存,可以延迟到,要访问这段内存时再分配。

举个例子:  A 找 B 租房子,需要 A 套房子。B 承诺 101 到 200 房间给 A 用(只是分配了线性地址出去)。如果 A 租下房子后没有使用,则 B 并不需要真正给 A 准备 100 套房子。什么时候 A 真正需要了某个房子了, B 再去给 A 找到房子对应的房号即可。

3.进程间通信

理论上进程间是独立的,但实际中,往往是多个进程之间互相配合,来完成复杂的工作。

比如:通过 workbench 进程 和 mysql 服务器进程进行通信,来实现数据的增删查改。

因此就有了进程之间交换数据的必要性了

当下问题:OS 进行资源分配是以进程为基本单位进行分配的,包括内存。分配给 A 进程的内存不会分配给 B 进程。所以,进程A、B 之间直接通过内存来进行数据交换的可能性完全不存在了。

所以 OS 需要提供一套机制,用于让 A、B 进程之间进行必要的数据交换——进程间通信

进程间通信的常见方式:

  1. 管道(pipe)
  2. 消息队列(message queue)
  3. 信号量(semaphore)
  4. 信号(signal)
  5. 共享内存(shared memory)
  6. 网络(network)—— workbench 和 mysqld 通信的方式

4.进程(process)和线程(thread)的关系

进程-线程 :1 :m 的关系

一个线程一定属于一个进程;一个进程下可以允许有多个线程

一个进程内至少有一个线程,通常被这个一开始就存在的线程,称为主线程(main thread)

主线程和其他线程之间低位是完全相等的,没有任何特殊性。

为什么 OS(操作系统)要引入 线程 这一概念?

进程这一概念天生就是资源隔离的,所以进程之间进行数据通信注定是一个高成本的工作。

现实中,一个任务需要多个执行流一起配合完成,是非常常见的

所以,需要一种方便数据通信的执行流概念出来,线程就承担了这一职责。

5.什么是线程(OS 系统层面上的线程)

线程是 OS 进行 调度(分配 CPU) 的基本单位

  • 进程:OS 进行资源分配的基本单位
  • 线程:OS 进行调度的单位

线程变成了独立执行流的承载概念,进程退化成只是资源(不含 CPU )的承载概念

比如:运行一个程序,没有线程之前,OS 创建进程,分配资源,给定一个唯一的 PC,进行运行。有了线程之后,OS 创建进程,分配资源。创建线程(主线程),给定一个唯一的 PC,进行运行。

程序的一次执行过程表现为一个进程,main 所在的线程就是主线程。主线程中可以运行对应的操作来创建运行其他线程。

只针对 OS 级别的线程:OS 针对同一个进程下的线程实现“连坐”机制:一旦一个线程异常退出,OS 会关闭该线程所在的整个进程。

线程 VS 进程

  1. 概念区别
  2. 由于进程把调度单位这一职责让渡给了线程,所以,使得单纯进程的创建销毁适当简单
  3. 由于线程的创建销毁不涉及资源分配、回收的问题,所以,通常理解,线程的创建/销毁成本要低于进程的成本

6.JVM 中规定的线程

“Java线程” VS “OS 线程(原生线程)”

不同的 JVM 有不同的实现,它们外在表现基本一致,除了极个别的几个现象。Java线程,一个线程异常关闭,不会连坐。我们使用的HotSpot实现(JVM)采用,使用一个 OS 线程来实现一个 Java 线程。

Java中由于有 JVM 的存在,所以使得 Java 中做多进程级别的开发基本很少。Java 中的线程还克服了很多 OS 线程的缺点。所以,在 Java开发中,我们使用多线程模型来进行开发,很少使用多进程模型。

1.Java 线程在 代码中是如何体现的        

        java.lang.Thread 类(包括子类)的一个对象        Thread——线程

2.如何让在代码中创建线程(最基本)

  1.  通过继承 Thread 类,并重写 run 方法。实例化该类的对象 -> Thread 对象
/*
1.继承 java.long.Thread 类
2.光继承的情况,这个线程中一条指令都没有
    所以我们需要给线程书写让它执行的指令(以语句的形式)
 */
public class MyFirstThreadClass extends Thread{
    @Override
    public void run() {
        // 这个方法下写的所有代码,如果正确创建线程的话,都会运行在新的线程执行流中
        System.out.println("这是我的第一个线程");
    }
}
public class Main {
    public static void main(String[] args) {
        MyFirstThreadClass t = new MyFirstThreadClass();
        // t 指向了一个创建出来的线程对象
        // 线程创建好了,但并没有运行起来
    }
}

只是创建线程,并没有启动线程,运行的结果并不会打印什么

        2.通过实现 Runnable 接口,并重写 run 方法。实例化 Runnable 对象。利用Runnable 对象去构建一个Thread 对象。

/*
1.实现 java.long.Runnable 接口
2.通过重写 run方法,来指定任务要做的工作
  这个线程要不要交给一个新的线程去执行,这里是不知道的
 */
public class MyFirstTask implements Runnable{

    @Override
    public void run() {
        System.out.println("这是我的第一个任务的第一句话");
    }
}
public class Main {
    MyFirstTask task = new MyFirstTask();   // 创建了一个任务对象
    Thread t = new Thread(task);            // 把 task 作为 Thread 的构造方法传入
                                            // 让这个新创建的 Thread 去执行 task 任务
                                            // 语句就运行在新的线程中
    // 这里只是创建线程,暂时还没有运行
}

3.启动线程

当手中有一个 Thread 对象时调用其 start()方法

注意:1.一个已经调用过 start()不能再调用start()了,再调用就会有异常发生

           2.不要调用成 run()

对于 Java 应用程序,我们需要通过 Thread 对象来控制线程的一切。

 启动线程:

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("正在执行");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();

        // 通过调用 Thread 对象的 start 方法,来开始线程的运行
        t.start();
    }
}

 4.start()做了什么?

t.start() 只做了一件事:把线程的状态从新建变成了就绪。不负责分配CPU

线程把加入到 线程调度器(不区分是 OS 还是 JVM 实现的)的就绪队列中,等待被调度器选中分配 CPU。

从子线程进入到就绪队列这一刻起,子线程和主线程在低位上就完全平等了

所以,哪个线程被选中去分配 CPU 就听天由命了。

先执行子线程中的语句还是主线程中的语句理论上是都有可能的

但大概率是主线程中的打印先执行? 为什么?

t.start() 是主线程的语句。因此,这条语句被执行了,说明主线程现在正在 CPU 上。(主线程是运行状态),所以,主线程刚刚执行完 t.start() 就马上发生线程调度的概率不大。

所以,大概率还是 t.start() 的下一条语句就先执行了。

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我是 MyThread 类下的 run 方法中的语句,会运行在子线程中");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        System.out.println("我是 main 类下的 main 静态方法中的语句,会运行在线程主线程中");
    }
}

什么时候子线程中的语句会先执行?

  • 刚刚执行完 t.start() 之后,输出(“sout”)之前,发生了依次线程调度
  • 主线程状态:运行 => 就绪。主线程不再持有 CPU。意味着主线程的下一条语句不再执行
  • 调取的时候,选中子线程调度 子线程状态:就绪 => 运行。子线程持有了 CPU ,所以,执行到子线程语句

注意:

  1. 一个已经调用过 start()不能再调用start()了,再调用就会有异常发生
  2.  不要调用成 run()

t.start() 只允许工作在“新建”状态下,调用两次 start()会出选非法的线程状态异常

调用 run 方法,就和线程没关系了。相当于是完全在主线程下运行代码。

5.什么情况下,会出现线程调度(开始选择一个新的线程分配 CPU)

1.CPU 空闲

        当前运行着的 CPU 执行结束了        运行 => 结束

        等待外部条件        运行 => 阻塞

        主动放弃               运行 => 就绪

2.被调度器主动调度

        1.高优先级线程抢占

        2.时间片耗尽(常见)

在多线程中,明明代码是固定的,但会出现现象是随机的可能性,主要原因就是调度的随机性体现在线程的运行过程中。

同一个程序启动多个线程:

public class Main {
    // 同一个程序启动多个线程
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();

        MyThread t3 = new MyThread();
        t3.start();
        
        MyThread t4 = new MyThread();
        t4.start();

    }
}

7.线程和方法调用栈的关系

每个线程都有自己独立的调用栈

由于每个线程都是独立的执行流,A 线程调用过哪些方法,和 B 线程根本就没有关系。表现为每个线程都有自己的独立的栈。

调用同一个方法:说明执行的是同一批指令

栈不同(帧不同):说明执行指令时,要处理的数据是不同

8.线程中最常见的属性:

1.id 本进程(JVM进程)内部分配的唯一的 id 只能 get 不能 set

2. name(名字) 为了给开发者方便看的,JVM 并不需要这个属性

    默认情况下,如果没有给过名字,线程名字遵守 Thread-... 。第一个是 Thread-0、Thread-1、Thread-2。

public class Main1 {
    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println(this.getName());
        }
    }

    public static void main(String[] args) {
        Thread thread = Thread.currentThread();     // 当前线程
        System.out.println(thread.getName());       // 主线程名称

        // 子线程
        MyThread t1 = new MyThread();
        t1.start();
        MyThread t2 = new MyThread();
        t2.start();
        MyThread t3 = new MyThread();
        t3.start();
        MyThread t4 = new MyThread();
        t4.start();
    }
}

可以get 也可以set,可以通过 setName(...)设置,也可以通过 Thread(...)构造方法设置。

public class Main2 {
    static class MyThread extends Thread {
//        // 方法一:通过 setName(...)设置
//        public MyThread(){
//            setName("我是方法一");
//        }

        // 方法二:通过 Thread(...)构造方法设置
        public MyThread(){
            super("我是方法二");  // 调用父类(Thread)的构造方法
        }

        @Override
        public void run() {
            System.out.println(this.getName());
        }
    }

    public static void main(String[] args) {
        Thread thread = Thread.currentThread(); // 当前线程
        System.out.println(thread.getName());

        MyThread t1 = new MyThread();
        t1.start();

        MyThread t2 = new MyThread();
        t2.start();

        MyThread t3 = new MyThread();
        t3.start();
    }
}

通过传入参数的方法可以分别改名:

public class Main3 {
    static class MyThread extends Thread {
        public MyThread(String name){
            super(name);  // 调用父类(Thread)的构造方法
        }

        @Override
        public void run() {
            System.out.println(this.getName());
        }
    }

    public static void main(String[] args) {
        Thread thread = Thread.currentThread(); // 当前线程
        System.out.println(thread.getName());

        MyThread t1 = new MyThread("我是t1");
        t1.start();

        MyThread t2 = new MyThread("我是t2");
        t2.start();

        MyThread t3 = new MyThread("我是t3");
        t3.start();
    }
}

id 就相当于一个线程的身份证号(出生被分配,无法被修改,不能重复);

name 就相当于一个线程的名称(可以重复、可以被修改)。

t1、t2、t3这三个线程是由 主线程 执行创造出来的,所以取名为“子线程”。创造上有父子关系,但调度时低位平等。t1、t2、t3 线程是一个动态的过程,MyThread 类只是给 线程 去执行的程序,是一个静态内容。

3.在 Java 代码中看到的线程状态

1)理论中的状态

 2)Java 代码中实际看到的状态

 线程可以 get/set 自己的优先级。这个优先级的设置,只是给JVM 一些建议,不能强制让哪个线程先被调度。

9.前台线程 VS 后台线程(精灵线程、守护线程)

前台线程一般是做一些有交互工作的

后台线程一般是做一些支持工作的线程

比如:写了一个音乐播放器

1.线程响应用户点击动作(前台)

2.线程去网络上下载歌曲(后台)

我们创造出来的线程默认都是前台线程,除非修改。

JVM 进程什么时候才退出:所有的前台线程都退出了,JVM 进程就退出了

  1. 必须要求所有前台都退出,和主线程没关系
  2. 和后台线程没关系,即使后台线程还在工作,也正常退出

10.总结 线程的 场景属性

我们都是通过 Thread 对象进行操作

id(get)、name(get/set)、状态(get)、优先级(get/set)、前后台线程(get/set)

11.JDK 中自带的观察线程的工具

JVM 运行中的一些相关情况,比如内存、类加载情况、线程情况

 TimeUnit.SECONDS.sleep(1);     休眠 1s 后再继续运行

12. Thread.join() 方法

  1.  b = new B();        b.start();
  2. 吃饭
  3. b.join(); // 这个方法会阻塞,直到 B 运行结束才返回
  4. 这个时候 B 一定已经结束了
  5. 打印 b 结束了
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Main {
    private static class B extends Thread {
        @Override
        public void run() {
            super.run();
            // 模拟 B 要做很久的工作
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            println("B:我的任务已经完成");
        }
    }

    private static void println(String msg){
        Date date = new Date();
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(format.format(date) + ": " + msg);
    }

    public static void main(String[] args) throws InterruptedException {
        B b = new B();
        b.start();
        println("A 自己先去吃饭");
        // 有 join 和 没有join 的区别
        b.join();
        println("B 送来了钱,结账走人");
    }
}

 如果没有 b.join() :B任务还没完,没给A 送钱 ,A就走了,这是不对的

join(); 等待线程死去

join(long millis);等待线程死去,但最多等 ? 毫秒

join(long millis,int nanos);有毫秒有纳秒,时间更精确

对比以下两种方法:

 1.多核环境下,并发排序的耗时 < 串行排序的耗时

        单线程一定跑在一个 CPU (核)上,多线程意味着可能工作在多个核上(核亲和性)

2.单核环境下,并发排序的耗时也能小于吗?

即使在单核环境下,并发的耗时也可能较少。

本身,计算机就有很多线程在等待分配 CPU ,比如,现在有100个线程。意味公平的情况下,我们的排序主线程,只会被分配1/100 的时间。

当并发时,我们使用 4 个线程分别排序,除其他的 99 个之外,计算机中共有 99 + 4 = 103 个线程。

我们 4 个线程同属于一个进程,分给我们进程的时间占比 4/103 > 1/100.

所以,即使单核情况下,我们一个进程中的线程越多,被分到的时间片是越多的。

线程越多越好吗?

no。1.创建线程本身也不是白嫖的。

2.即使理想情况下,不考虑其他耗时,极限也就是 100%。线程调度也需要耗时(OS 从 99 个线程中挑一个的耗时 和 从 9999 个线程中挑一个的耗时不同)

CPU 是公共资源,写程序的时候也是要考虑公德心的。如果是好的 OS 系统,可能也会避免这个问题。

3.并发排序的耗时就一定小于串行吗?

不一定。

串行的排序: t = t(排区间1) + t(排区间2) + t(排区间3)+ t(排区间4)

并发的排序:t = 4 * t(创建线程)+  t(排区间1) + t(排区间2) + t(排区间3)+ t(排区间4)+ 4 * t(销毁)

我们要写多线程的代码原因:1.提升整个进程的执行速度(尤其计算密集性的程序)

                                                2.当一个执行流阻塞时,为了还能处理其他任务,可以引入多线程

13. Thread 下的几个常见静态方法

1.Thread.sleep(...)

让线程休眠多少毫秒

TimeUnit.SECONDS.sleep(1) == Thread.sleep(1000)

从线程的状态的角度,调用sleep(?),就是让当前线程 从  "运行 "=> "阻塞"。

阻塞状态实际上就是等待某个条件:要求时间过去 ... 之后,当条件满足时(时间真的过去了 ... ),线程从 阻塞 => 就绪 状态,这个间隔很短,基本对人类无感,当线程被 调度器选中时,开始接着之前的指令执行,表现为sleep 之后的语句执行。从外部表现来讲,就是让线程休眠了一段时间。

2.Thread.currentThread();

Thread 引用,指向一个线程对象,执行的就是 在哪个线程中调用的该方法,就返回哪个对象。

public class Main {
    static class MyThread extends Thread{
        @Override
        public void run() {
            printCurrentThreadAttributes();
        }
    }

    private static void printCurrentThreadAttributes(){
        Thread t = Thread.currentThread();
        System.out.println(t.getId());
        System.out.println(t.getName());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        t1.setName("t1");
        MyThread t2 = new MyThread();
        t2.start();
        t2.setName("t2");
        MyThread t3 = new MyThread();
        t3.start();
        t3.setName("t3");
        printCurrentThreadAttributes();
    }
}

3.Thread.yield()

让线程让出 CPU 。线程从 运行 => 就绪状态。随时可以继续被调度回 CPU 

yield 主要用于执行一些耗时较久的计算任务时,为了防止计算机处于“卡顿”现象,时不时的让出一些 CPU 资源,给 OS 内的其他进程。

public class Main {
    static class PrintWhoAnI extends Thread{
        private final String who;

        PrintWhoAnI(String who) {
            this.who = who;
        }

        @Override
        public void run() {
            while (true){
                System.out.println("我是 " + who);
                if(who.equals("张三")){
                    Thread.yield();
                }
            }
        }
    }

    public static void main(String[] args) {
        PrintWhoAnI 张三 = new PrintWhoAnI("张三");
        PrintWhoAnI 李四 = new PrintWhoAnI("李四");
        张三.start();
        李四.start();
    }
}

可以发现张三出现的频率要低于李四,因为张三一直在让李四

 14.线程的控制:通知线程停止

A  叫 B 来干活。突发了一些情况,需要让 B 停止工作(即使分配给它的任务还没有完成)。所以A 需要让 B 停止。

1.暴力停止stop(),直接kill掉 B。

目前基本上已经不采用了,因为把 B 直接杀掉,不知道 B 把工作进行的如何了(不可控)

2.和 B 进行协商 interrupt()

A 主动给 B 发一个信号,代表 B 已经停止了(发消息)

B 在一段时间内,看到了停止信号后,就可以主动把手头工作做到一个阶段完成,主动退出。(需要我们写代码完成)

A 主动让 B 停止 b.interrupt(); 

只是发了一个消息,实际上并不会影响B 的运行

B 如何感知有人让他停止:

情况1:B 正在正常执行代码,可以通过一个方法来判定 static boolean interrupted()

        静态方法: Thread.interrupted()——检测当前线程是否被终止

        返回 true:有人让我们停止;返回 false:没人让我们停止

情况2: B 可能正处于休眠状态(比如sleep、join),意味着 B 无法立即执行 Thread.interrupted()。此刻,JVM 的处理方式是,以异常形式通知 B :InterruptedException

        当 B 处于休眠状态是,捕获了 InterruptedException,代表有人让我们停止,具体要不要停,什么时候停,怎么停,完全自己做主。

15.总结

  1. 如何在代码中创建、启动线程
  2. 线程在底层的原理(OS 中的线程 + 执行流)
  3. 线程结果的随机性
  4. 线程的常见属性(id,名字,状态,优先级,前后台线程)
  5. 相关工具:调试工具、jconsole
  6. 线程之间的协调工作:t.join() 等待 t 结束
  7. Thread 下的常见方法:sleep(...)、currenntThread(...)
  8. 休眠、让出 CPU
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值