多线程

多线程基础

1:把一个任务称为一个进程,某些进程内部还需要同时执行多个子任务,把子任务称为线程。一个进程可以包含一个或多个线程,但至少会有一个线程。操作系统调度的最小任务单位其实不是进程,而是线程。多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步

2:实现多任务的方法,有以下几种:

3: 多进程与多线程比较:

4:Java语言内置了多线程支持:当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

创建多线程

方法一:从Thread派生一个自定义类,然后覆写run()方法:

方法二:创建Thread实例时,传入一个Runnable实例:

Callable 、Runble

线程的状态

1:Java线程的状态有以下几种:

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

2:线程终止的原因有:

3:当main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。对已经运行结束的线程调用join()方法会立刻返回。主线程在等待子线程结束,这个时候主线程是在waiting状态。

中断线程

1:方法一:中断一个线程只需要在其他线程中对目标线程调用interrupt()方法,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。目标线程需要反复检测自身状态是否是interrupted状态while (! isInterrupted()),如果是,就立刻结束运行。标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程。interrupt()并不具备阻塞效果,传递完信号后直接继续执行下一句。

如果线程处于正常运行状态,调用interrupt()会向目标线程传递被中断信号,目标线程自行处理中断信号,不会出现抛出异常。Interrupt一般用于清理,他在接收到中断信号后自行处理,不强制要求立刻推出,被中断可以选择不退出。

