java多线程简单实例(秒懂|结合多本著作和个人开发经验,整理Java多线程入门手册)java教程 / Java多线程编程入门...

wufei123 发布于 2024-02-20 阅读(53)

满怀忧思,不如先干再说!通过学习,重新定义自己!前言前段时间推出的Java8新特性文章收到大家广泛关注和好评,非常感谢各位支持,这段时间苦思冥想,决定输出一波Java多线程技能点,希望可以在大家的工作和面试中有所帮助!本篇文章为多线程系列第一章,主要讲解一下几点:

多线程好处和应用场景多线程的相关概念和术语Java线程创建方式Thread类详解,线程的常用方法线程5种状态和6种状态,两种版本解释线程状态之间转换Java设计者写过一个很有影响力的白皮书,用来解释设计的初衷,并发布了一个简短的摘要,分为11个术语:

简单性面向对象分布式健壮性安全性体系结构中立可移植性解释型高性能多线程动态性其中多线程就是本次要接触的,白皮书中对多线程的解释:多线程可以带来更好的交互响应和实时行为如今,我们非常关注并发性,因为摩尔定律行将完结。

我们不再追求更快的处理器,而是着眼于获得更多的处理器,而且要让它们一直保持工作不过,可以看到,大多数编程语言对于这个问题并没有显示出足够的重视Java在当时很超前它是第一个支持并发程序设计的主流语言从白皮书中可以看到,它的出发点稍有些不同。

当时,多核处理器还很神秘,而Web编程才刚刚起步,处理器要花很长时间等待服务器响应,需要并发程序设计来确保用户界面不会"冻住"并发程序设计绝非易事,不过Java在这方面表现很出色,可以很好地管理这个工作。

在操作系统中有多任务【multitasking】,在同一刻运行多个程序【应用】的能力例如,在听音乐的同时可以边打游戏,边写代码如今我们的电脑大多都是多核CPU,但是,并发执行的进程【正在执行的应用】数目并不是由CPU数目制约的。

操作系统将CPU的时间片分配给每一个进程,给人并行处理的感觉相关概念程序【program】:为了完成特定任务,用某种语言编写的一组指令的集合程序就是一堆代码,一组数据和指令集,是一个静态的概念就说我们程序员写的那玩意。

比如:安装在电脑或者手机上的各种软件,今日头条、抖音、懂车帝等,如果一个程序支持多线程,这个程序就是一个多线程程序进程【Process】:是程序的一次执行过程或者说是正在运行的程序,是一个动态概念,进程存在生命周期,也就是说程序随着程序的终止而销毁

线程【Thread】:线程是进程中的实际运作的单位,是进程的一条流水线,是程序的实际执行者,是最小的执行单位通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程线程是CPU调度和执行的最小单位。

CPU时间片:时间片即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的,如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。

如果进程在时间片结束前阻塞或结束,则CPU当即进行切换而不会造成CPU资源浪费并行【parallel】:多个任务同时进行,并行必须有多核才能实现,否则只能是并发,比如:多名学生有问题,同时有多名老师可以辅导解决

串行【serial】:一个程序处理完当前进程,按照顺序接着处理下一个进程,一个接着一个进行,比如:多名学生有问题,只有一名老师,需要挨个解决并发【concurrency】:同一个对象被多个线程同时操作(这是一种假并行。

即一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉),比如:多名学生有问题,只有一个老师,他一会处理A同学,一会处理B同学,一会处理C同学,频繁切换,看起来好似在同时处理学生问题。

多线程意义实际应用中,多线程非常有用,例如,QQ音乐就是一个多线程程序,我们可以一边听音乐,一般下载音乐,还可以同时播放MV等非常方便一个Web服务器通过多线程同时处理多个请求,比如Tomcat就是多线程的。

注意:程序会因为引入多线程而变的复杂,多线程同时会带来一些问题,需要我们解决多线程应用场景程序需要同时执行两个或多个任务程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等需要一些后台运行的程序时。

多线程多数在浏览器、Web服务器、数据库、各种专用服务器【如游戏服务器】、分布式计算等场景出现在使用Java编写后台服务时,如果遇到并发较高、需要后台任务、需要长时间处理大数据等情况都可以创建线程单独的线程处理这些事项,多线程的目的就在于。

