文章目录
- 常用工具包commons-io
- Hutool工具包
- 一、多线程
-
- [1.1 多线程的实现方法](#1.1 多线程的实现方法)
- [1.2 多线程常见的成员方法](#1.2 多线程常见的成员方法)
- [1.3 线程的安全问题](#1.3 线程的安全问题)
- [1.4 同步方法](#1.4 同步方法)
- [1.5 lock锁](#1.5 lock锁)
- [1.6 线程的死锁](#1.6 线程的死锁)
- [1.7 生产者消费者问题(等待唤醒机制)](#1.7 生产者消费者问题(等待唤醒机制))
- [1.8 阻塞队列------同样解决生产和消费的问题](#1.8 阻塞队列——同样解决生产和消费的问题)
- [1.9 线程的状态](#1.9 线程的状态)
- 二、多线程的其他概念
-
- [2.1 线程池](#2.1 线程池)
- [2.2 自定义线程池](#2.2 自定义线程池)
常用工具包commons-io
-
当导入外部包时通常需要新建一个文件夹,一般命名为lib(libiary)
-
在lib中一般存储.jar的文件,这种文件相当于java中的压缩包,一般包含了多个类
-
放在该文件夹下使用时可以直接导入
4.右键jar文件,将包添加到库中
commons-io是一个集成的IO流,里面有许多现成的IO方法,不用自己去关注底层(用传统的IOf方法实现)
Hutool工具包
国内程序员开发的聚合多功能的工具包
Hutool工具包在官网中有下载方式和API文档
在该包下有许多已经封装好的函数可以直接使用
一、多线程
多线程的相关概念:
- 什么是多线程?
进程:进程是程序执行的基本实体,一个软件的运行通常便伴随着进程的创建
线程:它是操作系统中运算调度的最小单位,被包含在进程当中,是进程中的实际运作单位
多线程:在同一个进程中有多个调度同时被执行。例如QQ中可以发消息,打电话,看空间,这每一个功能是一个线程- 为什么引入多线程?
为了提高CPU的工作效率,减少空闲时间,让程序同时做多件事情。- 多线程的应用场景
软件中耗时的操作、聊天软件、服务器
并发和并行:
并发:同一时间有多个指令在CPU上交替 执行,类似接力跑
并行:同一时间有多个指令在CPU上同时执行,类似两条赛道上的对手
1.1 多线程的实现方法
1. 继承Thread类,并重写run方法
java
public class MyThread extends Thread{
@Override
public void run() {
//书写线程要执行代码
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "HelloWorld");
}
}
}
测试类:
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();
}
}
注意:这里使用的是strat方法,不可以直接用run方法
2. 用类实现Runable接口*
java
public class MyRun implements Runnable{
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 100; i++) {
//获取到当前线程的对象
/*Thread t = Thread.currentThread();
System.out.println(t.getName() + "HelloWorld!");*/
System.out.println(Thread.currentThread().getName() + "HelloWorld!");
}
}
}
java
public class ThreadDemo {
public static void main(String[] args) {
/*
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.重写里面的run方法
* 3.创建自己的类的对象
* 4.创建一个Thread类的对象,并开启线程
* */
//创建MyRun的对象
//表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
注意:
- 用此方法获取线程名时无法直接使用getName方法了,应用hread.currentThread()获取当前的线程对象再调用线程的方法
- 此时相当于同一个对象在多个线程中运行,此时的两个线程t1,t2操作的是同一个地址
3. 通过实现Callable接口可以实现带返回值的多线程函数
java
public class MyCallable implements Callable<Integer> {
//在实现接口时需要申明变量类型
@Override
public Integer call() throws Exception {
//求1~100之间的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
java
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式:
* 特点:可以获取到多线程运行的结果
*
* 1. 创建一个类MyCallable实现Callable接口
* 2. 重写call (是有返回值的,表示多线程运行的结果)
* 3. 创建MyCallable的对象(表示多线程要执行的任务)
* 4. 创建FutureTask的对象(作用管理多线程运行的结果)
* 5. 创建Thread类的对象,并启动(表示线程)
* */
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建FutureTask的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
System.out.println(t1.getName());
//获取多线程运行的结果
Integer result = ft.get();//从线程管理器中获取结果
System.out.println(result);
}
}
总结:
- 多线程的实现总共分为两类,一种是能返回函数的结果的,另一类是不可以的
- thread用于实现简单的多线程程序,因为java的单继承特性导致这个类只能继承于一个类从而失去灵活性,而Runable接口的方法相对灵活,一个类可以实现多个接口
- 方式2、3都是将要运行的程序放在线程当中,但方式1可以直接作为线程使用
1.2 多线程常见的成员方法
- 前四个方法:
java
线程函数的定义:
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "@" + i);
}
}
}
java
线程测试:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
String getName() 返回此线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
细节:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始的)
2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
static Thread currentThread() 获取当前线程的对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
细节:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2、方法的参数:就表示睡眠的时间,单位毫秒
1 秒= 1000毫秒
3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
//1.创建线程的对象
MyThread t1 = new MyThread("线程1号");
MyThread t2 = new MyThread("线程2号");
//2.开启线程
t1.start();
t2.start();
//哪条线程执行到这个方法,此时获取的就是哪条线程的对象
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);//main*///获取的main线程,不受1、2线程的影响
System.out.println("11111111111");
Thread.sleep(5000);
System.out.println("22222222222");
}
}
注意:在重写run方法是不能抛出异常,因为其继承的父类也没有抛出异常
- 线程优先级相关方法:
线程的优先级就是线程运行的优先顺序,优先级最大是10,最小是1,默认为5.
java
public class ThreadDemo {
public static void main(String[] args){
/*
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
*/
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t1 = new Thread(mr,"线程1");//在线程中加载对象
Thread t2 = new Thread(mr,"线程2");
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
注意 :优先级高只是抢占上资源的概率大,并不一定能长时间执行
- 守护线程
java
public class ThreadDemo {
public static void main(String[] args) {
/*
final void setDaemon(boolean on) 设置为守护线程
细节:
当其他的非守护线程执行完毕之后,守护线程会陆续结束
*/
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("非守护线程");
t2.setName("守护线程");
//把第二个线程设置为守护线程(备胎线程)
t2.setDaemon(true);
t1.start();
t2.start();
}
}
总结 :守护线程伴随着所有非守护线程的结束而结束
- 礼让线程(了解)
该方法是一个静态方法,可直接调用
会出让CPU的执行权让线程的运行尽可能的均匀
java
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
//表示出让当前CPU的执行权
Thread.yield();
}
}
}
- 插队线程(了解)
java
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
/*
public final void join() 插入线程/插队线程
*/
//当前线程: main线程
MyThread t = new MyThread();
MyThread t2 =new MyThread();
t2.setName("线程1");
t.setName("线程2");
t.start();
//t.join();
t2.start();
t2.join();//会占用当前的线程
//执行在main线程当中的
for (int i = 0; i < 100; i++) {
System.out.println("main线程" + i);
}
}
}
1.3 线程的安全问题
在制作一个多线程的抢票程序时
- 多个线程需要共享一个剩余票数的变量,设置类的静态变量
- 线程在同时执行时可能同时会有多个线程跨国判断导致变量错误计数,此时需要进程死锁,保证每次只能有一个进程进入被锁的代码块(同步代码块)
java
public class MyThread extends Thread {
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;//0 ~ 99
@Override
public void run() {
while (true) {
//同步代码块
synchronized (MyThread.class) { //此处的锁函数中需要传入锁对象,对象可以是任意的但一定要保证唯一性
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
注意:
- 同步代码块一定要写在循环的内部,这样可以保证多个线程各自监听,一有机会就进入同步代码块执行抢票程序段
- 一般用当前类的字节码文件:MyThread.class来充当锁对象
1.4 同步方法
- 当一整个方法都被包含在同步代码块内部时,可以考虑使用同步方法,可以有更好的封装性。
- 同步方法一般都是在同步代码块的基础上改进而来的
- 同步方法的格式 :
选中同步代码块部分按Ctrl+Alt+M
可以直接将同步代码块抽象成同步方法
下面用Runable方法举例:
java
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
while (true) {
//2.同步代码块(同步方法)
if (method())
break;
}
}
//this此处并没有用静态,表示锁对象是this
private synchronized boolean method() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
return true;
} else {
//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
}
return false;
}
}
java
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
利用同步方法完成
技巧:同步代码块
*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
注意 :
因为Runable实现方法的特点,所有的线程共享一个对象,因此在变量定义时不用使用静态变量,并且在申明同步方法时也不用静态方法(此时为this)
1.5 lock锁
为了解决同步代码块在运行过程中不能手动 上锁解锁的困境,在JDK5以后可以使用lock锁对代码块上锁和解锁。
Lock类是一个接口,一般会使用它的实现类ReentrantLock
java
public class MyThread extends Thread{
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
//1.循环
while(true){
//2.同步代码块
//synchronized (MyThread.class){
lock.lock(); //2 //3
try {
//3.判断
if(ticket == 100){
break;
//4.判断
}else{
Thread.sleep(10);
ticket++;
System.out.println(getName() + "在卖第" + ticket + "张票!!!");
}
// }
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
java
public class ThreadDemo {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
注意:
- 因为三个线程对象共用一把锁,此时应该设置锁为静态的
- 如果只是在之前的方法头尾改为上锁和解锁的话,会导致某个线程在break跳出循环后并未解锁,而其他的线程还在等锁解开,从而导致程序无法终止。
为了解决这个问题,将同步代码块的部分直接包裹在try...catch......finally语句中,致使每次运行结束后都能运行unlock()语句
快捷键:Ctrl+Alt+T
可语句体包裹
1.6 线程的死锁
这是一个线程问题中由于锁嵌套产生的意外情况
每个线程都占有着一部分资源,但是他们都需要更多的资源才能继续向下进行,导致所有的线程都停滞不动,并且一直占有着资源
如有一双筷子两个人用,他们每人抢到一支,但是都没办法吃饭
1.7 生产者消费者问题(等待唤醒机制)
这个模型较为理想化地展示了多线程之间的需求关系,生产者生产资料等消费者使用,用后再生产
为了方便理解用了厨师和食客的例子展示:
分析:
- 该程序分为三个类,两个线程。厨师和食客分别作为一个类和线程,桌子作为中间类用来调控两个类的进行
- 食客吃完就进入等待状态暂时不再参与锁的竞争,等厨师做好饭后将其唤醒继续参与锁竞争
- 同理,厨师做好饭也进入等待状态暂时不参与锁的竞争,等食客吃完后才被唤醒
厨师部分:
java
public class Cook extends Thread{
@Override
public void run() {
/*
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
while (true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
//判断桌子上是否有食物
if(Desk.foodFlag == 1){
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
食客部分:
java
public class Foodie extends Thread{
@Override
public void run() {
/*
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
//先判断桌子上是否有面条
if(Desk.foodFlag == 0){
//如果没有,就等待
try {
Desk.lock.wait();//让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//把吃的总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
第三方控制类(桌子):
java
public class Desk {
/*
* 作用:控制生产者和消费者的执行
* */
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象,必须是唯一的
public static Object lock = new Object();
}
在这部分定义了锁对象和共享变量
总结:
- 线程中一般在循环中嵌套同步代码块
- 用锁对象来调用等待和唤醒的方法
1.8 阻塞队列------同样解决生产和消费的问题
队列:就是我们熟知的队列结构
阻塞:队列中的产品装满 或者为空 时会产生阻塞现象
食客:
java
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
//带队列的构造方法
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断从阻塞队列中获取面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
厨师:
java
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;
//带队列的构造方法
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断的把面条放到阻塞队列当中
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类:
java
public class ThreadDemo {
public static void main(String[] args) {
/*
*
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
*
* */
//1.创建阻塞队列的对象,给出队列中的存储容量
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2.创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//3.开启线程
c.start();
f.start();
}
}
总结:
- 生产者和消费者共用同一队列,可以在线程初始化的时候将其传入
- 队列在定义时可以定义存储的大小
- 队列的put和take方法是自带同步代码块的,不用再将其嵌套在同步代码块中,否则可能会产生死锁现象
1.9 线程的状态
但是在JAVA中并没有定义线程的运行状态,此时线程是交由操作系统处理的,并不是虚拟机管理。
二、多线程的其他概念
2.1 线程池
线程在运行结束后会被清除,线程池的主要目的是为了避免多次创建同一个线程而消耗系统资源,当有同样的任务等待处理时可以直接调用上次创建的线程
线程池的实现
java
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
java
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
/*
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool (int nThreads) 创建有上限的线程池
*/
//1.获取线程池对象
//ExecutorService pool=Executors.newCachedThreadPool();//创建没有上限的线程池
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//2.提交任务,向线程池中提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
//pool1.shutdown();
}
}
下面的图片是线程池运行过程中的具体情况,可以看见当前活跃的线程和排队等待的线程数
2.2 自定义线程池
线程池的运行策略
- 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列(阻塞队列)里等待执行。
- 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程(临时线程)来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。
用ThreadPoolExecutor实现 :
其总共可以设置7个参数
-
corePoolSize:核心线程数,线程池中始终存活的线程数。
-
maximumPoolSize:最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
-
keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
-
unit:单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间。参数 keepAliveTime 的时间单位有以下 7 种可选:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
-
workQueue:一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全。它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种,包含以下 7 种类型:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关
-
threadFactory:线程工厂,主要用来创建线程。
-
handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
- AbortPolicy:拒绝并抛出异常。
- CallerRunsPolicy:使用当前调用的线程来执行此任务。
- DiscardOldestPolicy:抛弃任务队列头部(最旧)的一个任务,并执行当前任务。
- DiscardPolicy:忽略并抛弃当前任务。
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, //核心线程数量,能小于0
6, //最大线程数,不能小于0,最大数量 >= 核心线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
解析:
- 为什么要设置阻塞队列
因为线程的销毁和创建都很消耗系统的资源,涉及到系统底层的资源分配回收机制,所以不到不得已不会创建临时线程来解决超出等待队列的任务。