Java 实现多线程

Java 实现多线程

1、继承Thread类
优点:‌简单易用,‌可以直接操作线程。‌
访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
缺点:‌扩展性较差,‌继承Thread类后,‌不能再继承其他类。‌

public class TreadDemo extends Thread {
    //保证多线程中,原子性
    private AtomicInteger count;

    public TreadDemo() {

    }

    public TreadDemo(AtomicInteger count) {
        this.count = count;
    }

    @Override
    public void run() {
        System.out.println(this.getName() + " : "  + count + 1);
        System.out.println(Thread.currentThread().getName() + " : " + count + 1);
    }

    public static void main(String[] args) {
        TreadDemo treadDemo1 = new TreadDemo(new AtomicInteger(1));
        treadDemo1.setName("treadDemo1");
        treadDemo1.start();

        TreadDemo treadDemo2 = new TreadDemo(new AtomicInteger(2));
        treadDemo2.setName("treadDemo2");
        treadDemo2.start();
    }
}

2、实现Runnable接口
特点:run方法没有返回值。
run方法无法抛出异常。
优点:‌可以实现多个线程共享同一个目标对象,‌适合多个相同线程处理同一份资源的情况。‌
缺点:‌不能直接操作线程,‌需要借助Thread类或线程池来运行。‌
访问当前线程,则必须使用Thread.currentThread()方法

public class RunableDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
            try {
                Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread tread = new Thread(new RunableDemo());
        tread.setName("RunableDemo thread");
        tread.start();
    }
}

3、实现Callable接口
特点:call方法必须有返回值。
call方法可以抛出checked exception。
优点:‌可以获取线程的执行结果,‌支持返回值。‌
缺点:‌相比Runnable接口,‌实现相对复杂。‌
访问当前线程,则必须使用Thread.currentThread()方法

public class CallableDemo implements Callable {

    /**
     * 抛出异常,会导致剩下的线程中断不执行
     * @return
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 8; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
            Thread.sleep(1000); // 模拟耗时操作
            if (i == 6) {
                int a = 0;
                throw new Exception("Exception");
            }
        }
        return "任务完成";
    }

    public static void main(String[] args) {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask<String> futureTask = new FutureTask(callableDemo);
        Thread thread = new Thread(futureTask);
        tread.setName("CallableDemo thread");
        thread.start();

        try {
            // 获取Callable返回结果
            String result = futureTask.get();
            System.out.println(result + ".........");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4、线程池
优点:‌提高了线程的利用率,‌降低了资源消耗,‌提高了响应速度,线程可管理。‌
缺点:‌需要合理配置线程池参数,‌避免出现线程过多或过少的情况。‌

/**
 * 一个项目可以有多个线程池
 * 原因是:不同类型的任务可能有不同的执行时间、优先级、依赖关系等,如果放到同一个线程池中,就可能会出现以下几种情况:
 *
 * 如果一个任务执行时间过长,或者出现异常,那么它就会占用线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的吞吐量和响应时间。
 * 如果一个任务的优先级较低,或者不是很重要,那么它就可能抢占线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的可用性和正确性。
 * 如果一个任务依赖于另一个任务的结果,或者需要等待另一个任务的完成,那么它就可能造成线程池中的一个线程被阻塞,导致其他任务无法及时得到执行,甚至导致死锁的问题
 */
public class ThreadPoolDemo {
    /**
     * 线程池
     */
    private static ExecutorService executor = initDefaultExecutor();

    /**
     * 统一的获取线程池对象方法
     */
    public static ExecutorService getExecutor() {
        return executor;
    }

    private static final int DEFAULT_THREAD_SIZE = 5;
    private static final int DEFAULT_QUEUE_SIZE = 1024 * 3;

