多线程基础
什么是多线程

并发和并行

多线程的实现方式
继承Thread类的方式行进实现
继承Thread类实现多线程是通过创建Thread的子类并重写其run()方法以定义任务,再调用start()方法启动线程,但该方式受Java单继承局限且线程与任务紧密耦合,不利于资源共享。
创建Thread子类:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "HelloWorld!");
}
}
}
调用start方法:
public class ThreadDemo {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.setName("线程一");
myThread2.setName("线程二");
myThread1.start();
myThread2.start();
}
}
实现Runnable接口的方式进行实现
实现Runnable接口是把任务代码定义在实现类的run()方法中,再将该实例作为目标对象传入Thread类并调用start()启动,此方式解耦了线程与任务、规避了单继承局限,且更适合多线程共享同一资源。
实现Runnable接口:
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "Hello");
}
}
}
将该实例作为目标对象传入Thread类并调用start()启动
public class ThreadDemo {
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
利用Callable接口和Future接口的方式实现
实现Callable接口并重写其call()方法以定义带返回值和可抛出异常的任务,再通过FutureTask类包装后交由Thread启动或直接提交给线程池,最后利用Future接口的get()方法阻塞获取异步计算结果,从而支持返回值、异常捕获和任务取消等扩展功能。
实现Callable接口
public class MyCallable implements Callable<Integer> {
/*
* 求1-100的整数和*/
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
通过FutureTask类包装后交由Thread启动或直接提交给线程池,最后利用Future接口的get()方法阻塞获取异步计算结果,从而支持返回值
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
Integer sum = futureTask.get();
System.out.println(sum);
}
}
三种实现方式的对比

常见的成员方法

setName(String name)
细节:
- 如果没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始) - 如果要给线程设置名字,可以用set方法进行设置,也可以用构造方法设置
public class ThreadDemo {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
System.out.println(myThread1.getName());//Thread-0
MyThread myThread2 = new MyThread();
myThread2.setName("线程1");
System.out.println(myThread2.getName());//线程1
MyThread myThread3 = new MyThread("线程二");
System.out.println(myThread3.getName());//线程二
}
}
currentThread()
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在之前,写的所有的代码,其实都是运行在main线程中的
public class ThreadDemo2 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName());//main
}
}
sleep(long time)
细节:
- 哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
- 方法的参数:就表示睡眠的时间,单位毫秒
- 当时间到了之后,线程就会自动的醒来,继续执行下面的代码
public class ThreadDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread t = Thread.currentThread();
System.out.println("我要睡眠了");
long start = System.currentTimeMillis();
Thread.sleep(5000);
long end = System.currentTimeMillis();
System.out.println("我醒了");
System.out.println(t.getName() + "睡眠了" + (start - end) + "毫秒");//main睡眠了-5003毫秒
}
}
线程的优先级
线程的优先级默认为5,最小为1.最大为10
优先级的大小不能完全决定cpu的抢占,只是概率问题
守护进程
守护线程是在后台运行的辅助性线程(如JVM的垃圾回收),其核心特征是不会阻止JVM退出——当所有非守护(用户)线程执行完毕后,无论守护线程是否仍在运行,JVM都会强行终止它们并直接关闭程序。
出让线程
出让线程通过Thread.yield()方法使当前线程从运行状态主动退回到就绪状态,向线程调度器提示愿意让出当前CPU执行权以允许同优先级或更高优先级的其他线程获得运行机会,但该操作仅为建议且不释放任何锁资源。
插入线程
线程插入通过调用目标线程的join()方法,将目标线程“插入”到当前线程之前强制执行,迫使当前线程进入阻塞等待状态,直到目标线程运行结束或达到指定的超时时间,从而实现线程执行的顺序合并与同步协调。
线程的生命周期

线程安全
同步代码块
synchronized同步代码块通过获取指定对象的监视器锁(Monitor Lock),强制线程串行执行括号内的临界区代码,既保证了操作的原子性,又通过锁释放时的“happens-before”规则确保变量修改对后续获取锁的线程立即可见,从而解决多线程并发安全问题。

public class MyThread extends Thread{
private static int ticket = 0;
//卖票
@Override
public void run() {
while (true) {
synchronized (MyThread.class) {
if (ticket < 100) {
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + "正在卖第" + ++ticket + "张票!");
} else {
break;
}
}
}
}
}
同步方法

