多线程之Thread类
线程
一、进程
进程是正在运行的程序,是系统进行资源分配和调度的基本单位;每个进程都有自己的内存空间和系统资源;进程和进程之间的内存空间是相互隔离开来的,互不干扰;
二、线程
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。也就是说线程之间共享的是一个进程之间的资源。
三、并发和并行
从以下三个角度来进行分析:
一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生;
二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件;
三:并行是在多台处理器上同时处理多个任务
看一下对应的示意图
并行
并发
四、线程创建
java中本质上只有一种方式创建线程。也就是通过new Thread()方式来创建一个线程。但是因为java屏蔽了底层,所以对外提供了三个API,如下所示:
- 继承Thread类,重写run方法;
- 实现Runnable接口,重写run方法;
- 实现Callable接口,重写run方法;
其实本质上还是调用Thread类中的start方法。
4.1、继承Thread类重写run方法
4.2、实现Runnable接口
4.3、实现Callable接口
4.4、小结
4.4.1、线程的本质
本质上都是一种方式!!!!都是start方法之后调用run方法而已,那么来看下Callable方法中的调用
4.4.2、为什么需要多线程
看一段代码:
/**
* @author lg
* @Description
* @date 2022/10/25 14:27
*/
public class ThreadExtentTest extends Thread{
private static Logger logger = LoggerFactory.getLogger(ThreadExtentTest.class);
@Override
public void run() {
for (int i = 0; i < 2; i++) {
logger.info("当前线程名称是:{}",Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadExtentTest thread = new ThreadExtentTest();
Thread t1 = new Thread(thread, "t1");
Thread t2 = new Thread(thread, "t2");
Thread t3 = new Thread(thread, "t3");
t1.start();
t2.start();
t3.start();
}
}
结果如下所示:
2023-05-30 15:38:19.368 [t3] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t3
2023-05-30 15:38:19.368 [t1] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t1
2023-05-30 15:38:19.368 [t2] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t2
2023-05-30 15:38:19.371 [t3] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t3
2023-05-30 15:38:19.371 [t2] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t2
2023-05-30 15:38:19.371 [t1] INFO com.guang.thread.ThreadExtentTest - 当前线程名称是:t1
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
那么进一步就是乱序的代码中是否涉及到对共享资源的操作,这就涉及到了共享资源的安全问题。
4.4.3、Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 1):适合多个相同的程序代码的线程去处理同一个资源
- 2):可以避免java中的单继承的限制
- 3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
五、线程启动
5.1、start方法和run方法的区别
首先看下Thread类中关于start方法和run方法的描述
start方法:
Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.
The result is that two threads are running concurrently: the current thread (which returns from the call to the start method) and the other thread (which executes its run method).
It is never legal to start a thread more than once. In particular, a thread may not be restarted once it has completed execution.
Throws:
IllegalThreadStateException – if the thread was already started.
线程对象调用start方法将会致使线程开始运行,JVM将会调用当前线程的run方法。
最终的结果是两个线程并发执行,当前线程(从对start方法的调用返回)和另一个线程(执行其run方法)。
多次调用线程的start方法是非法的。而且,一个线程在它运行完成之后,可能并不会重新启动。
如果一个线程已经启动了,还来调用start方法,将会抛出异常。
run方法
If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns.
Subclasses of Thread should override this method.
如果Thread构造函数中是Runnable对象,然后它的run方法将会被调用,否则,调用之后不做任何事情,也没有返回。
Thread作为Runnable的子类应该重写这个方法。
根据上面的描述,在线程中调用了线程的start方法之后,JVM调用来调用线程的run方法
画个图表示一下:
对应的底层逻辑如下所示:
通过最简单的方式来创建一个线程并启动线程,这里来看下对应的原理。
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("thread run.........");
}
};
thread.start();
}
首先需要说明两点:
1、线程是操作系统的资源。java创建线程,本质上就是利用通过JVM来调用操作系统对外提供的函数而已。
在java中,将线程资源封装成一个线程,这个时候并不是真正的线程,只有通过JVM来调用操作系统提供的创建线程的函数之后,才会在操作系统中来创建一个线程。所以java中的线程和操作系统中的线程映射关系是:1:1
2、操作系统对外提供创建线程的API中,要求传入创建线程需要执行的任务,所以JVM在创建线程的时候就需要将任务传给API。而这个任务就是java线程对象中的run方法;
那么首先通过源码来看一下java是不是调用了JVM,直接看一下run方法:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
// 直接调用本地方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
java线程对象调用了start0方法来让JVM,而JVM拿到java线程的任务之后调用操作系统提供的API创建线程。
@Override
public void run() {
if (target != null) {
target.run();
}
}
这里相当于是操作系统使用对象调用run方法了!!!
5.2、线程的启动
对于java中的多线程来说,java调用了run方法之后,只是通过JVM告知了操作系统,当前的线程可以被调用。也就是说,当前的线程处于就绪状态。
但是对于一个就绪状态的线程来说,什么时候会被调用?这是不知道的,因为操作系统是会被随机调度的。
举个例子说明:
Thread thread1 = new Thread(() -> {
System.out.println("执行业务方法");
},"t1");
Thread thread2 = new Thread(() -> {
System.out.println("执行业务方法");
},"t2");
thread1.start();
thread2.start();
这里的线程映射到操作系统中去了之后,执行顺序是不一定的!!
操作系统可能会先调用线程t1先执行,线程t2后执行;也有可能是线程t2先执行,线程t1后执行。
只是在java层面上,按照顺序执行优先级,认为是线程t1先执行,线程t2后执行而已。
但是实际上在底层并非如此。完全取决于CPU随机调用哪个线程而已。
六、线程状态
6.1、线程状态说明
这里在Thread类中,进行了详细的说明。下面来看下源码中的叙述:
A thread state. A thread can be in one of the following states:
NEW A thread that has not yet started is in this state.
RUNNABLE A thread executing in the Java virtual machine is in this state.
BLOCKED A thread that is blocked waiting for a monitor lock is in this state.
WAITING A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
TERMINATED A thread that has exited is in this state.
A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.
最终补充了一句:JVM中的线程状态不代表操作系统的线程状态
java线程类中提供了一个枚举来进行说明:
public static enum State {
// Thread state for a thread which has not yet started.
NEW,
//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,
// 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 Object.wait.
BLOCKED,
//Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
// Object.wait with no timeout
// Thread.join with no timeout
// LockSupport.park
// A thread in the waiting state is waiting for another thread to perform a particular action.
//For example, a thread that has called Object.wait() on an object is waiting for another thread
//to call Object.notify() or Object.notifyAll() on that object.
//A thread that has called Thread.join() is waiting for a specified thread to terminate.
WAITING,
TIMED_WAITING,
// Thread state for a terminated thread. The thread has completed execution.
TERMINATED;
}
6.2、线程状态转换说明
看一下从网上找到的一个帖子,线程的转换状态:
New: 刚创建而未启动的线程就是这个状态。由于一个线程只能被启动一次,因此一个线程只可能有一次在这个状态。
Runnable:如上图,这个状态实际是个复合状态,包含两个状态:runalbe 和 running。runalbe 是就绪状态,可以被JVM 线程调度器(Scheduler) 进行调度,如果是单核CPU,同一时刻只有一个线程处于Running 状态,可能有多个线程处于 Ready 状态,Running 表示当前线程正在被CPU 执行,在Java 中就是Thread 对象只有run() 方法正在被执行。当 yield() 方法被调用,或者线程时间片被用完,线程就会从 Running 状态转为 Ready 状态。CPU时间片和主机时钟频率有关系,一般是10 ~ 20 ms。
Blocked:这里又分为两种情况:
-
一个线程发生一个阻塞式I/0 (文件读写I/O, 网络读写I/O)时;
线程在操作IO操作时,等到操作完成,CPU会发信号进行通知IO操作完成,线程继续操作;
-
试图获取其他线程持有的锁时,线程会进入此状态;
例如:获取别的线程已经持有的 synchronized 修饰的对象锁。在Blocked 状态的线程不会占用CPU 资源,但是程序如果出现大量处于这个状态的线程,需要警惕了,可以考虑优化一下程序性能。
Waiting: 一个线程执行了Object.wait( )、 Thread.join( ) 、LockSupport.park( ) 后会进入这个状态,这个状态是处于无限等待状态,没有指定等待时间,可以和Timed_Waiting 对比,Timed_Waiting是有等待时间的。这个状态的线程如果要恢复到Runnable 状态需要通过别的线程调用Object.notify( )、Object.notifyAll( )、LockSupport.unpark( thread )。
Timed_Waiting: 带时间限制的Waiting,不需要别的线程调用Object.notify( )、Object.notifyAll( )等方法,就能够恢复到可运行状态;
Terminated: 已经执行结束的线程处于此状态。Thread 的 run( ) 方法执行结束,或者由于异常而提前终止都会让线程处于这个状态。
这里需要注意的是Waiting和Blocked状态,也是有一个转换状态的,这里会在syncronized章节来进行描述。
七、线程消亡
JVM不推荐是停止一个线程,最好是让其运行完成之后正常结束生命周期。
那么停止一个线程无非是三种情况:1、正常结束;2、阻塞;3、陷入while(true)中
第一种不需要解决,因为这是非常正确的使用场景。而待解决的无非是后面两个场景。
对于后面两种情况分析
public class DemoOne {
static boolean flag = true;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (flag){
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {
Thread.sleep(3000);
flag = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
传统方式来进行操作的时候,可以发现这种是存在着延时的,不精确,准确来说是不可见问题。只有等到thread睡眠完成之后,才能够发现flag修改了,但是已经是10S之后的事情了。所以不精确。
那么如何让一个线程快速感知到?
Thread.interupt方法和Thread.isInterupted一级Thread.Interupted方法结合使用。
public class DemoTwo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 手动调用让外部感知到这个线程。思路来源于lock锁中的实现
Thread.currentThread().interrupt()
// 继续执行之后的逻辑,而线程还没有停止
System.out.println("因为中断而被唤醒的 .......");
}
}
});
thread.start();
try {
Thread.sleep(3000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台打印输出:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.guang.thread.stop.DemoTwo.lambda$main$0(DemoTwo.java:8)
at java.lang.Thread.run(Thread.java:748)
因为中断而被唤醒的 .......
JVM通过这个变量就已经完成了。interupted优雅停止一个线程,JVM不想让程序员停掉,因为突然停掉正在运行的线程,可能会导致线程当前所使用的资源没有释放,造成意想不到的问题。