简单通俗理解Java多线程
一.什么是线程、进程?
谈到多线程就要联想到线程和进程,那么什么是线程呢?什么又是进程呢?
进程就是一个运行的应用程序,每一个进程都有自己的独立空间,我们可以把浏览器当成一个线程、也可以把WPS当成另一个线程,他们之间互不影响。
而线程就是进程中一个执行流程,这里重点提醒,一个进程存在多个线程,并且线程是操作系统中最小的任务单位。
二.多进程和多线程
多进程顾名思义就是由多个进程组成
多线程指一个进程中存在多个线程
那么多进程与多线程之间有什么区别呢?
- 多进程的创建要比多线程的创建开销更大。
- 多进程的通信能力没多线程强。
- 多进程比多线程更加稳定,因为多进程中各进程互不影响,所以即使一个进程存在问题但不会影响到其他进程。但多线程却却截然相反,任何一个线程出现问题,都会影响到这个进程的运行。
三.多线程
在许多JAVA开发人员眼中,处理多个任务就是多线程。就比如在开发播放器的时候,它不仅需要显示歌词也需要通过播放器播放音频,在我们日常生活中也存在许多多线程例子,这里就不多介绍了。
那么就有人问呢?多线程相比较单线程有啥不同了,难道就是数量的区别吗?
非也!多线程与单线程的区别在于多线程能显著提高性能效率,可以同时处理不同的任务,让软件响应的更快。而单线程就得按顺序执一个任务。举例说明就是,多线程就像我们小时候喜欢边看电视边吃饭,而单线程就像我们在食堂排队打饭一样。
而多线程是JAVA最基本的并发模型,在项目开发中多线程运用极为广泛。
四.线程的创建
线程的创建主要是三种方式
1.继承Thread类
public class Main {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start(); // 线程启动
}
}
class MyThread extends Thread {
// 重写Run方法
@Override
public void run() {
System.out.println("线程启动!");
}
}
2.实现Runable方法
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 线程启动
}
}
// 实现Runable方法
class MyRunnable implements Runnable {
// 重写run方法
@Override
public void run() {
System.out.println("线程启动");
}
}
3.通过Callable创建线程
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
相比较这三种创建线程的方法,继承Thread类方法相比较简单而采用Runable或者Callable创建线程时,只实现了Runable接口或者Callable接口,在后续需求中,可以继承其他类。
提醒:虽然多线程能提高程序反应效率,但并不是线程越多,效率就越高。要知道创建线程是需要消耗资源的,如果创建过多的线程就会影响到CPU的处理速度,造成计算机的资源消耗过大。
五.线程的状态
线程的状态主要有以下几种:
- New:新线程创建。
- Runable:运行中的线程,正在执行Run()方法。
- Blocked:运行中的线程,因为某些操作被阻塞而挂起。
- Waiting:运行中的线程,因为某些操作在等待中。
- Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待; - Terminated:线程已终止,因为
run()
方法执行完毕。
线程生命周期图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ae2aujVz-1661828878666)(…/…/Library/Application Support/typora-user-images/截屏2022-08-29 22.59.53.png)]
六.中断线程
什么是中断线程呢?
这里可能许多人会问,线程执行的很好为啥要中断呢?
让我细细说来:中断程序就是当一个程序执行了很长一段时间,其余线程给该线程发出一个信号,这样可以让该线程立刻停止任务,结束Run()方法。举例说明就是大家都在食堂排队打饭,有个同学呢一直在前面打饭,后面的同学都等待着,这时为了维持食堂流通性,工作人员来提醒该同学不要在前面待久了。
那么怎么实现中断程序呢?
只需要在其他线程中执行Interrupt()方法,目标线程需要反复检查自己是否处于interrupted状态,如果是,线程就会被中断
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread thred = new MyThread();
thread.start(); // 线程thread启动
Thread.sleep(1); // 睡眠1毫秒
thread.interrupt(); // 向线程thread提出中断请求,
thread.join(); // 等待thread线程结束
System.out.println("结束");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " 执行");
}
}
}
上述代码中,主线程main向thread线程提出中断请求,但是是否中断还得看thread线程,而thread线程通过while不断循环自己是否处于Interruped状态,如果是就中断线程
七.守护线程
简单聊完中断线程,再聊聊守护线程
所谓的守护线程是指JVM中,所有非守护线程执行完之后,无论存不存在守护线程都会结束JVM,那么如果有的线程还灭结束呢,你总不能让JVM一直运行着吧,那么就有守护线程这个概念了,它就类似一种定时任务的线程,无限循环,由守护进程来结束该进程,就类似于公司晚上要关门了,但有些员工还在加班,不能因为一两个员工还在工作就大门一直由几个保安守着吧?这时公司可能会留下一个保安负责关门,等员工们都走完后,最后一个保安就会关闭大门。
那么肯定又有人问,那咋实现守护线程呢?
守护线程的创建其实很简单:
Thread thread = new MyThread();
thread.setDaemon(true); //调用setDaemon方法就是标记该线程为守护线程
thread.start();
上述代码中创建守护线程其实就和创建普通线程一个套路。
重点:守护线程不能有需要关闭的资源。比如有的线程需要手动关闭,且必须要在JVM关闭之前关闭,这时不能让守护线程有权利关闭它,不然会造成数据的丢失。
八.线程同步
说到线程同步,可能有人会想到上面所说的单线程,同步吗不就是按顺序进行。
在这里我表示只对了一半,为什么多线程要提同步,如果是单线程就当我没说,因为单线程不存在数据因为顺序问题导致输出值有异常的情况。但是放在多线程中,这种情况极有可能发生。我们还是举例说明,我们把每个线程当成一个点菜的客户,系统就是服务员,此时有个客户正在点菜,正常情况下应该让此客户点完菜,服务员才去下一桌,但是突发情况,有个客人很强势一把把点菜员喊过来了,那么此时菜单上已经存在上桌客户的菜品,再加上次桌客户的菜(因为举例,不要刻意在意服务员为啥只写一张纸上),那么可想而知最后上的菜觉得不对。那么怎么解决这种问题呢?
那么就要通过加锁和解锁的方式,这个加锁就好比服务员在被喊过去的同时,将手中的点菜单翻了一页新的,这就保留上一桌未点完的菜,也可以与此桌的菜不冲突。
理解:加锁就是让一个线程执行期间,其他线程无法干预此指令区间即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。那么存放线程数据的这一块就叫临界区(点菜单的每一页),任何时候临界区最多只有一个线程。
那么怎么加锁解锁呢?
Java中使用关键字synchronized,而使用Synchronized的加锁对象为this
说完线程同步就要谈到死锁。
我们先了解什么是可重入锁,可重入锁就是在JVM中允许同一个线程反复访问同一个锁,而这个锁就是可重入锁。
步入正题什么是死锁,所谓的死锁其实就是不同线程在获取不同对象锁的时候争斗激烈导致了拿不到,就比如甲拿着西瓜,乙拿着南瓜,
每个人手里只能拿一样东西,甲想要南瓜,乙想要西瓜,但甲他死倔不肯放手,乙说你不放手西瓜我怎么放南瓜拿西瓜了,就这样两人僵持不下。
死锁一旦发生,是没办法解决的,必须终止结束JVM进程才能解决。
那么该怎么防止死锁发生了,就得给甲乙两人制定一个规矩,要么想放西瓜在拿南瓜,要么先放南瓜再拿西瓜。
九.多线程方法
首先我们先要了解的是多线程的运行原则:就是当一个线程条件不满足时,应该讲线程放入等待队列中,等条件满足后,再将线程唤醒,继续执行任务。
而如何让线程进入等待状态,就要使用wait()方法,它主要作用就是将线程变为等待过程。
当然wait()方法不是随便就能用的,wait()必须在Synchronized中才能调用,然而我们无需担心wait()方法不会死拽锁对象不放,因为只要wait()方法调用,就会立刻释放锁。
那么有人会问总不能让线程一直等待吧?你上面不是说可以唤醒吗?
nice!那我们怎么唤醒线程了?这里就要用到notify()方法(只能唤醒一个)notifyAll方法(唤醒 全部)
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒所以在this锁等待的线程
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait(); // 让线程等待
}
return queue.remove();
}
}
十.线程池
什么是线程池?从字面上看就是一个装着线程的池子。从原理概念上讲,就是Java在每次创建线程和销毁线程是很浪费资源的,我们本着节约资源的理念,就想到了一种方法,就是设计一个池子,里面有许多线程,当没用的时候,让池子里的线程成等待状态如果有任务了,就安排一个线程去解决问题,那如果所有线程在都在忙,咋办?这时有两种情况,第一种就是将任务防止队列中进行等待,或者创建一个线程去解决它。
1.线程池的创建
ExecutorService executor = Executors.newFixedThreadPool(5); // 5表示这个线程池中有固定5个线程
当然线程池也有几种,这里就不做多的介绍了。如果大家有兴趣,我后续添加。
2.线程池的关闭
线程池在程序结束的时候要关闭。使用shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()
会立刻停止正在执行的任务,awaitTermination()
则会等待指定的时间让线程池关闭。