多线程与高并发笔记


1. 创建线程的四种方式
实现Runnable 重写run方法
继承Thread 重写run方法
线程池创建 Executors.newCachedThreadPool()
实现Callable接口
2. Thread线程操作方法
当前线程睡眠指定mills毫秒

Thread.sleep([mills])
当前线程优雅让出执行权

Thread.yield()
例如Thread t1, t2,在t2的run方法中调用t1.join(),线程t2将等待t1完成后执行

join
3. Thread状态
状态 使用场景
NEW Thread被创建之后,未start之前
RUNNABLE 在调用start()方法之后,这也是线程进入运行状态的唯一一种方式。
具体分为ready跟running,当线程被挂起或者调用Thread.yield()的时候为ready
WAITING 当一个线程执行了Object.wait()的时候,它一定在等待另一个线程执行Object.notify()或者Object.notifyAll()。
或者一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会等待该线程执行完成。当一个线程执行了LockSupport.park()的时候,其在等待执行LockSupport.unpark(thread)。当该线程处于这种等待的时候,其状态即为WAITING。需要关注的是,这边的等待是没有时间限制的,当发现有这种状态的线程的时候,若其长时间处于这种状态,也需要关注下程序内部有无逻辑异常。
TIMED_WAITING
这个状态和WAITING状态的区别就是,这个状态的等待是有一定时效的
Thread.sleep(long)
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()
BLOCKED 在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态
TERMINATED 线程执行结束之后的状态。
线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

4. synchronized
锁住的是对象而不是代码
this 等价于 当前类.class
锁定方法,非锁定方法同时进行
锁在执行过程中发生异常会自动释放锁
synchronized获得的锁是可重入的
锁升级 偏向锁-自旋锁-重量级锁
synchronized(object)不能用String常量/Integer,Long等基本数据类型
锁定对象的时候要保证对象不能被重写,最好加final定义
4. volatile
保证线程可见性
禁止指令重排序
volatile并不能保证多个线程修改的一致性,要保持一致性还是需要synchronized关键字
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
volatile关 键字只能用于变量而不可以修饰方法以及代码块
5. synchronized与AtomicLong以及LongAdder的效率对比
Synchronized 是需要加锁的,效率偏低;
AtomicLong 不需要申请锁,使用CAS机制;
LongAdder 使用分段锁,所以效率好,在并发数量特别高的时候,LongAdder最合适

6. ConcurrentHashMap的分段锁原理
分段锁就是将数据分段上锁,把锁进一步细粒度化,有助于提升并发效率。
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

7. ReentrantLock
ReentrantLock可以替代synchronized
但是ReentrantLock必须手动开启锁/关闭锁,synchronized遇到异常会自动释放锁,ReentrantLock需要手动关闭,一般都是放在finally中关闭
定义锁 Lock lock = new ReentrantLock();
开启 lock.lock();
关闭 lock.unlock();
使用Reentrantlock可以进行“尝试锁定”tryLock,这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待。
使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
可以根据tryLock的返回值来判定是否锁定
也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中,如果tryLock未锁定,则不需要unlock
使用ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做出响应,在一个线程等待锁的过程中,可以被打断
new ReentrantLock(true) 表示公平锁,不带参数默认为false,非公平锁

8. CountDownLatch
countDownLatch这个类可以使一个线程等待其他线程各自执行完毕后再执行。
是通过一个计数器来实现的,计数器的初始值是线程的数量。当调用countDown()方法后,每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

线程中调用countDown()方法开始计数;
在调用await()方法的线程中,当计数器为0是后续才会继续执行,否则一直等待;
也可以使用latch.await(timeout, unit)在等待timeout时间后如果计数器不为0,线程仍将继续。
countDown()之后的代码不受计数器控制
与join区别,使用join的线程将被阻塞,使用countDown的线程不受影响,只有调用await的时候才会阻塞

8. CyclicBarrier
作用就是会让指定数量的(数量由构造函数指定)所有线程都等待完成后才会继续下一步行动。
构造函数:
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
parties 是线程的个数;
barrierAction为最后一个到达线程要做的任务

所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

实现原理:在CyclicBarrier的内部定义了一个Lock对象,每当一个线程调用await方法时,将拦截的线程数减1,然后判断剩余拦截数是否为初始值parties,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁。

9. Phaser
可重复使用的同步屏障,功能类似于CyclicBarrier和CountDownLatch,但支持更灵活的使用。

Phaser使我们能够建立在逻辑线程需要才去执行下一步的障碍等。

我们可以协调多个执行阶段,为每个程序阶段重用Phaser实例。每个阶段可以有不同数量的线程等待前进到另一个阶段。我们稍后会看一个使用阶段的示例。

要参与协调,线程需要使用Phaser实例 register() 本身。请注意:这只会增加注册方的数量,我们无法检查当前线程是否已注册 – 我们必须将实现子类化以支持此操作。

线程通过调用 arriAndAwaitAdvance() 来阻止它到达屏障,这是一种阻塞方法。当数量到达等于注册的数量时,程序的执行将继续,并且数量将增加。我们可以通过调用getPhase()方法获取当前数量。

10. ReadWriteLock
ReadWriteLock的具体实现是ReentrantReadWriteLock

ReadWriteLock允许分别创建读锁跟写锁

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
1
2
3
使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。
ReadWriteLock可以保证:

只允许一个线程写入(其他线程既不能写入也不能读取);
没有写入时,多个线程允许同时读(提高性能)
读写分离锁可以有效地帮助减少锁竞争,以提高系统性能,读写锁读读之间不互斥,读写,写写都是互斥的

11. Semaphore
Semaphore 是一个计数信号量,必须由获取它的线程释放。常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。