提高处理速度,减少用户等待时间后台记录日志,创建额外线程来记录日志大量用户请求,创建多个线程共同处理请求下载大文件,可以创建单独的线程,不影响正常的业务流畅度......多线程创建方式线程创建有4种方式:

方式1:继承Thread类方式2:实现Runnable接口方式3:实现Callable接口方式4:使用线程池【这块后边单独说,它更像是管理线程的手段】继承Thread步骤:自定义类继承Thread类重写run方法,run方法就是线程的功能执行体

创建线程对象,调用start方法启动线程启动线程之后,线程不一定会立即执行,需要得到CPU分配的时间片,也就是拿到CPU执行权限才会执行JDK源码中,Thread类定义实现了Runnable接口

所以知道重写的run方法从哪来的了吧!就是从Runnable接口中来的需求:创建线程计算10以内的偶数线程类:publicclassThreadTestextendsThread{ // run方法是 线程体,启动线程时会运行run()方法中的代码

@Overridepublicvoidrun(){ // 输出10以内偶数for (int i = 0; i < 10; i++) { if (i % 2 ==

0){ System.out.println(i); } } } }测试类:测试类中输出了一句话:主线程publicclass

ThreadMain { publicstaticvoidmain(String[] args) { // 1、创建线程对象 ThreadTest t1 = new

ThreadTest(); // 2、调用start方法启动线程 t1.start(); System.out.println("主线程"); } }

打印结果:

实现Runnable接口步骤:自定义类实现Runnable接口实现run方法创建实现类对象创建Thread对象,在构造方法中传入实现类对象作为参数调用Thread对象的start方法启动线程同样的需求打印10以内的偶数

实现类:publicclassRunnableImplimplementsRunnable{ @Overridepublicvoidrun(){ // 输出10以内偶数for (

int i = 0; i < 10; i++) { if (i % 2 == 0){ System.out.println(i); } } } }

测试类:publicclassRunnableMain { publicstaticvoidmain(String[] args) { // 1、创建实现类对象 RunnableImpl runnable =

new RunnableImpl(); // 2、创建线程对象,接收实现类,因为实现类中的run方法承载了线程的功能 Thread t1 = new Thread(runnable);

// 3、启动线程 t1.start(); // 主线程 System.out.println("主线程"); } }Callable接口FutureTask类:

RunnableFuture接口:

步骤:新建类实现Callable接口,并指定泛型类型,类型就是线程计算之后的结果的类型实现call方法,call方法跟run方法类似,不过该方法有返回值和异常抛出,都是来封装线程功能体的在测试类中创建实现类对象,并且创建 FutureTask 对象将实现类对象当做构造方法参数传入

创建Thread线程对象,将 FutureTask 对象当做构造方法参数传入,并调用start方法启动线程可以通过 FutureTask 对象的get方法获取线程的运算结果案例:还是计算10以内的偶数,这一次将计算结果返回,因为有多个数据所以返回数据用集合存储,则Callable接口的泛型类型应该是集合

实现类:import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; // 1、实现Callable,指明泛型类型

publicclassCallableImplimplementsCallable {// 2、线程返回Integer类型数据,抛出异常 @Override

public List call() throws Exception { List list = new ArrayList<>();

for (int i = 0; i < 10; i++) { if (i % 2 == 0){ // 3、偶数存储到集合中list.add(i); } }

// 4、返回集合returnlist; } }测试类:import java.util.List; import java.util.concurrent.ExecutionException;

import java.util.concurrent.FutureTask; publicclassCallableMain {publicstaticvoidmain(String[] args)

{ // 1、创建Callable实现类对象 CallableImpl callable = new CallableImpl(); // 2、创建 FutureTask对象传入 callable

// FutureTask 实现 了 RunnableFuture,RunnableFuture实现了Runnable接口和Future接口 FutureTask task =

new FutureTask<>(callable); // 3、将 task 传入线程对象 Thread t1 = new Thread(task);

// 4、启动线程 t1.start(); // 5、获取线程返回数据try { List list = task.get(); System.out.println(

list); } catch (Exception e) { e.printStackTrace(); } } }三种实现方式区别Java单继承的特性决定,使用实现接口的方式创建线程更灵活

Callable实现call方法有返回值和异常抛出,方便定位问题,配合FutureTask可以获取线程运算结果,而run方法没有返回值,没有异常抛出如果线程运行结束后不需要返回值,则推荐使用实现Runnable接口方式

小贴士:有不少文章中写到【实现的方式更适合用来处理多个线程有共享数据的情况】,很多小伙伴也拿去背,这句话怎么看都不对吧,多线程共享数据不加锁,不同步怎么着也不能避免线程安全问题!线程开辟开辟线程需要通过Thread类创建对象

启动线程需要Thread对象调用start方法线程的功能可以装在Thread类的run方法或者Runnable接口实现类的run方法类中线程开辟需要在另一个线程中开启,新开辟的线程称为子线程

小贴士:对于Java应用程序java.exe来讲,至少会存在三个线程:main主线程gc垃圾回收线程异常处理线程,如果发生异常会影响主线程线程状态线程的状态网上有 5种状态 和 6种状态 两个版本五种状态版本:。

是基于现代操作系统线程状态角度解释的新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态就绪:处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在JDK5的时候Thread类中定义了一个State枚举类,其中定义了6种线程状态,这是Java官方定义的Java线程的6种状态

1)NEW:处于NEW状态的线程此时尚未启动只是通过new Thread()创建了线程对象,并未调用start()方法2)RUNNABLE:Java线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 。

就绪(ready) 和 运行(running) 两个状态的处于 RUNNABLE 状态的线程可能在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源3)BLOCKED:阻塞状态处于 BLOCKED 状态的线程正等待锁的释放以进入同步区,就好比你去食堂打饭,只有一个窗口你就得排队,等前边的人结束之后你完成打饭。

4)WAITING :等待状态处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒可以通过调用一下三个方法进入等待状态:Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;。

Thread.join():使当前线程等待另一个线程执行完毕之后再继续执行,底层调用的是 Object 实例的 wait() 方法;LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度

5)TIMED_WAITING:超时等待状态线程等待一个具体的时间,时间到后会被自动唤醒调用如下方法会使线程进入超时等待状态:Thread.sleep(long millis):使当前线程睡眠指定时间,sleep() 方法不会释放当前锁,但会让出 CPU,所以其他不需要争夺锁的线程可以获取 CPU 执行;。

Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify() / notifyAll() 唤醒;Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;

LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

6)TERMINATED:终止状态此时线程已执行完毕其实等待和锁定状态可以被笼统的称为阻塞状态,就是停着不动了嘛,在回答面试题时建议回答6种状态版本,就是是JDK源码中定义的,一来有官方支持,二来证明咱看过一点源码。

状态转换新建状态的线程调用start方法进入到运行状态运行状态线程如果遇到Object.wait()、Thread.join()或者LockSupport.park()方法则会放弃CPU执行权进入等待状态,这个装需要被唤醒之后才会再次进入就绪状态获得到CPU时间片进入运行状态

运行状态的线程遇到Thread.sleep(long)、Object.wait(long)、Thread.join(long)等方法,也就是可以传入时间的,就会进入超时等待状态,达到时间之后就会自动进入就绪状态,当CPU执行就进入运行状态

运行状态的线程如果被同步代码块或者同步方法包裹,执行时如果释放锁资源,就会进入阻塞状态或者叫锁定状态,只有再次获取到锁资源时才会进入就绪状态,等到CPU时间片后进入运行状态执行完的线程就会进入终止状态,线程结束

线程之间的状态转换可以参考下图

Thread类详解成员变量变量名类型作用namevolatile String线程名称priorityint线程的优先级,默认为5,范围1-10threadQThreadeetoplongsingle_step

boolean是否单步执行daemonboolean守护线程状态,默认为falsestillbornbooleanJVM状态,默认为falsetargettarget将被执行的Runnable实现类group

ThreadGroup当前线程的线程组contextClassLoaderClassLoader这个线程上下文的类加载器inheritedAccessControlContextAccessControlContext

该线程继承的AccessControlContextthreadInitNumberstatic int用于匿名线程的自动编号threadLocalsThreadLocal.ThreadLocalMap

属于此线程的ThreadLocal,这个映射关系通过ThreadLocal维持inheritableThreadLocalsThreadLocal.ThreadLocalMap这个线程的InheritableThreadLocal,其映射关系通过InheritableThreadLocal维持

stackSizelong此线程的请求的堆栈的大小,如果创建者的请求堆栈大小为0,则不指定堆栈大小,由jvm来自行决定一些jvm会忽略这个参数nativeParkEventPointerlong在本机线程终止后持续存在的jvm私有状态。

