Just Do Java

Java 's Blog


  • 首页

  • 分类

  • 作者

  • 归档

  • 关于

SpringBoot2.x 整合 thumbnailator 图片处理

发表于 2020-09-28 | 分类于 SpringBoot

1、序

在实际项目中,有时为了响应速度,难免会对一些高清图片进行一些处理,比如图片压缩之类的,而其中压缩可能就是最为常见的。最近,阿粉就被要求实现这个功能,原因是客户那边嫌速度过慢。借此机会,阿粉今儿就给大家介绍一些一下我做这个功能时使用的 Thumbnailator 库。

阅读全文 »

记一次项目中遇到的坑

发表于 2020-09-28 | 分类于 Java

hello~各位读者好,我是鸭血粉丝(大家可以称呼我为「阿粉」)。今天,阿粉带着大家来了解一下 最近在项目中遇到的一个坑。

阅读全文 »

Redis 实例对比工具之 Redis-full-check

发表于 2020-09-27 | 分类于 Java

Hello 大家好,我是鸭血粉丝,前面一篇文章给大家介绍了 SpringBoot 项目是如何从单机切换接入集群的,没看过的小伙伴可以去看一下SpringBoot 项目接入 Redis 集群 。这篇文章给大家介绍一个 Redis 工具 redis-full-check,主要是用来校验迁移数据过后的准确性,下面我们来看一下。

阅读全文 »

为何建议技术人写写博客?

发表于 2020-09-25 | 分类于 其他

前言

在阿粉刚刚选择走上程序员道路的时候,脑海中还没有技术博客这个概念。那个时候入门靠的是培训班的视频,初学的过程中总会遇到许多陌生的概念,视频里没听懂的话一般会选择到搜索引擎去搜一下,多看几种不同的解释进行消化,也就是从这时开始接触到的技术博客。

在最开始的时候,我对博客的理解和我们读书时候做的笔记差不多,就是把学到的知识记录下来。但是慢慢的,我开始接触到了一些比较优秀的博客,他们的共性是能够把一个知识点讲得很清楚,能给出自己的理解,或者有适当的示例代码帮助读者理解。慢慢地我也萌发了要写博客的念头,初衷很简单,就是要把自己理解的某个知识点用自己的话分享出来。

屈指一算,从开创博客园账号至今也已经一年半了,自己的博客记录了自己的技术成长过程,收获了很多,下面我就给大家分享一下写博客能给大家带来什么好处吧。

1. 帮助我们理解知识

首先写博客能帮助我们很好地梳理并掌握一个知识点。不知道大家有没有听过这样的话:在学一个知识点的时候,如果只是听一遍讲,那你大概能掌握10%;做了笔记,大概掌握25%;能做出相应的题目,掌握50%;如果能把这个知识点教授给别人的话,那么就掌握了80%了。如果我们把写博客当成一个给他人传授知识的过程,当我们的博客写完了,自然地我们也就掌握了这个知识。当然这个过程是非常不容易的,想要把知识传授给他人,首先我们自己要吃透了这个知识。我个人在写博客的时候常常会遇到这么一种情况:写着写着就写不下去了,发现自己其实没有搞懂,理解透彻某个知识。为了把博客写下去,会强迫自己去把相关的知识搞懂。当我们通过博客的形式把一个知识点讲懂之后,其实我们就明白了一个知识点的来龙去脉,这对我们形成系统的知识体系很有帮助。

2. 获得成就感

写博客除了能帮助我们巩固理解知识外,还能给我们带来一些学习之外的乐趣。就我自己而言,每当我写的文章被别人评论或点赞,心里都会暗暗一乐,毕竟大多数情况下人还是希望得到他人认可的。尽管有时候得到的也不全是认可,可能会有其他小伙伴指出你的不足,也不失是一次学习的机会。

3. 提高影响力

在我个人看来,写博客还能提高自己的影响力,你的每一篇博客都是一次能力的展示,如果你的内容足够优秀的话,这些博客就是你的一张名片。我曾经因为博客收到过出版社的出书邀约,大厂的面试邀约等,尽管当时知道自己还没有达到那个水平,但还是蛮开心的。

4. 面试加分项

在面试写简历的时候,我们可以直接放上自己的博客链接,在同等水平的情况下,长期精心维护的技术博客是很有可能成为加分项的,毕竟大家都喜欢乐于分享知识的人。我曾经经历过一场有趣的面试,那是一场腾讯的主管面试,采用视频面试。面试的形式很常规,面试官抛出一个知识点,让我能讲多少讲多少。当时不知道为啥,他问的内容很多都是我掌握的比较好的内容。等面试结束的时候他告诉我他从面试开始就一直在看我的博客,刚好我的博客涉及的知识点也比较多,他就直接从上面挑知识问了,并对我的博客给予了肯定。虽然这是小概率事件了,但是也从侧面说明总有一天你的博客会帮上忙的。

后话

不知道以上写博客的好处能不能打动你,在我个人看来,写博客是一件百利无一害的事情,可能唯一比较麻烦的就是写博客比较费时间,不过我想花出去的时间能获得以上四个方面的收获也是值得的。把写博客当成一种习惯吧,坚持下来一定会有所收获的。

阅读全文 »

原来使用 Spring 实现策略模式可以这么简单!

发表于 2020-09-23 | 分类于 Java

Hello,大家好,我是鸭血粉丝~

最近看同事的代码时候,学到了个小技巧,在某些场景下非常挺有用的,这里分享一下给大家。

Spring 中 @Autowired注解,大家应该不会陌生,用过 Spring 的肯定也离不开这个注解,通过这个注解可以帮我们自动注入我们想要的 Bean。

除了这个基本功能之外,@Autowired 还有更加强大的功能,还可以注入指定类型的数组,List/Set 集合,甚至还可以是 Map 对象。

比如说当前应用有一个支付接口 PayService,分别需要对接支付宝、微信支付、银行卡,所以分别有三个不同实现类 AliPayService,WechatPayservice,BankCardPayService。

四个类的关系如下图所示:

如果此时我需要获取当前系统类所有 PayService Bean,老的方式我们只能通过 BeanFactory或者 ApplicationContext 获取。

1
2
3
4
5
6
7
8
9
10
11
// 首先通过 getBeanNamesForType 获取 PayService 类型所有的 Bean
String[] names = ctx.getBeanNamesForType(PayService.class);
List<PayService> anotherPayService = Lists.newArrayList();
for (String beanName : names) {
    anotherPayService.add(ctx.getBean(beanName, PayService.class));
}
// 或者通过 getBeansOfType 获取所有 PayService 类型
Map<String, PayService> beansOfType = ctx.getBeansOfType(PayService.class);
for (Map.Entry<String, PayService> entry : beansOfType.entrySet()) {
    anotherPayService.add(entry.getValue());
}

但是现在我们可以不用这么麻烦了,我们可以直接使用 @Autowired 注入 PayService Bean 数组,或者 PayService List/Set 集合,甚至,我们还可以注入 PayService 的 Map 集合。

1
2
3
4
5
@Autowired
List<PayService> payServices;

@Autowired
PayService[] payServicesArray;

知道了这个功能,当我们需要使用 Spring 实现策略模式就非常简单。

可能有的小伙伴不太了解策略模式,没关系,这类阿粉介绍一个业务场景,通过这个场景给大家介绍一下策略模式。

还是上面的例子,我们当前系统需要对接微信支付、支付宝、以及银行卡支付。

