Java多线程:从基本概念到避坑指南(1)

如下面的代码,我们在调用lock之后,发生了异常,try中的执行逻辑将被中断,unlock将永远没有机会执行。在这种情况下,线程获取的锁资源,将永远无法释放。

private final Lock lock = new ReentrantLock();

void doJob(){

try{

lock.lock();

//发生了异常

lock.unlock();

}catch(Exception e){

}

}

正确的做法,就是将unlock函数,放到finally块中,确保它总是能够执行。

由于lock也是一个普通的对象,是可以作为函数的参数的。如果你把lock在函数之间传来传去的,同样会有时序逻辑混乱的情况。在平时的编码中,也要避免这种把lock当参数的情况。

2.3. wait要包两层

Object作为Java的基类,提供了四个方法wait wait(timeout) notify notifyAll ,用来处理线程同步问题,可以看出wait等函数的地位是多么的高大。在平常的工作中,写业务代码的同学使用这些函数的机率是比较小的,所以一旦用到很容易出问题。

但使用这些函数有一个非常大的前提,那就是必须使用synchronized进行包裹,否则会抛出IllegalMonitorStateException。比如下面的代码,在执行的时候就会报错。

final Object condition = new Object();

public void func(){

condition.wait();

}

类似的方法,还有concurrent包里的Condition对象,使用的时候也必须出现在lock和unlock函数之间。

为什么在wait之前,需要先同步这个对象呢?因为JVM要求,在执行wait之时,线程需要持有这个对象的monitor,显然同步关键字能够完成这个功能。

但是,仅仅这么做,还是不够的,wait函数通常要放在while循环里才行,JDK在代码里做了明确的注释。

重点:这是因为,wait的意思,是在notify的时候,能够向下执行逻辑。但在notify的时候,这个wait的条件可能已经是不成立的了,因为在等待的这段时间里条件条件可能发生了变化,需要再进行一次判断,所以写在while循环里是一种简单的写法。

final Object condition = new Object();

public void func(){

synchronized(condition){

while(<条件成立>){

condition.wait();

}

}

}

带if条件的wait和notify要包两层,一层synchronized,一层while,这就是wait等函数的正确用法。

2.4. 不要覆盖锁对象

使用synchronized关键字时,如果是加在普通方法上的,那么锁的就是this对象;如果是加载static方法上的,那锁的就是class。除了用在方法上,synchronized还可以直接指定要锁定的对象,锁代码块,达到细粒度的锁控制。

如果这个锁的对象,被覆盖了会怎么样?比如下面这个。

List listeners = new ArrayList();

void add(Listener listener, boolean upsert){

synchronized(listeners){

List results = new ArrayList();

for(Listener ler:listeners){

}

listeners = results;

}

}

上面的代码,由于在逻辑中,强行给锁listeners对象进行了重新赋值,会造成锁的错乱或者失效。

为了保险起见,我们通常把锁对象声明成final类型的。

final List listeners = new ArrayList();

或者直接声明专用的锁对象,定义成普通的Object对象即可。

final Object listenersLock = new Object();

2.5. 处理循环中的异常

在异步线程里处理一些定时任务,或者执行时间非常长的批量处理,是经常遇到的需求。我就不止一次看到小伙伴们的程序执行了一部分就停止的情况。

排查到这些中止的根本原因,就是其中的某行数据发生了问题,造成了整个线程的死亡。

我们还是来看一下代码的模板。

volatile boolean run = true;

void loop(){

while(run){

for(Task task: taskList){

//do . sth

int a = 1/0;

}

}

}

在loop函数中,执行我们真正的业务逻辑。当执行到某个task的时候,发生了异常。这个时候,线程并不会继续运行下去,而是会抛出异常直接中止。在写普通函数的时候,我们都知道程序的这种行为,但一旦到了多线程,很多同学都会忘了这一环。

值得注意的是,即使是非捕获类型的NullPointerException,也会引起线程的中止。所以,时刻把要执行的逻辑,放在try catch中,是个非常好的习惯。

volatile boolean run = true;

void loop(){

while(run){

for(Task task: taskList){

try{

//do . sth

int a = 1/0;

}catch(Exception ex){

//log

}

}

}

}

2.6. HashMap正确用法

HashMap在多线程环境下,会产生死循环问题。这个问题已经得到了广泛的普及,因为它会产生非常严重的后果:CPU跑满,代码无法执行,jstack查看时阻塞在get方法上。

至于怎么提高HashMap效率,什么时候转红黑树转列表,这是阳春白雪的八股界话题,我们下里巴人只关注怎么不出问题。

网络上有详细的文章描述死循环问题产生的场景,大体因为HashMap在进行rehash时,会形成环形链。某些get请求会走到这个环上。JDK并不认为这是个bug,虽然它的影响比较恶劣。

