侧边栏壁纸
  • 累计撰写 135 篇文章
  • 累计创建 1 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

多线程

概述(thread)

线程是程序内部的一条执行流程,如果只有一条执行流程,那么就是单线程的程序

多线程指的是从软硬件上实现的多条执行流程的技术(多线程由CPU负责调度执行)

创建多线程

Java是通过java.lang.Thread类的对象来表示线程

方式一 继承Thread类

  • 定义一个子类MyThread继承类Java.lang.Thread,重写run方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法)

优缺点

  • 优点:编码简单
  • 缺点:线程类已经继承了Thread,无法继承其他类,不利于功能的扩展

注意事项

  • 启动线程必须是调用start方法,不是调用run方法

  • 不要把主线程任务放在启动子线程之前

  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行

  • 只有调用start方法时,才是启动一个新的线程执行

demo

package com.xbxaq.thread_;

public class Test {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("MainThread Output:" + i);
        }
    }
}


package com.xbxaq.thread_;

public class MyThread extends Thread{
    //必须重写run方法

    @Override
    public void run() {
        //描述线程执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread Output:" + i);
        }
    }
}

image-20240401162423580

方式二 实现Runnable接口

  • 定义一个线程任务类MyRunnable实现Runnable接口
  • 创建MyRunnable任务给对象
  • 将MyRunnable任务对象交给Thread进行处理
  • 调用线程对象的start()方法启动线程
Thread类提供的构造器 说明
public Thread(Runnable target) 封装Runnable对象成为线程对象

优缺点

  • 优点:任务类只是实现了接口,可以继续继承其他类i、实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象

demo

package com.xbxaq.thread_;

public class Test {
    public static void main(String[] args) {
        Runnable target= new MyRunnable();
        new Thread(target).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("MainRunnable Output:" + i);
        }
    }
}



package com.xbxaq.thread_;

public class MyRunnable implements Runnable{
    //必须重写run方法

    @Override
    public void run() {
        //描述线程执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("MyRunnable Output:" + i);
        }
    }
}

image-20240401165656142

方式三 实现Callable接口

以上两种方式重写的run方法均不能直接返回结果

JDK 5.0提供了Callable接口和FutureTask类来实现,这种方式最大的优点:可以返回线程执行完毕后的结果

创建步骤

  • 创建任务对象,定义一个类实现callable接口,重写call方法,封装要做的事情和要返回的数据。将callable类型的对象封装成FutureTask(线程任务对象)
  • 将线程任务对象交给thread对象
  • 调用thread对象的start方法启动线程
  • 线程执行完毕之后,通过futureTask对象的get方法去获取线程任务执行的结果

FutureTask的API

FutureTask提供的构造器 说明
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
FutureTask提供的方法 说明
public V get() throws Exception 获取线程执行call方法返回的结果。

优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强,可以在线程执行完毕之后获取线程执行的结果
  • 缺点:编码复杂

demo

package com.xbxaq.thread_;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);

        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

package com.xbxaq.thread_;

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 2、重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是:" + sum;
    }
}

image-20240401170511169

三种方法对比

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

常用方法

Thread提供的常用方法 说明
public void run() 线程的任务方法
public void start() 启动线程
public String getName() 获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name) 为线程设置名称
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()… 让调用当前这个方法的线程先执行完!
Thread提供的常见构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target, String name) 封装Runnable对象成为线程对象,并指定线程名称

Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候再讲解。

线程安全

  • 当多个线程同时操作同一个资源的时候,可能会出现业务安全问题

线程安全问题的原因

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

业务场景

  • 小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

image-20240401181141872

场景分析

  • 需要提供一个账户类,接着创建一个账户对象代表两个人的共享账户
  • 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
  • 创建两个线程,传入同一个账户对象给两个线程处理
  • 启动两个线程,同时去同一个账户对象进去取钱10w

demo

test

package com.xbxaq.thread_;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        Account acc = new Account("ICBC-110", 100000);
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红

    }
}

account

package com.xbxaq.thread_;


public class Account {
    private String cardId; // 卡号
    private double money; // 余额。

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    // 小明 小红同时过来的
    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        if(this.money >= money){
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        }else {
            System.out.println(name + "来取钱:余额不足~");
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }


}

drawthread

package com.xbxaq.thread_;

public class DrawThread extends Thread{
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

image-20240401182107826

线程同步

  • 让多个线程实现先后依次访问共享资源

常见的方案为加锁,每次只允许一个线程加锁,加锁后才能进行访问,访问完毕之后自动解锁,然后其他线程才能再加锁进来

image-20240401182307514

方法一:同步代码块

  • 作用:把访问共享资源的核心代码进行上锁,保证线程的安全
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进行执行
synchronized(同步锁) {	
	访问共享资源的核心代码
}

对于实例方法建议使用this作为锁对象,对于静态方法建议使用字节码(类名.class)对象作为锁对象

注意事项:对于当前同时执行的线程来说,同步锁必须是同一个对象,否则会出bug!!!

demo

test

package com.xbxaq.thread_;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        Account acc = new Account("ICBC-110", 100000);
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红

    }
}

