JAVA中J,牧原网络面试分享经验

r.lock();

} finally{

w.unlock();

}

}

} finally{

r.unlock();

}

StampedLock对象分析与应用?


StampedLock类,在JDK1.8时引入,是对读写锁ReadWriteLock的增强,该类优化了读锁、写锁的应用,同时使读写锁之间可以互相转换,实现了更加细粒度的并发控制。StampedLock 支持三种模式,分别是写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是,StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

基于StampedLock 实现一个线程安全的cache对象。其关键代码试下如下:

class StampedMapCache{

private Map<String,Object> map=new HashMap<>();

private StampedLock lock=new StampedLock ();

public void writeObject(String key,Object value){

long stamp=lock.writeLock();

try {

map.put(key, value);

}finally {

lock.unlock(stamp);

}

}

public Object readObject(String key) {

long stamp=lock.readLock();

try {

return map.get(key);

}finally {

Lock.unlock(stamp);

}

}

}

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方 式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻 塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作 都被阻塞。 注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操 是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

StampedLock 对象基于乐观读的方式从缓存对象中获取数据。关键代码如下:

public String readWithOptimisticLock(String key) {

long stamp = lock.tryOptimisticRead();

String value = map.get(key);

if(!lock.validate(stamp)) {

stamp = lock.readLock();

try {

return map.get(key);

} finally {

lock.unlock(stamp);

}

}

return value;

}

其中:在代码中,首先通过调用 lock对象的tryOptimisticRead()方法 获取了一个 stamp,这里的 tryOptimisticRead() 就是我前面提到的乐观读。需要注意的是,由于 tryOptimisticRead() 是无锁的,因此最后读完之后,还需要再次验证一下是否存在写操作(这个验证操作是通过调 用 validate(stamp) 来实现的),来保证数据的一致性。

