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方法,这种方式可以获取到多线程执行结束后的返回结果。通过 FutureTask 的 get 方法阻塞获取线程执行结果。
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
线程池的运行原理:线程池在创建的时候默认不会初始化线程,只有在任务提交的时候才会去创建线程。
当当前运行线程未达到核心线程数时,任务提交的时候会创建线程执行。
当运行线程数量已经达到核心线程数时,会将任务放到阻塞队列当中。
阻塞队列满了之后会创建新线程直到达到最大线程数。
若线程数已经达到最大线程数,则会执行拒绝策略。
总结
多线程是为了帮助解决提高效率,减少响应时间的技术。同时也存在一定的线程安全问题,在使用的过程中需要注意原子性、可见性、和有序性。
一般真实项目中会使用线程池技术,对一些特定的任务设置特定的线程池,区分隔离开。例如一些导入导出的功能与正常的核心业务关联不大,且属于慢任务,此时即可开辟单独线程池执行相应的导入或者导出。