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

简介: JavaEE & 进程与线程

JavaEE & 进程与线程

1. 多线程

一个进程即同一个程序,内部有多个任务,多线程更合适,因为互相影响也仅限于此程序,这样就提高了效率~

例如:QQ的聊天,邮箱,游戏,录制,截图,QQ空间~

多个程序,即多个进程,那么他们之间就是相互隔离的,一个出bug,其他进程不受影响~

例如:CCtalk,QQ,微信~

有可能有人会说,微信摄像头和QQ摄像头或者CCtalk摄像头这些不同进程的线程之间也会影响~

这只是因为你摄像头就只有一个~

3037ec5c93954e36be337bea54549181.png

1.1 线程越多,越好?

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


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


并且,线程是需要有系统资源的~

一些线程等不及了,就要挤掉一些线程~


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


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


多加一个主机,多一个CPU就可以解决,即“分布式系统”~

这样很容易导致一个线程异常了,整个进程也就狗带了~


举个栗子:


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


1.2 进程与线程的区别总结

进程包含线程


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


一个进程的多个线程之间共用~

进程是操作系统分配内存的基本单位


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


宏观上:


957229dc05394ef18f23ffd0a5941ecf.gif

微观上:

7719aa6b306e412394c6a58f665ffbc3.gif


进程之间有“隔离性”,一个进程挂了并不会影响别的进程

同一进程不同线程共用同一份地址空间和文件描述符表,一个线程挂了,可能会导致整个进程直接崩了~

2. Java与多线程

Java不提倡多进程编程

Java的jdk并没有封装多少多进程API,有也很简陋~

Java很提倡多线程编程

Java的jdk有很多多线程API,很完善~

接下来的内容,新概念会很多,学习并且习惯就好~

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

6023ea6bd14447baae8f4f42f9069a6a.png


通过这个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);
        }
    }
}

main方法是一个线程


我们有时会遇到,不同类可以重复定义main方法

而这多个main是不同的线程

并且毫不相干

自己建立的线程也是一个线程


两个线程并发执行


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


补充知识:sleep方法,即休眠,让线程进入阻塞态~

* 该状态下,线程无法进行执行

* 输入一个long类型值(单位ms),表示休眠时间~

* 这个类是Thread系统类的一个静态方法,直接通过类名进行引用

* 这个方法附带一个“编译时异常”,InterruptedException,这个异常在这里不做赘述,但是在后面很重要,现在浅浅认为它就是个异常而已~


处理异常:(alt + 回车)

try catch 环绕

throws

main线程是可以用的

但是自己创建的线程不行,因为run方法可以throws,但是start方法并没有进行异常处理,也没有进行throws~

5a96d9558e6f42d39cd0626137fb6933.png

e9d5aeb228044ee895031544c4418b1e.png


调用start启动线程,run方法会被Java自动调用~

run方法可以看成是别的线程的main方法!

怎么实现与main线程并发的,不是我们需要想的~

run方法是线程运行,被核心调度的一个方法


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


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


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


16d337e958b74480a8110bd7111c8fe8.png


本质上,五种语法都是通过重写run方法~

测试:(按ctrl + F2结束程序)

两个线程“同时”执行:

d8bb19c1d1ce44e48ee4b86ca3761fa6.gif


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


9dd575b8894d408390b6cb2a8667955f.png

找到jconsole.exe,这是jdk提供的根据,可以观看也仅能观看Java中线程的详情~

双击~

可能会出现,啥都没有的情况,我们需要用管理员的身份打开~


58400947ce784d6bae51312f0f4c8920.png

6bc1997c82ce46eea4bc6c1e25b1f3c8.png



双击我们自己写的线程~

本来就不安全,知识提醒你罢了,点了~

允许~


487ecda77bb348d4b666d9a1e5732b56.png


4c292e04d7a54537a23d796dee2c89ec.png

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


dd463c175a9540b197e805b9ce212d16.png

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

2356ef67f1c646a59e277e4375a1c680.png


只有这两个是我们自己写的,其他都是jvm自己创建和启动的~

jvm启动这些去干脏活累活~

点击两个线程~

8f3abfc376504d86be8893911d2532c3.png



显示的信息就是执行情况~

比如执行到哪里~

用这个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结束程序)


两个线程“同时”执行:


5.1.3 Thread提供的构造方法,”传入工具“法

在之前构造搜索二叉树的时候,传入比较器,就是一个“传入工具”法~


类似的,Thread系统类也提供了几个构造方法

6cec87eeca254e9584dfad9ea4d21824.gif


重点掌握如图两种~


