logo 范 · 拾光录
网址收集 关于作者 Github Gitee
杂文随笔4
Hexo博客:基础使用Hexo博客:Next主题Hexo博客:Next进阶使用Hexo博客:Next高级配置
前端编程3
前端基础知识16
UniApp15
Vue框架19
Java编程7
Java基础知识14
SpringBoot31
SpringMVC14
MyBatis8
SpringCloud15
RocketMQ1
MyCat1
数据库4
MySQL6
Redis7
MongoDB10
H21
Java技术栈4
知识整理分布式锁的实现方案MySQL事务MySQL的锁
软件工具箱14
IDEAGitMavenGradleNginx安装Nginx配置JMeter压测OllamaPicGoRustFSVSCodeDockerObsidianObs录制
Linux知识11
树莓派安装及使用ArchLinux:常用软件ArchLinux:基础系统安装ArchLinux:深度优化ArchLInux:图形化界面安装ArchLinux:Nirifrp内网穿透Jar启动脚本Linux常用命令VirtualBox安装CentOSVirtualBox安装Ubuntu
Python编程6
Python基础知识Python语法yolo目标检测OpenCV的使用及树莓派平台condauv
创意设计2
Blender:入门知识UI设计基础知识
AI相关9
Claude CodeHermes AgentOpenAI基本使用OpenAI工具调用OpenAI记忆管理OpenAI推理执行OpenAI开发框架Langchainllama.cpp

多线程

让程序充分利用CPU的资源,提高CPU的使用率,解决高并发带来的负载均衡问题

优点:

缺点:

进程和线程

进程是计算机正在运行的一个独立应用程序,是一个动态的概念。线程是组成进程的基本单位,一个进程中包含一个或多个线程。进程和线程都是应用程序在执行过程中的概念,如果应用程序没有运行,则不存在进程和线程

区别在于进程运行时内存空间是独立的,每个进程互不干扰,线程共享内存空间,每个线程的运行相对独立,线程必须依赖进程才能执行,多线程就是一个进程中多个线程同时执行

在单核CPU的情况下多线程并非同时执行,而是交替执行。在多核CPU上,才是真正的同时执行

普通方法与多线程方法区别

普通方法与多线程方法的执行顺序如下

一个进程中可以有多个线程,进程由系统分配

多线程的创建方式

继承Thread类

/**
 * 多线程创建
 */
public class ThreadTest extends Thread {

    // 重写
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("多线程..." + i);
        }
    }

    public static void main(String[] args) {
        
        ThreadTest threadTest = new ThreadTest();
        // 启动线程
        threadTest.start();

        for (int i = 0; i < 50; i++) {
            System.out.println("主方法..." + i);
        }

    }

}

注意:线程不一定立即执行,由CPU安排调度

Thread类常见方法:

Thread类也是实现了Runnable,因为继承具有局限性(只能单继承),所以推荐使用实现Runnable方式

实现Runnable接口

/**
 * 多线程
 */
public class ThreadTest2 implements Runnable {

    // 重写
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("多线程..." + i);
        }
    }

    public static void main(String[] args) {
        ThreadTest1 threadTest2 = new ThreadTest1();
        // 启动线程
        new Thread(threadTest2).start();
        for (int i = 0; i < 50; i++) {
            System.out.println("主方法..." + i);
        }
    }

}

并发问题

例如抢票问题

/**
 * 多线程问题
 */
public class Ticket implements Runnable {

    // 票数
    private int ticketNum = 10;

    @Override
    public void run() {
        while (ticketNum > 0) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到第" + ticketNum-- + "张票");
        }
    }

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket, "张三").start();
        new Thread(ticket, "李四").start();
        new Thread(ticket, "王五").start();
    }
    
}

执行结果

王五拿到第10张票
张三拿到第9张票
李四拿到第10张票
张三拿到第8张票
李四拿到第7张票
王五拿到第6张票
张三拿到第3张票
王五拿到第5张票
李四拿到第4张票
王五拿到第2张票
李四拿到第1张票
张三拿到第0张票

数据出现紊乱,线程不安全

实现Callable接口

/**
 * 多线程创建
 */
public class ThreadTest3 implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        // 操作内容
        System.out.println(Thread.currentThread().getName());
        return false;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadTest3 item1 = new ThreadTest3();
        ThreadTest3 item2 = new ThreadTest3();
        ThreadTest3 item3 = new ThreadTest3();

        // 创建执行服务
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交执行并获取结果
        Boolean result1 = executorService.submit(item1).get();
        Boolean result2 = executorService.submit(item2).get();
        Boolean result3 = executorService.submit(item3).get();

        // 关闭服务
        executorService.shutdown();
    }
}

三种实现方式的区别

方式 有点 缺点
继承Thread类 编程简单,可直接使用Thread中方法 可扩展性差,无法获得结果
实现Runnable接口 扩展性强 不能直接使用Thread类中方法,无法获得结果
实现Callable接口 扩展性强,可获得结果 不能直接使用Thread类中方法,编程复杂