public class MyRunnable implements Runnable{
int ticket = 0;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (tickets()){
break;
}
}
}
private synchronized boolean tickets() {
if (ticket == 100) {
return true;
} else {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ++ticket + "张票");
}
return false;
}
}
lock锁
Lock锁是基于AQS(抽象队列同步器)实现的显式同步接口,需在try-finally代码块中手动执行lock()与unlock(),相比synchronized提供了tryLock非阻塞尝试、超时等待及可中断获取等更灵活的线程控制能力。
public class MyThread extends Thread{
private static int ticket = 0;
static Lock lock = new ReentrantLock();
//卖票
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.lock();
try {
if (ticket < 100) {
System.out.println(getName() + "正在卖第" + ++ticket + "张票!");
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
死锁
死锁是并发程序中的一种僵局状态,指两个或多个线程因各自持有对方所需的系统资源且相互等待(满足互斥、持有并等待、不可抢占、循环等待四个必要条件),导致所有参与线程永久阻塞而无法继续推进,最终引发应用无响应或系统挂起的严重故障。
代码示例:
public class MyThread extends Thread{
static Object obj1 = new Object();
static Object obj2 = new Object();
@Override
public void run() {
while (true) {
if ("线程A".equals(getName())) {
synchronized (obj1) {
System.out.println("线程A拿到了1锁,准备拿2锁");
synchronized (obj2) {
System.out.println("线程A拿到了2锁,顺利执行");
}
}
} else if ("线程B".equals(getName())) {
synchronized (obj2) {
System.out.println("线程B拿到了2锁,准备拿1锁");
synchronized (obj1) {
System.out.println("线程B拿到了1锁,顺利执行");
}
}
}
}
}
}
等待唤醒机制
等待唤醒机制是线程间基于共享锁对象的协作通信模型,线程在条件不满足时主动释放锁并进入阻塞等待(wait/await),待其他线程改变条件后发出通知(notify/signal),被唤醒线程需重新竞争锁并循环校验条件后才能继续执行,以此避免无效自旋并解决生产者-消费者等协同场景问题。

案例:
public class Foodie extends Thread{
@Override
public void run() {
//循环
while (true) {
synchronized (Desk.lock) {
//判断自己还能不能吃
if (Desk.count == 0) {
//吃不了了
break;
}
//还能吃,判断桌上是否有食物
if (Desk.foodFlag == 0) {
//没食物,等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//有,就吃,更新状态
Desk.count--;
System.out.println(getName() + "吃了一碗,还能吃" + Desk.count + "碗!!!");
Desk.foodFlag = 0;
// 唤醒厨师
Desk.lock.notify();
}
}
}
}
}
public class Cook extends Thread{
@Override
public void run() {
synchronized (Desk.lock){
while (true){
if(Desk.count == 0){
break;
}
if(Desk.foodFlag == 1) {
//等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//处理业务
Desk.foodFlag = 1;
System.out.println(getName() + "做好了食物,快来吃吧");
Desk.lock.notify();
}
}
}
}
}
public class Desk {
public static int foodFlag = 0;
public static Lock lock = new ReentrantLock();
public static int count = 10;
}
阻塞队列
阻塞队列(BlockingQueue)是JUC包下支持阻塞操作的线程安全队列,当队列为空时,获取(take)操作会阻塞,当队列满时,插入(put)操作会阻塞,它内部自动封装了wait/notify逻辑,让你无需手写复杂的等待唤醒机制即可优雅实现生产者-消费者模式
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 生产者(厨师)
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
String food = "面条" + i;
queue.put(food); // 队列满时阻塞
System.out.println("厨师做好了: " + food);
}
} catch (InterruptedException e) {}
}).start();
// 消费者(食客)
new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
String food = queue.take(); // 队列空时阻塞
System.out.println("食客吃完了: " + food);
}
} catch (InterruptedException e) {}
}).start();
}
}
线程的状态

线程池
原理:

实现:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建一个固定大小为 3 的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 2. 提交 5 个任务给线程池执行
for (int i = 1; i <= 5; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskId);
try {
Thread.sleep(500); // 模拟业务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 3. 关闭线程池(不再接收新任务,等待已有任务执行完毕)
pool.shutdown();
}
}
自定义线程池的七个参数:

底层原理:

demo:
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
// 1. corePoolSize:核心线程数(2个)
// 2. maximumPoolSize:最大线程数(5个)
// 3. keepAliveTime:非核心线程空闲存活时间(10秒)
// 4. unit:时间单位(秒)
// 5. workQueue:阻塞队列(容量为3的ArrayBlockingQueue)
// 6. threadFactory:自定义线程工厂(设置线程名和优先级)
// 7. handler:拒绝策略(自定义打印日志)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
10L, // 空闲存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 任务队列(容量3)
new CustomThreadFactory(), // 自定义线程工厂
new CustomRejectedHandler() // 自定义拒绝策略
);
// 提交 10 个任务(触发拒绝策略)
// 执行逻辑:
// - 第1、2个:由核心线程(core=2)执行
// - 第3、4、5个:放入队列(容量3)
// - 第6、7、8个:创建非核心线程(最大到5)
// - 第9、10个:队列已满 + 线程已达最大数 → 触发拒绝策略
for (int i = 1; i <= 10; i++) {
int taskId = i;
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try {
Thread.sleep(2000); // 模拟耗时,保证队列堆积
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
pool.shutdown();
}
/**
* 自定义线程工厂(参数6):给线程起有意义的名称
*/
static class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "MyPool-Thread-" + threadNumber.getAndIncrement());
t.setDaemon(false); // 非守护线程
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
/**
* 自定义拒绝策略(参数7):当任务过多时打印警告,防止静默丢失
*/
static class CustomRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("【拒绝警告】任务 " + r.toString() + " 被拒绝,队列已满且线程数已达上限!");
}
}
}
最大并行数
最大并行数是指系统在同一时刻能真正同时运行的任务数量上限,物理上受 CPU 核心数(Runtime.getRuntime().availableProcessors())制约;而线程池的 maximumPoolSize 仅是逻辑上的并发上限,超过物理核心数的线程只能通过时间片轮转并发执行(而非真正并行),因此对于 CPU 密集型任务,最大并行数建议设置为 CPU 核心数,避免频繁上下文切换导致性能下降。
线程池多大合适:

总结
根据java基础课程,多线程和JUC的知识就到这里了,但是我觉得很多东西都没有学到,所以我打算再去找一份专门针对java多线程JUC的学习课程进行进一步的学习。




