【Java多线程的使用详解】


前言

古有愚公移山,愚公一人移山需耗时多年。但是如果有十个百个愚公一起移山,那么效率将大大提升。而多线程就是由单线程演变来的一种并发处理技术,可以使多个任务并行,提高工作效率。


注意: 此处所有的代码都是基于JDK 1.8

一、多线程是什么

多线程从字面含义上来说就是多个线程。在实际应用当中,多线程可以用来解决一些并发问题,例如:抢红包、抢车票等功能都需要用到多线程实现。

二、使用方法

1.继承Thread类

继承 Thread 类需要重写 run 方法

package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:54
 */
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread is running");    }
}


package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");


        MyThread myThread = new MyThread();
        // 启动线程执行
        myThread.start();
    }
}

输出结果: 可以观察到执行的线程 一个是主线程main,而另一个是我们继承Thread类创建的线程

main: ThreadTestDemo is running
Thread-0: MyThread is running

2.实现Runnable接口

实现 Runnable 接口,需要重写run方法

package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:54
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
		System.out.println(Thread.currentThread().getName() + ": MyRunnable.run");
    }
}


package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");
		
		// 启动线程
        new Thread(new MyRunnable()).start();
    }
}

输出结果:

main: ThreadTestDemo is running
Thread-0: MyRunnable.run

3.实现Callable接口

实现 Callable 接口需要实现call方法,这种方式可以获取到多线程执行结束后的返回结果。通过 FutureTaskget 方法阻塞获取线程执行结果。

package com.curtis.demo.use;

import java.util.concurrent.Callable;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName() + ": myCallable result";
    }
}


package com.curtis.demo.use;

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

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        // 启动线程
        new Thread(futureTask).start();
        try {
        	// 获取任务结果
            System.out.println(futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println(Arrays.toString(e.getStackTrace()));
        }
    }
}

执行结果:

main: ThreadTestDemo is running
Thread-0: myCallable result

4.线程安全问题

例如简单的抢票问题,开启3个线程抢100张票,不能超卖或者买到同一张票

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:56
 */
public class SyncTaskDemo implements Runnable {

    private int ticket = 1;

    @Override
    public void run() {
        while (true) {
            if (ticket > 100) {
                break;
            }

            System.out.println(Thread.currentThread().getName() + ": " + ticket);
            ticket++;
        }
    }
}

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:58
 */
public class SyncThreadTestDemo {

    public static void main(String[] args) {

        SyncTaskDemo syncTaskDemo = new SyncTaskDemo();

        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
    }
}

结果其实跟预期料想的不同,会出现超卖和买到同一张票的情况
在这里插入图片描述

5.线程安全解决

线程安全问题一般是由于原子性、可见性、有序性其中之一导致的,而我们的代码当中是由于多线程抢占 ticket,并且对于 ticket++ 的操作其实并不是原子性的。这个时候我们需要对于抢票的动作进行加锁,确保同一时间内,仅有一个线程在抢票。

通过 Synchronized 关键字解决

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:56
 */
public class SyncTaskDemo implements Runnable {

    private int ticket = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 100) {
                    break;
                }

                System.out.println(Thread.currentThread().getName() + ": " + ticket);
                ticket++;
            }
        }
    }
}

由于线程是通过一个 Runnable 实例创建的,因此锁粒度可以直接锁在 this 也就是 Runnable 实例身上。
如果线程是通过继承 Thread 创建的,此时创建的三个线程需要共享资源 ticket,需要ticket 使用 static 修饰,否则会出现三个线程分别在卖一百张票的情况。
此时再次观察结果:已经正常售卖。

Thread-2: 91
Thread-2: 92
Thread-2: 93
Thread-2: 94
Thread-2: 95
Thread-2: 96
Thread-2: 97
Thread-2: 98
Thread-2: 99
Thread-2: 100

Java还提供了其他锁,可以更加灵活控制加锁和解锁,例如:ReentrantLock

package com.curtis.demo.sync;

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

/**
 * @author Curtis
 * @since 2024-04-18 21:03
 */
public class LockTaskDemo implements Runnable {

    private int ticket = 100;

    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                reentrantLock.lock();
                if (ticket <= 0) {
                    break;
                }

                System.out.println(Thread.currentThread().getName() + ": ticket-" + ticket);
                ticket--;
            } catch (Exception e) {
                System.out.println(Arrays.toString(e.getStackTrace()));
            } finally {
                reentrantLock.unlock();
            }
        }
    }
}

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:58
 */
public class SyncThreadTestDemo {

    public static void main(String[] args) {

        LockTaskDemo syncTaskDemo = new LockTaskDemo();

        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
    }
}

需要注意的是:unlock方法需要放在finally代码块中,确保程序不会出现锁死的情况,或者可以使用具有超时时间的加锁
输出结果:

Thread-2: ticket-10
Thread-2: ticket-9
Thread-2: ticket-8
Thread-2: ticket-7
Thread-2: ticket-6
Thread-2: ticket-5
Thread-2: ticket-4
Thread-2: ticket-3
Thread-2: ticket-2
Thread-2: ticket-1

6.多线程应用实例

例如:有5个人抢100块的红包,三个人可以抢到,其余两个人没抢到

题解:此时需要设置抢红包的数量,对其进行扣减。并且红包金额需要进行扣减,不超过100。

package com.curtis.demo.test;

/**
 * @author Curtis
 * @since 2024-04-18 22:17
 */
public class RedEnvelope implements Runnable {