如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻结束等待并抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,如果捕获了该方法的这个异常。依然可以根据代码正常运行。但是通常情况下该线程应该执行资源清理然后立刻结束运行。当前线程被`interrupt(),并不影响由它创建的子线程的正常运行。

 

2:方法二:一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

守护线程

1: Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。

2:但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程,他是为其他线程服务的,当被服务线程结束后,他也应该结束,但由于是死循环无法正常结束,所以可以他把标记为守护线程。守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。JVM退出时,不必关心守护线程是否已结束。

3:使用

方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

线程同步

1: 多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。通过加锁和解锁的操作,就能保证指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

2:使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行:

3:volatilesynchronized

volatile只保证:读主内存到本地副本;操作本地副本;回写主内存。这3步多个线程可以同时进行,一个线程读取变量后,另一个线程照样可以正常读取,即获取值和回写值时都不会阻塞线程,它只是保证了其他线程能更快的看到修改后的值,只保证线程读取内存的时效的问题,不保证原子性。所以 volatile 不能用于线程同步,只是用于提高程序执行效率。

synchronized除了加锁外,还具有内存屏障功能,并且强制读取所有共享变量的主内存最新值,退出synchronized时再强制回写主内存(如果有修改),已经包含了volatile的功能,所以变量不用额外加volatile修饰。

4:原语

JVM规范定义了几种原子操作:

单条原子操作的语句不需要同步,如果是多行赋值语句,就必须保证是同步操作

同步方法

1:如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)

2:Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。

3:同步只保证多线程执行的synchronized块是依次执行,最终状态对不对还取决于你的逻辑。比如要等所有线程操作完毕再去读取的结果才是正确的,如果各个线程正在执行,直接读取结果就会之错误的。

死锁

1:Java的线程锁是可重入的锁:JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁,原因是:不是方法获取锁,是线程获取锁,一个线程获取锁后就可以在线程里面的方法中传递。获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

2:两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。解决方法是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序。

使用wait和notify

1:多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

ReentrantLock

1: synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁

2:ReentrantLock可以尝试获取锁

3:在ReentrantLock下使用Condition对象来实现waitnotify的功能。

4:一个condition内部维护一个等待队列(不带头结点的链式队列),一个同步队列(双向队列)。所有调用condition.await方法的线程会程释放lock然后加入到等待队列中,并且线程状态转换为等待状态,直至被signal/signalAll(doSignal方法只会对等待队列的头节点进行操作,而doSignalAll等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。)后会使得当前线程/全部等待线程从等待队列中移至到同步队列中去,调用condition的signal/signalAll的前提条件是当前调用signal的线程已经获取了lock,调用condition的signal方法可以将等待队列中等待时间最长的节点移动到同步队列中,按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中,使得该节点能够有机会获得lock,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理,也即退出await方法必须是已经获得了condition引用(关联)的lock。signalAll把全部等待线程移到同步队列,多个线程竞争一个锁。

可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列和多个同步队列,所以多个同步队列间存在竞争关系。

线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法释放锁并进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会重新进入到同步队列的尾部。

ReadWriteLock

1:ReadWriteLock可以解决这个问题,它保证:

把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。

StampedLock

1:对于ReadWriteLock如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

2:乐观锁:乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。

悲观锁:读的过程中拒绝有写入,也就是写入必须等待。

显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

3:写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。

4:把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。

对于悲观锁:读写不能同时进行,一般来说写锁的优先级要高于读锁,read-write-lock假定读很多几乎不会间断,假设现在存在大量读取,如果突然来个写锁,那么只需等当前正在读的释放读锁后,写就立刻获得写锁,其它后续读都得等,不然在一直都有读的情况下,永远写不了。

对于乐观锁:读写可以同时进行。乐观锁其实不上锁,只检查版本号,也不必释放,它的目的是把read-write-lock的read加读锁这一步给去了,因为绝大多数情况下没有写,不需要加读锁,当发现读的时候发生了写,数据前后版本不一致,获取悲观锁,保证读取数据逻辑正确性。

concurrent

1:对ListMapSetDeque等,java.util.concurrent包也提供了并发集合类

interfacenon-thread-safethread-safe
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque

使用这些并发集合与使用非线程安全的集合类完全相同,所有的同步和加锁的逻辑都在集合内部实现,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样

2:把一个旧的非安全集合转换为线程安全集合

但是它实际上是用一个包装类包装了非线程安全的Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用。

Atomic

1:一组原子操作的封装类,如AtomicInteger,原子操作实现了无锁的线程安全;适用于计数器,累加器等。原子操作可以看做最小的执行单位,该操作在执行完毕前不会被任何其他任务或事件打断。最底层基于汇编语言实现,总体性能比Synchronized高很多。

2:Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。

线程池

1:线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

2:常用实现类有:

创建这些线程池的方法都被封装到Executors这个类中:ExecutorService es = Executors.newXXXX();

4:ScheduledThreadPool:放入ScheduledThreadPool的任务可以定期反复执行。内部维护一个DelayedWorkQueue,本质为一个优先队列,按任务到下次执行的时间间隔长短进行排序,一个任务在当前周期执行完成后不会一直持有线程资源,而是释放线程资源,以到下次执行时间长短为优先级重新入优先队列。注意如果线程池关闭,周期任务终止。

5:java.util.Timer类可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务

6:线程池线程数量的设置:

7:ThreadPoolExecutor

Executors.newXXXThreadPool()去创建线程池的底层实现中允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。前面几种方法内部也是通过ThreadPoolExecutor方式实现。提交执行:pool.execute(new ThreadTask());

Future

1,Runnable接口的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取。Java标准库还提供了一个Callable接口,它多了一个返回值:

2,ExecutorService.submit()方法返回了一个Future类型,一个Future类型的实例代表一个未来能获取结果的对象:

3,定义的方法有:

CompletableFuture

1,Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,主线程也会被迫等待。CompletableFuture针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,在任务线程中自动调用回调对象的回调方法。

2,多个CompletableFuture可以串行执行,串行化时各个CompletableFuture重用一个线程

3,多个CompletableFuture还可以并行执行

anyOf()可以实现“任意个CompletableFuture只要一个成功就执行下一步”,allOf()可以实现“所有CompletableFuture都必须成功后才进行下一步。用于并行化多个CompletableFuture

CompletableFuture的命名规则:

并行转串行:合并时重用并行任务线程中第一个完成任务的线程;

串行转并行:构建新的并行化线程时第一个并行线程重用串行线程。

ForkJoin

1,Fork/Join任务的原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTaskRecursiveAction。使用Fork/Join模式可以进行并行计算以提高效率。

2,

ThreadLocal

1,在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

ThreadLocal,它可以在一个线程中传递同一个对象,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。在JDK的实际代码中,为了提高速度,每个Thread自带一个Map,用ThreadLocal作为key存取Thread自己的Map,目的是避免单个全局Map多个线程访问带来的加锁问题。

了保证能释放ThreadLocal关联的实例,可以通过AutoCloseable接口配合try (resource) {...}结构,让编译器自动为我们关闭。

锁机制

参考

1,乐观锁与悲观锁

悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功。

悲观锁阻塞事务,乐观锁回滚重试,乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

乐观锁允许多个线程同时读取,但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已,java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,而是一个在循环里尝试CAS的算法。

2,自旋锁

就是一个 while(true) 无限循环,

只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

3,互斥锁

必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行。

现在的问题是资源状态属于业务逻辑代码,比如通过锁对资源列表进行存取操作,列表是否为空不能由能否获得锁衡量,需要不断判断,属于另一种自旋锁。

4,条件变量

使用条件变量,在资源不可用时线程在该资源的等待队列中,在资源可用时通知等待的线程,消费者线程不用不断判断条件是否成立。

 

5,synchronized锁

当加synchronized锁后锁对象会从无锁升级为偏向锁,再升级为轻量级锁(本质为自旋锁),最后升级为重量级锁。

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。此时并非只存在一个线程,当一个线程执行完同步去代码后,不主动释放锁,此时如果另外一个线程请求获得锁,则将空闲的锁给请求锁的线程,该线程执行完后同样不主动释放锁。此时虽然存在多个请求一个锁的线程,但他们之间能轮流获得锁,不用等待,并不构成竞争关系。

偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。之前是线程A持有偏向锁,现在当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,此时偏向锁升级为轻量级锁,然后虚拟机会让线程A尽快在安全点挂起,将线程A暂停,然后在它的栈中“伪造”一些信息,让线程A在被唤醒之后,认为自己一直持有的是轻量级锁。如果线程A被暂停之前正在同步代码块中,那么线程A苏醒后正常执行,执行完同步代码后主动释放锁,线程B自旋等待A释放锁即可。如果线程A被暂停之前不在同步代码块中,它会在被唤醒后检查到这一情况并立即释放锁,让线程B可以拿到,B立即执行。如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。(锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。)在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

忙等是有限度的(锁有一个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

6,可重入锁(递归锁) 可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作。JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

7,公平锁、非公平锁 如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他任务优先级排序的。对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大(按优先级排序,重要任务优先处理,而不是先来后到,能获得更大吞吐量),如果没有特殊要求,优先使用非公平锁。对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。

8,可中断锁:可以响应中断的锁

Java并没有提供任何直接中断某线程的方法,中断不能直接终止线程,只提供了中断机制,需要被中断的线程自己决定怎么处理。线程A向线程B发出“停止运行”的请求( interrupt()),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断(isInterrupted()),也可以直接忽略此中断。

如果线程A持有锁,线程B等待获取该锁。线程A持有锁,线程B申请锁,线程B加入锁对应的等待队列,由于线程A持有锁的时间过长,线程B不想继续等待,可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。

synchronized就是不可中断锁,在该锁等待队列的线程不可中断,而Lock的实现类都是可中断锁。

9,读写锁

很多情况下,线程知道自己读取数据后,是否是为了更新它。如果读取值是为了更新它,那么加锁的时候就直接加写锁,当前线程持有写锁的时候别的线程无论读还是写都需要等待;如果读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1),当读者数量为0时释放读锁。

读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程

AQS

原理

抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架

https://www.cnblogs.com/waterystone/p/4920797.html

https://www.cnblogs.com/wang-meng/p/12816829.html

https://developer.aliyun.com/article/779674#slide-16

https://pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html

image-20220402163429679

1,维护了一个volatile int state(代表共享资源,使用volatile修饰,保证多线程间的可见性。采用乐观锁思想的CAS算法,保证原子性操作。)和一个FIFO线程同步队列(多线程争用资源被阻塞此线程以及等待的状态等信息封装成Node加入到队列中,同时阻塞该线程,等待后续的被唤醒)。

2,Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身、前后节点、以及Node结点的等待状态(CANCELLED:表示当前结点已取消调度、SIGNAL:表示后继结点在等待当前结点唤醒、CONDITION:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁)

3,AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

独占式:以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后其他线程再tryAcquire()时就会失败,被park()操作挂起,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

共享式:即共享资源可以被多个线程同时占有,直到共享资源被占用完毕。以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

Condition

1,相比使用Object的wait()、notify(),使用Condition中的await()、signal()这种方式实现线程间协作更加安全和高效,每个lock可以对应多个condition,每个 Condition 对象都包含着一个 FIFO 队列,实现更精细控制,比如生产者消费者模型下生产满了就fullCondition.await()等待产品被消费后唤醒,往仓库添加元素后emptyCondition.signal()唤醒等待产品的线程。消费者消费产品后fullCondition.signal()唤醒等待空位的生产者,当产品空时emptyCondition.await()等待有产品后被唤醒。

image-20220402194810792

2,Condition可以精准的对多个不同条件进行控制,Lock只能唤醒一个或者全部的等待队列。Condition需要使用Lock进行控制,使用的时候要注意lock()后及时的unlock()。

Synchronize、ReentrantLock