    private static ExecutorService initDefaultExecutor() {
        int cpuCout = Runtime.getRuntime().availableProcessors();
        System.out.println("CPU COUNT: " + cpuCout);
        return new ThreadPoolExecutor(DEFAULT_THREAD_SIZE, // 最小核心线程数
                DEFAULT_THREAD_SIZE,// 最大线程数,当队列满时,能创建的最大线程数
                300, TimeUnit.SECONDS,// 空闲线程超过核心线程时,回收该线程的最大等待时间
                new ArrayBlockingQueue<>(DEFAULT_QUEUE_SIZE),// 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
                new CustomizableThreadFactory("ThreadPoolDemo"),// 自定义线程名
                new ThreadPoolExecutor.CallerRunsPolicy());// 线程执行的拒绝策略
    }

}

最核心的三个参数:
corePoolSize:核心线程数(能即时处理的线程)
CPU密集型任务(线程池数 = CPU核心数 N + 1),太多了就阻塞等待了,但是最好也不要设置为N,设置为 N + 1 的目的是:防止线程偶发的缺页中断,或者其它原因导致的任务暂停带来的影响。(类似内存中进行排序就是CPU密集型任务)
I/O密集型任务(线程池数 = CPU 核心数 N * 2),I/O并不会消耗过多CPU资源,所以设置为2N是没有问题的。(类似磁盘IO、网络传输、文件读取就是IO密集型任务)
maximumPoolSize:最大线程数(当线程数超过核心线程数,并且阻塞队列也满了,就会扩充线程池大小为 maximumPoolSize,如果线程池的数量已经达到了最大线程数,又来了一个线程,就会使用拒绝策略来阻止它)
workQueue: 队列可以用来保存暂时无法处理的任务(线程数如果超过核心线程了,新来的任务就会被放入队列)

其它常见参数:
keepAliveTime:线程数量大于 corePoolSize时,没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待时间超过了 keepAliveTime 才会被回收销毁;
unit:keepAliveTime 参数的时间单位
threadFactory:executor 创建线程时用到,使用默认即可
handler:拒绝策略(阻塞队列满了,线程池扩容了扩大为最大线程数了,且线程已经占满了线程池,再来一个线程,就会使用拒绝策略进行拒绝)

拒绝策略:
AbortPolicy:抛异常
CallerRunsPolicy:把当前运行 excute() 方法的线程拿来处理当前任务(但是会影响整体性能)
DiscardPolicy:不处理任务直接丢弃
DiscradOldestPolicy:丢弃最早的未处理的任务请求。

线程池创建的两种方式
通过 ThreadPoolExecutor 构造函数创建(推荐)
通过 Executor 框架的工具类 Executors 来创建(不推荐)
Executors 创建线程池? 它内部的实现会导致一些问题:
FixedThreadPool 和 SingleThreadExecutor:使用无界的阻塞队列LinkedBlockingQueue 作为任务队列(最大数量是Integer.MAX_VALUE),可能导致大量堆积的任务,导致OOM;
CachedThreadPool:使用同步队列,每次线程不够就新创建一个,而不是放入任务队列中,这也可能导致创建大量的线程,导致OOM;
ScheduledThreadPool 和 SingleThreadScheduledExecutor:使用无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量请求,导致OOM。

线程池队列类型
无界队列
代表实现:LinkedBlockingQueue(当不指定容量或指定容量为Integer.MAX_VALUE时)
特点:队列容量理论上是无限的,但受限于JVM的内存。当内存耗尽时,会抛出OutOfMemoryError。
适用场景:适用于任务量非常大,且任务执行时间较长,生产者生成任务的速度不会超过消费者处理任务的速度的场景。但需注意内存溢出的风险。
有界队列
代表实现:ArrayBlockingQueue、LinkedBlockingQueue(指定具体容量)
特点:队列有一个固定的容量限制,当队列满时,尝试添加新任务的操作会被阻塞,直到队列中有空间可用。
适用场景:适用于需要控制任务数量,防止资源耗尽的场景。通过调整队列大小和线程池大小,可以灵活控制任务的并发执行。
直接提交队列(SynchronousQueue)同步队列
特点:这种队列实际上并不存储任何元素,每个插入操作必须等待另一个线程的相应删除操作(也就是:要添加新任务必须得有空闲的线程才能添加),反之亦然。即一个线程尝试向队列中添加元素时,必须有另一个线程正在等待接收这个元素。
适用场景:适用于任务处理时间较短,且生产者和消费者速度大致匹配的场景。它可以有效减少任务在队列中的等待时间,提高系统的响应速度。
优先级队列
代表实现:PriorityBlockingQueue
特点:队列中的元素会根据其优先级进行排序,优先级高的元素会先被取出执行。
适用场景:适用于需要按照任务优先级顺序执行的场景。通过调整任务的优先级,可以确保重要任务得到优先处理。

在选择线程池中的队列时,需要根据具体的应用场景和需求来决定:
1、任务类型和特点:
如果任务处理时间较长,且任务量不确定,可以选择无界队列;如果任务量较大且需要控制并发数,可以选择有界队列。
如果低耗时、高并发的场景,适合配置较小的核心线程数和较大的队列;如果高耗时、高并发的场景,适合配置较大的核 心线程数和较小的队列;队列大小设置合理,就不需要走最大线程数造成额外开销,所以配置线程池的最佳方式是核心线程数搭配队列大小;线程池拒绝策略尽量以默认为主;
2、系统资源:考虑系统的内存和CPU资源。无界队列虽然可以处理大量任务,但存在内存溢出的风险;有界队列可以避免内存溢出,但需要合理设置队列大小和线程池大小。
3、性能要求:如果要求系统响应速度快,且任务处理时间较短,可以选择直接提交队列或优先级队列。
4、任务优先级:如果任务有明确的优先级要求,可以选择优先级队列。
线程池中的队列选择需要根据实际情况进行权衡和决策。在设计和配置线程池时,应充分考虑任务类型、系统资源和性能要求等因素。

execute() vs submit()
execute():没有返回值
submit():返回一个 Future 对象,可以异步监控任务完成情况、取消任务、获取任务的最终结果;可以通过 get() 方法获取返回值,get() 方法会阻塞直到任务完成,使用 get(long timeout, TimeUnit unit),如果timeout时间内没有完成,就会抛出异常

shutdown() vs shutdownNow()
shutdown():关闭线程池,状态变成 SHUTDOWN,不会再接受新任务,队列的任务会执行完毕;
shutdownNow():关闭线程池,状态变成 STOP,终止所有任务,停止处理排队的任务返回正在等待执行的List

isTerminated() vs isShutdown()
isShutdown: 调用 shutdown()方法后返回 true
isTerminated():调用shutdown() 方法后,并且所有提交的任务完成后返回 true

sleep() vs wait()
语法使用不同
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 的异常
sleep 可以单独使用,无需配合 synchronized 一起使用。
所属类不同
wait 方法属于 Object 类的方法
sleep 属于 Thread 类的方法
唤醒方式不同
sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。
wait 方法可以不传递任何参数,不传递任何参数时表示永久休眠,直到另一个线程调用了 notify 或 notifyAll 之后,休眠的线程才能被唤醒。即sleep 方法具有主动唤醒功能,而不传递任何参数的 wait 方法只能被动的被唤醒。
释放锁资源不同
wait 方法会主动的释放锁。
sleep 方法不会释放锁。
线程进入状态不同
sleep 方法线程会进入 TIMED_WAITING 有时限等待状态。
wait 方法,线程会进入 WAITING 无时限等待状态。

get() vs join()
异常处理:get()方法抛出的是ExecutionException,而join()方法抛出的是CompletionException。CompletionException是ExecutionException的子类,它提供了更多的信息,例如原始异常的类型和消息。
中断处理:get()方法在被中断时会抛出InterruptedException,而join()方法不会。如果你需要处理中断,应该使用get()方法。
使用场景:join()方法通常用于CompletableFuture链式调用中,因为它抛出的CompletionException可以被链式调用中的exceptionally方法捕获和处理。而get()方法通常用于需要捕获中断异常的场景。

小知识
耗时任务会占用线程池,导致线程池崩溃或者程序假死
耗时任务(网络请求、文件读写)可以采用 CompletableFuture 等其它异步操作方式处理,避免线程池的线程被阻塞。
线程池 和 ThreadLocal 共用 导致的脏数据(因为线程可以处理不同任务,导致 ThreadLocal 变量被重用,导致 ThreadLocal 并不是正确的值,变成脏数据),解决方案:使用 TransmittableThreadLocal,它在使用线程池会池化复用线程的执行组件的情况下,提供 ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。

使用场景:
1:快速响应用户请求
用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优惠、库存、基础信息等,站在用户体验的角度,希望商详页的响应时间越短越好,此时可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总rt。这种线程池用途追求的是最快响应速度,所以可以考虑不设置队列去缓冲并发任务,而是尽可能设置更大的corePoolSize和maxPoolSize;
2:快速处理批量任务
项目中在对接渠道同步商品供给时,需要查询大量的商品数据并同步给渠道,此时可以考虑使用线程池快速处理批量任务。这种线程池用途关注的是如何使用有限的机器资源,尽可能地在单位时间内处理更多的任务,提升系统吞吐量,所以需要设置阻塞队列缓冲任务,并根据任务场景调整合适的corePoolSize;
3、尽量在可以延迟且不是特别重要的场景下使用
4、线程池不要混用,特定业务记得隔离

public class PersonService {