StampedLock对象在应用时需要注意如下几个点:

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功。

  • 获取锁时返回一个Stamp值,值为0表示获取失败,其余都表示成功。

  • 释放锁时需要一个Stamp值,这个值必须是和成功获取锁时得到的Stamp值是一致的。

  • StampedLock是不可重入的。(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

  • StampedLock有三种访问模式:Reading,Writing,Optimistic reading。

  • StampedLock支持读锁和写锁的相互转换。

  • 无论写锁还是读锁,都不支持Conditon等待。

总之: 相比ReadWriteLock读写锁,StampedLock通过提供乐观读在多线程多写线程少的情况下可以提供更好的性能,因为乐观读不需要进行CAS设置锁的状态。

Java中的锁对象的最佳应用设置推荐?


我们在使用锁时,要尽量在更新对象的成员变量时加锁,在访问可变的成员变量时加锁,不在调用其他对象的方法时加锁。这三条规则,最后一条你可能会觉得过于严苛。为什么不再访问其它对象方法时加锁呢?因为双重加锁就很有可能会导致死锁。

JUC包中的原子(Atomic)类应用

=============================================================================

Java中的无锁对象应用?


我们先看个案例,关键代码如下:

int count=1;

int count() {

return count++;

}

其中:count() 这个方法不是线程安全的,问题就出在变量count的可见性和count++的原子性上。可见性问题可以用volatile来解决,而原子性问题我们前面一直都是采用的互斥锁方案。但这种方案,性能上会有一定的损失。其实对于简单的原子性问题,还有一种无锁方案。JUC并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。

AtomicLong 类型对象案例应用:

AtomicLong al=new AtomicLong(1);

long atomicCount(){

return al.getAndIncrement();

}

AtomicLong atomicLong=new AtomicLong(0);

LongStream.range(0, 1000)

.parallel()

.forEach((t)->atomicLong.incrementAndGet());

System.out.println(atomicLong.get());

无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、 解锁操作,而加锁、解锁操作本身就消耗性能。同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,可谓绝佳方案。

Java中的无锁对象原理分析?


无锁化实现的原理其实也很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。

简易的CAS算法的实现,关键代码如下:

class SimulatedCAS{

int count;

synchronized int cas(int expect, int newValue){

// 读⽬前 count 的值

int curValue = count;

// ⽐较⽬前 count 值是否 == 期望值

if(curValue == expect){

// 如果是,则更新 count 的值

count = newValue;

}

// 返回写⼊前的值

return curValue;

}

}

说明:原子类的方法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方案。

Java中的无锁对象问题分析?


Java中的无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题,但也会有一些问题,例如:

(1)ABA问题。

对于一个旧的变量值A,线程2将A的值改成B又改成可A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时,对应的版本也做出相应的变化,然后CAS操作时比较一下版本就知道变量有没有发生变化。此时可借助在java的atomic包下的AtomicStampedReference类进行实现。

(2)自旋问题。

无锁对象会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

LongStream.range(0, 1000)

.parallel()

.forEach((t)->longAdder.increment());

System.out.println(longAdder.sumThenReset());

JUC包中的并发工具类应用

=======================================================================

CountDownLatch对象分析及应用


CountDownLatch是一个辅助同步类,用来作计数使用,它的作用有点类似于生活中的倒数计数器,先设定一个计数初始值,当计数降到0时,将会触发一些事件。

CountDownLatch对象的初始计数值,在构造CountDownLatch对象时传入,每调用一次 countDown() 方法,计数值就会减1。线程可以调用CountDownLatch的await方法进入阻塞,当计数值降到0时,所有之前调用await阻塞的线程都会释放。其应用原理如图所示:

在这里插入图片描述

CountDownLatch 应用案例1:

public class TestCountDownLatch01 {

static String content;

public static void main(String[] args)throws Exception {

CountDownLatch cdl=new CountDownLatch(1);

new Thread(new Runnable() {

@Override

public void run() {

content=“helloworld”;

cdl.countDown();

}

}).start();

while(content==null)cdl.await();

System.out.println(content.toUpperCase());

}

}

说明:CountDownLatch的初始计数值一旦降到0,无法重置。如果需要重置,可以考虑使用CyclicBarrier。

CyclicBarrier对象分析及应用


CyclicBarrier可以认为是一个栅栏,其作用是阻挡前行。与CountDownLatch不同是,CyclicBarrier是一个可以循环使用的栅栏,它做的事情就是:让线程到达栅栏时被阻塞(调用await方法),直到到达栅栏的线程数满足指定数量要求时,栅栏才会打开放行。这个应用,其实有点像军训报数,报数总人数满足教官认为的总数时,教官才会安排后面的训练。

CyclicBarrier 对象是让一组线程相互等待,所有的执行结束以后,才继续向后执行,如图所示:

在这里插入图片描述

CyclicBarrier 应用案例分析,关键代码如下:

public class TestCyclicBarrier01{

static CyclicBarrier cBarrier=

new CyclicBarrier(3,new Runnable() {

@Override

public void run() {

System.out.println(“run()”);

}

});

static class SumTask implements Runnable{

@Override

public void run() {

try{

String tName=

Thread.currentThread().getName();

System.out.println(“开始计算:”+tName);

//TimeUnit.SECONDS.sleep(2);

System.out.println(“计算完成:”+tName);

cBarrier.await();

}catch(Exception e){e.printStackTrace();}

}

}

public static void main(String[] args) {

SumTask task=new SumTask();

for(int i=0;i<3;i++){

new Thread(task).start();

}

}

}

CyclicBarrier典型应用是一组任务,它们并行执行工作,然后在进行下一个步骤之前进行等待,直至所有的任务都完成。

Semaphore对象分析及应用


Semaphore,又名信号量,这个类的作用有点类似于“许可证”。有时,我们因为一些原因需要控制同时访问共享资源的最大线程数量,比如出于系统性能的考虑需要限流,或者共享资源是稀缺资源,我们需要有一种办法能够协调各个线程,以保证合理的使用公共资源。

Semaphore维护了一个许可集,其实就是一定数量的“许可证”。

当有线程想要访问共享资源时,需要先获取(acquire)的许可;如果许可不够了,线程需要一直等待,直到许可可用。当线程使用完共享资源后,可以归还(release)许可,以供其它需要的线程使用。

另外,Semaphore支持公平/非公平策略,这和ReentrantLock类似。基于Semaphore实现限流操作,关键代码如下

class LimitService {

private final Semaphore permit = new Semaphore(10, true);

public void process(){

try{

permit.acquire();

//业务逻辑处理

String tName=Thread.currentThread().getName();

System.out.println(tName+“:process”);

try{Thread.sleep(2000);}

catch(Exception e) {e.printStackTrace();}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

permit.release();

}

}

public static void main(String[] args) {

LimitService lService=new LimitService();

for(int i=0;i<30;i++) {

new Thread() {

public void run() {

lService.process();

};

}.start();

}

}

}

说明,当许可数 ≤ 0代表共享资源不可用。许可数 > 0,代表共享资源可用,且多个线程可以同时访问共享资源。

JUC包中的线程池应用

=====================================================================

Java线程池简述


Java中创建线程对象远不像创建一个普通对象那么简单。创建一般的对象,可能仅仅是在JVM的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的API,然后操作系统要为线程分配一系列的资源,这个创建成本一般会很高,所以可以把线程理解为一个重量级的对象,应该避免频繁创建和销毁。Java中为了优化线程对象应用,提供了一些线程池类型的对象。如图所示:

在这里插入图片描述

ThreadPoolExecutor对象应用


JUC包中最核心的线程池类型为ThreadPoolExecutor,其常用构造函数如下:

public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler)

构造函数中参数说明:

1)corePoolSize:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize个人坚守阵地。

2)maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地 加,最多就加到maximumPoolSize个人。当项目闲下来时,就要撤人了,最多能撤到corePoolSize个人。

3)keepAliveTime & unit:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这 个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。

4)workQueue:工作队列,和上面示例代码的工作队列同义。

5)threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

6)handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队 列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过handler这个参数来指定。

ThreadPoolExecutor已经提供了以下4种策略。

