Lock接口和AQS原理与实现(Java并发编程的艺术整理)
Lock接口
锁是用来控制多个线程访问共享资源的方式,一个锁能够防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
使用synchronized关键字会将隐式地获取锁,但是将锁的获取和释放固化了,也就是先获取在释放。缺点就是扩展行没有显示的锁获取和释放来的号。
Lock特性
特性 | 详解 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 于synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
Lock API
Lock是一个接口,定义了锁获取和释放的基本操作
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程。 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法或立即返回,如果能够获取则返回true,否则返回false. |
boolean tryLock(long time,TimeUnit unit) throws InterruptedExecption | 超时的获取锁,当前线程在以下3种情况下会返回:1)当前线程在超时时间内获得了锁。2)当前线程在超时时间内被中断. 3)超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。 |
队列同步器(AbstarctQueuedSynchronizer 简称AQS)
AQS是用来构建锁或者其他同步组件的基础框架,使用了int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用的方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程种要对同步状态进行更改。
同步器是实现锁的关键,在锁的实现种聚合同步器,利用同步器实现锁的语义。
锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待与唤醒等底层状态。锁和同步器隔离了使用者和实现者所需要关注的领域。
AQS的接口
AQS的设计是基于模板模式。使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组价的实现种,并调用提供的模板方法。
重写同步器指定方法是,需要使用同步器提供的3个方法来访问或修改同步状态。
方法 | 描述 |
---|---|
getState() | 获取当前同步状态 |
setState(int newState) | 设置当前同步状态 |
compareAndSetState(int expect,int update) | 使用CAS设置当前状态,该方法能够保证状态设置的原子性 |
同步器可重写的方法如下:
方法 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,让后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占。 |
实现自定义同步组件时,将会调用同步器提供模板方法,这些方法如下:
方法 | 描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg) |
void acquireInteruptibly() | 与acquire方法相同,但是该方法响应中断,当前线程为获取到同步状态而进入同步队列种,如果当前线程被中断,则该方法会抛出InteruptedException并返回 |
boolean tryAcquireNamos(int arg,long nanos) | 在acquireInteruptibly方法上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将返回false,如果获取到了返回true. |
void acquireShare(int arg) | 共享式的获取同步状态,如果当前前程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别式在同一时刻可以由多个线程获取到同步状态。 |
void acquireSharedInterruptibly(int arg) | 与acquireShare相同,该方法响应中断· |
boolean tryAcquireShareNanos(int arg,long nanos) | 在acquireSharedInterruptibly基础上增加了超时限制。 |
boolean release(int arg) | 独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列种第一个节点包含的线程唤醒 |
boolan releaseShared(int arg) | 共享式的释放同步状态 |
Collection getQueuedThreads() | 获取等待在同步队列上的线程集合 |
AQS实现
队列同步器实现主要包括:同步队列,独占式同步状态获取与释放,共享式同步状态获取与释放以及超时获取同步状态等AQS核心参数数据结构与模板方法
同步队列
AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败式,AQS会将当前线程以及等等待状态等信息构成一个节点(Node)并将节点加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把受节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)保存获取同步状态失败的线程引用,等待状态的节点信息如下:
属性类型和名称 | 描述 |
---|---|
int waitStatus | 等待状态。 包含如下状态:(1)cancelied,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消消息,节点进入该状态将不会变化。 (2)SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,时后继节点的线程得以运行。 (3)CONDITION 值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。(4)PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去。(5)INITAL,值为0,初始状态。 |
Node prev | 前驱节点,当前节点加入同步队列式被设置(尾部添加) |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点。如果当前节点式共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段 |
Thread thread | 获取同步状态的线程 |
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
独占式同步状态获取与释放
通过调用同步器的acquire(int arg)方法可获取同步状态,该方法对中断不敏感,就是说线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。方法的主要逻辑是:
首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueuued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移除队列(或停止自旋)的条件时前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
共享式同步状态获取与释放
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
eg:如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以时共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况是不同的。
通过调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态。在acquireShared(int arg)方法中,同步器调用tryAcuireShared(int arg)方法尝试获取同步状态,tryAcuireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。与独占式一样,共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态。
超时获取同步状态
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg, long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。针对超时获取,主要需要计算出需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout -= now - lastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时。
独占式超时获取同步状态doAcquireNanos(int arg, longnanosTimeout)和独占式获取同步状态acquire(int args)在流程上非常相似,其主要区别在于未获取到同步状态时的处理逻辑。acquire(int args)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos(int arg, long nanosTimeout)会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会从等待逻辑中自动返回。
基于AQS实现的锁
ReccntrantLock
重入锁ReentrantLock是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。ReentrantLock还支持获取锁时的公平和非公平性选择。
ReentrantLock没有向synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
重进入
重进入时指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题
(1)线程再次获取锁 。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果时,则再次成功获取。
(2)锁的最终释放 。线程重复n次获取了锁,随后在第N次释放了该锁后,其他线程获取到该锁。锁的最终释放要求锁对获取进行技术自增,计数表示当前锁被重复获取的次数,而锁别释放时,计数自减,当前计数等于0时表示锁已经成功释放。
公平锁与非公平锁
公平性与否时针对获取锁而言的,如果一个锁时公平的,那么锁的获取顺序就应该时符合请求的绝对时间顺序,也就是FIFO。
公平锁与非公平锁区别:同步队列中当前节点是否有前驱节点的判断,线程比当前线程更早的请求获取锁,因此需要等待前驱线程获取并释放锁之后,才能继续获取锁。
公平锁保证了锁的获取按照FIFO原则,代价时进行大量的线程切换。非公平锁虽然可能造成线程“饥饿”,但是极少的线程切换,保证了更大的吞吐量。
读写锁(ReentrantReadWriteLock)
ReentrantReadWriteLock的特性
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平由于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程获取了读锁之后,能够再次获取读锁,而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。 |
锁降级 | 遵循获取读写锁,获取读锁在释放写锁的次序,写锁能够降级称为读锁。 |
读写锁接口
ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而实现-ReentrantReadWriteLock 。
ReentrantReadWriteLock展示内部工作状态的方法
方法 | 描述 |
---|---|
int getReadLockCount() | 返回当前读锁被获取的次数,该次数不等于获取读锁的线程数。eg:仅一个线程,连续获取了n次说,那么占据读锁的线程数是1,但该方法返回N |
int getReadHoldCount() | 返回当前线程获取读锁的次数。该方法在java 6中胶乳到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数。 |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
读写锁的实现
ReentrantReadWriteLock的实现主要包括:读写状态的设计,写锁的获取与释放,读锁的获取与释放以及锁降级。
读写状态的设计
读写锁依赖AQS来实现同步功能,所有读写状态就是其AQS的同步状态。而ReentranLock中定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个线程和一个写线程的状态。
如果在一个整形变量上维护多种状态,就一定需要按位切割使用这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
写锁的获取与释放
写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁是,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
该实现的tryAcquire除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。
读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,所作只是线程安全的增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值时(1<<16).
锁降级
锁降级指的时写锁降级为读锁,如果当前线程拥有写锁,然后将其释放,最后在获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,在获取到读锁,随后释放(先前拥有的)写锁的过程。
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程.
Condition 接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object),主要包括wait(),wait(long timeout),notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类型Object的监控方法,与Lock配合可以实现等待/通知模式。
Condition接口方法
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的。
一般Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
Condition的部分方法
方法 | 描述 |
---|---|
void await()throws InterruptedException | 当前线程进入等待状态直到被通知(signal)或中断,当前线程将进入运行状态且从await()方法返回的情况,包括:其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒 (1)其他线程(调用interrupt方法)中断当前线程 (2)如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁 |
void awaitUninterruptibly() | 当前线程进入等待通知状态直到被通知,该方法对中断不敏感 |
long awaitNanos(long nanosTimeout)throws InterruptedException | 当前线程进入等待状体直到被通知,中断或者超时。返回值表示剩余时间,如果在nanosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout-实际耗时)。如果返回值是0或负数,那么可以认定已经超时了 |
boolean awaitUntil(Date deadline) throws InterruptedExcption | 当前线程进入等待状态直到被通知,中断或者到了某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,方法返回false. |
void signal() | 唤醒一个等待在Condition上线程,该线程从等待方法返回前必须获得与Condition相关联的锁 |
void signalAll() | 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关的锁 |
Condition实现分析
ConditionObject是同步器AQS的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也比较合理。每个Condition对象都包含者一个队列(一下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
Condition的实现主要包括:等待队列,等待和通知。
等待队列
等待队列是一个FIFO队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态。节点的定义服用了AQS的中节点的定义,也就是说,同步队列和等待队列中节点类型都是AbstractQueuedSynchronizer.Node
一个Condition包含一个等待对列,Condition拥有首节点和为节点。当前线程调用 Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待对列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中Lock拥有一个同步队列和多个等得队列。
等待
调用Condition的await()方法,会使当前线程进入到等得队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关的锁。
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造节点并加入到等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。
调用该方法的前置条件时当前线程必须获取了锁,可以看待signal()方法进行了isHeldExclusively()检查,当前线成必须时获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。通过调用AQS中的enq(Node node)方法,等待队列中的头节点线程安全移动到同步队列,当前节点移动到同步队列后,当前线程在使用LockSupport唤醒该节点的线程。被唤醒后的线程,将从await()方法中的while()循环中退出,进而调用AQS的acquireQueued()方法加入发哦获取同步状态的竞争中。成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
参考书籍:
《Java并发编程的艺术》
Lock接口和AQS原理与实现(Java并发编程的艺术整理)相关推荐
- 《Java并发编程的艺术》笔记
<Java并发编程的艺术>笔记 第1章 并发编程的挑战 1.1 上下文切换 CPU通过时间片分配算法来循环执行任务,任务从保存到再加载的过程就是一次上下文切换. 减少上下文切换的方法有4种 ...
- Java并发编程的艺术(推荐指数:☆☆☆☆☆☆)
文章目录 Java并发编程的艺术(推荐指数:☆☆☆☆☆☆) 并发编程的挑战 Java并发机制的底层实现原理 Volatile的应用 实现原理 synchronized的实现原理与应用 对象头 锁详解 ...
- 《Java并发编程的艺术》——Java中的并发工具类、线程池、Execute框架(笔记)
文章目录 八.Java中的并发工具类 8.1 等待多线程完成的CountDownLatch 8.2 同步屏障CyclicBarrier 8.2.1 CyclicBarrier简介 8.2.2 Cycl ...
- 《Java并发编程的艺术》——Java并发的前置知识(笔记)
文章目录 一.并发编程的挑战 1.1 上下文切换 1.1.1 多线程一定快吗 1.1.2 如何减少上下文的切换 1.2 死锁 死锁发生的条件 预防死锁 避免死锁 1.3 资源限制的挑战 1.3.1 什 ...
- Java并发编程的艺术_Conc
Java并发编程的艺术 1 并发编程的挑战 1.1 上下文切换 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制.时间片是CPU分配给各个线程的时间,因为时间片 ...
- [书]java并发编程的艺术笔记
本文属于自己整理的读书笔记,便于回顾.内容绝大部分来自书籍:java并发编程的艺术,版权归原作者所有. 第1章 并发编程的挑战 1.多线程一定比单线程快? 不一定,如同在同时阅读两本书时的来回切换切换 ...
- # Java 并发编程的艺术(二)
Java 并发编程的艺术(二) 文章目录 Java 并发编程的艺术(二) 并发编程的挑战 上下文切换 如何减少上下文的切换 死锁 资源限制的挑战 Java 并发机制的底层实现原理 volatile 的 ...
- 《JAVA并发编程的艺术》之Java内存模型
<JAVA并发编程的艺术>之Java内存模型 文章目录 <JAVA并发编程的艺术>之Java内存模型 Java内存模型的基础 并发编程模型的两个关键问题 Java内存模型的抽象 ...
- Java 并发编程的艺术 pdf 下载
并发编程领域的扛鼎之作,作者是阿里和1号店的资深Java技术专家,对并发编程有非常深入的研究,<Java并发编程的艺术>是他们多年一线开发经验的结晶.本书的部分内容在出版早期发表在Java ...
- 《Java并发编程的艺术》——线程(笔记)
文章目录 四.Java并发编程基础 4.1 线程简介 4.1.1 什么是线程 4.1.2 为什么要使用多线程 4.1.3 线程优先级 4.1.4 线程的状态 4.1.5 Daemon线程 4.2 启动 ...
最新文章
- 工业环境老鼠目标检测
- mybatisplus or查询_MybatisPlus的各种查询方式
- HTML5 Canvas 绘制旋转45度佛教万字
- Go报错:more than one character in rune literal
- 什么是Spring inner beans?
- Axis2 WebService(配置、发布、调用)
- 学习编程需要攻克的8个难关,一旦没有把握好,很可能会失败!
- Python中的时间转换和时间获取
- Matlab画图设置指数坐标
- PS图层批量处理插件,支持Win、Mac系统
- 红帽linux安装yum源
- js 利用audio buffers[int16Array]计算分贝
- 超强wifi6路由器推荐!不强你打我!
- java cpu100 解决办法_[Java] CPU 100% 原因查找解决
- 阿里MaxComputer基本操作
- 【程序员学理财】有哪些普通人应该知道的经济学常识?
- 买就赚到的以色列人工智能
- 学生端伽卡他彻底卸载
- 4070显卡相当于什么水平 4070显卡参数 rtx4070显卡功耗
- 百度网盘提速正规办法 无需破解!!!!!
热门文章
- MATLAB----小波去噪
- vue 响应式布局组件_今天如何使用响应式Web组件
- 图解大数据 | Spark GraphFrames-基于图的数据分析挖掘
- IGBT热模型基本原理及其建模方法
- 笔记本电脑开不了机怎么重装系统?小熊U盘重装win7系统教程
- 基于Pytorch的YoLoV4模型代码及作品欣赏
- 金蝶EAS 后台事务监控
- wps怎么链接html,wps怎么添加超链接 wps制作超链接的步骤教程
- 模糊聚类划分matlab代码,模糊C均值聚类算法(原理+Matlab代码)
- ipython安装成功后用不了_ipython安装避坑指南