tidlong线程的IDthreadSeqNumberstatic long用于生成线程的IDthreadStatusvolatile intjava线程状态,0表示未启动parkBlockervolatile Object

提供给LockSupport调用的参数blockervolatile Interruptible此线程在可中断的IO操作中被阻塞的对象,阻塞程序的中断方法应该在设置了这个线程中断状态之后被调用常量/** * The minimum priority that a thread can have. */

publicfinalstaticint MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */

publicfinalstaticint NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */public

finalstaticint MAX_PRIORITY = 10;常量名数据类型作用MIN_PRIORITYint线程最低优先级NORM_PRIORITYint分配给线程的默认优先级MAX_PRIORITY

int线程最大优先级Thread构造方法从源码看出Thread类一共有9个构造方法,除第三个为default修饰【同包可用】,其他都是public

构造方法作用Thread()分配新的Thread对象Thread(Runnable target)传入Runnable接口实现类,之后由JVM启动线程Thread(Runnable target, AccessControlContext acc)

在传入Runnable的时候还可以指定AccessControlContextThread(ThreadGroup group, Runnable target)指定线程组和Runnable接口Thread(String name)

指定线程名字,默认是【Thread-下一个线程编号,从0开始】Thread(ThreadGroup group, String name)指定线程组和线程名字Thread(Runnable target, String name)

指定Runnable接口和线程名Thread(ThreadGroup group, Runnable target, String name)指定线程组,Runnable接口和线程名Thread(ThreadGroup group, Runnable target, String name,long stackSize)

指定线程组,Runnable接口,线程名和此线程请求的堆栈大小,默认为0Thread常用方法方法返回值类型作用start()void启动线程run()void重写的Runnable接口方法,封装线程的功能体

currentThread()Thread静态方法,获取当前线程getName()String获取线程名setName(String name)void设置线程名yield()void主动释放当前线程的执行权

join()void在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去sleep(long millis)void线程休眠一段时间isAlive()boolean

判断线程是否还存活isDaemon()boolean判断是否为守护线程stop()void过时方法当执行此方法时,强制结束当前线程,因过于粗暴,会引发很多问题所以弃用setDaemon(boolean on)。

void设置为守护线程getPriority()int获取线程优先级setPriority(int newPriority)void设置线程优先级设置线程名实现类:publicclassRunnableImpl

implementsRunnable{ @Overridepublicvoidrun(){ // 输出10以内偶数for (int i = 0; i < 10; i++) {

if (i % 2 == 0){ // 获取当前线程 Thread thread = Thread.currentThread();

// 获取线程名 String threadName = thread.getName(); System.out.println(threadName +

"===>" + i); } } } }测试类:publicclassRunnableMain { publicstaticvoidmain(String[] args

) { // 1、创建实现类对象 RunnableImpl runnable = new RunnableImpl(); // 2、 创建线程对象,并指定线程名

Thread t1 = new Thread(runnable, "线程1"); // 3、启动线程 t1.start(); System.

out.println(Thread.currentThread().getName() + "主线程"); } }运行结果:

或者通过setName()方法设置线程名publicclassRunnableMain { publicstaticvoidmain(String[] args) { // 1、创建实现类对象

RunnableImpl runnable = new RunnableImpl(); // 2、 创建线程对象,不指定名字 Thread t1 = new

Thread(runnable); // 设置线程名 t1.setName("线程1"); // 3、启动线程 t1.start(); System.

out.println(Thread.currentThread().getName() + "主线程"); } }如果不设置线程名,默认为【"Thread-" + nextThreadNum()】,nextThreadNum方法使用

threadInitNumber静态变量,默认从0开始,每次+1

不设置线程名运行效果如下

sleep方法sleep方法可以让线程阻塞指定的毫秒数时间到了后,线程进入就绪状态sleep可用来研模拟网络延时,倒计时等每一个对象都有一个锁,sleep不会释放锁,锁的概念后边会详细讲解实现类:public

classRunnableImplimplementsRunnable{ @Overridepublicvoidrun(){ // 输出10以内偶数for (int i = 0

; i < 10; i++) { if (i % 2 == 0){ try { // 休眠1秒 Thread.sleep(

1000); System.out.println(Thread.currentThread().getName() + "===>" + i); }

catch (InterruptedException e) { thrownew RuntimeException(e); } } } } }