对于Semaphore来说,它要保证的是资源的互斥而不是资源的同步,在同一时刻是无法保证同步的,但是却可以保证资源的互斥。只是限制了访问某些资源的线程数,其实并没有实现同步。

常用方法:
1、acquire(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。

2、release(int permits)

释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少

3、availablePermits()

返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。

4、reducePermits(int reduction)

根据指定的缩减量减小可用许可的数目。

5、hasQueuedThreads()

查询是否有线程正在等待获取资源。

6、getQueueLength()

返回正在等待获取的线程的估计数目。该值仅是估计的数字。

7、tryAcquire(int permits, long timeout, TimeUnit unit)

如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。

8、acquireUninterruptibly(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。

12. Exchanger
用于两个工作线程之间交换数据的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。其定义为 Exchanger 泛型类型,其中 V 表示可交换的数据类型,对外提供的接口很简单,具体如下:

Exchanger():无参构造方法。

V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。

V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。

13. LockSupport
LockSupport 是一个非常方便实用的线程阻塞工具,他可以在任意位置让线程阻塞。

LockSupport 的静态方法 park()可以阻塞当前线程,类似的还有 parkNanos(),parkUntil()等,他们实现了一个限时的等待。

方法 描述
void park(): 阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
void park(Object blocker) 功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
void parkNanos(long nanos) 阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性;
void parkNanos(Object blocker, long nanos) 功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
void parkUntil(long deadline) 阻塞当前线程,直到deadline;
void parkUntil(Object blocker, long deadline) 功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
同样的,有阻塞的方法,当然有唤醒的方法,什么呢?unpark(Thread) 方法。该方法可以将指定线程唤醒。

需要注意的是:park 方法和 unpark 方法执行顺序不是那么的严格。比如我们在 Thread 类中提到的 suspend 方法 和resume 方法,如果顺序错误,将导致永远无法唤醒,但 park 方法和 unpark 方法则不会,因为 LockSupport 使用了类似信号量的机制。他为每一个线程准备了一个许可(默认不可用),如果许可能用,那么 park 函数会立即返回,并且消费这个许可(也就是将许可变为不可用),如果许可不可用,将会阻塞。而 unpark 方法则使得一个许可变为可用

14. AQS
AQS 为 AbstractQueuedSynchronizer 的简称

AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。
这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类。
AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。
#比如
Semaphore 用它来表现剩余的许可数,
ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;
FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)

使用须知
Usage
To use this class as the basis of a synchronizer, redefine the following methods, as applicable, by inspecting and/or modifying the synchronization state using {@link #getState}, {@link #setState} and/or {@link #compareAndSetState}:

{@link #tryAcquire}
{@link #tryRelease}
{@link #tryAcquireShared}
{@link #tryReleaseShared}>
{@link #isHeldExclusively}
以上方法不需要全部实现,根据获取的锁的种类可以选择实现不同的方法:
支持独占(排他)获取锁的同步器应该实现tryAcquire、 tryRelease、isHeldExclusively;
支持共享获取锁的同步器应该实现tryAcquireShared、tryReleaseShared、isHeldExclusively。

AQS浅析
AQS的实现主要在于维护一个”volatile int state”(代表共享资源)和
一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
队列中的每个节点是对线程的一个封装,包含线程基本信息,状态,等待的资源类型等。

#state的访问方式有三种:
getState()
setState()
compareAndSetState()

#AQS定义两种资源共享方式
Exclusive(独占,只有一个线程能执行,如ReentrantLock)
Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
不同的自定义同步器争用共享资源的方式也不同。
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,
至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例

state初始化为0,表示未锁定状态。

A线程lock()时,会调用tryAcquire()独占该锁并将state+1。

此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。

当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。

但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

以CountDownLatch以例

任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。

这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。

等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,

他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

但AQS也支持自定义同步器同时实现独占和共享两种方式,如”ReentrantReadWriteLock”。

15. 锁基本概念
公平锁/非公平锁
可重入锁
独享锁/共享锁
互斥锁/读写锁
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,
有可能后申请的线程比先申请的线程优先获取锁;
有可能会造成优先级反转或者饥饿现象。

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。

非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。
由于其并不像ReentrantLock是通过AQS的来实现线程调度,
所以并没有任何办法使其变成公平锁。

可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

ReentrantLock, Synchronized都是可重入锁。

可重入锁的一个好处是可一定程度避免死锁

独享(排他)锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。

共享锁是指该锁可被多个线程所持有。

对于ReentrantLock而言,其是独享锁。

但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

对于Synchronized而言,当然是独享锁。

互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock

读写锁在Java中的具体实现就是ReadWriteLock

乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁 (Synchronized 和 ReentrantLock)

认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。

因此对于同一个数据的并发操作,悲观锁采取加锁的形式。
悲观的认为,不加锁的并发操作一定会出问题。

乐观锁 (java.util.concurrent.atomic包)

认为对于同一个数据的并发操作,是不会发生修改的。
在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。
乐观的认为,不加锁的并发操作是没有事情的。

悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,

不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。

乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法。
典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,ConcurrentHashMap并发的实现就是通过分段锁的形式来实现高效的并发操作。

ConcurrentHashMap中的分段锁称为Segment,
它类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,
即内部拥有一个Entry数组,数组中的每个元素又是一个链表;
同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,
而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,
所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,
就仅仅针对数组中的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。
在Java 5通过引入锁升级的机制来实现高效Synchronized。
这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁

是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁

是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,
其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁

是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,
当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。
重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,
这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现.

原创文章,作者:3628473679,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/245584.html

(0)
上一篇 2022年4月18日 14:37
下一篇 2022年4月18日 14:37

相关推荐

发表回复

登录后才能评论