Java基础入门之——多线程

文章详细介绍了Java中创建线程的三种方式:继承Thread类、实现Runnable接口以及实现Callable接口,并探讨了它们的优缺点。接着,文章讲解了线程同步的概念,包括同步代码块、同步方法和Lock锁的使用,以解决线程安全问题。此外,还讨论了线程池的重要性和创建线程池的方法,以及线程池的工作原理和配置参数。
摘要由CSDN通过智能技术生成


创建线程

有三种方式创建线程:1. 继承Thread类;2. 实现Runnable接口并实现run方法;3. 实现Callable接口

public class Main {
    // main方法默认是一条主线程
    public static void main(String[] args) {
        // 创建子线程对象
        Thread t = new MyThread();
        // 启动子线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
class MyThread extends Thread{
    // 必须重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        // 创建任务对象
        Runnable target = new MyRunnable();
        // 启动子线程,可用匿名内部类和lambda表达式简化编程
        new Thread(target).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}

前两种创建线程方式都存在一个问题,如果线程执行完后有一些数据要返回,他们重写的run方法均不能返回结果。以下方式可以解决。

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建Callable对象
        Callable myCallable = new MyCallable(100);
        // 把Callable对象封装成FutureTask对象(任务对象)
        // FutureTask实现了Runnable接口,可以调用对象的get方法获取call的返回值
        FutureTask<String> f1 = new FutureTask<>(myCallable);
        // 把任务对象交给Thread对象,启动子线程
        new Thread(f1).start();

        // 获取子线程返回结果
        // 主线程执行到这里会等待子线程执行完毕,保证获取结果
        String s = f1.get();
        System.out.println(s);
    }
}
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 = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程输出:1-" + n + "的和是" + sum;
    }
}

注意:启动线程必须是调用start方法,不是调用run方法。直接调用run方法会被当成普通方法执行,此时还是单线程执行。只用调用了start方法后才是启动了一个新的线程。

优点缺点
第一种编程简单由于继承Thread类,不能在继承其他类,不方便扩展功能
第二种只是实现了Runnable接口,可以继承其他类,扩展性更强没有明显缺点
第三种可以继承其他类,扩展性更强,可以返回数据编码复杂

Thread类的常用方法

在这里插入图片描述
Thread还提供了如yield、interrupt、守护线程、线程优先级等线程控制方法。

public class Main {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        t1.setName("1号线程");
        t1.start();
        System.out.println(t1.getName());// Thread-0

        Thread t2 = new MyThread("2号线程");
        t2.start();
        System.out.println(t2.getName());// Thread-1

        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());// main
    }
}
class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
        super(name);
    }
    // 必须重写run方法
    @Override
    public void run() {
        Thread thread = Thread.currentThread();
        for (int i = 0; i < 5; i++) {
            System.out.println(thread.getName() + "线程输出:" + i);
        }
    }
}

线程同步(解决线程安全问题——加锁)

在这里插入图片描述
场景:小明和小红是一对夫妻,他们同时去银行从他们的共同账户取钱10w,而这个账户只有10w。
不加锁的代码如下

public class Main {
    public static void main(String[] args) throws Exception {
        Account account = new Account("中国银行", 100);
        new PeopleThread(account, "小明", 100).start();
        new PeopleThread(account, "小红", 100).start();
    }
}
public class PeopleThread extends Thread{
    private Account acc;
    private int money;
    public PeopleThread(Account acc, String name, int money) {
        super(name);
        this.acc = acc;
        this.money = money;
    }
    @Override
    public void run() {
        acc.drawMoney(money);
    }
}
public class Account {
    private String cardId;
    private int money;