account

package com.xbxaq.thread_;


public class Account {
    private String cardId; // 卡号
    private double money; // 余额

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    public static void test() {
        synchronized(Account.class){

        }
    }

    // 小明 小红同时过来的
    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        synchronized (this){
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            } else {
                System.out.println(name + "来取钱:余额不足~");
            }
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

}


drawthread

package com.xbxaq.thread_;

public class DrawThread extends Thread{
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

image-20240401183149397

方法二 同步方法

  • 作用:把访问共享资源的核心方法进行上锁,以此保证线程的安全
  • 原理:每次只能一个线程进入,执行完毕之后自动解锁,其他线程进行执行
修饰符 synchronized 返回值类型 方法名称(形参列表) {操作共享资源的代码}

同步方法底层原理

  • 同步方法其实底层也有隐式锁对象,只是锁的范围是整个方法代码
  • 如果方法是实例方法,同步方法默认用this作为锁的对象
  • 如果方法是静态方法,同步方法默认用类名.class作为锁的对象

demo

test

package com.xbxaq.thread_;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        Account acc = new Account("ICBC-110", 100000);
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红

    }
}

account

package com.xbxaq.thread_;


public class Account {
    private String cardId; // 卡号
    private double money; // 余额

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    public static void test() {
        synchronized(Account.class){

        }
    }

    // 小明 小红同时过来的
    //同步方法
    public synchronized void drawMoney(double money) {
  //  public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        } else {
            System.out.println(name + "来取钱:余额不足~");
        }

    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

}


drawthread

package com.xbxaq.thread_;

public class DrawThread extends Thread{
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

image-20240401183806427

方法三 lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
构造器 说明
public ReentrantLock() 获得Lock锁的实现类对象

常用方法

方法名称 说明
void lock() 获得锁
void unlock() 释放锁

demo

test

package com.xbxaq.thread_;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args) throws Exception {
        Account acc = new Account("ICBC-110", 100000);
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红

    }
}

account

package com.xbxaq.thread_;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    private String cardId; // 卡号
    private double money; // 余额

    private final Lock lk = new ReentrantLock();

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }


    // 小明 小红同时过来的

    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        try {
            lk.lock();
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            } else {
                System.out.println(name + "来取钱:余额不足~");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lk.unlock();
        }

    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

}


drawthread

package com.xbxaq.thread_;

public class DrawThread extends Thread{
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

image-20240401184355090

线程通信

  • 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺

Object类的等待和唤醒方法

方法名称 说明
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程

注意:上述方法应该使用当前同步锁对象进行调用。

线程通信的三个常见方法

方法名称 说明
void wait() 当前线程等待,直到另一个线程调用notify() 或 notifyAll()唤醒自己
void notify() 唤醒正在等待对象监视器(锁对象)的单个线程
void notifyAll() 唤醒正在等待对象监视器(锁对象)的所有线程

注意:上述方法应该使用当前同步锁对象进行调用

线程池

  • 线程池就是一个可以复用线程的技术

不使用线程池的问题:

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创新新线程来处理。
  • 而创建新线程的开销是很大的,并且请求过多时,会产生大量的线程出来,从而严重影响系统的性能。

image-20240401192306154

创建线程池

JDK 5.0起提供了代表线程池的接口:ExecutorService。

如何得到线程池对象

方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor构造器

image-20240401192543276

image-20240401192617269

注意事项:

  • 临时线程的创建时间:当新任务提交时发现核心线程都在忙,任务队列排满了,并且可以创建临时线程,才会创建临时线程
  • 什么时候开始拒绝新任务:核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行 Runnable 任务
Future<T> submit(Callable<T> task) 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown() 等全部任务执行完毕后,再关闭线程池!
List<Runnable> shutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

新任务拒绝策略

策略 详解
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行

线程如何处理Runnable任务

  • 使用executorservice的方法
  • void execute (Runnable target)

线程如何处理Callable任务,并得到任务执行完后返回的结果

  • 使用ExecuteService的方法
  • Future<T> submit(callable<T> command)

Executors

一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象

方法名称 说明
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorServicenewSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

Executors使用可能存在的陷阱

大型并发系统环境中使用Executors如果不注意可能会出现系统风险

image-20240401193457546

并发与并行

进程

  • 正在运行的程序(软件)就是一个独立的进程
  • 线程是属于进程的,一个进程中可以同时运行很多个线程
  • 进程中的多个线程其实是并发和并行执行的

并发

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发

image-20240401193758691

并行

  • 在同一个时刻上,同时有多个线程在被CPU调度执行

image-20240401193853334

多线程执行时,并发和并行是同时进行的

线程的生命周期

Java的线程状态

  • Java共有6中状态

image-20240401194103280

线程的 6种状态互相转换

image-20240401194339719

线程的6种状态总结

线程状态 说明
NEW(新建) 线程刚被创建,但是并未启动。
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡
0
博主关闭了所有页面的评论