自学内容网 自学内容网

多线程的详解

多线程

线程的创建:利用Callable接口、FutureTask类来实现:(项目常用)

步骤:

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

FutureTask的API

Api

优缺点:

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

代码:

package com.carat.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo1 {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        Callable<String> c1 = new MyCallable(100);
        //4.把Callable对象封装成一个真正的线程任务对象FutureTask对象
        /**
         * 未来任务对象的作用?
         * a.本身是一个Runnable线程任务对象,可以交给Thread线程对象处理
         * b.可以获取到线程任务的执行结果
         */
        //Runnable ft1 = new FutureTask<>(c1);
        FutureTask<String> ft1 = new FutureTask<>(c1);
        //5.把FutureTask对象交给Thread线程对象处理
        Thread t1 = new Thread(ft1);
        //6.启动线程
        t1.start();

        Callable<String> c2 = new MyCallable(50);
        FutureTask<String> ft2 = new FutureTask<>(c2);
        Thread t2 = new Thread(ft2);
        t2.start();

        //7.获取线程执行完毕后返回的结果
        try {
            //如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下继续执行
            System.out.println(ft1.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            //如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第二个线程执行完毕后,才会往下继续执行
            System.out.println(ft2.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

//1.定义一个实现类实现Callable接口
class MyCallable implements Callable<String>{

    private int n;

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

    //2.实现call方法,定义线程执行体
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程计算1~" + n + "的和为: " + sum;
    }
}

输出结果:

在这里插入图片描述

线程的常用方法:

Thread的常用方法

api

代码:

package com.carat.demo;

public class ThreadApiDemo {
    public static void main(String[] args) {
        //4.创建线程类的对象,代表线程
        Thread t1 = new MyThread("线程1");
        //t1.setName("线程1");
        //5.调用start方法,启动线程,还是调用run方法执行的
        t1.start();//启动线程,让线程执行run方法
        System.out.println(t1.getName());//线程默认名字是: Thread-索引

        Thread t2 = new MyThread("线程2");
        //t2.setName("线程2");
        t2.start();
        System.out.println(t2.getName());//线程默认名字是: Thread-索引

        //哪个线程调用这个代码,这个代码就拿到哪个线程
        Thread thread = Thread.currentThread();//主线程
        thread.setName("主线程");
        System.out.println(thread.getName());//main
    }
}

//1.定义一个子类继承Thread类,成为一个线程类
class MyThread extends Thread {
    public MyThread(String name) {
        super(name);//调用父类的构造方法
    }

    //2.重写Thread类的run方法
    public void run(){
        //3.在run方法中输出线程的任务代码(线程要干的活儿)
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "子线程输出: " + i);
        }
}
}

输出结果:

result

线程安全:

认识线程安全问题

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

线程安全问题的场景:取钱:

小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元

代码:

测试类:

package com.carat.demosynchronized;

public class ThreadDemo {
    public static void main(String[] args) {
        //模拟线程安全问题
        //1.设计一个账户类:用于创建小明和小红的共同账号对象,存入10万
        Account acc = new Account(100000, "123456");

        //2.创建一个线程类,模拟小明和小红同时取款
        new DrawThread("小明", acc).start();
        new DrawThread("小红", acc).start();
    }
}

账户类:

package com.carat.demosynchronized;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private double money;//余额
    private String 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 + "取钱失败,余额不足");
        }
    }
}

线程类:

package com.carat.demosynchronized;

//取钱线程类
public class DrawThread extends Thread{

    private Account account;//记住线程对象要处理的账户对象

    public DrawThread(String name, Account account) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        //模拟小明 小红取钱
        account.drawMoney(100000);

    }
}

输出结果:
在这里插入图片描述

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

认识线程同步

线程同步的核心思想:

让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。

线程同步的常见方案:

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式一:同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
    synchronized(同步锁) {访问共享资源的核心代码}
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
  • 同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
  • 锁对象的使用规范:
  1. 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
  2. 对于静态方法static建议使用**字节码(类名.class)**对象作为锁对象。
改进代码

账户类:

package com.carat.demosynchronized;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private double money;//余额
    private String cardId;//卡号

    //小明和小红都到这里来取钱
    public void drawMoney(double 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 + "取钱失败,余额不足");
            }
        }
    }
}

输出结果:
在这里插入图片描述

方式二:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
    修饰符 synchronized 返回值类型 方法名称(形参列表) {操作共享资源的代码}
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
  • 同步方法底层原理:
  1. 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  2. 如果方法是实例方法:同步方法默认用this作为的锁对象。
  3. 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