单纯传入一个“线程可执行器”

这样通过这个“线程可执行器”,就可以重写run方法了~

这个构造方法内部就会利用这个工具去重写run~

我们的工具类要实现Runnable接口~

Runnable是个函数式接口~【铺垫】

多传入一个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();
            }
        }
    }
}

测试跟上面是一样的,正常

看一看别名的那个案例吧~

aaed0fb44053480093ae380572aec920.png

810d93aa7a574e4bba5d79c6e6a6cdd2.png



是不是很显眼 ^ 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 后台线程

我们创建的线程都是前台线程,前台线程会阻止Java程序结束

而jvm自动创建的后台线程,不会阻止Java程序结束,一旦前台线程结束,后台线程无论是否执行完,都会结束程序~

创建的线程可以设置的后台,setDeMon(true)

设置后,不影响程序结束~


1f2196f241ed4f4c8dad2fb1d4740f59.png

3.2 是否存活

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


问题来了,在run方法内怎么获得线程引用呢,因为线程引用是在run方法重写后产生的,怎么获得呢?

Thread.currentThread()调用这个Thread的静态方法(一样会抛异常,但是我们已经解决了)

9c42654640e34a5ea38610ee86ebb02c.png


测试:


只有在执行前,执行完毕是false

执行中都是true

f453ed89267e491292d4551fccbb18bf.gif


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());
}


对于执行完毕,有如上代码

几乎每次运行结果都是这样的

d268b43a06f64206bbb62b2c0b36be55.png

但是极端情况下,如果该线程在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");
    }
}

测试:

绝大多数结果是这样的:


2c929b0a070a4ef3b515768a567868c8.gif

3.3.2 线程引用自带的一些方法

Thread.currentThread() 
//获取当前线程的引用
isInterrupted()  
//判断线程引用内置的标识符是真是假 
    //----反应是否可以被中断
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");
}


测试:


70f7787a4d9843c688328c2df8a52284.gif

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


极大概率是这样的~

恰好“睡醒”也不好说~

因为睡眠时间占99.999%d的时间~

原因就是:


interrupt()方法将线程引用的标识符改为了true

而线程的阻塞就会立即被疏通,或者说线程会立即醒来~

但是线程醒来,会将标识符情况成默认值(false)

并且抛出InterruptedException异常

catch到后打印异常

此时的循环判断仍然为真~

解决:在捕获到异常后,直接break~

c88e7a689c414a59a938d3dbc76bbede.png

测试:

d0804bea88824d329e68787e9e00c8aa.gif


3.4 join方法

join方法为等待方法,代表某线程XX时间,本线程再继续执行

join(long time), 最多等待time 毫秒

提前结束就不再等了~

join(), 等待线程完全结束~

确定此语句出现在哪个线程

调用此方法的线程引用对应的线程

前者等后者~

即前者线程进入阻塞态

等待后者线程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");
}


测试:

f0e6961ef20a4ef2affe0ac86dd4b080.gif


3.5 线程的状态

没有测试案例


操作系统里的线程,自身是有一个状态的

但是Java Thread 是对系统线程的封装,把这里的状态又细致化了~

状态获取:getState();

NEW 线程还没被创建出来,只是存在这个线程对象~


start 创建 + 启动

也就是说start之前~

TERMINATED 系统中的线程已经执行完毕~


但是线程引用还在~

RUNNABLE 就绪状态


正在CPU上运行

准备好,随时可以去CPU运行

这两种情况,基本“同时”

TIMED_WAITING


被指定时间的等待—sleep

对于join方法

即使正在等待的线程是完全阻塞的状态

我们可以通过全局性质的静态变量去获得线程引用并在lambda表达式中被捕获到~

如果有时间限制就是TIMED_WAITING~

否则是WAITING~

BLOCKED


表示等待锁出现的状态

不在本章中讲解

WAITING


使用 wait 方法出现的状态

不在本章中讲解


9dbd6ceb383b4048bf419dd7b5a47e74.png

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


相关文章
|
1天前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
16 5
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
2天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
11 3
|
2天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
54 2
|
2天前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
41 3
|
Java
Java多线程编程核心技术(三)多线程通信(下篇)
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
662 0
|
Java
Java多线程编程核心技术(三)多线程通信(上篇)
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
2529 0
|
Java 安全
Java多线程编程核心技术(二)volatile关键字
关键字volatile的主要作用是使变量在多个线程间可见。
836 0
|
Java
Java多线程编程核心技术(一)Java多线程技能
本文为《Java并发编程系列》第一章,主要介绍并发基础概念与API
2420 0