回眸重探锁机制(2)

独占锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。可以基于AQS来实现

2.5 互斥锁/读写锁

这和独占锁、共享锁很类似;

  • 互斥锁:独占锁,在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
  • 读写锁既是互斥锁,又是共享锁;处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。

2.6 可重入锁/不可重入锁

这个概念主要针对一个线程递归调用锁时,是否可以重复获取锁而不发生死锁;可以即为可重入锁,否则为不可重入锁;基于AQS和Synchronized关键字实现的均为可重入锁

2.7 公平锁/非公平锁

俗语理解,公平就是先到先得;不公平就是来的早不如来的巧,以及排队的按照排队顺序

公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

2.8 分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率

在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护

3 Thread类

多线程机制是由java虚拟机支持的,线程是实际计算任务的载体,由java虚拟机执行,用户往往创建新的线程来进行异步处理。java虚拟机需要线程存在,才能真正为用户工作。Thread类即为线程载体,调用start方法由native层进行创建以及其它的管理调用;线程执行方法:

默认实现

public void run() {
if (target != null) {
target.run();
}
}

也就是构造器中传入的Runnable接口实例中run方法即为执行主体;

常用方法

image.png

其它方法一些废弃了;一些我们平时很少用到;就比如上面的打断状态方法,其实是通过InterruptedException异常来触发的;

线程异常处理

采用UncaughtExceptionHandler机制进行处理;其android.jar包中处理流程如下

Thread类中

public final void dispatchUncaughtException(Throwable e) {
Thread.UncaughtExceptionHandler initialUeh =
Thread.getUncaughtExceptionPreHandler();
if (initialUeh != null) {
try {
initialUeh.uncaughtException(this, e);
} catch (RuntimeException | Error ignored) {
}
}
getUncaughtExceptionHandler().uncaughtException(this, e);
}

public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}

ThreadGroup类

public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread “”

  • t.getName() + “” ");
    e.printStackTrace(System.err);
    }
    }
    }

也就是优先级: Thread.getUncaughtExceptionPreHandler() | thread.getUncaughtExceptionHandler() > threadGroup.uncaughtException > Thread.getDefaultUncaughtExceptionHandler()

我们自定义时,可以进行如下几种方式:

  • Thread 静态方法setUncaughtExceptionPreHandler(UncaughtExceptionHandler eh),所有异常均会经过它的处理,但是不会抛出运行时异常和Error异常
  • Thread成员函数setUncaughtExceptionHandler(UncaughtExceptionHandler eh),仅仅针对当前线程
  • ThreadGroup重写void uncaughtException(Thread t, Throwable e),针对整个线程组处理
  • Thread静态方法setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh),针对所有线程,最后处理的默认方法

4 LockSupport类

LockSupport 和 CAS 是 Java 并发包中很多并发工具控制机制的基础,它们底层其实都是依赖 Unsafe 实现,而Unsafe类不是直接提供给android开发,因此,我们构建线程安全只能依赖原子类和LockSupport类,而系统却可以使用Unsafe类

其操作针对线程,也就是线程的暂停也重新启动,而且相对Thread里面的stop、resume方法,其对线程的操作是安全的;这里着重强调下是针对线程的暂停和启动,其它功能是没有的,没有其它功能,没有其它功能(不要意想,多想)

常用方法 image.png

其park、unpark方法,都在对一个称作"许可"的东西,进行生产和消费,而其消费的地方,必须在本线程,生产的地方在其它均可,且只有在需要消费的时候生产,不重复生产;

5 原子类

首先需要连接一个概念:原子性,指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。而我们平时编写的代码,虽然是一行或者自己认为一次执行,但机器处理时并不一定时一步操作;

这些原子操作都在java.util.concurrent.atomic包下,其均是利用sun.misc.Unsafe来处理,其用法也是套路操作,也即是通过Unsafe类来操作值的方法,基本都是原子操作处理;特别的

boolean compareAndSet(T expect, T update)

expect当前值,update修改置,返回是否成功修改,也就是如果当前值与实际值是一致的,那么修改会成功;不难发现,这可能会发生 a-b-a过程而继续修改成功;这个可以对数据加版本或者时间戳进而区别,而AtomicStampedReference就是为了解决这个问题

