赞
踩
古有愚公移山,愚公一人移山需耗时多年。但是如果有十个百个愚公一起移山,那么效率将大大提升。而多线程就是由单线程演变来的一种并发处理技术,可以使多个任务并行,提高工作效率。
注意: 此处所有的代码都是基于JDK 1.8
多线程从字面含义上来说就是多个线程。在实际应用当中,多线程可以用来解决一些并发问题,例如:抢红包、抢车票等功能都需要用到多线程实现。
继承 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
实现 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
实现 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
例如简单的抢票问题,开启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(); } }
结果其实跟预期料想的不同,会出现超卖和买到同一张票的情况
线程安全问题一般是由于原子性、可见性、有序性其中之一导致的,而我们的代码当中是由于多线程抢占 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
例如:有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:没抢到
首先,线程池本身是一个池化技术,是为了减少创建线程和线程销毁带来的资源损耗。能实现线程的复用,对线程进行统一管理。
线程池的创建: 自定义线程工厂,使用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
线程池的运行原理:线程池在创建的时候默认不会初始化线程,只有在任务提交的时候才会去创建线程。
当当前运行线程未达到核心线程数时,任务提交的时候会创建线程执行。
当运行线程数量已经达到核心线程数时,会将任务放到阻塞队列当中。
阻塞队列满了之后会创建新线程直到达到最大线程数。
若线程数已经达到最大线程数,则会执行拒绝策略。
多线程是为了帮助解决提高效率,减少响应时间的技术。同时也存在一定的线程安全问题,在使用的过程中需要注意原子性、可见性、和有序性。
一般真实项目中会使用线程池技术,对一些特定的任务设置特定的线程池,区分隔离开。例如一些导入导出的功能与正常的核心业务关联不大,且属于慢任务,此时即可开辟单独线程池执行相应的导入或者导出。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。