-
Locks包简介java.util.concurrent包下的locks包提供了一系列同步工具,其功能在大体上跟synchronized差不多,但量级更轻更灵活,也更复杂。juc包下的多种用于同步的工具包括CyclicBarrier、LinkedBlockingQueue等都依赖这些同步工具。
本次先研读Lock相关的接口及LockSupport类源码以了解常见的使用方式及需要注意的点。
介绍Lock接口给特定资源加锁,即同时只有一个线程能访问该资源,其他资源要么等待获取锁后访问资源,要么放弃访问资源(具有排他性)。语义同Synchronized但在使用上更为灵活,且需要手动释放锁。
在注释中提到的典型应用场景是:
获取A的锁->获取B的锁->释放A的锁->获取C的锁->释放B的锁->获取D的锁……
很明显,这种方式一旦使用不当非常容易产生死锁,这就是灵活带来的代价。
......
-
Atomic包简介及分类java.util.concurrent(一般简称juc)包下的atomic包提供了一系列在并发场景下尽量无锁实现原子操作的类,其核心思想是使用CAS+循环实现轻量级乐观锁,在并发竞争不激烈的情况下效率会比加锁实现好很多。除Striped64由JCP JSR-166 Expert Group成员协助完成外,作者都是Doug Lea大师。虽然juc包整体代码量不大但每次看都有新的思考和收获。
原子包装类:
AtomicBooleanAtomicIntegerAtomicLongAtomicReferenceAtomicIntegerArrayAtomicLongArrayAtomicReferenceArrayAtomicStampedReferenceAtomicMarkableReference
属性更新类:
AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
高并发计算类:
......
-
背景一位前辈在一次技术分享中指出我们目前的包管理不规范,模块间职责有重叠,理解成本高不易维护,提出在开发过程中应当明确按照职责将服务划分到对应的模块中。
比如我们把所有服务都放在service层,但其实服务也是分为基础服务和业务逻辑服务的,或许把类似业务数据查询组装服务放在service层,把具体业务逻辑服务统一放在business层会更好,更利于基础服务的复用。
但当服务拆离到不同模块进行复用时,可能在开发过程中出现服务依赖的问题,这部分依赖问题的解耦可以用到Java的SPI机制。显然,我从来没有听说过SPI是什么,也不明白这有什么好处。
SPI是什么翻遍各种网上资料,来来回回都是车轱辘话,互相抄来抄去讲得并不通俗易懂,这里就用我自己的理解来解释。
SPI(Service Provider Interface),大意是“服务提供者接口”,是指在服务使用方角度提出的“接口要求”,是对“服务提供方”提出的约定,简单说就是:“我需要这样的服务,现在你们来满足”。
API(Application Programming Interface)与之相对,是站在服务提供方角度提供的无需了解底层细节的操作入口,即“我有这样的服务可以给你使用”。
......
-
背景在前公司时参与了一个编码竞赛,虽然只拿到一个中游成绩,但在参赛过程中学习到很多其他人优秀的思考方式,也接受了前辈的指点,尤其是在参赛时的一些知识面拓展对我帮助不小。其中一些平常很少接触到的知识对于之后的工作会有所帮助。
题目很简单,大概是这样:
在4G内存的机器上实现对大文件内容的按行排序文件每行为小写字母组成的不重复的一段字符串,最长为128字节文件大小有1G/2G/5G/10G/20G多种,很明显一部分文件是无法全部加载到内存中的
具体过程及结果不细说,在这里简单介绍其中用到的部分NIO技术,这些技术无论在各种框架如Netty等,以及各种中间件如RocketMQ等都有用到。
基本概念堆外内存我们都知道,JVM需要申请一块内存用于进程的使用,类、对象、方法等数据均保存在JVM堆栈也就是申请的这块内存之中,JVM也会负责帮我们管理和回收再利用这块内存。
相对的,堆外内存就是直接调用系统malloc分配的内存,这部分内存不属于JVM直接管理,也不受JVM最大内存的限制,通过引用指向这段内存。
......
-
背景
某次在查看一个计时工具类时,发现这个工具类的实例被频繁创建和回收虽然这个类很轻,但考虑到是个基础工具类且这个功能需要频繁调用,希望尽量减轻这个工具对系统的影响优化目标是在线程安全的基础上池化某个类的对象,以复用这个对象
于是,初步方案是使用ThreadLocal为每个线程保存一个计时类对象。然而重构这个工具类之后,发现阿里规约插件提示“应该至少调用一次remove()方法”,还提示可能造成内存泄漏问题。奇怪了,记得之前看WeakReference时明确地看到ThreadLocal有用到弱引用,按理说不是GC的时候会自动回收吗?这还是Doug Lea写的呢。
源码探究带着如下问题分析一下源码:
ThreadLocal是如何实现每个线程保存一份独有变量的ThreadLocal使用了WeakReference,为什么阿里规约提示至少需要调用一次remove方法,真的会造成内存泄漏吗
ThreadLocal的实现思路ThreadLocal的实现非常巧妙,在每个线程增加了一个独有的“类似HashMap的结构”ThreadLocalMap,所有的ThreadLocal变量保存在这个ThreadLocalMap中。ThreadLocalMap是这样设计的:
......
-
背景在处理一个分页工作时,需要做一个向上取整的操作,类似这样:
// 总数
int totalSize = 799;
// 页大小
int pageSize = 200;
// 计算页数需要向上取整
......
-
背景在开发某个组件时,需要定期从数据库中拉取数据。由于整个逻辑非常简单,因此就启用了一个子线程(Thread)使用while循环+线程休眠来定期更新。这时候我又想起一个老生常谈的问题——如何优雅地停止线程?
思路大家都知道,Thread的stop方法早已废除,在高速上一脚猛刹,很可能人仰马翻,太危险。时至今日,这个问题早已有常规解决方案,即检测线程的interrupt变量值对应中断状态(下简称interrupt状态)时停止循环,也就是类似如下的形式:
while(!中断状态) { // interrupt状态
// do sth...
}
这个方案的确非常常规,但每次到用的时候总会忧心忡忡——要知道跟线程interrupt状态相关的方法可是有多种,他们有什么区别?这样做能保证正常中断吗?Java进程运行结束的时候这个线程会终止吗(涉及到Tomcat的重启问题)?
......
-
Java引用类型Java引用主要分为4种(其实似乎是5种):
Strong Reference 强引用,直接引用Soft Reference 软引用,间接引用Weak Reference 弱引用,间接引用Phantom Reference 虚引用,几乎无引用Final引用,这里不介绍
强引用
Object strongReference = new Object();
我们平常使用最多的就是强引用。按照JVM规范,在GC时通过可达性分析检测到强引用可达时,这个对象不会被回收。但是在某些情况下,强引用的这个特性会引起OutOfMemoryError,比如一直向集合中添加元素。
软引用软引用对象仅在内存不足时会被回收,JVM保证在抛出OutOfMemoryError之前已经将软引用对象全部清理了。
......
-
背景设想一个场景,我们需要将一个集合中满足条件的元素删除:客户端提交了一个Array类型的数据,经过Spring框架的转换我们接收到的是ArrayList,其中某些数据在校验后不合法,需要去除,仅保留校验通过的数据。这时候,我们通常有两种方案:
遍历,并将不合法数据删除遍历,将合法数据保存在另一个集合中
假设在考虑不同集合增删元素的效率,实现复杂度,以及不合法元素所占比例后(如果绝大多数是合法数据,那么方案2明显效率偏低),我们决定使用遍历删除的方案。这时候我们都会想到这个经典问题——如何在遍历List时删除其中元素。
很明显,我们都知道这样是错的:
// 例1. 这是错的,会抛出ConcurrentModificationException
for (Item i : inputList) {
......
-
背景介绍说到并发编程的问题,大多数人第一反应想到的就是大多数举线程安全例子时出现的一段代码:
...
i++; // 自增
...
然后很自然地想到,由于i++这个操作其实在底层是3个操作:
tmp1 = i;tmp2 = tmp1 + 1;i = tmp2;
......
-
LRU算法介绍LRU算法全称Least Recently Used,也就是检查最近最少使用的数据的算法。这个算法通常使用在内存淘汰策略中,用于将不常用的数据转移出内存,将空间腾给最近更常用的“热点数据”。初识这个算法忘了是在操作系统课还是计算机组成原理课上,其在Redis、Guava等工具中也有非常广泛的应用,甚至是最核心的思想之一。如果今后需要自己设计系统,即使不自己实现这个算法,LRU的思想也仍然是很重要的。
算法很简单,只需要将所有数据按使用时间排序,在需要筛选出LRU数据时,取排名靠后的即可。
算法实现Redis中的LRURedis中的数据量通常很庞大,如果每次对全量数据进行排序,势必将对服务吞吐量造成影响。因此,Redis在LRU淘汰部分key时,使用的是采样并计算近似LRU的,因此淘汰的是局部LRU数据。Redis内存淘汰策略maxmemory-policy配置可选参数:
noeviction:不淘汰,内存超限后写命令会返回错误(如OOM, del命令除外) allkeys-lru:所有key的LRU机制 在所有key中按照最近最少使用LRU原则剔除key,释放空间 volatile-lru:易失key的LRU 仅以设置过期时间key范围内的LRU(如均为设置过期时间,则不会淘汰) allkeys-random:所有key随机淘汰 一视同仁,随机 volatile-random:易失Key的随机 仅设置过期时间key范围内的随机 volatile-ttl:易失key的TTL淘汰 按最小TTL的key优先淘汰
Redis LRU的效果
左上-理论LRU效果;右上-Redis3.0中的近似LRU(采样值10);左下-Redis2.8中的近似LRU(采样值5);右下-Redis3.0中的近似LRU(采样值5)浅灰色-被淘汰;灰色-未被淘汰;绿色-新写入
......
-
synchronized锁加锁方式Java中的加锁大致分为两种:
通过synchronized关键字修饰通过Lock的实现类在代码逻辑中显示加锁,如ReentrantLock
锁的实现思路无论是通过synchronized关键字或是通过Lock加锁,其实现思路是一样的,是尽量保证无锁化。因此加锁过程都是通过锁升级的策略,不到万不得已不会使用资源消耗高的方法。
锁状态
偏向锁 这是在必须加锁的情况下最理想的状态,如果只有单一线程在访问资源,那么频繁的每次加锁、解锁也是非常消耗资源的。通俗描述是“在乐观情况下,避免每次都进行加锁解锁操作”。在偏向锁状态时:
“加锁” 实际上是将自身线程信息写入目标对象头信息中,并设置状态为“偏向锁状态”“解锁” 实际上并没有做任何事“尝试加锁” 实际上是检查目标对象头中的偏向锁拥有者是否是自己,如果是,那么说明没有其他线程修改,无需再次进行加锁操作在理想状态下,没有其他线程竞争,那么只有第一次加锁才需要消耗额外资源,其他情况下也是几乎没有额外消耗。
......