6 AQS实现

这里不会有很多源码分析,有兴趣的可以查看我AbstractQueuedSynchronizer原理解析;这里会从以下几个方面来介绍

  1. 锁数据结构
  2. 获取锁、释放锁流程

我把锁分为独占锁、共享锁、条件锁,其实条件锁也是一种独占锁;但这里的独占锁也并不绝对,是因为,AQS本身是抽象类,而控制获取、释放的逻辑却留待用户实现;

6.1 数据结构

静态内部类Node即为AQS的数据结构,具体如下:

image.png 也就是队列为双向链表;而nextWaiter,是为条件锁准备的向后单链表;这个单链表各个部分的意义还是值得思考的

  • prev、next这两个指针域,就是排队等待的线程节点

  • 数据域线程,方便使用LockSupport类进行暂停恢复

  • 数据域等待状态:取消态、默认态、通知态、条件态以及传播态

  1. 取消态:线程执行异常或者被打断
  2. 通知态:当前线程等待被唤醒,去竞争资源
  3. 条件态:说明现在资源获取需要的额外条件不满足,需要等待
  4. 传播态:共享允许获取资源时,从当前node或者从头连续的node均可共享获取资源
  • nextWaiter:有两种职责;如果为空时标识独占节点,为SHARED时表示共享节点,其它值时表示条件等待节点数据结构(后面介绍条件锁时,会把这个结构单独列出)

6.2 独占锁

6.2.1 获取资源

可以通过下面几种方法来获取

  • void acquire(int arg):获取锁,arg一般来说是1

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

  • void acquireInterruptibly(int arg):获取锁,抛出打断异常

public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

  • boolean tryAcquireNanos(int arg, long nanosTimeout):获取锁存在最长时间,抛出打断异常

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

可见,如果tryAcquire返回true时,相应方法也就结束了,线程后续方法即可继续执行,也就是获取锁了;而如果返回false时,后面方法则很大可能会触发线程暂停

protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

tryAcquire方法需要用户自实现;现有锁都是通过AQS中state整数变量来实现的

6.2.2 释放资源

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

这里tryRelease返回true时,才有可能通知其它线程去竞争资源;

protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

需要用户自实现;现有锁都是通过AQS中state整数变量来实现的

6.3 独占锁

6.3.1 获取资源

同样存在不同的入口

  • void acquireShared(int arg):获取共享锁

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

  • void acquireSharedInterruptibly(int arg):获取共享锁,会抛出打断异常

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

  • boolean tryAcquireSharedNanos(int arg, long nanosTimeout):获取锁有时间限制,超过则结束返回是否获取成功;会抛出打断异常

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
}

这里获取资源的条件是受 tryAcquireShared的返回值来控制的,>=0时,线程继续执行,否则,很可能会线程暂停; tryAcquireShared方法需要用户实现,也会通过AQS中state变量来处理

protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}

6.3.2 释放资源

释放时也只有一个入口

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

tryReleaseShared返回true时,才有可能去唤醒其它线程去竞争资源;也需要用户实现,同样,也是基于AQS中state变量来控制

protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}

6.4 可重入

可重入性,是持有资源者,再次自动持有资源的行为;这些在独占锁锁中,只需要线程判断即可,而共享锁,则是不仅仅需要线程判断,还需要一些列共享持有者单独比对,无疑当前线程比对可以优化这个过程;线程的存储AQS已经提供方法,这些方法来源于其继承的抽象类AbstractOwnableSynchronizer,而是否持有,则由方法isHeldExclusively提供,而这个方法需要用户实现

protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}

6.5 数据变化

数据变化,涉及到几个重要方法:

  • addWaiter:从双端队列,队尾加入节点;线程获取资源失败时,会生成节点并加入队列;而条件锁在等待唤醒时被唤醒后,通过enq方法加入队列
  • shouldParkAfterFailedAcquire:决定是否暂停当前线程,在循环中处理
  • parkAndCheckInterrupt:暂停线程,并返回打断状态且重置打断状态
  • unparkSuccessor:唤醒等待资源的线程,进行资源竞争获取
