1、线程-基础 2022-02-15 19:26 > 以下基于JDK1.8 > 从此篇开始会对Java中的多线程有一系列文章进行讲述,这些文章并非原创,也并非照搬照抄。而是我在学习路上看过的各种文章、视频、书籍等学到的东西,最后经由自己反复验证并总结出的内容。 > 有些Demo是参考了一些非常厉害的博主的文章,写的时候仓促了可能有些未标明来源,之后会收集起来一一标明的。 ### 线程的概念和状态 进程与线程的区别: **进程是资源分配的最小单位,线程是程序执行的最小单位。**一个进程中可以有多个线程协助执行,每个线程独立完成一件事情,他们共同完成一个工作。一个进程内的不同线程可以共享进程的资源,同时也可以拥有各自线程私有的资源。资源分配只能按照进程为单位分配,至于你进程内部怎么个执行,多少个线程执行,都是你内部自己决定的。【进程,通常是程序、应用的同义词】 大部分Java虚拟机都是作为单进程启动的,但我们也可以通过ProcessBuilder来创建附加的进程。 每个进程至少有一个线程,作为程序的入口,通常情况下这个线程我们称之为主线程。在Java中,程序的入口是main方法,因此main方法实际上就是运行在主线程中的。 Java中的线程是和操作系统线程一一对应的,但是Java线程中的状态并不反应操作系统线程的状态,只是一种模拟,是JVM虚拟机级别的状态。 Java中线程的6种状态: ```java /** * 线程在给定时间点只能处于一种状态。 * 这些状态是虚拟机状态,不反映任何操作系统线程状态. * * @since 1.5 * @see #getState */ public enum State { /** * 新建:尚未启动的线程处于此状态. * Thread state for a thread which has not yet started. */ NEW, /** * 准备就绪:在Java虚拟机中执行的线程处于此状态. * 处于此状态的线程可能正在执行,也可能在等待CPU来执行他。总之,他是runnable的,即随时可执行,表明了他已经拿到了所有资源(有锁抢占时表明了他已经抢到锁) * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * 阻塞:等待监视器锁而被阻止的线程处于此状态. * 锁被其他线程抢占了,当前线程正在等待锁,则他处于此状态:BLOCKED,阻塞 * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * 不见不散:无限期等待另一个线程执行特定操作的线程处于此状态. * 就是说,调用wait()或类似的方法,使线程处于此状态:WAITING,等待;调用wait()的线程是等待其他线程去调用notify()。由此可见,此状态天生就是为线程间协程/通信而生的 * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. */ WAITING, /** * 过时不候:等待另一个线程执行操作达指定等待时间的线程处于此状态. * 调用wait()或类似方法,传入指定等待时间的线程处于此状态 * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * 终结:已退出的线程处于此状态. * 已经结束的线程,线程已经完成了执行 * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; } ``` 值得注意的是,BLOCKED和WAITING的中文释义前者叫做”阻塞“,后者叫做”等待“,而这跟平常人们说的阻塞等待是不同的。BLOCKED和WAITING是两种线程的状态:当一个线程由于没有获取到锁而等待锁时,它处于BLOCKED”阻塞“状态,阻塞的线程在EntryList队列中;当一个线程主动执行了wait()方法,它处于WAITING状态,等待的线程在WaitSet队列中。 所以 阻塞 和 等待 并不能放在一起说,人们日常说的阻塞等待更趋于WAITING等待状态。 ### 创建线程的几种方式 1、继承Thread类,重写run()方法。 2、实现Runnable接口,重写run()方法。 3、实现Callable接口,重写call()方法。 4、使用线程池 线程池顾名思义是一个池子,一般来说池都是用来管理资源的,比如数据库链接池。线程池内部依旧是一个个线程,只不过进行了统一的资源管理,所以他不算是一种创新的创建线程的方式,其本质上还是使用前三种来创建线程的,且其内部管理流程复杂又重要,后面会单独开一个章节来介绍,这里先不表。 下面就用前三种方法分别创建线程来执行看看效果: ```java package com.example.demo.core.Thread.create; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; /** * @author: HanXu * on 2022/2/8 * Class description: 创建线程的几种方式 */ public class Demo1 { public static void main(String[] args) { //第一种:继承Thread类 MyThread myThread = new MyThread(); //第二种:实现Runnable接口 MyRunnable myRunnable = new MyRunnable(); Thread threadRunnable = new Thread(myRunnable); //第三种:实现Callable接口 MyCallable myCallable = new MyCallable(); FutureTask futureTask = new FutureTask<>(myCallable); Thread threadFuture = new Thread(futureTask); myThread.start(); threadRunnable.start(); threadFuture.start(); /* 这是MyThread线程内部做的事情 这是MyRunnable线程内部做的事情 这是MyCallable线程内部做的事情 */ } } class MyThread extends Thread { @Override public void run() { System.out.println("这是MyThread线程内部做的事情"); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("这是MyRunnable线程内部做的事情"); } } class MyCallable implements Callable { @Override public Object call() throws Exception { System.out.println("这是MyCallable线程内部做的事情"); return 1; } } ``` #### 区别及线程体系: 可以看出,Thread类本身是一个线程类,采用第一种方式直接就能使用线程;而第二种是将Runnable的实现类注入到Thread中、第三种则是将Callable的实现类先注入到FutureTask中,又将futureTask注入Thread中。即最后启动线程时都是使用Thread作为实际对象去启动线程。 来看一下Java的线程体系: ![](http://minio.riun.xyz/riun1/2022-02-08_23TgbFXRBBhEHpaFfT.jpg) 其中,Thread、Runnable是在JDK1.0版本就有的,而Callable、Future、RunnableFuture、FutureTask这些都是在JDK1.5及之后才引入的,出自Doug Lea之手。 #### Runnable与Callable: Runnable是接口,Callable也是接口,但是Callable的call()方法运行返回值,这就意味着线程在执行时可以返回给外部信息了,进一步来说,返回的信息肯定是被外部其他线程接收到,那么这就实现了线程间通信。 例如下面例子中,线程1和线程2各自循环n次,主线程最后知道他们循环的次数。 ```java package com.example.demo.core.Thread.create; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @author: HanXu * on 2022/2/8 * Class description: */ public class Demo2_Callable { public static void main(String[] args) throws ExecutionException, InterruptedException { CountCallable callable1 = new CountCallable(); CountCallable callable2 = new CountCallable(); FutureTask futureTask1 = new FutureTask<>(callable1); FutureTask futureTask2 = new FutureTask<>(callable2); Thread thread1 = new Thread(futureTask1); Thread thread2 = new Thread(futureTask2); thread1.start(); thread2.start(); while (!futureTask1.isDone() || !futureTask2.isDone()) { } Integer result1 = (Integer) futureTask1.get(); Integer result2 = (Integer) futureTask2.get(); System.out.println(result1 + result2); } } class CountCallable implements Callable { @Override public Object call() throws Exception { Random random = new Random(); int i = random.nextInt(100); for (int j = 0; j < i; j++) { //to do something } System.out.println(i); return i; } } ``` #### 用户线程与守护线程: Java中的线程分别用户线程和守护线程,可以主动设置,不设置则默认是用户线程: ```java MyThread myThread = new MyThread(); //true,则将该线程设置为守护线程 myThread.setDaemon(true); myThread.start(); ``` Java主线程不会等待守护线程执行,也就是说如果一个线程是守护线程的话,那么有可能他还没有执行完,程序就结束了。一般将一些不重要的子线程设置为守护线程。 注意:setDaemon()必须在启动线程之前,即线程start()后就不能在设置了,原因:Thread源码中,在检测到线程已启动时会抛出异常。 ```java `Thread` public final void setDaemon(boolean on) { checkAccess(); if (isAlive()) { throw new IllegalThreadStateException(); } daemon = on; } ``` #### 分析上述线程状态的变化: 上述代码中,三种方式创建线程之后,他们都处于NEW状态;当执行完各自的start()方法后,他们就变为RUNNABLE状态;分别执行完毕(各自run()或者call()方法内部的代码就是他们各自的线程任务),他们就都处于TERMINATED状态。main方法是由main线程(主线程)进入并执行的,main线程会等待子线程都执行完毕才会结束,子线程都TERMINATED结束了,main也没有代码可执行了,因此main线程结束,整个java程序结束运行。 #### 为什么要调用start()? 我们的代码中明明重写的是run()方法,为什么要调用start()方法开启线程呢? 这个问题很简单,稍微想一下:我们重写的是run(),如果我们直接调用run()那不就是普通的方法调用吗?那就仍然是主线程(main线程)去执行run()方法,就没有另外开启线程了。 所以我们不能用run()来开启线程,JDK规定的是start()方法,那就说明start()内部一定有开启线程的动作,来看一下源码: ```java `Thread` public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } ``` 其他代码都平平无奇,重要的是try代码块中的 `start0();`,是它开启了线程,那点进去看一下: ```java `Thread` private native void start0(); ``` `start0()`是一个native方法,native意味“本地”,由此关键字修饰的方法是由C/C++编写的底层源码,在JDK的源码中我们无法在Java中看到其实现。 ### 多线程有什么用? 从上述例子中我们大概知道了在Java中线程的概念和怎样创建一个线程并执行任务。多线程就是多个线程,那多个线程究竟有什么用呢? 多线程主要是用来提升程序执行效率。一方面,现在的服务器一般都是多核CPU,比如常见的配置:4C8G,就代表了4核8G。4个核心处理器就能在同一时间片同时并行执行4个线程(每个CPU执行一个线程任务),这样就能提供机器利用率从而提升程序执行效率。另一方面在业务逻辑中有些业务是耗时的,或者不那么重要的,此时如果我们单独开启一个线程对他进行异步处理,效果会好很多。例如文件上传,一个很大的文件需要由客户端上传到服务器,可能还要进行解析加工等操作,难道要一直卡着用户界面等待所有事情完成了吗?有了多线程就可以开一个线程去单独执行这个操作,用户上传文件的时候即时返回一个临时结果,可以让用户去其他操作界面操作,等这边处理完了再通知用户。还有就是程序中要记录日志,一些特定的日志也可以使用单独的系统,异步去记录。 #### 多线程的重点 通过上述说明,我们应该可以感觉到,多线程代码编写和执行过程中,重要的是多个线程间如何进行协作,如何通信,和关于共享资源的操作。 --END--
发表评论