线程
线程是什么(what)
线程是操作系统调度的最小单位,一个进程里面可以有多个线程交替执行,每个线程之间是相互独立的,每个线程都有自己的内存区域、局部变量、计数器、堆栈等等同时也可以共享进程里面的共享资源。
为什么使用线程(why)
主要是因为多个线程在多核处理器里面是可以同时执行的,在单核里面是通过调度策略来实现线程交替执行的。
- 提高CPU利用率:多线程可以充分利用多核CPU,实现并行执行,提高CPU的利用率。
- 提高响应速率:多线程可以使任务在同一时间段内并行执行,从而降低响应时间,提高系统的响应性能。
- 提高并发处理能力:多线程可以同时处理多个任务,提高系统的并发处理能力。
什么时候使用线程(when)
线程使用一般是指的多线程使用,以下场景适用于多线程:
- 并发场景需要使用多线程来实现并发处理的能力
- 异步编程去做其他事情,防止阻塞主线程。
- 响应速率要求高的场景需要通过多线程来降低响应时间
- 多任务场景
如何使用线程(how)
在Java中使用线程有以下几种方式:
线程常用方法:
// 普通方法
run(); // 线程中的执行逻辑
start(); // 启动线程
getName()/setName(String name); // 设置线程的名称
interrupt(); // 中断线程
isInterrupted(); // 判断线程是否中断
setPriority()/getPriority(); // 设置/获取线程的优先级
setDaemon()/isDaemon(); // 设置和判断是否是守护线程
join(); // 使当前线程等待被调用的线程执行完毕
notifyAll()/notify(); // 唤醒当前等待池的所有/随机线程
isAlive(); // 用于判断线程是否还活着,即线程是否已经启动并且尚未终止。
// 静态方法
sleep(long time); // 休眠一段时间
yield(); // 向调度程序提示当前线程愿意放弃其当前使用的处理器
currentThread();// 返回当前正在执行的线程引用
// 其他
wait(); // 使当前线程进入对象的等待池,这个是object的方法
wait(long time) // 使当前线程进入对象的等待池定时等待,这个是object的方法
-
继承Thread类重写run方法:
package thread.myThread; /** * @Classname ExtendsThread * @Description TODO * @Version 1.0.0 * @Date 2023/7/2 21:48 * @Created by wlh12 */ public class ExtendsThread extends Thread{ @Override public void run() { // 实现该线程的逻辑 for (int i = 0; i < 20; i++) { System.out.println("当前线程:"+this.getName()+",i="+i); try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
package thread; import thread.myThread.ExtendsThread; /** * @Classname QuikStart * @Description TODO * @Version 1.0.0 * @Date 2023/7/2 21:47 * @Created by wlh12 */ public class QuikStart { public static void main(String[] args) { // 继承thread类的方式 ExtendsThread extendsThread = new ExtendsThread(); extendsThread.start(); System.out.println("这是主线程"); } }
-
实现Runnable接口
package thread.myThread; /** * @Classname ImplementsRunnable * @Description TODO * @Version 1.0.0 * @Date 2023/7/3 9:44 * @Created by wlh12 */ public class ImplementsRunnable implements Runnable{ @Override public void run() { // 实现该线程的逻辑 for (int i = 0; i < 20; i++) { System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i); try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
使用:
package thread; import thread.myThread.ExtendsThread; import thread.myThread.ImplementsRunnable; /** * @Classname QuikStart * @Description TODO * @Version 1.0.0 * @Date 2023/7/2 21:47 * @Created by wlh12 */ public class QuikStart { public static void main(String[] args) { // 实现runnable接口 Thread thread = new Thread(new ImplementsRunnable()); thread.start(); } }
-
实现Callable接口
Callable是有返回值的线程,这也是和runnable的最主要的一个区别,可以得到线程执行的结果。
package thread.myThread; import java.util.concurrent.Callable; /** * @Classname ImplementsCallable * @Description TODO * @Version 1.0.0 * @Date 2023/7/3 9:47 * @Created by wlh12 */ public class ImplementsCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; // 实现该线程的逻辑 for (int i = 0; i < 20; i++) { System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i); sum += i; try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } return sum; } }
package thread; import thread.myThread.ExtendsThread; import thread.myThread.ImplementsCallable; import thread.myThread.ImplementsRunnable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @Classname QuikStart * @Description TODO * @Version 1.0.0 * @Date 2023/7/2 21:47 * @Created by wlh12 */ public class QuikStart { public static void main(String[] args) throws ExecutionException, InterruptedException { // 实现callable ImplementsCallable implementsCallable = new ImplementsCallable(); FutureTask<Integer> futureTask = new FutureTask<>(implementsCallable); Thread callThread = new Thread(futureTask); callThread.start(); // 输出线程的执行结果 System.out.println(futureTask.get()); } }
-
线程池创建
-
线程池是什么:
程池是一种管理和复用线程的机制。它由线程池管理器、工作队列和一组可重用的线程组成。线程池的主要目的是提高线程的执行效率和资源利用率。 线程池管理器负责创建、管理和销毁线程。工作队列用来存储需要执行的任务。可重用的线程会从线程池中获取任务并执行,执行完任务后会返回线程池,以供其他任务使用。
-
为什么使用线程池:
1、降低资源消耗:线程的创建和销毁是一项开销较大的操作,使用线程池可以避免频繁地创建和销毁线程,从而减少资源消耗。
2、提高系统稳定性:线程池可以控制线程的数量,防止无休止的创建线程导致系统资源匮乏,导致系统崩溃
3、提供任务排队和调度:线程池使用工作队列存储待执行的任务,可以根据队列的策略进行排队和调度,灵活控制任务的执行顺序和优先级。
4、统一管理线程:线程池提供了对线程的统一管理,包括线程的创建、销毁、异常处理等,简化了线程的管理和维护工作。
-
**什么时候使用线程池:**需要频繁使用多线程的场景下使用线程池,例如:大量临时并发任务。
-
线程池七大参数:
public ThreadPoolExecutor(int corePoolSize,// 核心线程数 int maximumPoolSize, // 最大线程数 long keepAliveTime, // 非核心线程的最大空闲存活时间 TimeUnit unit,// 前面的时间的单位 BlockingQueue<Runnable> workQueue,// 任务队列/阻塞队列 ThreadFactory threadFactory,// 线程工厂 RejectedExecutionHandler handler)//线程池拒绝策略
-
常用的线程池:
-
CachedThreadPool() // 缓存线程池,可以根据实际线程需求动态创建和销毁线程
-
FixedThreadPool(int nThreads) // 固定线程数量的线程
-
SingleThreadExecutor(); // 单线程线程池,只有一个线程的线程池
-
ScheduledThreadPool(int corePoolSize); // 定时/延时任务线程池
-
-
线程池常用方法:
execute(Runnable task)
:向线程池提交一个任务,该任务会由线程池中的一个线程执行。该方法不会返回任何结果,适用于不需要获取任务执行结果的场景。submit(Callable<T> task)
:向线程池提交一个有返回结果的任务(Callable),并返回一个 Future 对象。通过 Future 对象可以获取任务执行的结果。shutdown()
:平缓地关闭线程池,线程池不再接受新的任务提交,但会等待已提交的任务完成执行。已提交的任务和正在执行的任务会继续执行。shutdownNow()
:立即关闭线程池,线程池停止执行所有正在执行的任务,并尝试中断所有线程。返回尚未执行的任务列表。isShutdown()
:判断线程池是否已经调用了 shutdown() 方法。isTerminated()
:判断线程池中的所有任务是否已经完成执行。awaitTermination(long timeout, TimeUnit unit)
:等待线程池中的所有任务完成执行,或者超过指定的超时时间。
-
使用案例:
package thread; import thread.myThread.ExtendsThread; import thread.myThread.ImplementsCallable; import thread.myThread.ImplementsRunnable; import java.util.concurrent.*; /** * @Classname QuikStart * @Description TODO * @Version 1.0.0 * @Date 2023/7/2 21:47 * @Created by wlh12 */ public class QuikStart { public static void main(String[] args) throws ExecutionException, InterruptedException { // 线程池 // 动态的全部为非核心线程 ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 固定的全部都是核心线程,例如10,就是10个核心线程 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); // 一个核心线程的单线程线程池 ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); // 定时线程池,其中参数为核心线程数量,非核心线程数量为integer的最大值,也就是说这个也能在需要的时候动态的创建销毁线程 ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(20); //submit和execute方法都是一样的,就不一一使用了 cachedThreadPool.execute(() -> { // 实现该线程的逻辑 System.out.println("cachedThreadPool"); for (int i = 0; i < 20; i++) { System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i); try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(cachedThreadPool.isShutdown()?"已关闭":"未关闭"); Future<?> future = cachedThreadPool.submit(() -> { // 实现该线程的逻辑 int sum = 0; System.out.println("cachedThreadPool"); for (int i = 0; i < 20; i++) { System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i); sum+=i; try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } } return sum; }); System.out.println(future.get()); System.out.println(cachedThreadPool.isShutdown()?"已关闭":"未关闭"); cachedThreadPool.shutdown(); System.out.println(cachedThreadPool.isShutdown()?"已关闭":"未关闭"); } }
-
-
线程池常见问题:
- 初始化线程池时线程数的选择: 如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。 如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。
- 线程池都有哪几种工作队列(4种):
- ArrayBlockingQueue:基于数组的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。
- LikedBlockingQueue:基于链表的无界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:有优先级的无界阻塞队列
- 线程池的拒绝策略:(4种)
- 直接返回:哪个线程提交的任务就返回给哪个线程
- 抛异常:抛出一个RejectedExecutionException异常
- 不做处理:不做任务处理
- 与老线程去进行竞争
- 线程饥饿: 线程饥饿指的是线程池中的某些线程长时间处于空闲状态,无法获取到任务执行。这可能是由于线程优先级设置不当或线程调度算法问题导致某些线程无法获得执行的机会。解决线程饥饿问题的方法合理分配线程优先级和合理的实现线程调度
- 任务堆积: 任务堆积指的是线程池中的任务数量超过了工作队列的容量,导致任务无法及时处理。这可能是由于任务的生产速度大于任务的处理速度,或者线程池的配置不合理,工作队列容量过小。解决任务堆积问题的方法是适当增加工作队列的容量,或者调整任务的生产速度。
- 资源耗尽: 线程池中的线程数量过多,可能会消耗大量的系统资源,包括内存和CPU。这可能导致系统负荷过重,性能下降甚至系统崩溃。解决资源耗尽问题的方法是适当控制线程池的线程数量,根据系统的硬件资源和性能要求进行调整。
- 线程泄漏: 线程泄漏指的是线程在执行完任务后没有正确地释放,导致线程池中的线程数量不断增加。这可能是由于任务执行过程中发生了异常,未正确地释放线程。解决线程泄漏问题的方法是确保任务的执行过程中进行了异常处理,并在任务执行完毕后正确地释放线程。
- 线程安全问题: 线程池中的多个线程同时执行任务,可能会存在线程安全问题,如竞态条件、数据竞争等。解决线程安全问题的方法是使用合适的同步机制,如锁、信号量等,来保护共享资源的访问。
JUC其他的相关使用的类
了解AQS(Abstract Queued Synchronized)
AQS(AbstractQueuedSynchronizer)是 Java 并发包中一个非常重要的基础类,它提供了实现各种同步器的框架和机制。AQS 内部使用一个先进先出的双向链表来管理等待获取资源的线程,并通过状态变量来表示资源的可用性。
-
实现原理:AQS(AbstractQueuedSynchronizer)是一个用于实现锁和同步器的基础框架。它使用一个双向链表来维护等待线程的队列,并通过内置的同步状态来控制线程的获取和释放。
AQS 的实现原理可以归纳为以下几个关键点:
- 同步状态(State):AQS 内部维护了一个同步状态,通过
getState()
和setState()
方法来获取和修改该状态。同步状态的具体含义和用途由派生类自行定义和使用。 - 等待队列:AQS 使用一个双向链表(CLH 队列)来维护等待获取锁的线程。等待队列中的每个节点都代表一个等待线程,节点中保存了线程的引用以及其他控制信息。
- 获取锁:当一个线程需要获取锁时,它会调用
acquire()
方法,该方法会尝试获取锁。如果当前没有其他线程持有锁,或者该线程是持有锁的线程,则可以直接获取锁;否则,该线程将会被加入到等待队列中,进入等待状态。 - 释放锁:当一个线程释放锁时,它会调用
release()
方法,该方法会释放锁并唤醒等待队列中的下一个线程。如果等待队列中没有其他线程,则锁会变为可用状态,等待获取锁的线程可以继续竞争获取。 - 状态变更和线程调度:AQS 提供了一些方法来支持状态的变更和线程的调度,例如
compareAndSetState()
方法用于原子地修改同步状态,park()
和unpark()
方法用于线程的阻塞和唤醒等。
通过以上的机制,AQS 实现了基于 FIFO 等待队列的线程同步和协作。派生类可以根据具体的需求和场景,重写 AQS 的方法以实现不同的同步方式,例如独占锁、共享锁等。
- 同步状态(State):AQS 内部维护了一个同步状态,通过
-
状态state:一个volatile修饰的可见性变量,下面是对应方法
/** * The synchronization state. */ private volatile int state; /** * Returns the current value of synchronization state. * This operation has memory semantics of a {@code volatile} read. * @return current state value */ protected final int getState() { return state; } /** * Sets the value of synchronization state. * This operation has memory semantics of a {@code volatile} write. * @param newState the new state value */ protected final void setState(int newState) { state = newState; } /** * Atomically sets synchronization state to the given updated * value if the current state value equals the expected value. * This operation has memory semantics of a {@code volatile} read * and write. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that the actual * value was not equal to the expected value. */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
-
先进先出的队列(不存在的虚拟队列,存在的是拥有指向节点的链表也叫作双向链表)
// 节点的状态 volatile int waitStatus; // 当前节点的前一个节点 volatile Node prev; // 当前节点的后一个节点 volatile Node next; // 当前节点中所包含的线程对象 volatile Thread thread; // 等待队列中的下一个节点 Node nextWaiter;
-
常用的方法汇总
tryAcquire(); //独占式获取锁的方法,立即返回获取结果,不会阻塞线程。通常用于尝试获取锁的操作,并根据返回结果进行相应的处理。 tryRelease(); //独占式释放锁的方法。用于显式地释放锁,以便其他线程可以获取到锁。 tryAcquireShared(); // 共享式获取锁 tryAcRelease(); // 共享式释放锁 acquire(); //获取锁的方法,会阻塞线程并加入等待队列,直到获取到锁为止。适用于需要等待其他线程释放锁的场景。 release(); //释放锁的方法,用于释放已经持有的锁。释放锁后,会唤醒等待队列中的其他线程。 getState(); //获取当前的同步状态(state)的方法。 setState(); //设置同步状态(state)的方法。 compareAndSetState(); //CAS(Compare and Set)比较交换的原子操作,用于原子地设置同步状态。只有当当前状态与期望状态相同时,才会将状态更新为新的值。 setExclusiveOwnerThread(); //设置持有锁的线程。用于记录当前拥有锁的线程。 getExclusiveOwnerThread(); //获取持有锁的线程。用于获取当前拥有锁的线程。
Lock接口及其实现类
-
ReentrantLock
-
可重入锁、独占锁,只能有一个线程获取到锁
-
优点:
- 可重入性:同一个线程可以多次获取同一个ReentrantLock对象的锁,避免了死锁。这对于一些复杂的逻辑和嵌套的锁定场景非常有用。
- 公平性:ReentrantLock可以选择公平模式或非公平模式。公平模式下,多个线程按照先来先得的顺序获取锁,减少饥饿现象。
- 线程阻塞和唤醒:ReentrantLock提供了Condition对象,可以通过它实现线程之间的等待和唤醒,更细粒度地控制线程的执行。
- 锁状态查询:ReentrantLock提供了一些方法,可以查询锁的状态,如获取当前持有锁的线程数、判断当前线程是否持有锁等。
缺点:
- 复杂性:相对于synchronized关键字,ReentrantLock的使用更复杂。需要手动获取和释放锁,容易出错,需要谨慎设计。
- 容易造成死锁:如果锁的获取和释放不当,可能会造成死锁的情况,导致线程无法继续执行。
- 风险:ReentrantLock属于显式锁,需要手动释放锁。如果在获取锁之后没有正确释放锁,可能会导致资源泄漏的问题。
-
常用方法
lock(); // 阻塞式获取锁 unlock(); // 释放锁 tryLock(); // 尝试获取锁,不阻塞 getHoldCount(); // 返回当前获取到锁的线程 getQueueLength(); // 返回等待队列的长度 /**获取到条件变量(Condition):ReentrantLock可以创建多个与之关联的条件变量,用于线程之间的等待和唤醒。**/ newCondition(); // 获取condition条件对象 condition.await(); // 阻塞当前线程,使其等待条件发生。await()方法会释放持有的锁,并将线程置于等待状态,直到其他线程发出信号通知或者被中断。 condition.signal(); // 唤醒等待队列中的一个线程 condition.signalAll(); // 唤醒全部线程 condition.awaitNanos(long nanosTimeout); // 阻塞当前线程,使其等待条件发生,但最多等待指定的时间。与await()类似,但是可以指定等待的最长时间,超过指定时间后线程会自动被唤醒。 condition.awaitUntil(Date deadline); //阻塞当前线程,使其等待条件发生,直到指定的截止时间。与await()类似,但是可以指定等待的截止时间,如果到达截止时间条件还未发生,线程会被自动唤醒。
-
适用场景:读写线程竞争激烈,或者需要更灵活的锁控制,比如支持公平性或定时锁等。
-
-
ReentrantReadWriteLock
-
在可重入锁的升级,实现了读写锁,意味着读读可以共享,读写和写写互斥,有多个线程能并行读,提高了并发能力。
-
优点:
- 并发性增强:相对于独占锁(如ReentrantLock),读写锁允许多个线程同时读取共享资源,提高了并发读取的能力,因此适用于读多写少的场景。这样可以显著提升系统的性能和吞吐量。
- 公平性:ReentrantReadWriteLock可以配置为公平锁或非公平锁。公平锁会按照请求的顺序进行获取锁的操作,减少了线程饥饿的可能性。
- 可重入性:与ReentrantLock类似,ReentrantReadWriteLock也支持可重入性,即线程可以多次获取读锁或写锁而不会被阻塞。
缺点:
- 写锁的优先级:如果有线程持有写锁且有线程在等待写锁时,读锁的获取会被阻塞,这可能导致写锁的优先级较高,而读锁的获取被延迟。这可能会导致写线程饥饿的问题。
- 锁竞争:在高并发情况下,读写锁的细粒度锁竞争可能会导致性能下降。特别是当有大量的写线程和读线程同时竞争锁时,写线程需要等待读线程释放读锁。
-
适用场景:读多写少的场景
-
常用方法:
readLock(); // 获取写锁,返回的是读锁对象,拿到锁对象之后就和ReentrantLock一样的操作 writeLock(); // 获取读锁,返回的是写锁对象,拿到锁对象之后就和ReentrantLock一样的操作 lock(); // 阻塞式获取锁 unlock(); // 释放锁 tryLock(); // 尝试获取锁,不阻塞 getHoldCount(); // 返回当前获取到锁的线程 getQueueLength(); // 返回等待队列的长度 /**获取到条件变量(Condition):ReentrantLock可以创建多个与之关联的条件变量,用于线程之间的等待和唤醒。**/ newCondition(); // 获取condition条件对象 condition.await(); // 阻塞当前线程,使其等待条件发生。await()方法会释放持有的锁,并将线程置于等待状态,直到其他线程发出信号通知或者被中断。 condition.signal(); // 唤醒等待队列中的一个线程 condition.signalAll(); // 唤醒全部线程 condition.awaitNanos(long nanosTimeout); // 阻塞当前线程,使其等待条件发生,但最多等待指定的时间。与await()类似,但是可以指定等待的最长时间,超过指定时间后线程会自动被唤醒。 condition.awaitUntil(Date deadline); //阻塞当前线程,使其等待条件发生,直到指定的截止时间。与await()类似,但是可以指定等待的截止时间,如果到达截止时间条件还未发生,线程会被自动唤醒。
-
-
StampLock
-
StampedLock是Java 8引入的一种新的读写锁,它提供了一种优化的读写锁实现,比ReentrantReadWriteLock在某些场景下性能更好。
- StampedLock提供了三种模式的锁:
- 读锁(乐观读模式):通过tryOptimisticRead()方法获取,获取一个乐观读锁的stamp值,此时可以进行读操作。读操作完成后,需要使用validate(stamp)或者tryConvertToReadLock(stamp)方法验证stamp是否有效,如果无效则需要重新获取锁。
- 写锁(悲观写模式):通过writeLock()方法获取,获取一个写锁的stamp值,此时其他读锁和写锁都会被阻塞。
- 读锁(悲观读模式):通过readLock()方法获取,获取一个读锁的stamp值,此时其他写锁会被阻塞,但是乐观读模式不会被阻塞。
- StampedLock提供了三种模式的锁:
-
优点:支持乐观读模式,允许读操作无锁执行,避免了悲观读模式下的竞争,从而提高并发性能。写锁可降级为读锁,读锁可升级为写锁,提供了更大的灵活性。
-
缺点:不支持重入,同一个线程多次获取锁会导致死锁。
-
适用场景:读多写少
-
常用方法:
readLock(); // 获取读锁。返回一个用于读操作的票据(stamp),读操作可以并发执行,不会阻塞其他读线程,只会阻塞写线程。 tryReadLock(); // 尝试获取读锁。返回一个非负数的票据(stamp)表示获取到读锁,返回0表示锁已被写锁占用,获取读锁失败。 writeLock(); // 获取写锁。返回一个用于写操作的票据(stamp),写操作会独占锁,并阻塞其他读写线程。 tryWriteLock(); // 尝试获取写锁。返回一个非负数的票据(stamp)表示获取到写锁,返回0表示锁已被其他线程占用,获取写锁失败。 tryOptimisticRead(); // 尝试获取一个乐观读锁。返回一个非负数的票据(stamp),不阻塞其他读写线程,使用乐观读锁时,需要在操作数据之前检查票据是否仍然有效。 validate(); // 验证乐观读锁。返回true表示在获取乐观读锁后没有写操作发生,读操作可以继续,返回false表示在获取乐观读锁后有写操作发生,需要重新获取锁。 unlockRead(); // 释放读锁。 unlockWrite(); // 释放写锁。 /**获取到条件变量(Condition):ReentrantLock可以创建多个与之关联的条件变量,用于线程之间的等待和唤醒。**/ newCondition(); // 获取condition条件对象 condition.await(); // 阻塞当前线程,使其等待条件发生。await()方法会释放持有的锁,并将线程置于等待状态,直到其他线程发出信号通知或者被中断。 condition.signal(); // 唤醒等待队列中的一个线程 condition.signalAll(); // 唤醒全部线程 condition.awaitNanos(long nanosTimeout); // 阻塞当前线程,使其等待条件发生,但最多等待指定的时间。与await()类似,但是可以指定等待的最长时间,超过指定时间后线程会自动被唤醒。 condition.awaitUntil(Date deadline); //阻塞当前线程,使其等待条件发生,直到指定的截止时间。与await()类似,但是可以指定等待的截止时间,如果到达截止时间条件还未发生,线程会被自动唤醒。
-
-
使用demo
package thread.aqs; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.StampedLock; /** * @Classname LockDemo * @Description TODO 常见lock的使用 * @Version 1.0.0 * @Date 2023/7/16 18:32 * @Created by wlh12 */ public class LockDemo { public static void main(String[] args) { // 使用ReentrantLock ReentrantLockThreadDemo reentrantLockThreadDemo = new ReentrantLockThreadDemo(); new Thread(reentrantLockThreadDemo).start(); new Thread(reentrantLockThreadDemo).start(); // ReentrantReadWriteLock使用:先获取到ReentrantReadWriteLock,在分别获取读写锁 ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); new Thread(new ReadThread(readLock),"ReadThread-1").start(); new Thread(new WriteThread(writeLock),"WriteThread-2").start(); new Thread(new ReadThread(readLock),"ReadThread-2").start(); // StampLock StampedLock stampedLock = new StampedLock(); new Thread(new ReadOptimismThread(stampedLock),"stampedLock-op-1").start(); new Thread(new WriteStampThread(stampedLock),"stampedLock-write-1").start(); new Thread(new ReadStampThread(stampedLock),"stampedLock-read-1").start(); } static void readTest() { System.out.println(Thread.currentThread().getName()+"开始读操作"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()+",读取完成"); } static void writeTest() { System.out.println(Thread.currentThread().getName()+"开始写操作"); try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()+",写完成"); } /** * 使用ReentrantLock */ static class ReentrantLockThreadDemo implements Runnable { // 可重入锁 Lock lock = new ReentrantLock(); // 共享变量 int p = 10; @Override public void run() { // 获取锁 lock.lock(); try { /** * 业务逻辑 */ for (; p>0; ) { System.out.println("当前线程:"+Thread.currentThread().getName()+"共享变量值="+p); p --; Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); } } } /** * 使用读锁ReentrantReadWriteLock */ static class ReadThread implements Runnable { private Lock lock; public ReadThread(Lock lock) { this.lock = lock; } @Override public void run() { lock.lock(); try { readTest(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } /** * 使用写锁ReentrantReadWriteLock */ static class WriteThread implements Runnable { private Lock lock; public WriteThread(Lock lock) { this.lock = lock; } @Override public void run() { lock.lock(); try { writeTest(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } /** * 使用读锁StampLock */ static class ReadStampThread implements Runnable { private StampedLock lock; public ReadStampThread(StampedLock lock) { this.lock = lock; } @Override public void run() { // 获取读锁(悲观) long stamp = lock.readLock(); try { readTest(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlockRead(stamp); } } } /** * 使用读锁StampLock */ static class ReadOptimismThread implements Runnable { private StampedLock lock; public ReadOptimismThread(StampedLock lock) { this.lock = lock; } @Override public void run() { // 获取读锁(乐观锁) long stamp = lock.tryOptimisticRead(); boolean flag = false; try { // 验证是否有其写锁修改了 if (!lock.validate(stamp)) { // 有修改,重新去获取锁或者升级为悲观锁 // 升级为悲观锁 stamp = lock.tryConvertToReadLock(stamp); if (stamp == 0L) { stamp = lock.readLock(); flag = true; } } readTest(); } catch (Exception e) { e.printStackTrace(); } finally { // 判断是否升级为了悲观锁,如果是乐观锁,则不需要显式的释放锁 if (flag) { lock.unlockRead(stamp); } } } } /** * 使用写锁StampLock */ static class WriteStampThread implements Runnable { private StampedLock lock; public WriteStampThread(StampedLock lock) { this.lock = lock; } @Override public void run() { long stamp = lock.writeLock(); try { writeTest(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlockWrite(stamp); } } } }
下面所有的实现都是基于非公平实现的锁
基于AQS实现互斥锁
思路:
互斥锁,指当前只有一个线程能获取到锁并且不能重入。state值只有0和1的情况,获取得到之后就修改为1,之后不可重入,释放锁变为0.
步骤:
- 定义自己的锁类
- 实现一个AQS的同步器和lock接口
- 重写里面的tryAcquire和tryRelease方法用于获取锁和释放锁,再实现lock接口的对应的方法
- 定义获取锁和释放锁的方法,里面调用自己实现的同步器去尝试获取锁和释放锁
实现:
实现的独占锁示例代码
package thread.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Classname MyAqs
* @Description TODO 基于aqs实现独占锁
* @Version 1.0.0
* @Date 2023/7/14 22:27
* @Created by wlh12
*/
public class ExclusiveLock implements Lock{
public final MySync sync = new MySync();
// 阻塞式获取锁
@Override
public void lock() {
sync.acquire(1);
}
// 获取锁的过程中响应中断
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 尝试获取锁立马返回结果,不阻塞和不加入等待队列中
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
//尝试在指定的时间范围内获取锁。
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
// 释放锁
@Override
public void unlock() {
sync.release(1);
}
// 创建一个条件对象来协调线程之间的通信
@Override
public Condition newCondition() {
return sync.newCondition();
}
// 实现一个sync继承与aqs(Abstract Queued Synchronized)
// 重写核心方法实现锁的获取和释放
static class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 原子操作获取锁
// if (!hasQueuedPredecessors() && compareAndSetState(0, arg)) 实现公平性
// hasQueuedPredecessors用于判断当前线程前面是否还有线程排队,有返回ture,否则false
if (compareAndSetState(0, arg)){
// 说明没有获取到锁的线程,可以获取锁
// 这个方法是设置当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
System.out.println("当前获取到锁的线程是:"+Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (!isHeldExclusively()){
// 持有锁的线程和当前线程不同
throw new IllegalMonitorStateException("当前线程未持有锁,释放失败");
}
int state = getState();
setExclusiveOwnerThread(null);
return compareAndSetState(state, state-arg);
}
// 判断该线程是否占有锁
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 创建一个condition对象来实现线程之间的通信、协调,例如等待、唤醒等
public Condition newCondition() {
return new ConditionObject();
}
}
}
使用该锁
下面的代码里面使用的是runnable来实现对共享变量sum的操作,保证结果线程安全
class TestRunnable implements Runnable {
MyAqs myAqs = new MyAqs();
int sum = 20;
@Override
public void run() {
// 上锁
myAqs.lock();
for (int i = 0; i < 10; i++) {
System.out.println("当前线程:"+Thread.currentThread().getName()+",sum="+sum);
sum --;
}
// 释放锁
myAqs.unlock();
}
}
完整demo代码:
package thread.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Classname MyAqs
* @Description TODO 基于aqs实现独占锁
* @Version 1.0.0
* @Date 2023/7/14 22:27
* @Created by wlh12
*/
public class ExclusiveLock implements Lock{
public final MySync sync = new MySync();
// 阻塞式获取锁
@Override
public void lock() {
sync.acquire(1);
}
// 获取锁的过程中响应中断
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 尝试获取锁立马返回结果,不阻塞和不加入等待队列中
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
//尝试在指定的时间范围内获取锁。
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
// 释放锁
@Override
public void unlock() {
sync.release(1);
}
// 创建一个条件对象来协调线程之间的通信
@Override
public Condition newCondition() {
return sync.newCondition();
}
// 实现一个sync继承与aqs(Abstract Queued Synchronized)
// 重写核心方法实现锁的获取和释放
static class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 原子操作获取锁
if (compareAndSetState(0, arg)){
// 说明没有获取到锁的线程,可以获取锁
// 这个方法是设置当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
System.out.println("当前获取到锁的线程是:"+Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (!isHeldExclusively()){
// 持有锁的线程和当前线程不同
throw new IllegalMonitorStateException("当前线程未持有锁,释放失败");
}
int state = getState();
setExclusiveOwnerThread(null);
return compareAndSetState(state, state-arg);
}
// 判断该线程是否占有锁
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 创建一个condition对象来实现线程之间的通信、协调,例如等待、唤醒等
public Condition newCondition() {
return new ConditionObject();
}
}
public static void main(String[] args) {
TestRunnable runable = new TestRunnable();
Thread thread = new Thread(runable);
Thread thread1 = new Thread(runable);
thread.start();
thread1.start();
}
static class TestRunnable implements Runnable {
Lock lock = new ExclusiveLock();
int sum = 20;
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.println("当前线程:"+Thread.currentThread().getName()+",sum="+sum);
sum --;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
基于AQS实现可重入锁(ReenTrant Lock)
参考源码的实现思路:
这是在上面的一个改造,当一个线程重复获取一个锁的时候无法获取,这个时候就需要实现可重入。state状态值就代表的拿到锁的次数,当获取锁的时候先判断state的值,不为0就说明已经有线程持有锁,然后判断当前线程是否等于持有锁的线程,如果是则可以再次获取到锁实现可重入。在释放的时候根据state的值进行释放,在state为1的时候就是只有一个拿到了,这时不仅需要修改state的值还需要将ExclusiveThread置为null。
步骤:
- 自定义锁类实现lock接口
- 内部自定义Sync同步器实现AQS
实现:
实现可重入锁:
package thread.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @Classname MyReentrantLock
* @Description TODO 实现可重入锁
* @Version 1.0.0
* @Date 2023/7/15 12:40
* @Created by wlh12
*/
public class MyReentrantLock implements Lock{
Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 先获取state,看是否已经获取到锁了
// 尝试获取锁的线程
Thread thread = Thread.currentThread();
int state = getState();
if (state == 0) {
// 没有线程拿到锁,直接获取到
// if (!hasQueuedPredecessors() && compareAndSetState(0, arg)) 实现公平性
// hasQueuedPredecessors用于判断当前线程前面是否还有线程排队,有返回ture,否则false
if (compareAndSetState(0,arg)) {
setExclusiveOwnerThread(thread);
return true;
}
} else {
// 已经有线程拿到锁了,判断是否是当前线程
if (getExclusiveOwnerThread() == thread){
return compareAndSetState(state,state + arg);
}
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 获取到当前线程
Thread thread = Thread.currentThread();
// 判断当前线程是否获取到了锁
if (thread != getExclusiveOwnerThread()) {
// 不是当前的线程拿到的锁
throw new IllegalMonitorStateException("当前线程未获得锁");
}
// 可重入锁减一,在最后需要释放
int state = getState();
System.out.println("释放锁:当前state="+state);
if (state == 1){
// 只有一个获取到了锁(state代表重入锁的次数)
setExclusiveOwnerThread(null);
}
return compareAndSetState(state,state-arg);
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
public Condition newCondition(){
return new ConditionObject();
}
}
}
模拟实现可重入
public class MyReentrantLockDemo{
static class MyRunnable implements Runnable {
Lock lock = new MyReentrantLock();
int sum = 20;
@Override
public void run() {
try {
lock.lock();
while (sum > 0){
System.out.println("当前线程:"+Thread.currentThread().getName()+",sum="+sum);
try {
// 当前线程再次尝试获取锁,锁重入
lock.lock();
sum--;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
}
}
基于AQS实现的读写锁ReentrantReadWriteLock思路解析
读写锁就是读读可以共享,写写和读写互斥的一种可重入锁。
ReentrantReadWriteLock内部维护了两个锁:读锁和写锁。在获取读锁时,会先检查是否有写锁被持有,如果有则将当前线程加入等待队列,直到写锁被释放。如果没有写锁被持有,则允许当前线程获取读锁。
在获取写锁时,需要检查是否有读锁被持有或者写锁被持有,如果有,则将当前线程加入等待队列,直到读锁和写锁都被释放。如果没有读锁和写锁被持有,则允许当前线程获取写锁。
通过使用AQS的状态值和等待队列,实现了读写锁的共享特和互斥性。读操作可以并发进行,只有当写操作存在时才会阻塞,从而提高了并发性能。
因为state是int类型的8个字节32位,所以将AQS的state分为高16位和低16位来分别代表读锁和写锁的状态
static final int SHARED_SHIFT = 16;// 读锁的偏移,用于在根据state获取到读锁的数量
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 读锁的计量单位,当新增读锁的时候加一这个SHARED_UNIT
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 锁的最大值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 用于根据state获取写锁的数量
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
具体的思路就是state是int类型的,4个字节共32位,高16位是读锁,低16位是写锁,在判断的时候就可以通过得到对应的高位和低位进行判断对应的锁的个数和状态。然后其他的就是和可重入锁一样的了,只是在state这个地方的管理不一样。
Countdownlatch同步器
-
原理:
Countdownlatch
是基于AQS来实现的同步器,其state代表剩余需要等待的线程,创建的时候会创建一个阻塞队列和将state赋值,当一个线程调用await()
方法时,它会尝试获取锁,但只有当state
的值为 0 时才能成功获取锁,否则会被阻塞加入到阻塞队列中去。当其他线程调用countDown()
方法时,会将state
的值减一。当state
的值减到 0 时,所有等待的线程都会被释放。而这里是在初始化的时候state的值就是需要等待的数量,然后再各个线程里面执行完了之后调用countDown()
去减少state的值,此时主线程阻塞等待全部执行完成之后唤醒主线程的实现方式 -
常用方法
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。 await();//阻塞当前线程,将当前线程加入阻塞队列。 await(long timeout, TimeUnit unit);// 在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行, countDown(); //对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
-
适用场景:需要获取到多个任务的执行结果进行汇总整合的场景或类似场景
-
示例
package thread.aqs; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; /** * @Classname CountDownLatchDemo * @Description TODO 使用CountDownLatch同步器完成一组线程 * @Version 1.0.0 * @Date 2023/7/3 21:35 * @Created by wlh12 */ public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch downLatch = new CountDownLatch(3); Map<String,Integer> map = new HashMap<>(); map.put("sum1",0); map.put("sum2",0); map.put("sum3",0); // 计算1-20 Thread thread1 = new Thread(() -> { for (int i = 1; i < 21; i++) { map.put("sum1",map.get("sum1")+i); } System.out.println("thread1完成"); downLatch.countDown(); }); // 计算21-40 Thread thread2 = new Thread(() -> { for (int i = 1; i < 41; i++) { map.put("sum2",map.get("sum2")+i); } System.out.println("thread2完成"); downLatch.countDown(); }); // 计算41-60 Thread thread3 = new Thread(() -> { for (int i = 1; i < 61; i++) { map.put("sum3",map.get("sum3")+i); } try { Thread.sleep(1500); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("thread3完成"); downLatch.countDown(); }); // 启动对应的线程 thread1.start(); thread2.start(); thread3.start(); // 阻塞当前线程来等待上面的线程执行完成 downLatch.await(); System.out.println(map); } }
Cyclicbarrier同步器
-
原理:和CountDownLatch类似,也是基于AQS实现的,其state代表剩余需要等待执行到指定位置的线程。在线程调用了
await()
方法之后会加入到阻塞队列并阻塞线程,然后再state的值减一,等state的值为0的时候,意味着全部都执行到了指定的位置调用了await()
方法,此时全部线程加入到了阻塞队列且state为0,这时唤醒全部线程一起开始执行即可。Cyclicbarrier可以重复使用,CountdownLatch只能使用一次 -
常用方法:
CyclicBarrier(int parties); // 构造函数,用于创建一个CyclicBarrier对象,指定参与线程的数量。 int await(); // 线程调用此方法来在屏障处等待,直到所有参与线程都调用了await()方法。当调用await()方法时,线程会被阻塞,并等待其他线程到达屏障。当所有线程都到达屏障后,屏障被打破,所有线程一起继续执行。返回一个整数值,表示到达屏障的次数。 int await(long timeout, TimeUnit unit); // 线程调用此方法来在指定的时间内等待,直到所有参与线程都调用了await()方法,或者超时。 int getParties(); // 获取参与线程的数量。 int await(int parties); // 设置一个新的屏障,指定需要等待的线程数量。可以用于重置CyclicBarrier的计数器。 boolean isBroken(); // 判断当前屏障是否被破坏。如果在等待过程中发生了异常,则屏障会被破坏。
-
适用场景:并行计算、多阶段任务等
-
示例:
package thread.aqs; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; /** * @Classname CyclibarrierDemo * @Description TODO 使用Cyclibarrier类来实现同步多个线程之后再一起执行 * @Version 1.0.0 * @Date 2023/7/3 21:35 * @Created by wlh12 */ public class CyclibarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(3,()->{ System.out.println("--------全部线程都到达规定阶段,下面一起执行后面的代码--------"); System.out.println("全部一起执行中"); }); ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3); for (int i = 0; i < 2; i++) { int finalI = i; threadPool.execute(()->{ for (int j = 1; j < 10; j++) { } System.out.println("thread"+ finalI +"完成循环操作,等待执行下一步"); try { cyclicBarrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } System.out.println("thread" + finalI+"完成"); }); } threadPool.execute(()-> { int i; for (i = 1; i < 10; i++) { } try { Thread.sleep(1500); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("thread" + i + "完成循环操作,等待执行下一步"); try { cyclicBarrier.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (BrokenBarrierException e) { throw new RuntimeException(e); } System.out.println("thread" + i+"完成"); }); // 关闭线程池 threadPool.shutdown(); } }
Semapore信号量
-
原理:基于AQS实现的一个类似于锁的东西,只不过他是通过state定义了可以获取到的一个个数称为令牌数量,当state的值为0的时候意味着不能在获取了令牌了,很像
-
常用方法
acquire(); // 尝试获取一个许可证,如果当前没有可用的许可证,则线程将阻塞,直到有其他线程释放许可证。 acquire(int permits); // 尝试获取指定数量的许可证,如果当前可用的许可证数量不足,则线程将阻塞,直到有足够数量的许可证可用。 release(); // 释放一个许可证,将许可证的数量增加1。如果有其他线程正在等待许可证,那么其中一个线程将被唤醒。 release(int permits); // 释放指定数量的许可证,将许可证的数量增加相应的值。唤醒等待许可证的线程数量取决于参数值和当前等待线程的数量。 drainPermits(); // 清空令牌把可用令牌数置为0,返回清空令牌的数量。 availablePermits(); // 返回可用的令牌数量。 tryAcquire(); // 尝试获取一个许可证,如果当前没有可用的许可证,则立即返回false,而不是阻塞线程。 tryAcquire(int permits); // 尝试获取指定数量的许可证,如果当前可用的许可证数量不足,则立即返回false,而不是阻塞线程。 tryAcquire(long timeout, TimeUnit unit); // 在指定的时间内尝试获取一个许可证,如果在超时时间内没有可用的许可证,则返回false。 tryAcquire(int permits, long timeout, TimeUnit unit); // 在指定的时间内尝试获取指定数量的许可证,如果在超时时间内没有足够数量的许可证可用,则返回false。 hasQueuedThreads(); // 查询是否有线程正在等待获取许可证。 getQueueLength(); // 返回当前等待获取许可证的线程数量。
-
适用场景:限流、控制并发访问
-
示例
package thread.aqs; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Semaphore; /** * @Classname SemaporesDemo * @Description TODO * @Version 1.0.0 * @Date 2023/7/14 22:25 * @Created by wlh12 */ public class SemaphoreDemo { public static void main(String[] args) { // 定义信号量车位5 Semaphore semaphore = new Semaphore(5); // 创建10个线程来模拟入库获取车位 ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(() -> { try { System.out.println(Thread.currentThread().getName()+",开始获取车位"); if (semaphore.availablePermits() == 0){ System.out.println("当前车位为0,请稍后"); } semaphore.acquire(); System.out.println(Thread.currentThread().getName()+",获取成功"); Thread.sleep(1500); semaphore.release(); System.out.println(Thread.currentThread().getName()+",释放车位"); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } executorService.shutdown(); } }
常见问题
-
线程的创建方式
前面提到的四种方式:
继承Thread类、实现Runnable接口、实现Callable接口以及使用FutureTask获取执行结果、线程池
-
线程的生命周期
线程的生命周期可以分为以下几个阶段:
- 新建(New):使用了new Thread()或其他方法创建出来的时候。
- 就绪(Runnable):调用了线程的start()方法,表示已就绪,等待被调度执行
- 运行(Running):线程调度到了此线程时开始了运行
- 阻塞(Blocked):因为运行中途的某些原因导致无法继续执行,例如,线程可能需要等待某个锁的释放或者等待某个输入/输出操作完成
- 等待(Wait):调用了Wait()方法进入了对象等待池。
- 计时等待(Timed Wait):类似于等待状态,但可以指定等待的时间。例如,线程可以调用
sleep()
方法暂停执行一段时间 - 终止(Terminated):线程完成了它的任务或者发生了异常而终止时,线程进入终止状态。
-
线程的数据传递和返回:
- 共享变量:多个线程可以通过访问共享变量来传递和返回数据。但需要注意的是,如果多个线程同时读写一个共享变量,可能会引发线程安全问题,需要使用同步机制(如锁)来保证数据的正确性。
- 线程参数和返回值:通过线程的构造函数传递参数,或者通过线程对象的方法来设置和获取线程的返回值。
- 线程间的队列:可以使用线程安全的队列(如
BlockingQueue
)来传递和返回数据。一个线程可以将数据放入队列,另一个线程从队列中获取数据,实现数据的传递和返回。 - 线程间的共享对象:多个线程可以共享一个对象,通过对象的方法来传递和返回数据。需要注意的是,对共享对象的访问需要进行同步,以保证数据的正确性。
-
线程间的数据同步:
- 锁:使只有一个线程能能够修改数据,保证多线程下的数据安全问题。
- 原子类:原子类提供了一组原子操作,能够确保对共享变量的读取和写入操作具有原子性,即不会被其他线程中断。原子类可以保证多线程环境下的原子操作,避免出现竞态条件和数据不一致的问题。
- volatile关键字:在写入操作时会立即将该变量的值刷新到主内存中,同时强制其他线程中的工作内存的该变量的副本失效。而在读取该变量时,会从主内存中重新获取最新的值,而不是使用线程私有内存中的副本。
- 同步器:是一种用于协调多个线程之间的操作和状态的机制,通过使用同步器,可以实现线程之间的协调和同步,确保线程按照特定的顺序执行,避免竞态条件和数据不一致等问题。