当接到这个需求,我们首先需要拿到相应接口文档,分析三者的共性。

假设我们这里发现,三者模式比较类似,只是部分传参不一样。

所以我们根据三者的共性,抽象出一组公共的接口 PayService,

1
2
3
public interface PayService {
    PayResult epay(PayRequest request);
}

然后分别实现三个实现类,都继承这个接口。

那么现在问题来了,由于存在三个实现类,如何选择具体的实现类?

其实这个问题很好解决,请求参数传入一个唯一标识,然后我们根据标识选择相应的实现类。

比如说我们在请求类 PayRequest 搞个 channelNo 字段,这个代表相应支付渠道唯一标识,比如说支付宝为:00000001,微信支付为 00000002,银行卡支付为 00000003。

接着我们需要把唯一标识与具体实现类一一映射起来,刚好我们可以使用 Map 存储这种映射关系。

我们实现一个 RouteService,具体代码逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class RouteService {

    @Autowired
    Map<String, PayService> payServiceMap;

    public PayResult epay(PayRequest payRequest) {
        PayService payService = payServiceMap.get(payRequest.getChannelNo());
        return  payService.epay(payRequest);
    }

}

我们在 RouteService 自动注入 PayService 所有相关 Bean,然后使用唯一标识查找实现类。

这样我们对外就屏蔽了支付渠道的差异,其他服务类只要调用 RouteService 即可。

但是这样实现还是有点小问题,由于我们唯一标识为一串数字,如果像我们上面直接使用 @Autowired注入 Map,这就需要我们实现类的 Bean 名字为 00000001 这些。

但是这样命名不是很优雅,这样会让后来同学很难看懂,不好维护。

所以我们需要做个转换,我们可以这么实现。

首先我们改造一下 PayService 这个接口,增加一个方法,每个具体实现类通过这个方法返回其唯一标识。

1
2
3
4
5
6
public interface PayService {

    PayResult epay(PayRequest request);

    String channel();
}

具体举个支付宝实现类的代码,其他实现类实现类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service("aliPayService")
public class AliPayService implements PayService {

    @Override
    public PayResult epay(PayRequest request) {
        // 业务逻辑
        return new PayResult();
    }
    @Override
    public String channel() {
        return "00000001";
    }
}

最后我们改造一下 RouteService,具体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class RouteService {

    @Autowired
    Set<PayService> payServiceSet;
    
    Map<String, PayService> payServiceMap;

    public PayResult epay(PayRequest payRequest) {
        PayService payService = payServiceMap.get(payRequest.getChannelNo());
        return  payService.epay(payRequest);
    }

    @PostConstruct
    public void init() {
        for (PayService payService : payServiceSet) {
            payServiceMap = new HashMap<>();
            payServiceMap.put(payService.channel(), payService);
        }
    }
}

上面代码首先通过自动注入 ` PayService 一个集合,然后我们再将其转为一个 Map`,这样内部存储刚好是唯一标识与实现类的映射了。

好了,今天的小技巧就分享到这里,学到小伙伴,不妨点个赞吧。

阅读全文 »

如果时间可以倒流

发表于 2020-09-22 | 分类于 程序生活

阿粉一定鼓足勇气,跟喜欢的女生表白

今天同事问阿粉一个问题,觉得挺有意义的,在这里也问问各位读者们:如果时间可以倒流,你最想做什么呢?为什么呢?

这个问题阿粉也问了问身边的一些朋友们,下面是他们的答案,或许可以给你一些启发

朋友 A :如果时间可以倒流,我特别想要回到高中的时候,好好学习,踏踏实实的去努力,好好读书。不是那种死板教条的读书,是有计划有效率的读书,希望自己能够死不要脸一些,多向老师和同学请教问题,我可能天资不够聪慧,但是如果能够有效率一些,死不要脸一些,最起码会比现在要好得多吧

朋友 B :我想回到高三结束的那个暑假啊,跟喜欢的女生表白。我也是后来才知道,当时我喜欢的那个女生,对我也有好感,我当时但凡是再勇敢一点儿,也不至于母胎 solo …