    private Random random = new Random();
    private String[] names = {"黄某人", "负债程序猿", "谭sir", "郭德纲", "蔡徐鸡", "蔡徐老母鸡", "李狗蛋", "铁蛋", "赵铁柱"};
    private String[] addrs = {"二仙桥", "成华大道", "春熙路", "锦里", "宽窄巷子", "双子塔", "天府大道", "软件园", "熊猫大道", "交子大道"};
    private String[] companys = {"京东", "腾讯", "百度", "小米", "米哈游", "网易", "字节跳动", "美团", "蚂蚁", "完美世界"};

    /**
     * 1、建造者模式的意义
     * 在构建时,对外部隐藏内部细节,将构建过程和部件都可以进行扩展,使他们的耦合程度降低。
     * 
     * 2、使用场景
     * (1)相同的方法,执行不同的顺序,产生不同的事件结果
     * (2)多个部件或零件,都可以装配到一个对象中,但是产生的结果又不一样
     * (3)产品类非常复杂,或者产品类的调用顺序不同产生了不同的作用
     * (4)当初始化一个对象特别复杂,参数很多,很多参数都有默认值时。
     *
     * @return
     */
    //@Builder  建造者模式
    private Person getPerson() {
        Person person = Person.builder()
                .name(names[random.nextInt(names.length)])
                .phone(18800000000L + random.nextInt(88888888))
                .salary(new BigDecimal(random.nextInt(99999)))
                .company(companys[random.nextInt(companys.length)])
                .ifSingle(random.nextInt(2))
                .sex(random.nextInt(2))
                .address("四川省成都市" + addrs[random.nextInt(addrs.length)])
                .createUser(names[random.nextInt(names.length)]).build();
        return person;
    }

