Java多线程
图
Java给多线程编程提供了内置的支持,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销

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

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

图

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

多线程的创建方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

继承Thread类

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
/**
 * 多线程创建
 */
public class ThreadTest1 extends Thread {

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

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

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

    }

}

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

Thread类常见方法:

图

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

实现Runnable接口

  • 自定义类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象调用start()方法启动线程
/**
 * 多线程
 */
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接口

  • 实现Callable接口,需要返回值类型
  • 重写call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务
  • 提交执行
  • 获取结果
  • 关闭服务
/**
 * 多线程创建
 */
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

CopyOnWriteArrayList

JUC下的一个集合,线程安全

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();
    }
}

synchronized和lock

  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域就自动释放锁
  • 使用Lock时JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供了更多的子类)

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

生产者和消费者

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

图

管程法

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

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

// 生产者同理

信号灯法

同理

线程池

线程的频繁创建和销毁,使用了大量的资源,尤其在并发情况下,对性能影响很大

解决:提前创建好多个线程,放入线程池中,使用时直接获取,用完放回池中

优点:

  • 提高响应速度(减少创建新线程时间)
  • 降低资源消耗
  • 便于线程管理
    • corePoolSize:核心池大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后终止

线程池使用

JDK5.0提供了线程池相关API:ExecutorService和Executors

ExecutorService:真正的线程池接口

  • void execute(Runnable command):执行任务,没有返回值,一般用来执行Runnable
  • Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
  • void shutdown:关闭连接池

Executors:工具类、连接池的工厂类,用于创建并返回不同类型的线程池

public class ThreadTest2 {

    public static void main(String[] args) {
        // 创建线程池,参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        // 启动线程
        service.execute(new ThreadTest());
        service.execute(new ThreadTest());
        service.execute(new ThreadTest());
        // 关闭线程池
        service.shutdown();
    }

}

class ThreadTest implements Runnable {

    @Override
    public void run() {
        System.out.println("执行");
    }

}

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);