	// 总金额
    private int totalMoney = 100;
    // 总抢次数
    private int totalCount = 5;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
            	// 次数为0结束程序
                if (totalCount <= 0) {
                    break;
                }
				// 剩余两次都为没抢到
                if (totalCount <= 2) {
                    System.out.println(Thread.currentThread().getName() + ":没抢到");
                    totalCount--;
                    break;
                }
				// 随机金额每次不大于40, 这里只是为了保证三个红包都有金额, 比较简单, 可以自行修改
                int money = (int) (Math.random() * 40);
                if (totalCount != 3) {
                    System.out.println(Thread.currentThread().getName() + ":抢到了" + money);
                } else {
                	// 第三次的金额为剩余的金额
                    System.out.println(Thread.currentThread().getName() + ":抢到了" + totalMoney);
                }
                totalMoney -= money;
                totalCount--;
            }
        }
    }
}

package com.curtis.demo.test;

/**
 * @author Curtis
 * @since 2024-04-18 22:23
 */
public class RedEnvelopeTest {

    public static void main(String[] args) {
        RedEnvelope redEnvelope = new RedEnvelope();

        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
    }
}

执行结果为:

Thread-0:抢到了23
Thread-0:抢到了30
Thread-0:抢到了47
Thread-0:没抢到
Thread-4:没抢到

6.线程池的使用

首先,线程池本身是一个池化技术,是为了减少创建线程和线程销毁带来的资源损耗。能实现线程的复用,对线程进行统一管理。

线程池的创建: 自定义线程工厂,使用ThreadPoolExecutor创建。或者直接使用Executors创建。

package com.curtis.demo.pool;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Curtis
 * @since 2024-04-19 21:17
 */
public class MyThreadPoolFactory implements ThreadFactory {

    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public MyThreadPoolFactory(String poolName) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = poolName + poolNumber.getAndIncrement() + "-thread-";
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

package com.curtis.demo.pool;

import java.util.concurrent.*;

/**
 * @author Curtis
 * @since 2024-04-19 21:07
 */
public class MyThreadPoolTest {

    public static void main(String[] args) {

		// 工具类直接生成
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + ": execute Executors.newSingleThreadExecutor()");
            return "Executors.newSingleThreadExecutor()";
        }, executorService);
        executorService.shutdown();
		
		// 高级用法,自定义
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                60, // 线程存活时间
                TimeUnit.SECONDS, // 线程存活时间单位
                new LinkedBlockingQueue<>(), // 阻塞队列
                new MyThreadPoolFactory("my-thread-pool"), // 线程工场
                new ThreadPoolExecutor.AbortPolicy()); // 拒绝策略

        CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
        }, threadPoolExecutor);
        threadPoolExecutor.shutdown();
    }
}

注意:线程池使用完毕之后需要关闭
执行结果:可以看到一个是自定义的线程名称,一个是默认的线程池名称

// executors 默认创建线程池线程名称
pool-1-thread-1: execute Executors.newSingleThreadExecutor()
// 自定义线程池线程名称
my-thread-pool-1-thread-1

线程池的运行原理:线程池在创建的时候默认不会初始化线程,只有在任务提交的时候才会去创建线程。
在这里插入图片描述
当当前运行线程未达到核心线程数时,任务提交的时候会创建线程执行。
在这里插入图片描述
当运行线程数量已经达到核心线程数时,会将任务放到阻塞队列当中。
在这里插入图片描述
阻塞队列满了之后会创建新线程直到达到最大线程数。
在这里插入图片描述
若线程数已经达到最大线程数,则会执行拒绝策略。
在这里插入图片描述


总结

多线程是为了帮助解决提高效率,减少响应时间的技术。同时也存在一定的线程安全问题,在使用的过程中需要注意原子性、可见性、和有序性。
一般真实项目中会使用线程池技术,对一些特定的任务设置特定的线程池,区分隔离开。例如一些导入导出的功能与正常的核心业务关联不大,且属于慢任务,此时即可开辟单独线程池执行相应的导入或者导出。

  • 22
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
`newFixedThreadPool` 是 Java 中线程池的一种实现方式,它可以创建一个固定大小的线程池,并且只有在池中的所有线程都处于忙碌状态时,才会将新的任务加入到队列中等待执行。 以下是 `newFixedThreadPool` 的详细解释: 1. 创建一个固定大小的线程池,该线程池中的线程数量是固定的,一旦创建便无法更改。这意味着,如果池中的所有线程都处于忙碌状态并且有更多的任务需要执行,那么这些任务将会被放置在一个队列中,等待空闲线程的出现。 2. 线程池中的所有线程都是可重用的,这意味着在执行完任务之后,线程将返回线程池并等待下一个任务的到来。 3. 线程池中的所有线程都是后台线程,这意味着它们不会阻止应用程序的关闭。 4. 线程池中的任务可以是任何实现了 `Runnable` 接口或 `Callable` 接口的对象。使用 `Callable` 接口可以允许任务返回一个值,并且可以抛出异常。 5. 线程池中的任务将按照加入队列的顺序进行执行。 6. `newFixedThreadPool` 的底层实现是一个无界的工作队列和一个固定数量的线程池。 使用 `newFixedThreadPool` 可以有效地控制线程的数量,从而避免创建过多的线程而导致系统的资源浪费和性能下降。但是,如果任务的数量过多,而线程池中的线程数量过少,那么仍然会出现任务排队等待的情况。因此,在使用 `newFixedThreadPool` 时,需要根据实际情况来确定线程池的大小。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值