1)CallerRunsPolicy:提交任务的线程自己去执行该任务。

  1. AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException

  2. DiscardPolicy:直接丢弃任务,没有任何异常抛出。

  3. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入 到工作队列。

ThreadPoolExecutor 应用案例实现,创建TaskExecutorUtil工具类,然后在类中添加创建线程池和关闭池的方法。

第一步:定义创建池对象的方法。

public static ThreadPoolExecutor doCreateThreadPoolExecutor() {

int corePoolSize=3;

int maximumPoolSize=5;

long keepAliveTime=60;

BlockingQueue workQueue=

new ArrayBlockingQueue<>(3);

RejectedExecutionHandler handler=

new ThreadPoolExecutor.AbortPolicy();

ThreadFactory threadFactory=new ThreadFactory() {

AtomicInteger at=new AtomicInteger(1);

@Override

public Thread newThread(Runnable r) {

return new Thread(r,

“pool-thread->”+at.getAndIncrement());

}

};

ThreadPoolExecutor pExecutor=

new ThreadPoolExecutor(corePoolSize,

maximumPoolSize,

keepAliveTime,

TimeUnit.SECONDS,

workQueue,

threadFactory,

handler);

return pExecutor;

}

第二步:定义关闭池对象的方法。

public static void doCloseExecutor(ThreadPoolExecutor executor) {

try {

System.out.println(“attempt to shutdown executor”);

executor.shutdown();

executor.awaitTermination(5, TimeUnit.SECONDS);

}catch (InterruptedException e) {

System.err.println(“tasks interrupted”);

}finally {

if (!executor.isTerminated()) {

System.err.println(“cancel non-finished tasks”);

}

executor.shutdownNow();

System.out.println(“shutdown finished”);

}

}

shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,新的任务将会被拒绝。但这个方法不会等待提交的任务执行完,我们可以用awaitTermination来等待任务执行完。shutdownNow()方法是线程池处于STOP状态,此时线程池不再接受新的任务,并且会去尝试终止正在执行的任务,然后清空并返回队列。整个过程类似超市或商场关门。

第三步:应用池对象执行任务。

private static void doTestPoolExecutor01() throws Exception {

ThreadPoolExecutor pExecutor =

doCreateThreadPoolExecutor();

Future future =

pExecutor.submit(new Callable() {

@Override

public Integer call() throws Exception {

TimeUnit.SECONDS.sleep(5);

return new Random().nextInt();

}

});

System.out.println("future done? " + future.isDone());

Integer result=future.get();

System.out.println("future done? " + future.isDone());

System.out.println(result);

doCloseExecutor(pExecutor);

}

线程池通过线程执行任务时,假如需要获取任务的执行结果,一般建议使用submit方法,而此方法的返回结果为Futrue类型,此类型常用的方法有5个, 它们分别是取消任务的方法cancel()、判断任务是否已取消的方法isCancelled()、判断任务是否已结束的方法isDone()以及2个获得任务执行结果的get()和get(timeout, unit),其中最后一个get(timeout, unit)支持超时机制。通过Future接口的这5个方法你会发现,我们提交的任务不但能够获取任务执行结果,还可以取消任务。不过需要注意的是,这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。

通过线程池执行批量任务,关键代码如下:

private static void doTestPoolExecutor02() throws Exception {

ThreadPoolExecutor pExecutor = doCreateThreadPoolExecutor();

List<Callable> callables = Arrays.asList(

() -> “task1”,

() -> “task2”,

() -> “task3”);

pExecutor.invokeAll(callables)

.stream()

.map(future -> {

try {

return future.get();

}

catch (Exception e) {

throw new IllegalStateException(e);

}

}).forEach(System.out::println);

doCloseExecutor(pExecutor);

}

ScheduledThreadPoolExecutor对象应用


ScheduledThreadPoolExecutor继承自ThreadPoolExecutor类,其内部将所有的Runnable任务包装成RunnableScheduledFuture类型,用于满足任务的延迟和周期性调度。案例分析如下:

案例一:创建一延迟任务并执行,关键代码如下:

static void doTestSchedule01() {

ScheduledExecutorService executor =

new ScheduledThreadPoolExecutor(1);

Runnable task = () ->

System.out.println("Scheduling: " +

System.nanoTime());

executor.schedule(task, 3, TimeUnit.SECONDS);

}

案例二:创建一按固定频率执行的任务,启动频率与任务执行时长无关,关键代码如下:

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

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

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

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后总结

搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析

最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化

image

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

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

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-J3addRDy-1711954077038)]
[外链图片转存中…(img-rorsppII-1711954077038)]
[外链图片转存中…(img-9lQVScmX-1711954077039)]
[外链图片转存中…(img-1DEZmBPj-1711954077039)]
[外链图片转存中…(img-R42c13EC-1711954077039)]
[外链图片转存中…(img-vgYW4oYI-1711954077039)]

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-JLwg4F6l-1711954077040)]

最后总结

搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析

最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化

[外链图片转存中…(img-uGOMwrq9-1711954077040)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值