【JavaEE】进程与线程-Java多线程编程


哈!抓住你了
别走啦啦啦啦啦啦~
在这里插入图片描述


文章目录

  • JavaEE & 进程与线程
    • 1. 多线程
      • 1.1 线程越多,越好?
      • 1.2 进程与线程的区别总结
    • 2. Java与多线程
      • 2.1 Java标准库提供的一个类 Thread(普通类)
        • 5.1.1 实例化子类法
        • 5.1.2 实例化子类法 & 匿名内部类法
        • 5.1.3 Thread提供的构造方法,”传入工具“法
        • 5.1.4 "传入工具"法 + 匿名内部类
        • 5.1.5 lambda表达式法
    • 3. Thread常见属性
      • 3.1 后台线程
      • 3.2 是否存活
      • 3.3 线程的中断
        • 3.3.1 自己的方法去中断
        • 3.3.2 线程引用自带的一些方法
      • 3.4 join方法
      • 3.5 线程的状态

JavaEE & 进程与线程

1. 多线程

  1. 一个进程即同一个程序,内部有多个任务,多线程更合适,因为互相影响也仅限于此程序,这样就提高了效率~
    • 例如:QQ的聊天,邮箱,游戏,录制,截图,QQ空间~
  2. 多个程序,即多个进程,那么他们之间就是相互隔离的,一个出bug,其他进程不受影响~
    • 例如:CCtalk,QQ,微信~
  • 有可能有人会说,微信摄像头和QQ摄像头或者CCtalk摄像头这些不同进程的线程之间也会影响~
    • 这只是因为你摄像头就只有一个~

【JavaEE】进程与线程-Java多线程编程_第1张图片

1.1 线程越多,越好?

  • 由于CPU核心数有限,即打工仔有限

  • 线程太多,打工仔们也分身乏术~

    • 并且,线程是需要有系统资源的~
  • 一些线程等不及了,就要挤掉一些线程~

  • 也就是说,线程之间的并发,并不满足肉眼“同时”~

  • “多开一个进程”,核心数没变

    • 多加一个主机,多一个CPU就可以解决,即“分布式系统”~
  • 这样很容易导致一个线程异常了,整个进程也就狗带了~

举个栗子:

《一群人吃猪扒》,一张大桌子有100份猪扒(进程),人数高达1000个,每个人要吃若干量猪扒(线程),如果有一个人一直吃不到,直接干架了,把桌子给掀了,“得了,都别吃了”

1.2 进程与线程的区别总结

  1. 进程包含线程

  2. 进程有自己独立的内存空间和文字描述符表

    • 一个进程的多个线程之间共用~
  3. 进程是操作系统分配内存的基本单位

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

  • 宏观上:

【JavaEE】进程与线程-Java多线程编程_第2张图片

  • 微观上:

【JavaEE】进程与线程-Java多线程编程_第3张图片

  1. 进程之间有“隔离性”,一个进程挂了并不会影响别的进程
  2. 同一进程不同线程共用同一份地址空间和文件描述符表,一个线程挂了,可能会导致整个进程直接崩了~

2. Java与多线程

  • Java不提倡多进程编程
    • Java的jdk并没有封装多少多进程API,有也很简陋~
  • Java很提倡多线程编程
    • Java的jdk有很多多线程API,很完善~
  • 接下来的内容,新概念会很多,学习并且习惯就好~

2.1 Java标准库提供的一个类 Thread(普通类)

【JavaEE】进程与线程-Java多线程编程_第4张图片

  • 通过这个Thread类表示线程

我这边提供五种语法,即描述一个线程的方式,以及启动一个线程的方式~