朋友 C :我跟你讲,要是能回到过去的话,我就回到 2006 年 6 月 1 日,中国船泊 5 元钱,全部身价买了后 2007 年 9 月 298 抛,然后买房子,然后入股腾讯阿里,在微信刚起来的时候就去做公众号,这样的话 2020 年的我可能算不上首富,但起码也可以算是人生赢家了(狗头保命

朋友 D :我希望我能回到初中的时候,告诉自己你很棒,自信一些。我以前就是一个比较敏感的小女生,非常在意别人看法,心情不愉快的时候就是自己憋着,也不会和别人人沟通,一个人硬抗。有的时候,朋友就会跟我说比较喜欢我,对我称赞有加,但是因为我不是很自信,所以对自己的闪光点视而不见。这些年在男朋友的耐心陪伴下才慢慢建立起自己的自信心,才敢表达自己的想法,说到这里,想和所有的女生说一句,你就是最棒的,不要怀疑自己,要自信一些!(PS : 这个小姐姐好暖,阿粉爱了

朋友 E :希望在大学的时候,自己少在宿舍睡会儿觉,打会儿游戏,多出去走走,多交朋友。一定要积极结交朋友,因为他们可以或多或少带给你快乐,在未来的生活里朋友是非常重要的一个存在,很多事情都可以帮到你,真的到那个时候,你会无比的庆幸自己有好朋友在身边陪伴着自己

朋友 F :如果时间可以倒流,我想回到高中,跟我的老班道个歉,谢谢她当时是一直为我考虑,当时自己太年少轻狂了,根本没有把她的话放在心上,反而以为是老班想要放弃我这个学生了,处处和她顶撞,说那么难听的话。当时如果能够站在她的角度去想想…现在想要和她道个歉,都已经联系不到她了,特别遗憾

朋友 G : 如果时间可以倒流么?我就回到母亲的子宫里,脐带绕颈三圈勒死我自己,哈哈哈,阿粉你肯定没想到我会这样回答

朋友 G 是个调皮鬼,平时就是段子频出,问他的时候他给我这个回答,真是让我惊呆了,脑回路总是这么清奇

其实阿粉对于这个问题也是认真的想了想,阿粉也有想要回去的时候,比如阿粉小的时候,一家人在冬天围着火炉,边取暖边和父母说笑,现在一年可能也就过年的时候回去;比如阿粉做错事惹母亲生气的时候;但是怎么可能会回去呢

只是当我们有这么念头的时候,肯定是当时是哪里自己没有做好,所以想要回去,尽自己的努力去做的更好一些

过去的已经过去,已经是抓不住的了,其实我们不如将那份心情和遗憾,放在未来,尽全力去做好当下的事情,去抓住未来。

故事还长,不要失望~

阅读全文 »

SpringBoot 项目接入 Redis 集群

发表于 2020-09-21 | 分类于 Java

Hello 大家好,我是鸭血粉丝,Redis 想必大家一定不会陌生,平常工作中或多或少都会用到,不管是用来存储登录信息还是用来缓存热点数据,对我们来说都是很有帮助的。但是 Redis 的集群估计并不是每个人都会用到,因为很多业务场景或者系统都是一些简单的管理系统,并不会需要用到 Redis 的集群环境。

阿粉之前也是这样,项目中用的的 Redis 是个单机环境,但是最近随着终端量的上升,慢慢的发现单机已经快支撑不住的,所以思考再三决定将 Redis 的环境升级成集群。下面阿粉给大家介绍一下在升级的过程中项目中需要调整的地方,这篇文章不涉及集群的搭建和配置,感兴趣的同学自行搜索。

阅读全文 »

面试官让我手写一个读写锁出来,我...

发表于 2020-09-21 | 分类于 程序生活

扶我起来,我还能学

上周六发了一篇文章: 有个程序媛女朋友是什么体验? ,把阿粉酸的,周六日都没缓过来。但是阿粉又是一个比较负责任的博主,酸归酸,文章还是要更新的

题目是个标题党啦,就是想带你过一遍 ReentrantReadWriteLock ,为了让可爱的读者们多看几眼阿粉的文章,可真是费劲了心思,就问你我暖不暖

ReentrantReadWriteLock 与 ReentrantLock 区别?

在这篇文章中: 阿粉写了八千多字,就是为了把 ReentrantLock 讲透 阿粉对 ReentrantLock 已经做了非常详细的讲解了

那么,今天想要说的 ReentrantReadWriteLock 和 ReentrantLock 有什么区别呢?如果只是从名字上来说的话,就是多了一个 ReadWrite 嘛

如果对 ReentrantLock 比较熟的话,那么当阿粉问你, ReentrantLock 是独占锁还是共享锁,你的第一反应就是: 独占锁

ReentrantReadWriteLock 是在 ReentrantLock 的基础上做的优化,什么优化呢? ReentrantLock 就是不管操作是读操作还是写操作都会对资源进行加锁,但是聪明的你想想嘛,如果好几个操作都只是读的话,并没有让数据的状态发生改变,这样的话是不是可以允许多个读操作同时运行?这样去处理的话,相对来说是不是就提高了并发呢

很多事情都是说起来容易,具体是怎么实现的呢?

啥也不多说,咱们直接上源码好吧

在使用 ReentrantReadWriteLock 时,一般都是调用 writeLock 和 readLock 两种方法,它在源码中定义如下:

1
2
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

而 writeLock 和 readLock 是 ReentrantReadWriteLock 的两个内部类,其中这两种锁的实现如下(其中省略了一些代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class ReadLock implements Lock, java.io.Serializable {
    public void lock() {
    	// 共享锁
        sync.acquireShared(1);
    }
		
	public void unlock() {
		// 共享锁
        sync.releaseShared(1);
    }
}

public static class WriteLock implements Lock, java.io.Serializable {
    public void lock() {
    	// 独占锁
        sync.acquire(1);
    }
	
	public void unlock() {
		// 独占锁
        sync.release(1);
    }
}

从源码就能够看出,对于读锁 readLock 它使用的是共享锁,也就是多个线程读没问题 但是对于写锁 writeLock 它使用的是独占锁,就是当一个线程要进行写操作时,其他的线程都要停下来等待

简单点儿说就是:一个资源可以被多个读线程访问,或者被一个写线程访问,但是不能同时存在读线程和写线程,这也是读写锁的定义

ReadLock 和 WriteLock 共享一个变量

如果让你设计一个读写锁的话,会怎样设计?

阿粉还真的认真想了想这个问题,如果让我设计的话,我应该会用两个变量去控制读和写,当线程获取到读锁时就对读变量进行 +1 操作,当获取到写锁时,就对写变量进行 +1 操作

但是通过看 ReentrantReadWriteLock 源码发现,它只是通过一个 state 来实现的

具体实现如下:

1
2
3
4
5
6
7
8
9
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

有两个关键方法 sharedCount 和 exclusiveCount ,乖,光是从名字意思来看应该也是可以猜出来的吧: sharedCount 就是共享锁的数量,而 exclusiveCount 则是独占锁的数量

通过看源码,能够看出来,对于 sharedCount 来说,它的计算方式就是无符号右移 16 位,空位都以 0 来补齐( c >>> SHARED_SHIFT; )

对于 exclusiveCount 来说,它的计算方式就是将传进来的 c 和 EXCLUSIVE_MASK 做 “&” 运算,那么 EXCLUSIVE_MASK 的值是什么呢?就是 (1 << SHARED_SHIFT) - 1 ,如果对位运算比较熟的话,应该会很容易看出来 (1 << SHARED_SHIFT) - 1 的值就是 65535 ,化成二进制就是 16 个 1,传进来 c 的值,和 16 位全为 1 做 “&” 运算的话,只有 1 & 1 才为 1 ,也就是说,传进来的 c 值经过这样转换之后,还是原来的值

说到这里,可能有点儿懵了,没关系,咱们来个总结就好说了(为了好理解,我就用大白话说了,争取你们都能看懂

对于 sharedCount 来说,只要传进来的值不大于 65535 ,那么经过计算之后,值都是 0

对于 exclusiveCount 来说,传进来的值是多少,经过计算之后还是多少

不管是 sharedCount 还是 exclusiveCount ,最大值都是 65535 ,因为是和 16 做位运算,其实这个数字也是相当够用了

那么,看到这里,各位应该就比较了解了吧,对于 ReadLock 和 WriteLock 来说,在源码层次其实并不是用两个变量去做的,而是通过一个 state 来实现的,思路真的是非常的巧妙

对于阿粉上面说的,如果还是不清楚的话,可以自己写个 demo 去验证一下,很简单的,就比如下面这样:

1
2
3
4
5
6
7
public static void main(String[] args) {
    int shareCount = 3000 >>> 16;
    System.out.println("shareCount : " + shareCount);

    int exclusiveCount = 1 & ((1 << 16) - 1);
    System.out.println("exclusiveCount : " + exclusiveCount);
}

等你运行完之后,你就发现,哇,怎么和我说的一样,哈哈哈哈

对于 sharedCount 来说,它是针对读锁的,所以不管多少进程进行读取资源,都没关系,所以它的值就是 0

对于 exclusiveCount来说,它是针对写锁的,那么只要有一个进程在进行写入,其他线程都要停下来等待,所以它的值就是传进来的值

综上,使用一个状态的话,我们只需要去判断 state 这个状态就可以了

WriteLock 的具体实现

OK ,既然你都看到了这里,我就默认上面的内容你都理解了

WriteLock 说白了就是独占锁,所以在获取 WriteLock 时,不能只考虑是否有写锁在占用,还要考虑有没有读锁.接下来咱们就去探究一下, WriteLock 它具体是怎么实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void lock() {
    sync.acquire(1);
}
        
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    // 获取到锁的状态
    int c = getState();
    // 获取写锁的数量
    int w = exclusiveCount(c);
    // c != 0 说明有读锁/写锁
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // w == 0 说明此时没有写锁,有读锁 或者 持有写锁的线程不是当前线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 如果写锁数量超出了最大值,没啥说的,抛异常就完事儿了
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 当前线程持有写锁,为重入锁,直接 +acquires 即可
        setState(c + acquires);
        return true;
    }
    // CAS 操作,确保修改值成功
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

如果对 ReentrantLock 比较熟的话,你会发现,上面的代码大部分都是见过的 有一点区别就是调用了 exclusiveCount 方法,看当前是否有写锁存在,接下来通过 c != 0 and w == 0 判断了当前是否有读锁存在

ReadLock 的具体实现

WriteLock 探究完了,接下来瞅瞅 ReadLock ,话不多说,直接上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void lock() {
    sync.acquireShared(1);
}

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

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 写锁不等于 0 时,看看当前写锁是否在尝试获取读锁
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 获取读锁数量
    int r = sharedCount(c);
    // 读锁不需要阻塞,而且读锁需要小于最大读锁数量,同时 cas 操作进行加 1 操作
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 当前线程是第一个并且第一次获取读锁时
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 如果当前线程再次获取读锁,则直接进行 ++ 操作即可
            firstReaderHoldCount++;
        } else {
            // 当前线程不是第一个获取读锁的线程,就放入线程本地变量
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

看完有没有觉得和写锁那块挺像的,不同就在于因为是读锁嘛,所以只要没有写锁占用,而且读锁的数量没有超过最大的获取数量,就都可以获取读锁

在上面, firstReader firstReaderHoldCount cachedHoldCounter 都是为 readHolds 服务的,它是为了获取当前线程持有锁的数量,在 ThreadLocal 基础上添加了 Int 变量来统计,这样比较方便嘛

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

static final class HoldCounter {
    // 当前线程持有锁的数量
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    // 当前线程 ID
    final long tid = getThreadId(Thread.currentThread());
}

回到题目,手写一个读写锁出来?

接下来,再回到题目,如果面试官让手写一个读写锁出来,你会如何实现呢?

在读了源码之后,相信你心里应该有谱了

首先来个 state 变量,然后高 16 位设置为读锁数量,低 16 位设置为写锁数量低,然后在进行读锁时,先判断下是不是有写锁,如果没有,直接读取即可,如果有的话那就需要等待;在写锁想要拿到锁的时候,就要判断写锁和读锁是不是都存在了,如果存在那就等着,如果不存在才进行接下来的操作

在这里阿粉给出一个简单版的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 public static class ReadWrite{
    // 定义一个读写锁共享变量 state
    private int state = 0;

    // state 高 16 位为读锁数量
    private int getReadCount(){
        return state >>> 16;
    }

    // state 低 16 位为写锁数量
    private int getWriteCount(){
        return state & (( 1 << 16 ) - 1 );
    }

    // 获取读锁时,先判断是否有写锁
    // 如果有写锁,就等待
    // 如果没有,进行加 1 操作
    public synchronized void lockRead() throws InterruptedException{
        while ( getWriteCount() > 0){
            wait();
        }

        System.out.println("lockRead --- " + Thread.currentThread().getName());
        state = state + ( 1 << 16);
    }

    // 释放读锁数量减 1 ,通知其他线程
    public synchronized void unLockRead(){
        state = state - ( 1 << 16 );
        notifyAll();
    }

    // 获取写锁时需要判断读锁和写锁是否都存在,有则等待,没有则将写锁数量加 1
    public synchronized void lockWrite() throws InterruptedException{

        while (getReadCount() > 0 || getWriteCount() > 0) {
            wait();
        }
        System.out.println("lockWrite --- " + Thread.currentThread().getName());
        state ++;
    }

    // 释放写锁数量减 1 ,通知所有等待线程
    public synchronized void unlockWriters(){
        state --;
        notifyAll();
    }
}

阿粉自己测试了下,没啥大问题

但是如果细究的话,还是有问题的,就比如,如果现在我有好多个读锁,如果一直不释放的话,那么写锁是一直没办法获取到的,这样就造成了饥饿现象的产生嘛

解决的话也蛮好解决的,就是在上面添加一个记录写锁数量的变量,然后在读锁之前,去判断一下是否有线程要获取写锁,如果有的话,优先处理,没有的话再进行读锁操作

这块聪明的读者,就试试自己实现吧,阿粉这里就不给具体实现了

阅读全文 »

“科班出身”的程序员和“培训出身”的程序员的大型辩论(甩锅)现场

发表于 2020-09-21 | 分类于 Java

前几天阿粉说阿粉最近换了公司,而且入职之后干掉了公司里面的测试数据库的事情,而接下来的事就比较有意思了,来自“科班出身”的哥们和来自“培训出身”的我的大型辩论(SIBI)现场,也不能说是通俗的甩锅,但是确实有那么点意味。

阅读全文 »

大数据杀熟行为10月1日起明令禁止,作为开发的你怎么看?

发表于 2020-09-16 | 分类于 Java

再过一段时间就是国庆假期加上中秋假期了,很多小伙伴都准备趁着这个时间好好出去玩耍,而就在昨天,文化和旅游部最新公布的《在线旅游经营服务管理暂行规定》将于今年10月1日起正式施行,其中特地声明了禁止大数据的杀熟的行为,那么大数据都干了些什么,让的文化和旅游部门专门提醒呢?

阅读全文 »

这是阿粉经历过最严重的一次生产事故了,说多了都是泪啊

发表于 2020-09-15 | 分类于 Java

Hello 大家好,我是鸭血粉丝,想必看到标题你们就猜到了,阿粉这次又写 bug 了,不过这次不是直接把服务器搞挂了,而是多写了一个判断导致线上业务接口少下放了一条链接数据,没有这个链接,就会导致线上的数据跟其他合作方对不上,换句话说就是会收不到钱!收不到钱意味着就是损失。

阅读全文 »

有个程序媛女朋友是什么体验?

发表于 2020-09-13 | 分类于 程序生活

有人问阿粉,有个程序媛女朋友是一种什么体验。阿粉虽然没有,但是身边有案例,这不为了满足大家的好奇心,去问了一圈,结果问下来之后,阿粉酸了…

程序员 A :你问我有个程序媛女朋友是啥体验?第一体验就是脱单了!还有什么体验比这更好么?

阿粉摸了摸自己的头发,默默的低下了头

程序员 B :记得有次跟我女朋友表白,告诉她我爱她,在我心里她永远都是第一位的,结果遭到她的质问,在我心里谁是第零?!!!

程序员 C :我女朋友就是程序媛,然后有次她在家也加班来着,让我帮忙看一下代码

“你为啥要用要敲空格键呢?用 tab 键不香么?”“用空格键不管怎么看,格式都不会变”“怎么会有人用空格?空格键除了制造噪音外哪里比 tab 强了,而且我跟你讲,用 tab 键可以让减少文件的占用空间”“我就用空格键怎么了,我就爱用怎么了”“你是老大,代码自己看吧”

程序员 D :什么体验么?和程序媛在一起,周末的时候就不是逛街了,是对着撸代码,时不时再吐吐槽对方的代码写的都是啥

阿粉想到周末都是自己在看技术视频,我的眼泪才没有流下来,我明明在笑…

程序员 E :我调试喜欢打 log ,女朋友调试喜欢打断点,每次都是她求我让我帮忙看看她的程序,然后调着调着我俩就开始抢笔记本了,“你给我,你这么写不行,给我我来写”“不要,我觉得是这里的问题,让我再试试,肯定是这里的问题”“不对不对,不是那里的问题,我有思路,把笔记本给我,我给你写”……到最后我俩差点儿打起来,好委屈,刚开始不是她来求我的么,而且还要嘲笑我一番喜欢打 log ,调试明明打 log 无敌好吧……

程序员 F :乖,咱们左括号换个行成不

对方:不要!!!

遇上这么一个女朋友,我能怎样?

程序员 G :最大的体验就是,平时的饭就不要想着在家吃了,特别是晚饭,在家吃是不可能的,因为 50% 的概率大家都加班, 50% 的概率一个人等另一个人加班

最后一条笑到你了吗?你有没有什么想要和阿粉聊聊的?

阅读全文 »

想在生产搞事情?那试试这些 Redis 命令

发表于 2020-09-09 | 分类于 Java

哎,最近阿粉又双叒叕犯事了。

事情是这样的,前一段时间阿粉公司生产交易偶发报错,一番排查下来最终原因是因为 Redis 命令执行超时。

可是令人不解的是,生产交易仅仅使用 Redis set 这个简单命令,这个命令讲道理是不可能会执行这么慢。

那到底是什么导致这个问题那?

为了找出这个问题,我们查看分析了一下 Redis 最近的慢日志,最终发现耗时比较多命令为 keys XX*

看到这个命令操作的键的前缀,阿粉才发现这是自己负责的应用。可是阿粉排查一下,虽然自己的代码并没有主动去使用 keys命令,但是底层使用框架却在间接使用,于是就有了今天这个问题。

阅读全文 »

Redis Keys

发表于 2020-09-09
想在生产搞事情?那试试这些 Redis 命令

哎,最近阿粉又双叒叕犯事了。

事情是这样的,前一段时间阿粉公司生产交易偶发报错,一番排查下来最终原因是因为 Redis 命令执行超时。

可是令人不解的是,生产交易仅仅使用 Redis set 这个简单命令,这个命令讲道理是不可能会执行这么慢。

那到底是什么导致这个问题那?

为了找出这个问题,我们查看分析了一下 Redis 最近的慢日志,最终发现耗时比较多命令为 keys XX*

看到这个命令操作的键的前缀,阿才发现这是自己负责的应用。可是阿粉排查一下,虽然自己的代码并没有主动去使用 keys命令,但是底层使用框架却在间接使用,于是就有了今天这个问题。

问题原因

阿粉负责的应用是一个管理后台应用,权限管理使用 Shiro 框架,由于存在多个节点,需要使用分布式 Session,于是这里使用 Redis 存储 Session 信息。

画外音:不知道分布式 Session ,可以看看阿粉之前写的 一口气说出 4 种分布式一致性 Session 实现方式,面试杠杠的~

由于 Shiro 并没有直接提供 Redis 存储 Session 组件,阿粉不得不使用 Github 一个开源组件 shiro-redis。

由于 Shiro 框架需要定期验证 Session 是否有效,于是 Shiro 底层将会调用 SessionDAO#getActiveSessions 获取所有的 Session 信息。

而 shiro-redis 正好继承 SessionDAO 这个接口,底层使用用keys 命令查找 Redis 所有存储的 Session key。

public Set<byte[]> keys(byte[] pattern){
    checkAndInit();
    Set<byte[]> keys = null;
    Jedis jedis = jedisPool.getResource();
    try{
        keys = jedis.keys(pattern);
    }finally{
        jedis.close();
    }
    return keys;
}

找到问题原因,解决办法就比较简单了,github 上查找到解决方案,升级一下 shiro-redis 到最新版本。

在这个版本,shiro-redis 采用 scan命令代替 keys,从而修复这个问题。

public Set<byte[]> keys(byte[] pattern) {
    Set<byte[]> keys = null;
    Jedis jedis = jedisPool.getResource();

    try{
        keys = new HashSet<byte[]>();
        ScanParams params = new ScanParams();
        params.count(count);
        params.match(pattern);
        byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
        ScanResult<byte[]> scanResult;
        do{
            scanResult = jedis.scan(cursor,params);
            keys.addAll(scanResult.getResult());
            cursor = scanResult.getCursorAsBytes();
        }while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0);
    }finally{
        jedis.close();
    }
    return keys;

}

虽然问题成功解决了,但是阿粉心里还是有点不解。

为什么keys 指令会导致其他命令执行变慢?

为什么Keys 指令查询会这么慢?

为什么Scan 指令就没有问题?

Redis 执行命令的原理

首先我们来看第一个问题,为什么keys 指令会导致其他命令执行变慢?

回答这个问题,我们首先看下 Redis 客户端执行一条命令的情况:

站在客户端的视角,执行一条命令分为三步:

  1. 发送命令
  2. 执行命令
  3. 返回结果

但是这仅仅客户端自己以为的过程,但是实际上同一时刻,可能存在很多客户端发送命令给 Redis,而 Redis 我们都知道它采用的是单线程模型。

为了处理同一时刻所有的客户端的请求命令,Redis 内部采用了队列的方式,排队执行。

于是客户端执行一条命令实际需要四步:

  1. 发送命令
  2. 命令排队
  3. 执行命令
  4. 返回结果

由于 Redis 单线程执行命令,只能顺序从队列取出任务开始执行。

只要 3 这个过程执行命令速度过慢,队列其他任务不得不进行等待,这对外部客户端看来,Redis 好像就被阻塞一样,一直得不到响应。

所以使用 Redis 过程切勿执行需要长时间运行的指令,这样可能导致 Redis 阻塞,影响执行其他指令。

KEYS 原理

接下来开始回答第二个问题,为什么Keys 指令查询会这么慢?

回答这个问题之前,请大家回想一下 Redis 底层存储结构。

不太清楚朋友的也没关系,大家可以回看一下阿粉之前的文章「阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!」。

这里阿粉复制之前文章内容,Redis 底层使用字典这种结构,这个结构与 Java HashMap 底层比较类似。

keys命令需要返回所有的符合给定模式 pattern 的 Redis 中键,为了实现这个目的,Redis 不得不遍历字典中 ht[0]哈希表底层数组,这个时间复杂度为 O(N)(N 为 Redis 中 key 所有的数量)。

如果 Redis 中 key 的数量很少,那么这个执行速度还是也会很快。等到 Redis key 的数量慢慢更加,上升到百万、千万、甚至上亿级别,那这个执行速度就会很慢很慢。

下面是阿粉本地做的一次实验,使用 lua 脚本往 Redis 中增加 10W 个 key,然后使用 keys 查询所有键,这个查询大概会阻塞十几秒的时间。

eval "for i=1,100000  do redis.call('set',i,i+1) end" 0

这里阿粉使用 Docker 部署 Redis,性能可能会稍差。

SCAN 原理

最后我们来看下第三个问题,为什么scan 指令就没有问题?

这是因为 scan命令采用一种黑科技-基于游标的迭代器。

每次调用 scan 命令,Redis 都会向用户返回一个新的游标以及一定数量的 key。下次再想继续获取剩余的 key,需要将这个游标传入 scan 命令, 以此来延续之前的迭代过程。

简单来讲,scan 命令使用分页查询 redis 。

下面是一个 scan 命令的迭代过程示例:

scan 命令使用游标这种方式,巧妙将一次全量查询拆分成多次,降低查询复杂度。

虽然 scan 命令时间复杂度与 keys一样,都是 O(N),但是由于 scan 命令只需要返回少量的 key,所以执行速度会很快。

最后,虽然scan 命令解决 keys不足,但是同时也引入其他一些缺陷:

  • 同一个元素可能会被返回多次,这就需要我们应用程序增加处理重复元素功能。
  • 如果一个元素在迭代过程增加到 redis,或者说在迭代过程被删除,那个这个元素会被返回,也可能不会。

以上这些缺陷,在我们开发中需要考虑这种情况。

除了 scan以外,redis 还有其他几个用于增量迭代命令:

  • sscan:用于迭代当前数据库中的数据库键,用于解决 smembers 可能产生阻塞问题
  • hscan命令用于迭代哈希键中的键值对,用于解决 hgetall 可能产生阻塞问题。
  • zscan:命令用于迭代有序集合中的元素(包括元素成员和元素分值),用于产生 zrange 可能产生阻塞问题。

总结

Redis 使用单线程执行操作命令,所有客户端发送过来命令,Redis 都会现放入队列,然后从队列中顺序取出执行相应的命令。

如果任一任务执行过慢,就会影响队列中其他任务的,这样在外部客户端看来,迟迟拿不到 Redis 的响应,看起来就很阻塞了一样。

所以不要在生产执行 keys、smembers、hgetall、zrange这类可能造成阻塞的指令,如果真需要执行,可以使用相应的scan 命令渐进式遍历,可以有效防止阻塞问题。

阅读全文 »

这是我经历过最惨的转正答辩了

发表于 2020-09-06 | 分类于 Java

Hello 大家好,我是鸭血粉丝,试用期是每个刚入职的人都会经历的一段时间,时间不固定,少则一两月多则半年,具体的时间长短根据公司和个人表现不尽相同。而且试用期过后一般都会有一个转正答辩,这不阿粉最近就接到一个小伙伴的哭诉说转正答辩太难了,事情是这个样子的。

阅读全文 »

阿粉昨天说我动不动就内存泄漏,我好委屈...

发表于 2020-09-06 | 分类于 并发

大家好,我是 ThreadLocal ,昨天阿粉说我动不动就内存泄漏,我蛮委屈的,我才没有冤枉他嘞,证据在这里: ThreadLocal 你怎么动不动就内存泄漏?

因为人家明明也考虑到了很多情况,做了很多事情,保证了如果没有 remove ,也有对 key 值为 null 时进行回收的处理操作

啥?你竟然不信?我 ThreadLocal 难道会骗你么

今天为了证明一下自己,我打算从组成的源码开始讲起,在 get , set 方法中都有对 key 值为 null 时进行回收的处理操作,先来看 set 方法是怎么做的

set

下面是 set 方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
        // 如果 e 不为空,说明 hash 冲突,需要向后查找
        e != null;
        // 从这里可以看出, ThreadLocalMap 采用的是开放地址法解决的 hash 冲突
        // 是最经典的 线性探测法 --> 我觉得之所以选择这种方法解决冲突时因为数据量不大
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 要查找的 ThreadLocal 对象找到了,直接设置需要设置的值,然后 return
        if (k == key) {
            e.value = value;
            return;
        }

        // 如果 k 为 null ,说明有 value 没有及时回收,此时通过 replaceStaleEntry 进行处理
        // replaceStaleEntry 具体内容等下分析
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 如果 tab[i] == null ,则直接创建新的 entry 即可
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 在创建之后调用 cleanSomeSlots 方法检查是否有 value 值没有及时回收
    // 如果 sz >= threshold ,则需要扩容,重新 hash 即, rehash();
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

通过源码可以看到,在 set 方法中,主要是通过 replaceStaleEntry 方法和 cleanSomeSlots 方法去做的检测和处理

接下来瞅瞅 replaceStaleEntry 都干了点儿啥

replaceStaleEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 从当前 staleSlot 位置开始向前遍历
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = prevIndex(i, len))
        if (e.get() == null)
            // 当 e.get() == null 时, slotToExpunge 记录下此时的 i 值
            // 即 slotToExpunge 记录的是 staleSlot 左手边第一个空的 Entry
            slotToExpunge = i;

    // 接下来从当前 staleSlot 位置向后遍历
    // 这两个遍历是为了清理在左边遇到的第一个空的 entry 到右边的第一个空的 entry 之间所有过期的对象
    // 但是如果在向后遍历过程中,找到了需要设置值的 key ,就开始清理,不会再继续向下遍历
    for (int i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果 k == key 说明在插入之前就已经有相同的 key 值存在,所以需要替换旧的值
        // 同时和前面过期的对象进行交换位置
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果 slotToExpunge == staleSlot 说明向前遍历时没有找到过期的
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 进行清理过期数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果在向后遍历时,没有找到 value 被回收的 Entry 对象
        // 且刚开始 staleSlot 的 key 为空,那么它本身就是需要设置 value 的 Entry 对象
        // 此时不涉及到清理
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果 key 在数组中找不到,那就好说了,直接创建一个新的就可以了
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果 slotToExpunge != staleSlot 说明存在过期的对象,就需要进行清理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在 replaceStaleEntry 方法中,需要注意一下刚开始的两个 for 循环中内容(在这里再贴一下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (e.get() == null)
    // 当 e.get() == null 时, slotToExpunge 记录下此时的 i 值
    // 即 slotToExpunge 记录的是 staleSlot 左手边第一个空的 Entry
    slotToExpunge = i;

if (k == key) {
    e.value = value;

    tab[i] = tab[staleSlot];
    tab[staleSlot] = e;
                                        
    // 如果 slotToExpunge == staleSlot 说明向前遍历时没有找到过期的
    if (slotToExpunge == staleSlot)
        slotToExpunge = i;
        // 进行清理过期数据
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        return;
}

这两个 for 循环中的 if 到底是在做什么?

看第一个 if ,当 e.get() == null 时,此时将 i 的值给 slotToExpunge

第二个 if ,当 k ==key 时,此时将 i 给了 staleSlot 来进行交换

为什么要对 staleSlot 进行交换呢?画图说明一下

如下图,假设此时表长为 10 ,其中下标为 3 和 5 的 key 已经被回收( key 被回收掉的就是 null ),因为采用的开放地址法,所以 15 mod 10 应该是 5 ,但是因为位置被占,所以在 6 的位置,同样 25 mod 10 也应该是 5 ,但是因为位置被占,下个位置也被占,所以就在第 7 号的位置上了

按照上面的分析,此时 slotToExpunge 值为 3 , staleSlot 值为 5 , i 为 6

假设,假设这个时候如果不进行交换,而是直接回收的话,此时位置为 5 的数据就被回收掉,然后接下来要插入一个 key 为 15 的数据,此时 15 mod 10 算出来是 5 ,正好这个时候位置为 5 的被回收完毕,这个位置就被空出来了,那么此时就会这样:

同样的 key 值竟然出现了两次?!

这肯定是不希望看到的结果,所以一定要进行数据交换

在上面代码中有一行代码 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ,说明接下来的处理是交给了 expungeStaleEntry ,接下来去分析一下 expungeStaleEntry

expungeStaleEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
        (e = tab[i]) != null;
        i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 如果 k == null ,说明 value 就应该被回收掉
        if (k == null) {
            // 此时直接将 e.value 置为 null 
            // 这样就将 thread -> threadLocalMap -> value 这条引用链给打破
            // 方便了 GC
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 这个时候要重新 hash ,因为采用的是开放地址法,所以可以理解为就是将后面的元素向前移动
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

因为是在 replaceStaleEntry 方法中调用的此方法,传进来的值是 staleSlot ,继续上图,经过 replaceStaleEntry 之后,它的数据结构是这样:

此时传进来的 staleSlot 值为 6 ,因为此时的 key 为 null ,所以接下来会走 e.value = null ,这一步结束之后,就成了:

接下来 i 为 7 ,此时的 key 不为 null ,那么就会重新 hash : int h = k.threadLocalHashCode & (len - 1); ,得到的 h 应该是 5 ,但是实际上 i 为 7 ,说明出现了 hash 冲突,就会继续向下走,最终的结果是这样:

可以看到,原来的 key 为 null ,值为 V5 的已经被回收掉了。我认为之所以回收掉之后,还要再次进行重新 hash ,就是为了防止 key 值重复插入情况的发生

假设 key 为 25 的并没有进行向前移动,也就是它还在位置 7 ,位置 6 是空的,再插入一个 key 为 25 ,经过 hash 应该在位置 5 ,但是有数据了,那就向下走,到了位置 6 ,诶,竟然是空的,赶紧插进去,这不就又造成了上面说到的问题,同样的一个 key 竟然出现了两次?!

而且经过 expungeStaleEntry 之后,将 key 为 null 的值,也设置为了 null ,这样就方便 GC

分析到这里应该就比较明确了,在 expungeStaleEntry 中,有些地方是帮助 GC 的,而通过源码能够发现, set 方法调用了该方法进行了 GC 处理, get 方法也有,不信你瞅瞅:

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果能够找到寻找的值,直接 return 即可
    if (e != null && e.get() == key)
        return e;
    else
        // 如果找不到,则调用 getEntryAfterMiss 方法去处理
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 一直探测寻找下一个元素,直到找到的元素是要找的
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            // 如果 k == null 说明有 value 没有及时回收
            // 调用 expungeStaleEntry 方法去处理,帮助 GC
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

get 和 set 方法都有进行帮助 GC ,所以正常情况下是不会有内存溢出的,但是如果创建了之后一直没有调用 get 或者 set 方法,还是有可能会内存溢出

所以最保险的方法就是,使用完之后就及时 remove 一下,加快垃圾回收,就完美的避免了垃圾回收

我 ThreadLocal 虽然没办法做到 100% 的解决内存泄漏问题,但是我能做到 80% 不也应该夸夸我嘛

阅读全文 »

ThreadLocal 你怎么动不动就内存泄漏?

发表于 2020-09-03 | 分类于 并发

如果说 ThreadLocal 的话,那肯定就会涉及到内存泄漏,为啥嘞

因为 吧啦吧啦 ~

ThreadLocal 解决了什么问题呢?

它是为了解决对象不能被多线程共享访问的问题,通过 threadLocal.set() 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,这样的话每个线程都使用自己的对象实例,彼此不会影响从而达到了隔离的作用,这样就解决了对象在被共享访问时带来的线程安全问题

啥意思呢?打个比方,现在公司所有人都要填写一个表格,但是只有一支笔,这个时候就只能上个人用完了之后,下个人才可以使用,为了保证”笔”这个资源的可用性,只需要保证在接下来每个人的获取顺序就可以了,这就是 lock 的作用,当这支笔被别人用的时候,我就加 lock ,你来了那就进入队列排队等待获取资源(非公平方式那就另外说了),这支笔用完之后就释放 lock ,然后按照顺序给下个人使用

但是完全可以一个人一支笔对不对,这样的话,你填写你的表格,我填写我的表格,咱俩谁都不耽搁谁。这就是 ThreadLocal 在做的事情,因为每个 Thread 都有一个副本,就不存在资源竞争,所以也就不需要加锁,这不就是拿空间去换了时间嘛

在开始之前,咱们先把 Thread, ThreadLocal, ThreadLocalMap 的关系捋一捋:

可以看到,在 Thread 中持有一个 ThreadLocalMap , ThreadLocalMap 又是由 Entry 来组成的,在 Entry 里面有 ThreadLocal 和 value

ThreadLocal 为啥动不动就内存泄漏呢?

在这里先给个解释,后面咱们再详细分析:

首先是因为 ThreadLocal 是基于 ThreadLocalMap 实现的,其中 ThreadLocalMap 的 Entry 继承了 WeakReference ,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说, Entry 中的 key 是一个弱引用类型,对于弱引用来说,它只能存活到下次 GC 之前

如果此时一个线程调用了 ThreadLocalMap 的 set 设置变量,当前的 ThreadLocalMap 就会新增一条记录,但由于发生了一次垃圾回收,这样就会造成一个结果: key 值被回收掉了,但是 value 值还在内存中,而且如果线程一直存在的话,那么它的 value 值就会一直存在

这样被垃圾回收掉的 key 就会一直存在一条引用链: Thread -> ThreadLocalMap -> Entry -> Value :

就是因为这条引用链的存在,就会导致如果 Thread 还在运行,那么 Entry 不会被回收,进而 value 也不会被回收掉,但是 Entry 里面的 key 值已经被回收掉了

这只是一个线程,如果再来一个线程,又来一个线程…多了之后就会造成内存泄漏

知道是怎么造成内存泄漏之后,接下来要做的事情就好说了,不是因为 value 值没有被回收掉所以才会导致内存泄露的嘛

那使用完 key 值之后,将 value 值通过 remove 方法 remove 掉,这样的话内存中就不会有 value 值了,也就防止了内存泄漏嘛

ThreadLocal 是基于 ThreadLocalMap 实现的?

OK ,上面的内容讲完了,接下来一一来看

首先,你怎么知道 ThreadLocal 是基于 ThreadLocalMap 实现的呢?

从源码知道的~

在源码中能够看到下面这几行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

代码中说的很清楚了,在 ThreadLocal 内部维护着 ThreadLocalMap ,而它的 Entry 则继承自 WeakReference 的 ThreadLocal<?> ,其中 Entry 的 k 为 ThreadLocal , v 为 Object ,在调用 super(k) 时就将 ThreadLocal 实例包装成了一个 WeakReference

强弱引用这块内容阿粉就直接放一个表格吧:

引用类型 功能特点
强引用 ( Strong Reference ) 被强引用关联的对象永远不会被垃圾回收器回收掉
软引用( Soft Reference ) 软引用关联的对象,只有当系统将要发生内存溢出时,才会去回收软引用引用的对象
弱引用 ( Weak Reference ) 只被弱引用关联的对象,只要发生垃圾收集事件,就会被回收
虚引用 ( Phantom Reference ) 被虚引用关联的对象的唯一作用是能在这个对象被回收器回收时收到一个系统通知

从表格中应该能够看出来,弱引用的对象只要发生垃圾收集事件,就会被回收

所以弱引用的存活时间也就是下次 GC 之前了

在这里阿粉就有个问题想问问了:为什么 ThreadLocal 采用弱引用,而不是强引用嘞?

在 ThreadLocalMap 上面有些注释,我在这里摘录一部分,或许可以从中窥探一二:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys

翻译一下就是:(虽然我英语不是很好

为了解决非常大且长期使用的问题,哈希表使用了弱引用的 key

假设,假设, ThreadLocal 使用的是强引用,会怎样呢?

如果是强引用的话,在表格中也能够看出来,被强引用关联的对象,永远都不会被垃圾回收器回收掉

如果引用的 ThreadLocal 对象被回收了,但是 ThreadLocalMap 还持有对 ThreadLocal 的强引用,如果没有 remove 的话, 在 GC 时进行可达性分析, ThreadLocal 依然可达,这样就不会对 ThreadLocal 进行回收,但是我们期望的是引用的 ThreadLocal 对象被回收,这样不就达不到目的了嘛

使用弱引用的话,虽然会出现内存泄漏的问题,但是在 ThreadLocal 生命周期里面,都有对 key 值为 null 时进行回收的处理操作

所以,使用弱引用的话,可以在 ThreadLocal 生命周期中尽可能保证不出现内存泄漏的问题

啥?在 ThreadLcoal 生命周期里面,都有对 key 值为 null 时进行回收的处理操作?有证据么?

那必须得有证据,毕竟阿粉可是个负责任的博主,不过阿粉考虑到这篇文章内容已经是比较多的了,所以下篇文章阿粉再带你进行源码分析好不好

乖~

阅读全文 »

刚刚入职新公司,我就差点因为自己的失误被开除

发表于 2020-09-02 | 分类于 HTTP

阿粉最近刚刚入职了新公司,结果不曾想,差点就被公司开除,一个删除的操作,把表给粉碎的彻彻底底的,得亏了是在测试服务器上,虽然测试服务器,也算是有了备份,但是还是被领导diss了一波,那么我们在刚入职需要做些什么准备才能不会被公司疯狂的diss。

<–more–>

技术是第一生产力

都说程序员在公司混,全凭技术说话,不知道大家最近有没有看一个新闻,网易内部倡导用昵称代替“哥、姐、总”等称呼,大家对这个新闻有什么感想的呢?

在程序员里面,就分成了大佬-小弟,大佬肯定就是那种各种技术各种牛逼的人,而小弟肯定就是什么都不会,凡事都要去问大佬的人,一般嘴甜的都会叫个哥什么的,但是这种情况在程序员行业不足为奇,毕竟技术牛逼的都会让人佩服,而我们能做的就是把所有的之前能拿出来的水准一定麻溜的展示出来,不然你在公司会成为他们茶余饭后的小笑话。

尽快的熟悉业务

之前也有刚刚步入这个行业的初级程序员问过阿粉,说我现在入职之后,最开始需要干什么,阿粉给他提出的建议就是,抓紧熟悉业务,把公司的业务完整的拿下来,至少有个需求来了,你能分分钟知道在那些地方加功能,在哪里修改逻辑。

阿粉在刚开始入职的时候,曾经入职过一家公司,一个非常非常小的公司,开发加起来一共就6个人,招人的要求却很高,要求你前后端都得会,重点来了,一般刚入职的程序员很多都是从后端程序开始做的,对于前段的页面,CSS 什么的压根都不接触,忽然给你一个图片,说把这个做出来,那时候的阿粉瞬间蒙了,都不知道该怎么办了,花了一上午的时间,写了一个很low的页面,而中间有一个搞业务的顺嘴说了一句,你说着让你写个页面这么难么?阿粉的年轻让自己付出了惨重的代价,直接就提辞职了,顺便说了可能达不到公司的要求,第二天直接就走人了。

而从这次之后,阿粉明白了,去公司第一件事就是赶紧去熟悉公司的业务,而且面对那些特别小型的公司,还没有正规的开发流程的公司,真心去不得,去了会让自己身心及其压抑。

这种压抑你呆久了,会让自己感觉到倍感打击,所以阿粉这次在跳槽之后的第一件事就是把公司的项目跑起来,能看文档的就看文档,能问问题的就问问题,和我交接的那个人还行,还算不错,很多东西都给我交接的听明白的,阿粉也不再是当初的新人了,在第二天的时候,已经感觉自己能在公司的项目上修修改改了,毕竟看着写的比较头大的代码,有点忍不住想去给他改改。

情商也是非常非常重要的一件事

阿粉在入职之后还在想,为什么没有新员工的培训呢?毕竟现在的公司也不算是小公司了,在阿粉提出疑问的时候,阿粉的领导说的是,在入职一个月之后才会有员工培训,中间会让你学习一些公司的规章制度,然后在让给你灌输一些思想,这些东西在程序员眼中可能都是“垃圾”,但是这些东西,其实在你以后是非常需要的,在一个公司最容易混熟的是什么人,就是刚刚和你一起入职的新人,让你熟悉,并不是让你搞小团体。

你初来乍到,首先需要熟悉的是代码,但是在熟悉代码的同时,还有一件事也是需要你做得,那就是和产品沟通需求,你和产品沟通好了,那么需求明确了才能完整的开发。

不知道大家看过阿里巴巴网红美女产品经理又玄么?那个在台上被嘉宾怼得体无完肤,但是她在实际中却是实实在在的学霸,而且提出的要求也没有那种“根据心情,变换壁纸的,根据手机壳,变换壁纸的操作”。比如说,阿里全园区供26万阿里人使用的人脸门禁,阿里的网红级产品未来餐厅识别系统等这些都是她的杰作。

程序员眼中的产品经理:我们程序员忙着用技术添砖加瓦,啥技术都不懂的产品经理忙着给我们添堵,看我们哪儿都不好。

就算是忙着添堵,那我们也一定要和产品经理搞好关系,这对你未来的开发是非常有帮助,防止以后给你一句话就是一个需求的这种情况。

而怎么才能更好的融入团队?

眼里有活

先不说你之后在公司会有什么摸鱼的情况,但是在最开始融入团队的最好方法一定是给自己找活,功能做的又快又好,试问,什么样子的小领导不希望自己手下的人干活麻利。

有问题该怎么问?

阿粉认为,在刚开始入职的时候,千万不要有什么问题就对其他员工发出提问,不然这时候你会发现,用不了几天,就会有人diss你,这时候你需要做的是 :

百度

Google

看文档

发出提问

为什么我会把发出提问放在最后边呢,因为刚开始的时候,千万不要什么问题都去提问,而是经过自己的思考和考虑之后,自己如果真的对业务上的问题解决不了的话,那么再去发文,我当初的推荐就是加班,而现在阿粉做的依旧是入职开始加班熟悉业务,不会的问题赶紧百度,发出提问的时候,很多都是业务上的问题,而不是技术上的问题,这时候,你就会发现,他们很多时候是很乐意和你沟通的,而你频繁的发出提问,那么就会让别人觉得,你这技术废物,是怎么被召进来的。

所以,问问题的时候是需要技巧的,而你学会把握这些技巧之后,你就会在入职之后就不会出现被公司其他的同时diss,而且也不会因为自己不适应公司被迫的离开刚入职的公司。

学会控制自己

为什么阿粉会在这里给大家说要学会控制自己呢?之前的案例已经说明了,如果你不控制自己,直接就按照自己的想法办事,遇到个不会的,也不加班,就开始和领导顶撞,那可能自己很快就会辞职。

当我们新入职一个公司的时候,对这个公司的环境和业务并不是特别熟悉的时候,我们在问完问题,老员工和领导给你提出建议的时候,虚心接受,而他们有时候也会给你分享一些工作中的经验,产生分歧的时候,尤其是技术,这块内容,最好虚心接受,不然你会发现你会被慢慢的疏远,就像下面。

而阿粉给大家的建议就这些了,你想不想换个公司呢?想的话就赶紧准备面试题把,关注公众号,回复面试题,即可获取精心准备的面试题一份呦。

阅读全文 »

踩坑记:后端返回的Long型参数,前端js取值时精度丢失

发表于 2020-09-01 | 分类于 java

最近几天一直在改造工程,采用雪花算法生成主键ID,突然踩到一个天坑,前端 JavaScript 在取 Long 型参数时,参数值有点不太对!

阅读全文 »

分布式服务注册发现与统一配置管理之 Consul

发表于 2020-08-31 | 分类于 java

Hello 大家好,我是阿粉,前面的文章给大家介绍过 Nacos,用于服务注册发现和管理配置的开源组件,今天给大家分享另一个组件 Consul 也有相应的功能,我们一起来看一下吧!

阅读全文 »
1 … 3 4 5 … 26
Java Geek Tech

Java Geek Tech

一群热爱 Java 的技术人

514 日志
113 分类
24 作者
RSS
GitHub 知乎
Links
  • 纯洁的微笑
  • 沉默王二
  • 子悠
  • 江南一点雨
  • 炸鸡可乐
  • 郑璐璐
  • 程序通事
  • 懿
© 2019 - 2021 Java Geek Tech
由 Jekyll 强力驱动
主题 - NexT.Mist