Java的多线程,看这一篇足够!!!

Java创建多线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现CallAble

继承Thread类

使用方法
  1. 定义一个子类MyThread继承Thread类,然后重写run()方法。
  2. 创建MyThread类对象
  3. 调用线程对象的start()方法启动线程,线程启动后会执行run方法
Thread代码实现
/**
 * 子类继承Thread类
 */
public class MyThread extends Thread{

    // 重写Run方法
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 10; i++) {
            System.out.println("MyThread执行");
        }
    }
}


public class ThreadTest01 {
    // main是有一条主线程在执行
    public static void main(String[] args) {
        // 创建MyThread线程类表示一个实例
        Thread myThread = new MyThread();
        // 启动线程,自动指定Run方法
        myThread.start();

        // 主线程的方法执行
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程输出");
        }
    }
}

继承Thread的优缺点

优点:实现简单,编码方便。
缺点:线程类继承了Thread类,就无法继承其他的类,不利于功能的扩展。

注意事项
  1. 启动线程一定是调用start方法,不能是调用run方法,要不然只是系统只是把它当做方法调用,不会当做一个独立的线程,只用调用start方法才是启动一个新的线程
  2. 不要把主线程任务放到子线程启动之前,因为这样主线程一直是先跑完的,相当于还是一个单线程的效果。

实现Runnable

使用方法
  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。(因为Thread有一个有构造器传入的参数就是Runnable)
  4. 调用线程对象的start()方法启动线程
Runnable代码实现
public class MyRunnable implements Runnable{

    @Override
    public void run() {
         // 线程的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程");
        }
    }
}

public class ThreadTest02 {
    public static void main(String[] args) {
        // 创建任务对象
        Runnable myRunnable = new MyRunnable();
        // 把任务对象交给线程对象处理
        new Thread(myRunnable).start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程");
        }
    }
}
实现Runnable的优缺点

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

Runnable代码实现其他方式
public class ThreadTest02_1 {
    public static void main(String[] args) {
        // 直接创建Runnable接口的匿名内部类
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程1");
                }
            }
        };
        new Thread(target).start();

        // 简化形式
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程2");
                }
            }
        }).start();

        // 简化形式
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程3");
            }
        }).start();




        for (int i = 0; i < 5; i++) {
            System.out.println("主线程");
        }

    }
}

前两种方式存在的问题,如果我需要线程执行完毕之后,获取线程的返回的结果该如何处理???

利用Callable接口、FutureTask类来实现

使用方法
  1. 创建任务对象
    1. 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    2. 把Callable类型的对象封装成FutureTask(线程任务对象)。
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
Callable代码实现
import java.util.concurrent.Callable;

/**
 * <String>是执行结束之后要返回的结果
 */
public class MyCallable implements Callable<String> {
    // 用于存储返回的结果
    private int n;
    // 构造器
    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return "线程求出1-" + n + "的和是:" + sum;
    }
}

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程任务对象
        MyCallable result = new MyCallable(100);

        // 把result对象封装成FutureTask的对象
        FutureTask<String> f1 = new FutureTask<>(result);

        new Thread(f1).start();
        String s = f1.get();
        System.out.println(s);

    }
}

实现Callable的优缺点

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

Java创建多线程方式总结

在这里插入图片描述

Java线程常用的一些方法

在这里插入图片描述

线程安全问题

什么是线程安全问题?

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

举个例子

你和你女朋友在银行有一个共同的账户,余额是10万元,如果你和你女朋友同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
你去银行柜台取钱10万,银行判断你的账户余额足够10万,然后正准备给你取钱的时候,你女朋友在ATM同时在取钱10万,正好她在取钱的时候也判断余额足够,所以你们都取10万,银行亏了10万。

出现线程安全的原因

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

程序模拟线程安全的问题


/**
 * 账户类
 */
public class Account {
    // 账户余额
    private double money;
    // 账户id
    private String cardId;

    public Account() {
    }

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

    public double getMoney() {
        return money;
    }

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

    public String getCardId() {
        return cardId;
    }

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

    public void drawMoney(double money) {
        // 是谁来取钱
        String name = Thread.currentThread().getName();
        // 判断余额
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功");
            this.money -= money;
            System.out.println(name+"取钱后还有"+this.money);
        } else {
            System.out.println(name + "来取钱但是钱不够");
        }


    }
}

/**
 * 取钱线程
 */
public class DrawThread extends Thread{
    private Account account;


    public DrawThread(Account account, String name){
        super(name);
        this.account = account;
    }
    // 线程任务就是去取钱
    @Override
    public void run() {
        // 取钱方法
        account.drawMoney(100000);
    }
}
// 取钱操作
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个账户对象
        Account account = new Account(100000, "XL-111");

        // 创建两个线程
        new DrawThread(account,"小李").start();// 小李
        new DrawThread(account,"小王").start(); // 小王


    }
}

结果展示
在这里插入图片描述

如何解决线程安全问题

让多个线程实现先后依次访问共享资源,这样就解决了安全问题
加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题

解决线程安全三种实现方法

同步代码块

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

同步代码块注意事项
  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
public class Account {
    private double money;
    private String cardId;

    public Account() {
    }

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