测试类:publicclassRunnableMain {publicstaticvoidmain(String[] args){ // 1、创建实现类对象 RunnableImpl runnable =

new RunnableImpl(); // 2、 创建线程对象,不指定名字 Thread t1 = new Thread(runnable,"线程1");

// 3、启动线程 t1.start(); } }运行结果:

"善用"sleep年入百万不是梦:

yield方法提出申请释放CPU资源,至于能否成功释放取决于JVM决定,调用yield()方法后,线程仍然处于RUNNABLE状态,线程不会进入阻塞状态,保留了随时被调用的权利实现类:publicclass

RunnableImplimplementsRunnable{ @Override publicvoidrun() { System.out.println(Thread.currentThread().getName() +

"开始执行"); Thread.yield(); System.out.println(Thread.currentThread().getName() + "执行结束"

); } }测试类:publicclassRunnableMain {publicstaticvoidmain(String[] args){ // 1、创建实现类对象 RunnableImpl runnable =

new RunnableImpl(); // 2、 创建线程对象,不指定名字 Thread t1 = new Thread(runnable,"线程1"); Thread t2 =

new Thread(runnable,"线程2"); // 3、启动线程 t1.start(); t2.start(); } }运行结果:第五次执行是线程2执行开始结束后输出的线程1开始结束,这就说明CPU并没有切换到别的线程,说明并没有释放CPU资源

join方法将当前的线程挂起,当前线程阻塞,待其他的线程执行完毕,当前线程才能执行,可以把join()方法理解为插队,谁插到前面,谁先执行在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。

如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景publicclass

RunnableMain { publicstaticvoidmain(String[] args) { // 1、lambda创建线程 Thread t1 =

new Thread(() -> { for (int i = 0; i < 5; i++) { try {

// 模拟耗时操作 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() +

"join方法===>" + i); } catch (InterruptedException e) { thrownew RuntimeException(e); } } });

// 2、 启动线程 t1.start(); try { // t1调用join 方法 t1.join(); }

catch (InterruptedException e) { thrownew RuntimeException(e); } System.out

.println("main线程"); } }运行结果:

设置优先级改变、获取线程的优先级Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行线程的优先级用数据表示,范围1~10线程的优先级高只是表示他的权重大,获取CPU执行权的几率大。

先设置线程的优先级,在执行start()方法publicclassRunnableMain { publicstaticvoidmain(String[] args) { // 1、lambda创建线程

Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() +

"线程优先级" + Thread.currentThread().getPriority()); },"线程1"); Thread t2 = new Thread(() -> { System.

out.println(Thread.currentThread().getName() + "线程优先级" + Thread.currentThread().getPriority()); },

"线程2"); // 2、设置线程优先级 t1.setPriority(1); t2.setPriority(10); // 3、 启动线程

t1.start(); t2.start(); System.out.println("main线程"); } }结束线程JDK提供的【stop()、destroy()】两种方法已废弃,不推荐再使用。

推荐线程自动停止下来,就比如上边的所有案例,都是执行完了run方法中的所有代码之后线程就自然结束了如果线程需要循环执行,建议使用一个标识位变量进行终止,当flag=false时,则终止线程运行比如:定义一个名为【线程1】的子线程,当主线程执行3次循环之后,线程1停止运行

实现类:publicclassRunnableImplimplementsRunnable{ // boolean变量标记是否需要继续执行privateboolean flag = true;

publicbooleanisFlag(){ return flag; } publicvoidsetFlag(boolean flag){ this.flag = flag; }

@Overridepublicvoidrun(){ // 循环执行,flag为false时停止while (flag) { System.out.println(Thread.currentThread().getName() +

"正在运行"); } } }测试类:publicclassRunnableMain { publicstaticvoidmain(String[] args) { RunnableImpl runnable =

new RunnableImpl(); Thread t1 = new Thread(runnable, "线程1"); t1.start(); for (

int i = 0; i < 5; i++) { System.out.println("主线程====》" + i); // 当循环三次时if(i ==

3) { // 设置flag值为false runnable.setFlag(false); } } } }

总结掌握多线程的使用场景和术语熟练创建和启动线程掌握线程状态和状态之间转换掌握Thread类中的常用方法如:join、sleep、yield等后边会继续深入多线程知识点,绝对深,点赞,关注支持一下吧!

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

宝骏汽车 新闻85661