5.1.1 实例化子类法
class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("好耶 ^v^");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test1 {
    public static void main1(String[] args) throws InterruptedException {
        int x = 1;
        Thread t = new MyThread();

        t.start();
        while(true) {
            System.out.println("不好耶 T.T");
            Thread.sleep(100);
        }
    }
}
  1. main方法是一个线程

    • 我们有时会遇到,不同类可以重复定义main方法
      • 而这多个main是不同的线程
      • 并且毫不相干
  2. 自己建立的线程也是一个线程

  3. 两个线程并发执行

  • 为了体现并发性,我写了两个死循环

  • 补充知识:sleep方法,即休眠,让线程进入阻塞态~
    * 该状态下,线程无法进行执行
    * 输入一个long类型值(单位ms),表示休眠时间~
    * 这个类是Thread系统类的一个静态方法,直接通过类名进行引用
    * 这个方法附带一个“编译时异常”,InterruptedException,这个异常在这里不做赘述,但是在后面很重要,现在浅浅认为它就是个异常而已~

    • 处理异常:(alt + 回车)
      • try catch 环绕
      • throws
        • main线程是可以用的
        • 但是自己创建的线程不行,因为run方法可以throws,但是start方法并没有进行异常处理,也没有进行throws~
        • 【JavaEE】进程与线程-Java多线程编程_第5张图片

【JavaEE】进程与线程-Java多线程编程_第6张图片

  • 调用start启动线程,run方法会被Java自动调用~
    • run方法可以看成是别的线程的main方法!
  • 怎么实现与main线程并发的,不是我们需要想的~
  1. run方法是线程运行,被核心调度的一个方法

  2. start方法是启动一个线程,让Java自己去调用run,自己完成并发

  • 不难看出,run方法一旦被调用,就只会让run方法执行完,即该线程执行完,才能轮到下一个语句,其他线程全部都处于阻塞态~

  • 而start方法则是让CPU的核心调度线程的时候再自动调用run方法,完成并发编程~

【JavaEE】进程与线程-Java多线程编程_第7张图片

  • 本质上,五种语法都是通过重写run方法~
  • 测试:(按ctrl + F2结束程序)
    • 两个线程“同时”执行:

【JavaEE】进程与线程-Java多线程编程_第8张图片

  • 调用run方法,则只执行一个线程,等该线程结束,才能继续执行后续操作~

【JavaEE】进程与线程-Java多线程编程_第9张图片

  • 找到jconsole.exe,这是jdk提供的根据,可以观看也仅能观看Java中线程的详情~
  • 双击~
  • 可能会出现,啥都没有的情况,我们需要用管理员的身份打开~

【JavaEE】进程与线程-Java多线程编程_第10张图片

【JavaEE】进程与线程-Java多线程编程_第11张图片

  • 双击我们自己写的线程~
    • 本来就不安全,知识提醒你罢了,点了~
    • 允许~

【JavaEE】进程与线程-Java多线程编程_第12张图片

【JavaEE】进程与线程-Java多线程编程_第13张图片

  • 观察详情(要让线程一直运行才能看到,否则会连接失败)

【JavaEE】进程与线程-Java多线程编程_第14张图片

  • 可以看出,我们代码层面看到的是两个线程,但是实际上不止两个~

【JavaEE】进程与线程-Java多线程编程_第15张图片

  • 只有这两个是我们自己写的,其他都是jvm自己创建和启动的~
    • jvm启动这些去干脏活累活~

点击两个线程~

【JavaEE】进程与线程-Java多线程编程_第16张图片

  • 显示的信息就是执行情况~
    • 比如执行到哪里~
  • 用这个Java监视器 jconsole去调试是一个普遍的方法~
5.1.2 实例化子类法 & 匿名内部类法
public class Test2 {
    public static void main1(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true) {
                    System.out.println("好耶 ^v^");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {//sleep抛出中断异常,我们要让jvm去安抚它~
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while(true) {
            System.out.println("不要耶~~~");
            Thread.sleep(100);
        }
    }
}
  • 不做过多阐述,就是用匿名内部类对类进行重写的基本操作~

  • 本质上跟上面那个方法完全一样

  • 只不过我不需要专门写一个类去继承,系统不需要加载多一个类

    • 我们也不需要编个名字哈哈 ^ v ^
  • 测试:(按ctrl + F2结束程序)

    • 两个线程“同时”执行:
线程2
5.1.3 Thread提供的构造方法,”传入工具“法
  • 在之前构造搜索二叉树的时候,传入比较器,就是一个“传入工具”法~

  • 类似的,Thread系统类也提供了几个构造方法
    【JavaEE】进程与线程-Java多线程编程_第17张图片

  • 重点掌握如图两种~