线程状态

线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 线程休眠时间,毫秒
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程
boolean isAlive() 测试线程是否处于活动状态

线程停止

JDK提供的方法不建议使用了,推荐线程自己结束,示例

public class ThreadTest implements Runnable {

    // 线程是否结束标识
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            System.out.println("run...");
        }
    }

    // 对外提供结束方法
    public void stop() {
        this.flag = false;
    }

}

线程休眠

sheep()方法能指定当前线程阻塞多少毫秒,存在InterruptedException异常,当阻塞结束后会进入就绪状态,用于模拟网络延时,倒计时等,每个对象都有一个锁,sheep()不会释放锁

守护进程

// 默认是false表示用户进程,设置为true为守护进程
thread.setDaemon(true);

线程分为用户进程守护进程,虚拟机不用等待守护进程执行完毕,也就是说,当同一个方法中的用户线程结束后,即便守护进程是个死循环在下一轮监监控也会将守护进程结束

并发

同一个资源(对象、数据)被多个线程同时操作

线程同步

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问的准确性,在访问时会加入锁机制(synchronized),当一个对象获得对象的排它锁,独占资源,其他线程必须等待,使用后再释放锁

如何判断多个线程是否实现了同步,需要看锁的资源在内存中有几份,如果只有一份,多线程需要排队,如果有多份,则不会排队,也不会实现同步

存在问题:

同步方法

public synchronized void method() {
    // TODO
}

synchronized方法控制对象的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象锁才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面阻塞的线程才能获得锁,继续执行

缺陷:若将一个大的方法声明为synchronized将会影响效率

锁的对象就是变化的量,需要增删改的对象

同步代码块

synchronized (Obj) {
	// TODO
}

Obj称为同步监视器,可以是任何对象,是需要锁的变化的量,同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,即对象本身,或者class

Lock锁

从JDK5.0开始,Java提供了更强大的线程同步锁机制–显式定义同步锁对象来实现同步

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具

private final ReentrantLock lock = new ReentrantLock();

@Override
public void run () {
    try {
        // 加锁
        lock.lock();
    } finally {
        // 解锁
        lock.unlock();
    }
}

这是可重入锁,能指定重入时间,在指定时间内能否拿到锁

private final ReentrantLock lock = new ReentrantLock();

try {
    // 获取锁,3秒内,即等待3秒是否能得到锁
    if (lock.tryLock(3, TimeUnit.SECONDS)) {
        System.out.println(Thread.currentThread().getName() + "获取到了锁");
        Thread.sleep(5000);
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    // 判断是否是当前线程持有锁
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

synchronized和lock

使用顺序:Lock > 同步代码块 > 同步方法

生产者和消费者

涉及到线程之间的通信问题,生产者生产出来后通知消费者消费,如果消费者消费发现没有就等待

管程法

核心在于需要一缓冲区,来负责商品的控制

// 消费者消费商品
public synchronized void pop() {
    // 没有货物
    if(countGoods == 0) {
        try {
            // 等待
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 等待生产者通知开始消费
    countGoods--;
    // 消费完,通知生产者生成
    this.notifyAll();
}

// 生产者同理

信号灯法

同理

死锁

多个线程因为争夺同一个资源而形成的一种互斥状态,导致每个线程都无法继续进行,程序一直处于卡顿状态

Callable结合FutureTask获取结果

// 查询用户总数
Callable<Integer> callableUserCount = () -> userService.count();
FutureTask<Integer> taskUserCount = new FutureTask<>(callableUserCount);
Thread threadUserCount = new Thread(taskUserCount);

// 查询用户总数(直接写代码方式)
Callable<Long> callableAll = () -> mongoTemplate.count(new Query(), InvitationRegister.COLLECTION_NAME);
FutureTask<Long> taskAllUserNum = new FutureTask<>(callableAll);
Thread threadAllUser = new Thread(taskAllUserNum);

// 查询会员总数
Callable<Integer> callableVipCount = () -> userService.count(wrapperVip);
FutureTask<Integer> taskVipCount = new FutureTask<>(callableVipCount);
Thread threadVipCount = new Thread(taskVipCount);

// 统一启动线程
threadUserCount.start();
threadVipCount.start();

// 获取结果
Integer userCount = taskUserCount.get(10, TimeUnit.SECONDS);
Integer vipCount = taskVipCount.get(10, TimeUnit.SECONDS);
多线程
进程和线程
普通方法与多线程方法区别
多线程的创建方式
继承Thread类
实现Runnable接口
并发问题
实现Callable接口
三种实现方式的区别
线程状态
线程方法
线程停止
线程休眠
守护进程
并发
线程同步
同步方法
同步代码块
Lock锁
synchronized和lock
生产者和消费者
管程法
信号灯法
死锁
Callable结合FutureTask获取结果