    public double getMoney() {
        return money;
    }

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

    public String getCardId() {
        return cardId;
    }

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

    public void drawMoney(double money) {
        // 是谁来取钱
        String name = Thread.currentThread().getName();
        // 判断余额
        // 利用同步代码块
        // 这个this代表他们自己的账户account
        // 如果使用字符串来锁,那范围太大了
        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 + "来取钱但是钱不够");
            }
        }
    }
}

在这里插入图片描述

同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法底层原理
  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
实现代码
/**
 * 账户类
 */
public class  Account {
    // 账户余额
    private double money;
    // 账户id
    private String cardId;

    public Account() {
    }

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

    public double getMoney() {
        return money;
    }

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

    public String getCardId() {
        return cardId;
    }

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

    public synchronized void drawMoney(double money) {
        // 是谁来取钱
        String name = Thread.currentThread().getName();
        // 判断余额
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功");
            this.money -= money;
            System.out.println(name + "取钱后还有" + this.money);
        } else {
            System.out.println(name + "来取钱但是钱不够");
        }


    }
}

其他代码与代码块锁相同

是同步代码块好还是同步方法好一点?

范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
可读性:同步方法更好。

Lock锁
  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
    在这里插入图片描述
代码实现
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    private double money;
    private String cardId;
    // 创建一个锁对象
    private final Lock lk = new ReentrantLock();

    public Account() {
    }

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

    public double getMoney() {
        return money;
    }

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

    public String getCardId() {
        return cardId;
    }

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

    public void drawMoney(double money) {
        // 是谁来取钱
        String name = Thread.currentThread().getName();
        // 判断余额
        // 利用同步代码块
        // 这个this代表他们自己的账户account
        // 如果使用字符串来锁,那范围太大了
        synchronized (this) {
            // 获取锁
            lk.lock();;
            try {
                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) {
                throw new RuntimeException(e);
            } finally {
                // 不管中间出现什么异常,一定要释放锁
                lk.unlock();
            }

        }
    }
}

Java线程池

什么是线程池

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

不使用线程池的问题

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

如何创建线程池

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

如何得到线程池对象?

  1. 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
  2. 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

ThreadPoolExecutor构造器

/*int corePoolSize, // 核心线程数量
        int maximumPoolSize, // 最大线程数量
        long keepAliveTime,// 临时线程的最大存货时间
        TimeUnit unit,// 存货时间的单位
        BlockingQueue<Runnable> workQueue, // 工作队列
        ThreadFactory threadFactory, // 谁来创这些线程
        RejectedExecutionHandler handler*/ // 线程使用完了,任务也占据满了,新来的任务怎么处理
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,                          RejectedExecutionHandler handler) 

临时线程什么时候创建?

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

什么时候会开始拒绝新任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
在这里插入图片描述
在这里插入图片描述

线程池代码示例

线程池处理Runnable任务
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 哪个线程做什么任务
        System.out.println(Thread.currentThread().getName()+"正在输出");
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
import java.util.concurrent.*;

public class ThreadPoolTest1 {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        // 使用线程池处理Runnable任务
        MyRunnable target = new MyRunnable();
        // 线程池执行
        pool.execute(target); // 线程池创建一个线程然后执行这个任务
        pool.execute(target); // 线程池创建一个线程然后执行这个任务
        pool.execute(target); // 线程池创建一个线程然后执行这个任务
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 此时核心线程3个满了,任务队列4个也满了,然后就会创建临时线程
        pool.execute(target);
        pool.execute(target);
        //此时核心线程3个满了,任务队列4个也满了,2个临时线程也满了,再来就拒绝
        pool.execute(target);


        // 等着线程池的任务全部执行完毕之后然后再关闭线程池
        // pool.shutdown();
        // 立即关闭线程池,不管任务有没有执行完毕
        // pool.shutdownNow();



    }
}

线程池处理Callable任务
/**
 * <String>是执行结束之后要返回的结果
 */
public class MyCallable implements Callable<String> {
    private int n;

    // 构造器


    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"线程计算出了1-" + n + "的和是:" + sum;
    }
}

import java.util.concurrent.*;

public class ThreadPoolTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*public ThreadPoolExecutor(
        int corePoolSize, // 核心线程数量
        int maximumPoolSize, // 最大线程数量
        long keepAliveTime,// 临时线程的最大存货时间
        TimeUnit unit,// 存货时间的单位
        BlockingQueue<Runnable> workQueue, // 工作队列
        ThreadFactory threadFactory, // 谁来创这些线程
        RejectedExecutionHandler handler)*/ // 线程使用完了,任务也占据满了,新来的任务怎么处理
        // 创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());


        // 返回未来任务对象
        Future<String> future1 = pool.submit(new MyCallable(100));
        Future<String> future2 = pool.submit(new MyCallable(100));
        Future<String> future3 = pool.submit(new MyCallable(100));
        Future<String> future4 = pool.submit(new MyCallable(100));
        // 获取返回的结果
        System.out.println(future1.get());
        System.out.println(future2.get());
        System.out.println(future3.get());
        System.out.println(future4.get());



    }
}

参考

黑马哔哩哔哩Java线程教学

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值