如果你判断你的集合类会被多线程使用,那就可以使用线程安全的ConcurrentHashMap来替代它。

HashMap还有一个安全删除的问题,和多线程关系不大,但它抛出的是ConcurrentModificationException,看起来像是多线程的问题。我们一块来看看它。

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

map.put(“xjjdog0”, “狗1”);

map.put(“xjjdog1”, “狗2”);

for (Map.Entry<String, String> entry : map.entrySet()) {

String key = entry.getKey();

if (“xjjdog0”.equals(key)) {

map.remove(key);

}

}

上面的代码会抛出异常,这是由于HashMap的Fail-Fast机制。如果我们想要安全的删除某些元素,应该使用迭代器。

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();

while (iterator.hasNext()) {

Map.Entry<String, String> entry = iterator.next();

String key = entry.getKey();

if (“xjjdog0”.equals(key)) {

iterator.remove();

}

}

2.7. 线程安全的保护范围

使用了线程安全的类,写出来的代码就一定是线程安全的么?答案是否定的。

线程安全的类,只负责它内部的方法是线程安全的。如我我们在外面把它包了一层,那么它是否能达到线程安全的效果,就需要重新探讨。

比如下面这种情况,我们使用了线程安全的ConcurrentHashMap来存储计数。虽然ConcurrentHashMap本身是线程安全的,不会再出现死循环的问题。但addCounter函数,明显是不正确的,它需要使用synchronized函数包裹才行。

private final ConcurrentHashMap<String,Integer> counter;

public int addCounter(String name) {

Integer current = counter.get(name);

int newValue = ++current;

counter.put(name,newValue);

return newValue;

}

这是开发人员常踩的坑之一。要达到线程安全,需要看一下线程安全的作用范围。如果更大维度的逻辑存在同步问题,那么即使使用了线程安全的集合,也达不到想要的效果。

2.8. volatile作用有限

volatile关键字,解决了变量的可见性问题,可以让你的修改,立马让其他线程给读到。

虽然这个东西在面试的时候问的挺多的,包括ConcurrentHashMap中队volatile的那些优化。但在平常的使用中,你真的可能只会接触到boolean变量的值修改。

volatile boolean closed;

public void shutdown() {

closed = true;

}

千万不要把它用在计数或者线程同步上,比如下面这样。

volatile count = 0;

void add(){

最后

既已说到spring cloud alibaba,那对于整个微服务架构,如果想要进一步地向上提升自己,到底应该掌握哪些核心技能呢?

就个人而言,对于整个微服务架构,像RPC、Dubbo、Spring Boot、Spring Cloud Alibaba、Docker、kubernetes、Spring Cloud Netflix、Service Mesh等这些都是最最核心的知识,架构师必经之路!下图,是自绘的微服务架构路线体系大纲,如果有还不知道自己该掌握些啥技术的朋友,可根据小编手绘的大纲进行一个参考。

image

如果觉得图片不够清晰,也可来找小编分享原件的xmind文档!

且除此份微服务体系大纲外,我也有整理与其每个专题核心知识点对应的最强学习笔记:

  • 出神入化——SpringCloudAlibaba.pdf

  • SpringCloud微服务架构笔记(一).pdf

  • SpringCloud微服务架构笔记(二).pdf

  • SpringCloud微服务架构笔记(三).pdf

  • SpringCloud微服务架构笔记(四).pdf

  • Dubbo框架RPC实现原理.pdf

  • Dubbo最新全面深度解读.pdf

  • Spring Boot学习教程.pdf

  • SpringBoo核心宝典.pdf

  • 第一本Docker书-完整版.pdf

  • 使用SpringCloud和Docker实战微服务.pdf

  • K8S(kubernetes)学习指南.pdf

image

另外,如果不知道从何下手开始学习呢,小编这边也有对每个微服务的核心知识点手绘了其对应的知识架构体系大纲,不过全是导出的xmind文件,全部的源文件也都在此!

image

记(一).pdf

  • SpringCloud微服务架构笔记(二).pdf

  • SpringCloud微服务架构笔记(三).pdf

  • SpringCloud微服务架构笔记(四).pdf

  • Dubbo框架RPC实现原理.pdf

  • Dubbo最新全面深度解读.pdf

  • Spring Boot学习教程.pdf

  • SpringBoo核心宝典.pdf

  • 第一本Docker书-完整版.pdf

  • 使用SpringCloud和Docker实战微服务.pdf

  • K8S(kubernetes)学习指南.pdf

[外链图片转存中…(img-YEtwLEg7-1714355353294)]

另外,如果不知道从何下手开始学习呢,小编这边也有对每个微服务的核心知识点手绘了其对应的知识架构体系大纲,不过全是导出的xmind文件,全部的源文件也都在此!

[外链图片转存中…(img-L5394mzy-1714355353294)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值