同步代码块好还是同步方法好?
  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
  • 可读性:同步方法更好
改进代码

账户类:

package com.carat.demo1safe;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private double money;//余额
    private String 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锁对象。
    在这里插入图片描述
Lock的常用方法:

在这里插入图片描述

改进代码

账户类:

package com.carat.demolock;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private double money;//余额
    private String cardId;//卡号
    private final Lock lk = new ReentrantLock();//创建一个锁对象,final保护锁对象不能被修改

    //小明和小红都到这里来取钱
    public void drawMoney(double money) {
        //拿到当前是谁来取钱
        String name = Thread.currentThread().getName();
        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 + "取钱失败,余额不足");
            }
        } finally {
            lk.unlock();//解锁
        }
    }
}

小结:
  • 锁对象建议加上什么修饰?
    建议使用final修饰,防止被别人篡改
  • 释放锁的操作建议放到哪里?
    建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放

线程池(项目常用)

认识线程池

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

创建线程池(重点)

  • JDK 5.0起提供了代表线程池的接口:ExecutorService。
方式一:通过ThreadPoolExecutor创建线程池

在这里插入图片描述

在这里插入图片描述

处理Runnable任务

ExecutorService的常用方法:

在这里插入图片描述

代码:

测试类:

package com.carat.demo4ExecutorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        //目标:创建线程池对象来使用
        //1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2.使用线程池处理Runnable任务,看会不会服用线程
        Runnable task = new MyRunnable();//创建runnable任务
        pool.execute(task);//提交第一个任务 创建第一个线程 自动启动线程处理这个任务
        pool.execute(task);//提交第二个任务 创建第二个线程 自动启动线程处理这个任务
        pool.execute(task);//提交第三个任务 创建第三个线程 自动启动线程处理这个任务

        pool.execute(task);//复用线程

        //3.关闭线程池:一般不关闭线程池
        //pool.shutdown();//等所有任务执行完毕后再关闭线程池
        //pool.shutdownNow();//立即关闭,不管任务是否执行完毕
    }
}

线程任务类实现Runnable接口:

package com.carat.demo4ExecutorService;
//1.定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable{
    //2.重写run方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出: " + i);
        }
    }
}

输出结果:
有线程复用的情况.

线程池的注意事项:

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

任务拒绝策略

在这里插入图片描述

代码:

测试类:

package com.carat.demo4ExecutorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        //目标:创建线程池对象来使用
        //1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2.使用线程池处理Runnable任务,看会不会服用线程
        Runnable task = new MyRunnable();//创建runnable任务
        pool.execute(task);//提交第一个任务 创建第一个线程 自动启动线程处理这个任务
        pool.execute(task);//提交第二个任务 创建第二个线程 自动启动线程处理这个任务
        pool.execute(task);//提交第三个任务 创建第三个线程 自动启动线程处理这个任务
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);//到了临时线程的创建时机
        pool.execute(task);//到了临时线程的创建时机
        pool.execute(task);//到了任务拒绝策略,忙不过来


    }
}

线程任务类实现Runnable接口:

package com.carat.demo4ExecutorService;
//1.定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable{
    //2.重写run方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出: " + i);
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:
在这里插入图片描述

小结:

线程池如何处理Runnable任务?

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

处理Callable任务

代码:

测试类:

package com.carat.demo4ExecutorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo1 {
    public static void main(String[] args) {
        //目标:创建线程池对象来使用
        //1.使用线程池1的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2.使用线程池处理Callable任务,看会不会服用线程
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

线程实现类实现Callable接口:

package com.carat.demo4ExecutorService;

import java.util.concurrent.Callable;

//1.定义一个线程实现类实现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 {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "计算1~" + n + "的和为: " + sum;
    }
}

输出结果:
在这里插入图片描述

小结:

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

  • 使用ExecutorService的方法:
  • Future submit(Callable command)

方式二:通过Executors创建线程池

  • 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
    在这里插入图片描述

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

代码:
package com.carat.demo4ExecutorService;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExectorsDemo {
    public static void main(String[] args) {
        //目标:通过线程池工具类:Executors,调用其静态方法直接得到线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

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

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
小结:

Executors是否适合做大型互联网场景的线程池方案?

  • 不合适。
  • 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

并发、并行

简单说说多线程是怎么执行的?

  • 并发和并行同时进行的
  • 并发:CPU分时轮询的执行线程。
  • 并行:同一个时刻同时在执行。

原文地址:https://blog.csdn.net/qq_60396437/article/details/143689894

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!