  1. 单纯传入一个“线程可执行器”
    • 这样通过这个“线程可执行器”,就可以重写run方法了~
    • 这个构造方法内部就会利用这个工具去重写run~
    • 我们的工具类要实现Runnable接口~
      • Runnable是个函数式接口~【铺垫】
  2. 多传入一个String name,别名
    • 这个的用处就是给线程起一个别名,更加显眼~
    • 这个别名会在jconsole监视会很显眼~
class Goble implements Runnable{
    @Override
    public void run() {
        while(true) {
            System.out.println("好耶 ^v^");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Test3 {
    public static void main1(String[] args) {
        Thread t = new Thread(new Goble()); //传一个工具过去
        t.start();
        while(true) {
            System.out.println("不好耶T.T");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 测试跟上面是一样的,正常
  • 看一看别名的那个案例吧~

【JavaEE】进程与线程-Java多线程编程_第18张图片

【JavaEE】进程与线程-Java多线程编程_第19张图片

  • 是不是很显眼 ^ v ^
5.1.4 "传入工具"法 + 匿名内部类

不做赘述~

public class Test4 {
    public static void main1(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("好耶^v^");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        while(true) {
            System.out.println("不好耶T.T");
            Thread.sleep(100);
        }
    }
}
5.1.5 lambda表达式法
  • 这个很重要,也是最常使用的方法
  • lambda表达式可以模拟匿名内部类~
    • 并且这必须是函数式接口
    • 所以5.1.2 的那个方法是不能写成lambda表达式的,因为Thread不是函数式接口
    • 而Runnable就是一个函数式接口~
  • lambda表达式会根据对应环境,确定函数式接口
    • 并且对那个单一的方法进行重写~
public class Test5 {
    public static void main1(String[] args){
        Thread t = new Thread(() -> {
            while(true) {
                System.out.println("好耶^v^");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
		try {
            System.out.println("不好耶T.T");
            Thread.sleep(100);
        } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
        }
    }
}
  • 测试结果一致~
  • 最简洁的一个方式~

lambda表达式知识点所在博客:JAVA数据结构 & Java语法完善补充_java数据结构语法_s:103的博客-CSDN博客

  • 有时候测试的时候会发现要么main线程不见了,要么个别线程不见了,原因是执行太快,立马结束了~

3. Thread常见属性

属性 封装方法
ID线程编号 getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否为后台线程 isDaeMon()
是否存活 isAlive()
是否被中断 isInterrupted()

3.1 后台线程

  1. 我们创建的线程都是前台线程,前台线程会阻止Java程序结束
  2. 而jvm自动创建的后台线程,不会阻止Java程序结束,一旦前台线程结束,后台线程无论是否执行完,都会结束程序~
  3. 创建的线程可以设置的后台,setDeMon(true)
    • 设置后,不影响程序结束~

【JavaEE】进程与线程-Java多线程编程_第20张图片

3.2 是否存活

如果一个线程引用对应的线程未执行或者已经执行完毕,那么在jconsole里也不会有这个线程,那么这个引用调用isAlive,就显示false或者,执行中都是true~

  • 问题来了,在run方法内怎么获得线程引用呢,因为线程引用是在run方法重写后产生的,怎么获得呢?
  • Thread.currentThread()调用这个Thread的静态方法(一样会抛异常,但是我们已经解决了)

【JavaEE】进程与线程-Java多线程编程_第21张图片

测试:

  • 只有在执行前,执行完毕是false
  • 执行中都是true

【JavaEE】进程与线程-Java多线程编程_第22张图片

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        System.out.println("好耶 ^ v ^");
    });
    t.start();
    Thread.sleep(1000);
    System.out.println(t.isAlive());
}
  • 对于执行完毕,有如上代码
  • 几乎每次运行结果都是这样的

【JavaEE】进程与线程-Java多线程编程_第23张图片

  • 但是极端情况下,如果该线程在main线程打印t.isAlive()的时候都没到达CPU,那么就会是
  • true[回车]好耶 ^ v ^
  • 特别极端,但是线程那么多,还真有可能
  • 主要原因是,线程的调度是随机的~

3.3 线程的中断

3.3.1 自己的方法去中断
  • 线程的中断主要是中断循环
    • 而需要一个变量充当循环条件
    • 这个循环变量应该在特定的时候被置为假~
  • 所以有以下算法
    • 全局性质的静态变量:flag
    • 当flag为true,表示t1线程可以中断:结束
  • 注意:由于lambda/匿名类 的变量捕获机制
    • 局部变量必须是”final“ / ”实际final“
    • 而全局性质的变量【属性】,不需要这个要求
public class Test5 {
    public static boolean flag;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(!flag) {
                System.out.println("好耶^v^");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        Thread.sleep(3000);
        flag = true;
        System.out.println("不好耶T.T");
    }
}
  • 测试:
    • 绝大多数结果是这样的:

【JavaEE】进程与线程-Java多线程编程_第24张图片

3.3.2 线程引用自带的一些方法
  1. Thread.currentThread() 
    //获取当前线程的引用
    
  2. isInterrupted()  
    //判断线程引用内置的标识符是真是假 
        //----反应是否可以被中断
    
  3.   interrupt() 
     //线程引用变量的标识符进行取非
    
  • 初步设计
public static void main1(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println("好耶^v^");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread.sleep(3000);
    t1.interrupt();
    System.out.println("不好耶T.T");
}
  • 测试:

【JavaEE】进程与线程-Java多线程编程_第25张图片

  • 貌似main线程已经结束了,但是t1线程似乎还没有中断~

    • 极大概率是这样的~
    • 恰好“睡醒”也不好说~
    • 因为睡眠时间占99.999%d的时间~
  • 原因就是:

    1. interrupt()方法将线程引用的标识符改为了true
    2. 而线程的阻塞就会立即被疏通,或者说线程会立即醒来~
    3. 但是线程醒来,会将标识符情况成默认值(false)
      • 并且抛出InterruptedException异常
    4. catch到后打印异常
    5. 此时的循环判断仍然为真~
  • 解决:在捕获到异常后,直接break~

【JavaEE】进程与线程-Java多线程编程_第26张图片

  • 测试:

【JavaEE】进程与线程-Java多线程编程_第27张图片

3.4 join方法

  • join方法为等待方法,代表某线程XX时间,本线程再继续执行
    • join(long time), 最多等待time 毫秒
      • 提前结束就不再等了~
    • join(), 等待线程完全结束~
  1. 确定此语句出现在哪个线程
  2. 调用此方法的线程引用对应的线程
  3. 前者等后者~
    • 即前者线程进入阻塞态
    • 等待后者线程XX时间
    • 再转化为就绪态
  • 注意:千万不要搞个”死锁bug“,就是“他等他,他又等他”
    • 这不是本章节的问题,以后会讲~
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println("好耶^v^");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    });
    t1.start();
    t1.join(5000);
    //在这里不带参数,那该t1线程就死循环了,无法结束的~
    Thread.sleep(3000);
    t1.interrupt();
    System.out.println("不好耶T.T");
}
  • 测试:

【JavaEE】进程与线程-Java多线程编程_第28张图片

3.5 线程的状态

没有测试案例

  • 操作系统里的线程,自身是有一个状态的
  • 但是Java Thread 是对系统线程的封装,把这里的状态又细致化了~
  • 状态获取:getState();
  1. NEW 线程还没被创建出来,只是存在这个线程对象~

    • start 创建 + 启动
    • 也就是说start之前~
  2. TERMINATED 系统中的线程已经执行完毕~

    • 但是线程引用还在~
  3. RUNNABLE 就绪状态

    • 正在CPU上运行
    • 准备好,随时可以去CPU运行
    • 这两种情况,基本“同时”
  4. TIMED_WAITING

    • 被指定时间的等待—sleep
    • 对于join方法
      • 即使正在等待的线程是完全阻塞的状态
      • 我们可以通过全局性质的静态变量去获得线程引用并在lambda表达式中被捕获到~
      • 如果有时间限制就是TIMED_WAITING~
      • 否则是WAITING~
  5. BLOCKED

    • 表示等待锁出现的状态
    • 不在本章中讲解
  6. WAITING

    • 使用 wait 方法出现的状态
    • 不在本章中讲解

【JavaEE】进程与线程-Java多线程编程_第29张图片

  • 掌握线程的状态,可以更好的进行多线程代码的调试,知己知彼百战百胜

文章到此结束!谢谢观看 !
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭

后续会有几篇关联博客,线程安全问题…

敬请期待吧~



你可能感兴趣的:(JavaEE,java,java-ee,jvm)