    public List<Person> getPersonList(int count) {
        List<Person> persons = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            persons.add(getPerson());
        }
        return persons;
    }

}
/**
 * CompletableFuture多任务异步,获取返回值,汇总结果,返回前端,减少RT
 * 有几个方法比较关键: 
 * supplyAsync(): 异步处理任务,有返回值
 * whenComplete():任务完成后触发,该方法有返回值。两个参数,第一个参数是任务的返回值,第二个参数是异常。
 * allOf():就是所有任务都完成时触发。可以配合get()一起使用
 */
public class BusiDemo {
    Logger logger = LoggerFactory.getLogger(BusiDemo.class);
    public static ExecutorService pool = ThreadPoolDemo.getExecutor();

    private static PersonService personService = new PersonService();

    /**
     * 获取CPU数量
     *
     * @param args
     */
    public static void main(String[] args) {
        new BusiDemo().getPersonList();
    }

    public void getPersonList() {
        //线程安全的list,适合写多读少的场景
        List<Person> resultList = Collections.synchronizedList(new ArrayList<>(60));
        CompletableFuture<List<Person>> completableFuture1 = CompletableFuture.supplyAsync(
                () -> personService.getPersonList(2), pool)
                .whenComplete((result, throwable) -> {
                    //任务完成时执行。用list存放任务的返回值
                    if (result != null) {
                        //int a = 1/0;
                        resultList.addAll(result);
                    }
                    //触发异常
                    if (throwable != null) {
                        logger.error("completableFuture1  error:{}", throwable);
                    }
                });

        CompletableFuture<List<Person>> completableFuture2 = CompletableFuture.supplyAsync(
                () -> personService.getPersonList(30), pool)
                .whenComplete((result, throwable) -> {
                    if (result != null) {
                        resultList.addAll(result);
                    }
                    if (throwable != null) {
                        logger.error("completableFuture2  error:{}", throwable);
                    }

                });

        List<CompletableFuture<List<Person>>> futureList = new ArrayList<>();
        futureList.add(completableFuture1);
        futureList.add(completableFuture2);

        try {
            //多个任务
            CompletableFuture[] futureArray = futureList.toArray(new CompletableFuture[0]);
            //将多个任务,汇总成一个任务,总共耗时不超时2秒
            CompletableFuture.allOf(futureArray).get(10, TimeUnit.SECONDS);
        } catch (Exception e) {
            logger.error("CompletableFuture.allOf Exception error.", e);
        }
        List<Person> list = new ArrayList<>(resultList);

        list.forEach(System.out::println);
        System.out.println("list size: " + list.size());

        //pool.shutdown();
    }

}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Person {
    private Long id;
    private String name;//姓名
    private Long phone;//电话
    private BigDecimal salary;//薪水
    private String company;//公司
    private Integer ifSingle;//是否单身
    private Integer sex;//性别
    private String address;//住址
    private LocalDateTime createTime;
    private String createUser;
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值