Java多线程的具体介绍与使用笔记小结
一、基本概念:线程、进程
1.1、进程与线程的具体介绍
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间à它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
进程(process),是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
- 运行中的qq,运行中的mp3播放器
- 程序是静态的,进程是动态的进程作为资源分配的单位,系统在运行时会为每个
- 进程分配不同的内存区域
1.2、对于cpu而言的理解
单核cpu和多核cpu的理解
- 单核cpu,其实是因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是一种假的多线程,收费站只有一个工作人员在收费,只有收了费才能通过,那么cpu就好比收费人员。如果有某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费)。但是因为cpu时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个 java 应用程序 java.exe,其实至少有三个线程:main() 主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发
- 并行:多个 cpu 同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个 cpu (采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
1.3、为什么要使用多线程
- 背景:
以单核cpu 为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统 cpu 的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
二、线程的创建与使用
2.1、如何去创建和启动一个线程
- java语言的 jvm 允许程序运行多个线程,它通过 java.lang.thread 类来体现。
- thread 类的特性:
每个线程都是通过某个特定 thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体通过该 thread 对象的 start() 方法来启动这个线程,而非直接调用 run()
2.2、thread类的具体分析
构造器:
thread():创建新的 thread 对象
thread(string threadname):创建线程并指定线程实例名
thread(runnable target):指定创建线程的目标对象,它实现了 runnable 接口中的 run 方法
thread(runnable target, string name):创建新的 thread 对象
创建线程的两种方式:
jdk1.5 之前创建新执行线程有两种方法:
继承 thread 类的方式
实现 runnable 接口的方式
方式一:继承 thread 类
定义子类继承 thread 类。
子类中重写 thread 类中的 run 方法。
创建 thread 子类对象,即创建了线程对象。
调用线程对象 start 方法:启动线程,调用 run 方法。
代码演示:
* 多线程的创建,方式一:继承于thread类 * 1. 创建一个继承于thread类的子类 * 2. 重写thread类的run() --> 将此线程执行的操作声明在run()中 * 3. 创建thread类的子类的对象 * 4. 通过此对象调用start() * 例子:遍历100以内的所有的偶数 //1、创建一个继承于thread类的子类 class thread01 extends thread { //2、重写thread类的run() @override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { system.out.println(thread.currentthread().getname() + ":" + i); } } } } public class threadtest { public static void main(string[] args) { //3、创建thread类的子类的对象 thread01 thread01 = new thread01(); //4.通过此对象调用start() thread01.start(); //创建第二个线程 thread01 thread02 = new thread01(); //注意:这里我们不能直接手动调用 run()方法 //thread01.run(); //注意:当我们再次调用start()时会直接报错:illegalthreadstateexception,所以一个线程只能用一次 //thread01.start(); thread02.start(); //当前操作仍然是在main线程中执行 for (int i = 0; i < 100; i++) { if (i % 2 == 0) { system.out.println(thread.currentthread().getname() + ":" + i + "主线程被执行了"); } } } }
方式二:实现 runnable 接口
- 定义子类,实现 runnable 接口。
- 子类中重写 runnable 接口中的 run 方法。
- 通过 thread 类含参构造器创建线程对象。
- 将 runnable 接口的子类对象作为实际参数传递给 thread 类的构造器中。
- 调用 thread 类的 start 方法:开启线程,调用 runnable 子类接口的 run 方法。
2.3、两种实现方式的联系与区别
联系:
thread 内部其实也是实现了 runnable 接口
区别:
- 继承 thread: 线程代码存放 thread子类 run 方法中。
- 实现 runnable: 线程代码存在接口的子类的 run 方法。
实现方式的好处:
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
2.4、注意事项:
- 如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。
- run() 方法由 jvm 调用,什么时候调用,执行的过程控制都有操作系统的 cpu 调度决定。
- 想要启动多线程,必须调用 start 方法。
- 一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上的异常 illegalthreadstateexception。
2.5、代码测试
需求:创建两个分线程,让其中一个线程输出1-100之间的偶数,另一个线程输出1-100之间的奇数。
public class threadtest01 { public static void main(string[] args) { //线程一:偶数 new thread() { @override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { system.out.println(thread.currentthread().getname() + "线程一偶数的执行数:" + i); } } } }.start(); //线程二:奇数 new thread() { @override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 != 0) { system.out.println(thread.currentthread().getname() + "线程二奇数的执行数:" + i); } } } }.start(); } }
2.6、thread类的相关方法
void start(): 启动线程,并执行对象的 run() 方法
run(): 线程在被调度时执行的操作string
getname(): 返回线程的名称
void setname(string name): 设置该线程名称
static thread currentthread(): 返回当前线程。在 thread 子类中就是 this,通常用于主线程和 runnable 实现类static void yield(): 线程让步
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程若队列中没有同优先级的线程,忽略此方法
join() : 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
低优先级的线程也可以获得执行
static void sleep(long millis):(指定时间:毫秒)
令当前活动线程在指定时间段内放弃对 cpu 控制,使其他线程有机会被执行,时间到后重排队。
抛出 interruptedexception 异常
stop(): 强制线程生命期结束,不推荐使用
boolean isalive(): 返回 boolean,判断线程是否还活着
2.7、测试以上方法:
* @description: 多线程具体方法的使用 * @date 2021/4/16 18:32 */ class mythread extends thread { @override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { system.out.println(thread.currentthread().getname() + ":" + thread.currentthread().getpriority() + ":" + i); //设置线程睡眠时间 /* try { sleep(100); } catch (interruptedexception e) { e.printstacktrace(); }*/ } //当满足当前条件时,终止线程 /*if (i % 20 == 0) { yield(); }*/ } } } public class threadmethodtest { public static void main(string[] args) { mythread thread = new mythread(); thread.start(); //给主线程命名 thread.setname("我是线程一号"); //将分线程优先级设置成最大 thread.setpriority(thread.max_priority); thread.currentthread().setname("我是主线程"); //将主线程的优先级设置成最小 thread.currentthread().setpriority(thread.min_priority); for (int i = 0; i < 100; i++) { if (i % 2 == 0) { system.out.println(thread.currentthread().getname() + ":" + thread.currentthread().getpriority() + ":" + i); } //join()表示:线程a中调用线程b的join()方法,那么此时线程a进入阻塞状态,直到线程b完全执行完以后,线程a才 //结束阻塞状态。 /*if (i == 20) { try { thread.join(); } catch (interruptedexception e) { e.printstacktrace(); } }*/ } //isalive():判断当前线程是否存活 // system.out.println("当前线程是否存活:" + thread.isalive()); } }
结果:
使用 sleep() 方法时的结果
使用 join方法时的结果
三、线程的生命周期
jdk中用thread.state类定义了线程的几种状态:
要想实现多线程,必须在主线程中创建新的线程对象。java 语言使用 thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个 thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪: 处于新建状态的线程被 start() 后,将进入线程队列等待 cpu 时间片,此时它已具备了运行的条件,只是没分配到 cpu 资源
运行: 当就绪的线程被调度并获得 cpu 资源时,便进入运行状态, run() 方法定义了线程的操作和功能
阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 cpu 并临时中止自己的执行,进入阻塞状态
死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
生命周期结构图
四、线程的同步
4.1、为什么要线程同步
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对账本的共享,会造成操作的不完整性,会破坏数据
4.2、举例说明:
创建三个窗口卖票,总票数为100张.使用继承thread类的方式
class window extends thread { private static int ticket = 100; @override public void run() { while (true) { if (ticket > 0) { system.out.println(getname() + "买票编码号为:" + ticket); ticket--; } else { break; } } } } public class ticketwindowtest { public static void main(string[] args) { //创建线程 window w1 = new window(); window w2 = new window(); window w3 = new window(); //设置线程名字 w1.setname("窗口一:"); w2.setname("窗口二:"); w3.setname("窗口三:"); //开启线程 w1.start(); w2.start(); w3.start(); } }
正常情况下的效果:
不正常的情况下的效果:
上段代码出现的漏洞:
问题: 卖票过程中,出现了重票、错票,出现了线程的安全问题
原因: 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
解决: 当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket。这种情况即使线程a出现了阻塞,也不能被改变
4.3、synchronized的使用方法
java对于多线程的安全问题提供了专业的解决方式:同步机制
1、同步代码块: synchronized (对象){ // 需要被同步的代码; } 2、synchronized还可以放在方法声明中,表示整个方法为同步方法。 例如: public synchronized void show (string name){ …. }
4.3、 同步机制中的锁
同步锁机制:
在《thinking in java》中,是这么说的: 对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
- 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)
- 同步方法的锁: 静态方法(类名.class)、非静态方法(this)
- 同步代码块: 自己指定,很多时候也是指定为this或类名.class
注意:
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
4.4、同步的范围
如何找问题,即代码是否存在线程安全?(非常重要)
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
注意:
- 范围太小: 没锁住所有有安全问题的代码
- 范围太大: 没发挥多线程的功能。
对于购票代码的 bug 改进
class windows02 implements runnable { private int ticke = 100; @override public void run() { while (true) { //使用同步代码方式来解决线程安全问题,this:表示当前对象:【windows02】 // synchronized (this) { // if (ticke > 0) { // try { // thread.sleep(100); // } catch (interruptedexception e) { // e.printstacktrace(); // } // system.out.println(thread.currentthread().getname() + "购票号码为:" + ticke); // ticke--; // } else { // break; // } // } show(); } } //这里我们直接使用【同步方法】的方式来处理线程安全问题 //在方法中加入:synchronized的效果等同上面的this,因为指代的都是当前对象,只是在同步方法中帮我们做了隐试操作。 private synchronized void show() { if (ticke > 0) { try { thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } system.out.println(thread.currentthread().getname() + "购票号码为:" + ticke); ticke--; } } } public class tickewindowtest02 { public static void main(string[] args) { windows02 windows = new windows02(); thread thread = new thread(windows); thread thread01 = new thread(windows); thread thread02 = new thread(windows); thread.setname("窗口一:"); thread01.setname("窗口二:"); thread02.setname("窗口三:"); thread.start(); thread01.start(); thread02.start(); } }
测试结果:
4.5、释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的 error 或 exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。
4.6、不释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用 thread.sleep()、thread.yield() 方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用 suspend() 和 resume() 来控制线程
4.7、线程的死锁的问题
1、 什么事死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
代码演示:
//死锁的演示 class a { public synchronized void foo(b b) { //同步监视器:a类的对象:a system.out.println("当前线程名: " + thread.currentthread().getname() + " 进入了a实例的foo方法"); // ① // try { // thread.sleep(200); // } catch (interruptedexception ex) { // ex.printstacktrace(); // } system.out.println("当前线程名: " + thread.currentthread().getname() + " 企图调用b实例的last方法"); // ③ b.last(); } public synchronized void last() {//同步监视器:a类的对象:a system.out.println("进入了a类的last方法内部"); } } class b { public synchronized void bar(a a) {//同步监视器:b system.out.println("当前线程名: " + thread.currentthread().getname() + " 进入了b实例的bar方法"); // ② // try { // thread.sleep(200); // } catch (interruptedexception ex) { // ex.printstacktrace(); // } system.out.println("当前线程名: " + thread.currentthread().getname() + " 企图调用a实例的last方法"); // ④ a.last(); } public synchronized void last() {//同步监视器:b system.out.println("进入了b类的last方法内部"); } } public class deadlock implements runnable { a a = new a(); b b = new b(); public void init() { thread.currentthread().setname("主线程"); // 调用a对象的foo方法 a.foo(b); system.out.println("进入了主线程之后"); } public void run() { thread.currentthread().setname("副线程"); // 调用b对象的bar方法 b.bar(a); system.out.println("进入了副线程之后"); } public static void main(string[] args) { deadlock dl = new deadlock(); new thread(dl).start(); dl.init(); } }
测试结果:概率性的出现
2、解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义尽量
- 避免嵌套同步
3、什么是lock锁
- 从 jdk 5.0 开始,java 提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用 lock 对象充当。
- java.util.concurrent.locks.lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 lock 对象加锁,线程开始访问共享资源之前应先获得 lock 对象。
- reentrantlock 类实现了 lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 reentrantlock,可以显式加锁、释放锁。
4、具体如何使用:
5、synchronized 与 lock 锁有何区别
lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域自动释放
lock 只有代码块锁,synchronized 有代码块锁和方法锁
使用 lock 锁,jvm将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
lock →同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
五、线程的通信
5.1、方法介绍与注意事项:
wait() 与 notify() 和 notifyall()
wait(): 令当前线程挂起并放弃 cpu、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用 notify()或notifyall() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyall (): 唤醒正在排队等待资源的所有线程结束等待.
注意事项:
这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用,否则会报 java.lang.illegalmonitorstateexception 异常。
因为这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 object 类中声明。
sleep() 和 wait() 有何不同之处:
相同点: 一旦执行方法,都可以使得当前的线程进入阻塞状态。
不同点:
两个方法声明的位置不同: thread 类中声明 sleep() , object 类中声明 wait()
调用的要求不同: sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中
关于是否释放同步监视器: 如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait()会释放锁。
案例一:
使用两个线程打印 1-100。线程1, 线程2 交替打印
class number implements runnable { private int number = 1; @override public void run() { while (true) { synchronized (this) { //因为现在使用的是当前对象,所以前面省略this. //如果使用的是其他对象,那么就用对象.的方式去调用该方法 notify(); if (number <= 100) { try { thread.sleep(10); } catch (interruptedexception e) { e.printstacktrace(); } system.out.println(thread.currentthread().getname() + " :" + "打印数为" + number); number++; try { //调用该方法时,线程进入阻塞状态 wait(); } catch (interruptedexception e) { e.printstacktrace(); } } else { break; } } } } } public class communicationtest { public static void main(string[] args) { number number = new number(); thread thread = new thread(number); thread thread01 = new thread(number); thread.setname("线程一"); thread01.setname("线程二"); thread.start(); thread01.start(); } }
执行结果:
六、jdk5.0新增的线程创建方式
新增方式一:实现callable接口
与使用 runnable 相比, callable 功能更强大些
相比 run() 方法,可以有返回值
方法可以抛出异常
支持泛型的返回值需要
借助 futuretask 类,比如获取返回结果
- future 接口
可以对具体 runnable、callable 任务的执行结果进行取消、查询是否完成、获取结果等。
futruetask 是 futrue 接口的唯一的实现类
futuretask 同时实现了 runnable, future 接口。它既可以作为 runnable 被线程执行,又可以作为 future 得到 callable 的返回值
- 新增方式二:使用线程池
没使用线程池: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
使用线程池后: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具。
- 好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corepoolsize: 核心池的大小
maximumpoolsize: 最大线程数
keepalivetime: 线程没有任务时最多保持多长时间后会终止
- 线程池相关的api
jdk 5.0 起提供了线程池相关 api:executorservice 和 executors
executorservice: 真正的线程池接口。常见子类 threadpoolexecutor
void execute(runnable command) : 执行任务命令,没有返回值,一般用来执行 runnable
future submit(callable task): 执行任务,有返回值,一般又来执行 callable
- void shutdown() : 关闭连接池
executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
executors.newcachedthreadpool(): 创建一个可根据需要创建新线程的线程池
executors.newfixedthreadpool(n): 创建一个可重用固定线程数的线程池
executors.newsinglethreadexecutor() : 创建一个只有一个线程的线程池
executors.newscheduledthreadpool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- 代码演示:
创建线程的方式三:使用 callable 接口
//1.创建一个实现callable的实现类 class numthread implements callable<integer> { //2.实现call方法,将此线程需要执行的操作声明在call()中 @override public integer call() throws exception { int sum = 0; for (int i = 1; i <= 100; i++) { if(i % 2 == 0){ system.out.println(i); sum += i; } } return sum; } } public class threadnew { public static void main(string[] args) { //3.创建callable接口实现类的对象 numthread numthread = new numthread(); //4.将此callable接口实现类的对象作为传递到futuretask构造器中,创建futuretask的对象 futuretask<integer> futuretask = new futuretask<integer>(numthread); //5.将futuretask的对象作为参数传递到thread类的构造器中,创建thread对象,并调用start() new thread(futuretask).start(); try { //6.获取callable中call方法的返回值 //get()返回值即为futuretask构造器参数callable实现类重写的call()的返回值。 integer sum = futuretask.get(); system.out.println("总和为:" + sum); } catch (interruptedexception e) { e.printstacktrace(); } catch (executionexception e) { e.printstacktrace(); } } }
创建线程方式四:使用线程池技术
class numberthread implements runnable{ @override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ system.out.println(thread.currentthread().getname() + ": " + i); } } } } class numberthread1 implements runnable{ @override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 != 0){ system.out.println(thread.currentthread().getname() + ": " + i); } } } } public class threadpool { public static void main(string[] args) { //1. 提供指定线程数量的线程池 executorservice service = executors.newfixedthreadpool(10); threadpoolexecutor service1 = (threadpoolexecutor) service; //设置线程池的属性 // system.out.println(service.getclass()); // service1.setcorepoolsize(15); // service1.setkeepalivetime(); //2.执行指定的线程的操作。需要提供实现runnable接口或callable接口实现类的对象 service.execute(new numberthread());//适合适用于runnable service.execute(new numberthread1());//适合适用于runnable // service.submit(callable callable);//适合使用于callable //3.关闭连接池 service.shutdown(); } }
到此这篇关于java多线程的具体介绍与使用笔记小结的文章就介绍到这了,更多相关java多线程介绍与使用内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
推荐阅读
-
Java泛型的基本介绍与使用
-
Java分享笔记:泛型类的定义与使用
-
Java基础之XML介绍与SAX解析、DOM解析XML、JDOM解析、DOM4J解析、XMLEncoder与XMLDecoder的使用以及xstream工具的使用 189~195
-
Java NIO知识汇总详细介绍(与IO的区别、如何使用)
-
Java多线程的具体介绍与使用笔记小结
-
Java多线程之FutureTask的介绍及使用
-
Lombok的介绍与使用_Java 开发小效率工具
-
java TCP/IP通信与多线程,线程池的使用
-
学习springboot小笔记(七) ----启动类不能放在java目录下、缓存的使用步骤介绍
-
Java泛型的基本介绍与使用