6.5.1 添加节点

独占资源

image.png

共享资源

image.png

条件资源

image.png

6.5.2 线程执行异常或者被打断

独占资源

image.png

共享资源

image.png

6.5.3 等待状态

相对于5.5.2,其中变化的仅仅waitStatus = -1

6.5.4 条件唤醒状态

这是条件锁特有状态;实现原理是:首先节点在条件单列表条件链表中等待;两个等待循环条件为是否在资源竞争的双向列表中(就是独占/共享的队列),如果被唤醒,其就会在此队列了,然后调用独占锁的逻辑获取锁 其关键方法:

  • isOnSyncQueue:是否存在AQS中的双向队列中,被唤醒后存在
  • doSignal:移除条件单向列表列中头节点,并改变状态waitStatus=0,加入AQS双向列表中去
6.6 条件锁使用条件

final int fullyRelease(Node node) {
try {
int savedState = getState();
if (release(savedState))
return savedState;
throw new IllegalMonitorStateException();
} catch (Throwable t) {
node.waitStatus = Node.CANCELLED;
throw t;
}
}

这个方法是在获取资源时均会调用的方法;会首先释放state个资源使用权,然后通过加入队列后通过相应方法获取state个使用权;释放不了state个使用权会直接报异常;因此,条件锁使用时,必须在当前已经获取资源使用权的情况使用下,且仅仅只有其自己获取资源使用权;

6.7 小结

其在性能上有许多要学习的地方:比如在获取资源执行权时,并没有立刻去暂停线程;在状态变化过程中,也没有仅仅考虑当前状态,也进行有可能的唤醒线程竞争、去除无效资源等;整体采用自旋+CAS机制处理;

7 android中的锁

7.1 synchronized关键字

上面介绍锁概念时大致介绍了这个关键字,其通过编译时在代码块加入同步指令处理的;是以特殊的一个对象作为锁的标志:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

通过锁对象来实现条件策略;这写方法来源于Object的wait/notify/notifyAll方法

7.2 ReentrantLock锁

可重入锁、独占锁;有两种模式:公平、非公平,可通过构造器进行设定;公平不公平,就是在获取资源执行权时,是否按照排队顺序优先获取来定的

公平锁

final void lock() {
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
return false;
}

这里有两种获取锁的方式:

  1. 当前state=0,AQS队列中无排队线程,且CAS操作state成功,则把记录当前线程
  2. 当前线程为上次纪律线程,则把state再次增加

为什么公平,就是因为,可获取资源时,先检查当前排队队列是否为空,为空才会获取

非公平锁

final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error(“Maximum lock count exceeded”);
setState(nextc);
return true;
}
return false;
}

获取锁资源成功条件

  1. 对state进行CAS操作,成功;记录线程
  2. 如果当前资源可被获取也即state = 0时,对state进行CAS操作成功;记录线程
  3. 当前线程为记录线程

唤醒竞争

protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState©;
return free;
}

state为0时,才会唤醒其它线程进行竞争;而不为0时,只是进行减少,这于可重入处理时相加相对应;所以加锁和解锁要成对出现

7.3 ReentrantReadWriteLock锁

可重入锁;独占锁和共享锁共存; 读写锁;其读锁调用AQS的共享资源方法,写锁调用AQS独占资源方法;其也存在两种模式:公平、非公平,可通过构造器进行设定;

公平/非公平模式

  1. 对于写锁,公平时查看当前是否排队的,排队优先,非公平时,尝试获取资源的线程优先
  2. 对于读锁,公平模式时同样查看当前是否有排队的,排队优先,非公平时,根据排队等待的对头是否为独占节点

也即是依靠如下方法:返回true表示公平模式

abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock()

条件锁

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

这次面试问的还是还是有难度的,要求当场写代码并且运行,也是很考察面试者写代码
因为Android知识体系比较庞大和复杂的,涉及到计算机知识领域的方方面面。在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

总结

这次面试问的还是还是有难度的,要求当场写代码并且运行,也是很考察面试者写代码
因为Android知识体系比较庞大和复杂的,涉及到计算机知识领域的方方面面。在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-ICEllwMt-1713535365875)]
里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值