    public void drawMoney(int 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 Account(String cardId, int money) {
        this.cardId = cardId;
        this.money = money;
    }

    public String getCardId() {
        return cardId;
    }

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

    public int getMoney() {
        return money;
    }

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

输出结果之一:
在这里插入图片描述
很明显银行亏钱了。
下面三种方式给以上程序枷锁,解决这个线程安全问题。

同步代码块

作用:把访问资源的核心代码给上锁,以此保证线程安全。
在这里插入图片描述
原理:每次只允许一个线程加锁后进入,执行完后立刻自动解锁,其他线程才可以进来执行。
对于当前执行的线程来说,同步锁必须是同一把(同一个对象),在案例中就像是小红和小明取钱的账户要是同一个,否则会出Bug。
只需要对drawMoney方法修改。

	public void drawMoney(int money) {
        // 谁来取钱
        String name = Thread.currentThread().getName();
        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 synchronized void drawMoney(int 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 + "来取钱,但是余额不足!!!");
        }
    }

同步方法的底层原理:

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

Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、方便、强大。
Lock是一个接口,不能直接被实例化,可以采用它的实现类ReentrantLock来构建锁对象。
修改Account中的代码。

    // 创建一个锁对象
    private final Lock lk = new ReentrantLock();
    
    public void drawMoney(int money) {
        // 谁来取钱
        String name = Thread.currentThread().getName();
        // 核心代码中可能有bug,异常抛出去了而没有解锁
        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();// 解锁
        }
    }

线程池

线程池就是一个可以复用线程的技术。
不使用线程池可能出现的问题:用户每发起一个请求,后台就需要创建一个新的线程出来,下次新任务来了又要创建新的线程。而创建线程的开销是很大的,并且请求过多时,会产生大量的线程出来,严重影响系统的性能。

在这里插入图片描述
线程池指定一片区域放置线程,还有一片区域放任务,当线程数量达到上限,新的任务就会排队等待线程忙完当前的任务。
我们把区域里的线程称为工作线程或者核心线程,这里面的线程是可以重复利用的。
任务队列里面的每一个任务都是一个对象,都实现了Runnable或Callable接口。

总之,线程池可以控制线程的数量来处理任务。

注意事项:
1、临时线程什么时候创建?
新的任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2、什么时候开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会拒绝。

创建线程池(一)

代表线程池的接口:ExecutorService
方法一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象
在这里插入图片描述

  • 参数一:corePoolSize,指定线程池的核心线程数量
  • 参数二:maximumPoolSize,指定线程池的最大线程数量
  • 参数三:keepAliveTime,临时线程的存活时间
  • 参数四:unit,临时线程的存活时间单位(秒、分、时、天)
  • 参数五:workQueue,指定线程的消息队列
  • 参数六:threadFactory,指定线程池的线程工厂,创建核心线程和临时线程
  • 参数七:handler,指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新的任务来了怎么处理)
new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
	new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy());

以下常见的新任务拒绝策略:
在这里插入图片描述

处理线程池(一)

在这里插入图片描述
执行Runnable任务

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();
        pool.execute(target);// 创建一个核心线程,自动处理
        pool.execute(target);// 创建一个核心线程,自动处理
        pool.execute(target);// 创建一个核心线程,自动处理
        pool.execute(target);// 任务队列排队等待
        pool.execute(target);// 任务队列排队等待
        pool.execute(target);// 任务队列排队等待
        pool.execute(target);// 任务队列排队等待
        // 到了创建临时线程的时机
        pool.execute(target);// 创建临时线程
    }
}
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "运行中");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }
}

执行Callable任务

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(100));
        Future<String> f3 = pool.submit(new MyCallable(100));
        Future<String> f4 = pool.submit(new MyCallable(100));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}
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 = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "输出:1-" + n + "的和是" + sum;
    }
}

创建线程池(二)

方法二:使用Executors(线程池工具类)调用方法返回不同特点的线程池对象
在这里插入图片描述
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
用法大同小异。

public class Main {
    public static void main(String[] args) throws Exception {
//        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
//                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
//                new ThreadPoolExecutor.AbortPolicy());

        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(100));
        Future<String> f3 = pool.submit(new MyCallable(100));
        Future<String> f4 = pool.submit(new MyCallable(100));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

值得注意的是:

  • 底层都是实现类ThreadPoolExecutor
  • 核心线程数设置多少?计算密集型的任务:CPU的核数 + 1;IO密集型的任务:CPU的核数 * 2。
  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

在这里插入图片描述

细节知识

并发和并行

并发:并发是指两个或多个事件在同一时间间隔发生。
并行:并行是指两个或者多个事件在同一时刻发生。
从微观的层面来说,并发在某一时刻只在执行一个事件。

线程的生命周期

在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值