🏡个人主页:謬熙,欢迎各位大佬到访❤️❤️❤️~
👲个人简介:本人编程小白,正在学习互联网开发求职知识……
如果您觉得本文对您有帮助的话,记得点赞👍、收藏⭐️、评论💬,如果文章有什么需要改进的地方还请大佬不吝赐教🙏🙏🙏
目录
- 操作系统中相关概念解释
- 线程与进程
- 并发与并行
- 线程的生命周期和状态
- Java 相关包
- 多线程实现与调度
- 直接继承`Thread`类(无返回值)
- 实现`Runable`接口后,创建`Thread`对象(避免了单继承问题,无返回值)
- 实现`Callable`接口和`Future`接口(有返回值)
- 守护线程、礼让线程、插入线程
- 线程同步
- synchronized同步
- lock锁
- 死锁(一种错误,需要避免)
- 等待唤醒机制
- 阻塞队列方式
- 线程池
- Executors执行器自动创建线程池(不规范)
- ThreadPoolExecutor自定义创建线程池
操作系统中相关概念解释
线程与进程
进程是程序的基本执行实体;(不同软件)
线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位。(同一软件不同功能)
并发与并行
并发:多个指令在同一个CPU上交替执行;
并行:同一时刻,多个指令在多个CPU上同时执行。(多核CPU)
线程的生命周期和状态
线程的生命周期分为创建(new)、就绪(Runnable)、运行(running)、阻塞(Blocked)、死亡(Dead)五种状态。
Java 相关包
核心包中的线程类和接口
java.lang.Thread
java.lang.Runnable
JUC高并发编程包中的部分包
java.util.concurrent.Callable
java.util.concurrent.locks.Lock
锁接口
java.util.concurrent.locks.ReentrantLock
锁的实现类
java.util.concurrent.ArrayBlockingQueue
有界阻塞队列
java.util.concurrent.LinkedBlockingQueue
无界阻塞队列(int上限)
java.util.concurrent.Executors
线程池
多线程实现与调度
直接继承Thread
类(无返回值)
继承
Thread
类实现多线程有以下几步:
- 我们要先自定义一个类然后继承
Thread
类;- 在继承
Trread
的类中重写run
方法;- 通过创建该类的对象即可创建线程,创建多个对象就可以实现多线程;
Thread
类常用成员方法:
start()
开启线程getname
返回线程名称setname
设置线程名称currentthread
静态方法,获取并返回当前线程对象,无其他线程则返回默认主线程main
sleep
静态方法,让当前进程休眠,单位毫秒setPriority
设置线程优先级,共有1-10个优先级,越小抢占CPU的概率越高getPriority
获取线程优先级,默认为5
package thread;public class MyThread extends Thread {@Overridepublic void run() {//书写线程要输出的方法for (int i = 0; i < 10; i++) {System.out.println(getName() + "hello");}}
}
package thread;public class ThreadDemo {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}
}
运行结果:
实现Runable
接口后,创建Thread
对象(避免了单继承问题,无返回值)
通过实现
Runnable
接口的方式实现多线程也大致可以分为以下几步
- 自定义一个了类实现
Runnable
接口;- 重写
run
方法;- 创建自定义类的对象;
- 创建
Thread
类对象,将自定义对象作为参数传递给Thread
对象;- 通过调用
start
方法启动线程实现多线程;
package thread;public class MyRun implements Runnable {@Overridepublic void run() {Thread t = Thread.currentThread();for (int i = 0; i < 10; i++) {System.out.println(t.getName()+"hello");//这里由于不是继承自thread类,所以不能使用getname方法,所以先得获取一个线程对象}}
}
package thread;public class ThreadDemo2 {public static void main(String[] args) {MyRun mr = new MyRun();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);t1.setName("线程1");t1.start();t2.setName("线程2");t2.start();}
}
运行结果:
实现Callable
接口和Future
接口(有返回值)
通过实现
Runnable
接口的方式实现多线程也大致可以分为以下几步
- 创建一个自定义类实现 Callable 接口;
- 重写
call
,(有返回值的,表示多线程的运行结果);- 创建自定义类的对象,执行要执行的任务;
- 创建
Future
的对象,它可以管理多线程运行的结果,但是Future
是一个接口,所以我们需要创建它的是实现类FutureTask
的对象。- 创建
Thread
的对象,并调用 start 启动线程。
package thread;import java.util.concurrent.Callable;public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 10 ;i++){sum += i;}return sum;}
}
package thread;import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadDemo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();FutureTask<Integer> ft =new FutureTask<>(mc);Thread t1 = new Thread(ft);t1.start();System.out.println(ft.get());}
}
守护线程、礼让线程、插入线程
setDeamon()
将线程设置为守护线程
作用是陪着非守护线程,当其结束时,守护线程也结束。
yield()
静态方法,出让当前CPU的使用权join()
静态方法,将当前开启的线程插入到其他线程前
线程同步
synchronized同步
场景——电影院三个窗口同时出售100张票
package buyticket;public class Buy extends Thread{static int ticket = 0;public Buy(String name) {super(name);}@Overridepublic void run() {while(true){if(ticket<100){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}ticket++;System.out.println(getName()+"已卖出第"+ticket+"张票!");}else{break;}}}
}
ticket
不为静态变量引发的结果——三个窗口售票重复
修改
ticket
后还是有问题——售出过多的票
原因:多个线程还在抢夺CPU,当线程1ticket
自增到100后,还没来得及打印,线程2和线程3就相继苏醒也可能到了自增这一步操作,这时三个线程输出的值就变化了;
synchronized
同步代码块
当线程进入后,将代码自动锁起来,当里面的代码全部执行完毕后,锁打开其他线程才进入
package buyticket;public class Buy extends Thread {static int ticket = 0;public Buy(String name) {super(name);}@Overridepublic void run() {while (true) {synchronized (Buy.class) {if (ticket < 100) {try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}ticket++;System.out.println(getName() + "已卖出第" + ticket + "张票!");} else {break;}}}}
}
细节1——
synchronized
需要写在循环里面
否则一个线程线进入后会直接锁住这个循环体,当循环全部结束后才轮到其他线程。
细节2——
synchronized
小括号里锁的对象必须唯一
如果锁对象不唯一,这个锁就没意义了。类似于抢厕所,锁对象唯一就是只能一个厕所轮流进,不唯一就是多个坑位,这个坑有人了我换一个呗。
一般用这个类的字节码文件对象类名.class
,这个必唯一
synchronized
同步方法
将同步代码块中的核心方法提取出来,选中Ctrl+Alt+M,去掉同步代码块,给方法加上synchronized
关键字即可。
package buyticket;public class Buy_syn extends Thread {static int ticket = 0;public Buy_syn(String name) {super(name);}@Overridepublic void run() {while (true) {if (extracted()) {break;}}}private synchronized boolean extracted() {if (ticket < 100) {try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}ticket++;System.out.println(getName() + "已卖出第" + ticket + "张票!");} else {return true;}return false;}
}
lock锁
- 对象锁:在java中每个对象都有一个唯一的锁,对象锁用于对象实例方法或者一个对象实例上面的 —— 一个对象一把锁,100个对象100把锁。
- 类锁:是用于一个类静态方法或者class对象的,一个类的实例对象可以有多个,但是只有一个class对象 —— 100个对象,也只是1把锁。上述
synchronized
同步代码块实现:在静态方法添加synchronized这把锁属于类了,所有这个类的对象都共享这把锁。
package buyticket;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Buy_lock extends Thread {static int ticket = 0;static Lock lock = new ReentrantLock();//Lock 为接口,必须用实现类实例化public Buy_lock(String name) {super(name);}@Overridepublic void run() {while (true) {lock.lock();try {if (ticket < 100) {Thread.sleep(10);ticket++;System.out.println(getName() + "已卖出第" + ticket + "张票!");} else {break;}} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}}
}
细节1——锁对象必须唯一,所以采用静态属性
细节2——如果直接break跳出循环,程序不会停止,所以必须关锁程序才结束运行。
死锁(一种错误,需要避免)
死锁是指在执行过程中,两个或两个以上的进程(或线程)由于竞争资源或彼此通信而阻塞,导致无法继续执行的情况。
直观例子:
老板:你给我好好干,我就给你加薪
员工:你给我加薪,我就好好干
等待唤醒机制
生产者消费者模型是一种经典的多线程同步模型,用于解决生产者和消费者之间的协作问题。在这个模型中,生产者负责生产数据并将其放入缓冲区(共享锁对象,也就是共享资源),消费者负责从缓冲区中取出数据并进行处理。生产者和消费者之间通过缓冲区进行通信,彼此之间不需要直接交互。这样可以降低生产者和消费者之间的耦合度,提高系统的可维护性和可扩展性。
方法:
wait()
当前线程等待,直到被其他线程唤醒
notify()
随机唤醒单个线程
notifyAll()
唤醒所有线程
场景——顾客吃饭,厨师做饭,缓冲区就是订单
职能 | flag = 0 | flag = 1 |
---|---|---|
缓冲区 | 没订单 | 有订单 |
生产者 | wait 等待下单 | 操作处理订单,然后notify 唤醒消费者 |
消费者 | 下单,然后notify 唤醒生产者 | wait 等待吃完 |
缓冲区:
package wait;public class Desk {public static int flag = 0;public static int count = 0;public static Object lock = new Object();
}
生产者:
package wait;public class Cooker extends Thread{public Cooker(String name) {super(name);}public void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 3) {break;} else {if (Desk.flag == 0) {try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {System.out.println("厨师已收到订单,做了一碗面条");Desk.flag = 0;Desk.lock.notifyAll();}}}}}
}
消费者:
package wait;public class Customer extends Thread {public Customer(String name) {super(name);}@Overridepublic void run() {while(true){synchronized (Desk.lock) {if (Desk.count == 3) {break;} else {if (Desk.flag == 1) {try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {Desk.count++;System.out.println("客人正在吃" + Desk.count + "面条,已下一个新订单");Desk.flag = 1;Desk.lock.notifyAll();}}}}}
}
运行结果:
两个线程协调运作
阻塞队列方式
阻塞队列是一种特殊的队列,同样遵循“先进先出”的原则,支持入队操作和出队操作。在此基础上,阻塞队列会在队列已满或队列为空时陷入阻塞,使其成为一个线程安全的数据结构,它具有如下特性:
- 当队列已满时,继续入队列就会阻塞,直到有其他线程从队列中取走元素
take()
。 - 当队列为空时,继续出队列也会阻塞,直到有其他线程向队列中插入元素
put
。
主程序:
package waitblock;import java.util.concurrent.ArrayBlockingQueue;public class Main {public static void main(String[] args) {ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(1);Cooker cooker = new Cooker(queue);Customer customer = new Customer(queue);cooker.start();customer.start();}
}
消费者:
package waitblock;import java.util.concurrent.ArrayBlockingQueue;public class Customer extends Thread {ArrayBlockingQueue<String> queue;public Customer(ArrayBlockingQueue queue) {this.queue = queue;}@Overridepublic void run() {while (true) {try {String food = queue.take();System.out.println("顾客已收到食物——"+food);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
生产者:
package waitblock;import java.util.concurrent.ArrayBlockingQueue;public class Cooker extends Thread {ArrayBlockingQueue<String> queue;public Cooker(ArrayBlockingQueue queue) {this.queue = queue;}@Overridepublic void run() {while (true) {try {queue.put("面条");System.out.println("厨师已做完一碗面条");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
细节——生产者和消费者的队列必须一致,所以在主程序实例化了一个阻塞队列对象,在两个类中利用构造函数实例化,这样队列就共享了。
线程池
Executors执行器自动创建线程池(不规范)
Executors
相当于执行器的工厂类,包含各种常用执行器的静态工厂方法,可以直接创建常用的执行器。几种常用的执行器如下:
Executors.newCachedThreadPool
,根据需要可以创建新线程的线程池。线程池中曾经创建的线程,在完成某个任务后也许会被用来完成另外一项任务。Executors.newFixedThreadPool(int nThreads)
,创建一个可重用固定线程数的线程池。这个线程池里最多包含nThread个线程。起到限制并发线程数的作用。
下面是创建定长线程池(FixedThreadPool)的一个例子,严格来说,当使用如下代码创建线程池时,是不符合编程规范的。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
原因在于:(摘自阿里编码规约)
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM(Out Of Memory,来源于java.lang.OutOfMemoryError
。当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error)
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
ThreadPoolExecutor自定义创建线程池
七个核心参数:
- int corePoolSize,//核心线程数,线程池中始终存活的线程数
- int maximumPoolSize,//最大线程数,当线程池的任务队列满了之后可以创建的最大线程数
- long keepAliveTime,//空闲线程最大存货时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程
- TimeUnit unit,//时间单位,
TimeUnit.SECONDS
设置秒- BlockingQueue workQueue,//任务队列,一个阻塞队列
- ThreadFactory threadFactory,//创建线程工厂,创建线程
- RejectedExecutionHandler handler//拒绝策略,拒绝处理任务时的策略,默认策略为
AbortPolicy
:拒绝并抛出异常。
本次关于Java多线程的内容就总结到这里了,上述内容只是一个大纲,每个部分细节其实还蛮多的,后续会慢慢完善✏️。有疑问的地方或者纰漏的地方欢迎大佬们沟通指正,希望和大家一起进步~💪💪💪