首页 文章 精选 留言 我的

精选列表

搜索[视频解析],共10000篇文章
优秀的个人博客,低调大师

Metric模块源码解析

分布式系统的监控告警及运维服务离不开指标监控,开务作为浪潮自主研发的一款分布式数据库自然也不例外。在兼顾强一致性、高可用分布式架构、在线水平扩展、企业级安全等特性下,开务的metric模块可提供监控指标,实现预先定义指标的周期性采集。同时,可以提供兼容 Prometheus 标准格式的 API 接口,方便与外部的 Prometheus 服务进行集成。 开务数据库metric 模块收集各模块相关统计的metric 信息,并将其作为 Prometheus 格式的指标储存起来用于进一步查阅,对判断开务数据库的运行情况有着重要作用,同时也是开务数据库adminUI 指标的数据来源。本期内容将围绕下图展示的metric模块基本框架,带领大家深入了解开务数据库metric 模块的源码,图中各模块的详细介绍将持续为大家更新。 1、定义接口介绍 1.IterableIterable:提供了一个同步访问内部对象的方法。方法如下: GetName() string 返回指标名 GetHelp() string 返回指标帮助文本 GetMeasurement() string 返回指标的lable GetUnit() Unit 返回指标使用的单位 GetMetadata() Metdata 返回指标的Metadata Inspect(func(interface{})) Inspect对每个包含的项调用给定的闭包 2.PrometheusExportable:是标准独立指标接口,可供指标导入 Prometheus。方法如下: GetName() string 返回指标名 GetHelp() string 返回指标帮助文本 GetType() *prometheusgo.MetricType 返回指标的Prometheus类型 GetLables() []*prometheusgo.LabelPair Metadata中的一个方法,返回指标的标签 ToPrometheusMetric() *prometheusgo.Metric 返回一个完成值填充的Prometheus指标 3.PrometheusIterable:是 PrometheusExportable 的扩展,用于指示该指标由增加父标签值的子指标组成。包含成员:PrometheusExportable。方法如下: Each([]*prometheusgo.LabelPair, func(metric *prometheusgo.Metric)) “Each”获取与父指标相关联的标签对切片,并使用每个子指标调用所传递的函数 2、Metric Metadata介绍 Metadata 包含关于指标的元数据,它必须嵌入到每个 metric object 中。它用于将有关指标的信息导出到 Promethues 和 adminUI 图表。 type Metadata struct { Name string Help string Measurement string Unit Unit MetricType _go.MetricType Labels []*LabelPair } // 方法 GetName() string GetHelp() string GetMeasurement() string GetUnit() Unit GetLabels() []*prometheusgo.LabelPair Addlabel(name value string)//给一个指标添加标签/值映射 3、指标类型介绍 1.Histogram:在一段时间范围内对数据进行采样(通常是请求持续时间、响应大小等),并将其计入可配置的存储桶(bucket)中,后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。 Prometheus 的 Histogram 是一种累积直方图,与上面的区间划分方式是有差别的。它的划分方式如下:假设每个 bucket 的宽度是0.2s,那么第一个 bucket 表示响应时间小于等于0.2s 的请求数量,第二个 bucket 表示响应时间小于等于0.4s 的请求数量,以此类推。也就是说,每一个 bucket 的样本包含了之前所有 bucket 的样本,所以叫累积直方图。 type Histogram { Metadata maxVal int64 mu struct { syncutil.Mutex cumulative *hdrhistogram.Histogram sliding *slidingHistogram } //hdrhistogram.Histogram type Histogram struct { lowestTrackableValue int64 highestTrackableValue int64 unitMagnitude int64 significantFigures int64 subBucketHalfCountMagnitude int32 subBucketHalfCount int32 subBucketMask int64 subBucketCount int32 bucketCount int32 countsLen int32 totalCount int64 counts []int64 } //slidingHistogram type slidingHistogram struct { windowed *hdrhistogram.WindowedHistogram nextT time.Time duration time.Duration } type WindowedHistogram struct { idx int h []Histogram m *Histogram Current *Histogram } //相关方法介绍 func (h *Histogram) Windowed() (*hdrhistogram.Histogram, time.Duration) 返回一份当前的窗口化直方图的数据和其中的时间间隔 func (h *Histogram) Snapshot() *hdrhistogram.Histogram 返回累积(即所有样本)直方图数据的副本 func (h *Histogram) RecordValue(v int64) RecordValue将给定的值添加到直方图。记录超过该直方图配置最大值使用方法 func (h *Histogram) TotalCount() int64 TotalCount返回样本的(累计)数量 func (h *Histogram) Min() int64 返回最小值 func (h *Histogram) Inspect(f func(interface{})) 调用带有空字符串和接收方的闭包 func (h *Histogram) GetType() *prometheusgo.MetricType 返回此指标的Prometheus类型enum func (h *Histogram) ToPrometheusMetric() *prometheusgo.Metric 返回正确类型的已填充的Prometheus度量值 func (h *Histogram) GetMetadata() Metadata 返回指标的元数据,包括Prometheus MetricType func NewHistogram(metadata Metadata, duration time.Duration, maxVal int64, sigFigs int) (*Histogram) 实例化一个新histogram func NewLatency(metadata Metadata, histogramWindow time.Duration) *Histogram NewLatency 返回一个带有适当默认值的直方图来跟踪延迟。数值以ns表示,截断为间隔[0,MaxLatency],并以1位精度记录(即误差在100ms时<10ms,在60s时<6s) 2.Counter:代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置。例如,你可以使用 Counter 类型的指标来表示服务的请求数、已完成的任务数、错误发生的次数等。 type Counter struct { Metadata metrics.Counter } type Counter interface { Clear() Count() int64 Dec(int64) Inc(int64) Snapshot() Counter } //相关方法介绍 func (c *Counter) Dec(int64) Dec重载了metric.Counter的方法。不能使用这种方法,它只用于防止误用metric类型 func (c *Counter) GetType() *prometheusgo.MetricType 返回此指标的Prometheus类型enum func (c *Counter) Inspect(f func(interface{})) 调用带有空字符串和接收方的闭包,即返回自己c func (c *Counter) MarshalJSON() ([]byte, error) MarshalJSON将数据封装到JSON func (c *Counter) GetMetadata() Metadata 返回指标的元数据,包括Prometheus MetricType 3.Gauge:代表一种样本数据可以任意变化的指标,即可增可减。Guage 通常用于像温度或者内存使用率这种指标数据,也可以表示能随时增加或减少的“总数”,例如:当前并发请求的数量。 type Gauge struct { Metadata value *int64 fn func() int64 } //相关方法介绍 func (g *Gauge) Snapshot() metrics.Gauge Snapshot返回Gauge的只读副本 func (g *Gauge) Update(v int64) 更新Gauge的值 func (g *Gauge) Inc(i int64) 增加Gauge的当前值 func (g *Gauge) Dec(i int64) 减少Gauge的当前值 func (g *Gauge) Value() int64 Value返回Gauge的当前值 func (g *Gauge) GetType() *prometheusgo.MetricType 返回此指标的Prometheus类型enum func (g *Gauge) ToPrometheusMetric() *prometheusgo.Metric 返回此指标的Prometheus类型enum func (g *Gauge) GetMetadata() Metadata 返回指标的元数据,包括Prometheus MetricType 4.Rate:用来计算某个指标在最近一个区间时间内的变化率。 type Rate struct { Metadata mu syncutil.Mutex // protects fields below curSum float64 wrapped ewma.MovingAverage interval time.Duration nextT time.Time } //相关方法介绍 func (e *Rate) GetType() *prometheusgo.MetricType GetType返回该指标的Prometheus类型enum func (e *Rate) Inspect(f func(interface{})) Inspect用自身调用给定的闭包 func (e *Rate) ToPrometheusMetric() *prometheusgo.Metric 返回此指标的Prometheus类型enum func (c *Counter) MarshalJSON() ([]byte, error) MarshalJSON将数据封装到JSON func (e *Rate) GetMetadata() Metadata GetMetadata返回指标的元数据,包括Prometheus MetricType func (e *Rate) Value() float64 Value返回Rate的当前值 func (e *Rate) tick() Rate时间前进 func (e *Rate) nextTick() time.Time 返回Rate的当前时间。 func (e *Rate) Add(v float64) 添加将给定的测量值添加到Rate 4、注册器Registry介绍 Registry 是 metric 的列表,它提供了一种处理指标的方法,可以将 metric 编组成 JSON,并生成 Prometheus 格式的 metric。同时可以给注册的指标打上标签,当导出到 Prometheus 时,这些标签将应用于它的所有指标。 type Registry struct { syncutil.Mutex labels []*prometheusgo.LabelPair tracked []Iterable } //相关方法介绍 func (r *Registry) AddLabel(name, value string) AddLabel为这个注册表添加一个标签/值对 func (r *Registry) AddMetric(metric Iterable) AddMetric将传入的metric添加到注册表 func (r *Registry) WriteMetricsMetadata(dest map[string]Metadata) WriteMetricsMetadata将所有跟踪metric的元数据写入参数映射 func (r *Registry) Each(f func(name string, val interface{})) 每个函数对所有metric调用给定的闭包 func (r *Registry) MarshalJSON() ([]byte, error) 格式化到JSON格式 5、注册新Registry步骤 // 以txnMetric说明 //txn_metric.go //声明定义的指标结构体类型 type TxnMetrics struct { Commits *metric.Counter ... } //定义指标的metadata var( metaCommitsRates = metric.Metadata{ Name: "txn.commits", Help: "Number of committed KV transactions (including 1PC)", Measurement: "KV Transactions", Unit: metric.Unit_COUNT, } ... ) //将定义的指标类型和metadata相关联 func MakeTxnMetrics(histogramWindow time.Duration) TxnMetrics { return TxnMetrics{ Commits: metric.NewCounter(metaCommitsRates), } //server.go: //注册进Registry txnMetrics := kvcoord.MakeTxnMetrics(cfg.HistogramWindowInterval()) registry.AddMetricStruct(txnMetrics) 开务数据库是一款浪潮集团核心研发的先进、安全的云原生分布式数据库;具备云原生、多中心、高可用、事务强一致等特性,满足 HTAP 场景需求。业务范围覆盖能源、工业互联网、政务、教育、金融等多行业。我们是一支平均年龄 30 岁的年轻团队,在短短不到三年的时间里,我们已取得近 300 项发明专利受理,10 项自有产品软著授权。热烈欢迎广大伙伴加入我们的团队,热门岗位火热招聘中,简历投递邮箱: zhoubeili@inspur.com / bixueting@inspur.com 数据库存储内核研发工程师 工作职责: 1、负责存储子系统的研发路线规划、架构设计和关键技术问题攻关; 2、负责编写功能测试用例,测试工具进行系统验证; 3、负责数据库的系统性能诊断与调优; 4、负责数据库相关关键技术的预研和在团队中的引导; 5、深入理解业务场景的数据库存储需求,针对性的为不同业务场景提供最合适的存储方案。 任职要求: 1、学历:本科或者本科以上学历; 2、专业:计算机或相关专业; 3、专业知识: — 3 年及以上 GO/C++ 开发经验; — 精通 C/C++/GO 语言,Linux 系统编程。熟悉无锁数据结构,熟悉现代硬件体系结构 (CPU/Cache/Memory/Storage), 熟悉并发编程; — 熟练使用 MySQL、PostgeSQL 等主流数据库; — 熟悉数据库存储系统的基本理论,熟悉事务处理,日志与恢复策略,多版本并发控制技术的实现,对数据库的基本理论和内部实现机制有深刻的理解; — 技术视野开阔,有一定的系统性能优化经验,掌握各种性能诊断工具和各种优化方法; — 熟悉时序数据库,有实际的时序数据库开发经验优先; — 熟悉 RocksDB、Arrow、Parquet 等开源存储项目源码者优先。 Base 地: 上海 / 天津 / 济南 / 北京 数据库方案工程师 工作职责: 1、负责分布式数据库,或其相关工具、平台等产品的梳理、规划、设计和推进工作; 2、进行解决方案的调研、设计和验证; 3、设计、撰写和维护产品红皮书; 4、跨部门沟通,协调各类资源以确保产品顺利上线,推进产品迭代。 任职要求: 1、5 年以上的数据库运维及方案设计经验(ORACLE/Mysql/PostgreSQL 任意一种),对部署,优化,灾备,恢复,高可用有实际经验; 2、1 年左右的分布式数据库经验,了解国内任意一款分布式数据库,有部署,POC,问题处理经验; 3、对 OLTP 和 OLAP 系统或其中一种有实际运维设计经验; 4、对数据库灾备,同步方案有实际项目经验; 5、会一种数据库 benchmark 工具,设计相应场景进行测试并结合已有经验给与相应调整优化; 6、有基本的编程能力,如 go,shell,python 其中一项,可以写简单程序对数据库进行并发测试,功能验证; 7、有项目管理能力,很好的沟通能力,可以与开发人员顺畅沟通,并于合作高校学生完成实验及文档编写; 8、扎实的技术,linux 和数据库方面有一定积累,能对开发人员及学生进行一定指导,促使相关工作顺利推进; 9、较强的文档编写组织能力,根据实验文档及相关手册,编写用户解决方案手册; 10、有一定语言表达能力,能做数据库相关功能培训。 Base 地: 上海 / 天津 / 济南 / 北京

优秀的个人博客,低调大师

深度解析ThreadLocal原理

今天呢,和大家聊一下ThreadLocal。 1. 是什么? JDK1.2提供的的一个线程绑定变量的类。 他的思想就是:给每一个使用到这个资源的线程都克隆一份,实现了不同线程使用不同的资源,且该资源之间相互独立 2. 为什么用? 思考一个场景:数据库连接的时候,我们会创建一个Connection连接,让不同的线程使用。这个时候就会出现多个线程争抢同一个资源的情况。 这种多个线程争抢同一个资源的情况,很常见,我们常用的解决办法也就两种:空间换时间,时间换空间 没有办法,鱼与熊掌不可兼得也。就如我们的CAP理论,也是牺牲其中一项,保证其他两项。 而针对上面的场景我们的解决办法如下: 空间换时间:为每一个线程创建一个连接。 直接在线程工作中,创建一个连接。(重复代码太多) 使用ThreadLocal,为每一个线程绑定一个连接。 时间换空间:对当前资源加锁,每一次仅仅存在一个线程可以使用这个连接。 通过ThreadLocal为每一个线程绑定一个指定类型的变量,相当于线程私有化 3. 怎么用? ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); threadLocal.get(); threadLocal.set(1); threadLocal.remove(); 没错,这四行代码已经把ThreadLocal的使用方法表现得明明白白。 get从ThreadLocal拿出一个当前线程所拥有得对象 set给当前线程绑定一个对象 remove将当前线程绑定的当前对象移除 记住在使用的以后,一定要remove,一定要remove,一定要remove 为什么要remove。相信不少小伙伴听到过ThreadLocal会导致内存泄漏问题。 没错,所以为了解决这种情况,所以你懂吧,用完就移除,别浪费空间(渣男欣慰) 看到这,脑袋上有好多问号出现了(小朋友你是否有很多问号?) 为啥会引发内存泄漏? 为啥不remove就内存泄漏了 它是怎么讲对象和线程绑定的 为啥get的时候拿到的就是当前线程的而不是其他线程的 它怎么实现的??? 来吧,开淦,源码来 4. 源码解读 先来说一个思路:如果我们自己写一个ThreadLocal会咋写? 线程绑定一个对象。**这难道不是我们熟知的map映射?**有了Map我们就可以以线程为Key,对象为value添加到一个集合中,然后各种get,set,remove操作,想怎么玩就怎么玩,搞定。😀 这个时候,有兄弟说了。你这思路不对啊,你这一个线程仅仅只能存放一个类型的变量,那我想存多个呢? 摸摸自己充盈的发量,你说出了一句至理名言:万般问题,皆系于源头和结果之中。 从结果考虑,让开发者自己搞线程私有(估计被会开发者骂死) 来吧,从源头考虑。现在我们的需求是:线程可以绑定多个值,而不仅仅是一个。嗯,没错,兄弟们把你们的想法说出来。 让线程自己维护一个Map,将这个ThreadLocal作为Key,对象作为Value不就搞定了 兄弟,牛掰旮旯四 此时,又有兄弟说了。按照你这样的做法,将ThreadLocal扔到线程本身的的Map里,那岂不是这个ThreadLocal一直被线程对象引用,所以在线程销毁之前都是可达的,都无法GC呀,有BUG啊??? **好,问题。**这样想,既然由于线程和ThreadLocal对象存在引用,导致无法GC,那我将你和线程之间的引用搞成弱引用或者软引用不就成了。一GC你就没了。 啥,你不知道啥是弱引用和软引用??? 前面讲过的东西,算啦再给你们复习一波。 JDK中存在四种类型引用,默认是强引用,也就是我们经常干的事情。疯狂new,new,new。这个时候创建的对象都是强引用。 强引用。直接new 软引用。通过SoftReference创建,在内存空间不足的时候直接销毁,即它可能最后的销毁地点是在老年区 弱引用。通过WeakReference创建,在GC的时候直接销毁。即其销毁地点必定为伊甸区 虚引用。通过PhantomReference创建,它和不存也一样,非常虚,只能通过引用队列在进行一些操作,主要用于堆外内存回收 好了,回到正题,上面的引用里最适合我们当前的场景的就是弱引用了,为什么这个样子说: 在以往我们使用完对象以后等着GC清理,但是对于ThreadLocal来说,即使我们使用结束,也会因为线程本身存在该对象的引用,处于对象可达状态,垃圾回收器无法回收。这个时候当ThreadLocal太多的时候就会出现内存泄漏的问题。 而我们将ThreadLocal对象的引用作为弱引用,那么就很好的解决了这个问题。当我们自己使用完ThreadLocal以后,当GC的时候就会将我们创建的强引用直接干掉,而这个时候我们完全可以将线程Map中的引用干掉,于是使用了弱引用,这个时候大家应该懂了为啥不使用软引用了吧 还有一个问题:为什么会引发内存泄漏呢? 了解Map结构的兄弟们应该清楚,内部实际就一个节点数组,对于ThreadLocalMap而言,内部是一个Entity,它将Key作为弱引用,Value还是强引用。如果我们在使用完ThreadLocal以后,没有对Entity进行移除,会引发内存泄漏问题。 ThreadLocalMap提供了一个方法expungeStaleEntry方法用来排除无效的Entity(Key为空的实体) 说到这里,有一个问题我思考了蛮久的,value为啥不搞成弱引用,用完直接扔了多好 最后思考出来得答案(按照源码推了一下): 不设置为弱引用,是因为不清楚这个Value除了map的引用还是否还存在其他引用,如果不存在其他引用,当GC的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用。 而为了解决这个强引用的问题,它提供了一种机制就是上面我们说的将Key为Null的Entity直接清除 到这里,这个类的设计已经很清楚了。接下来我们看一下源码吧! 需要注意的一个点是:ThreadLocalMap解决哈希冲突的方式是线性探测法。 人话就是:如果当前数组位有值,则判断下一个数组位是否有值,如果有值继续向下寻找,直到一个为空的数组位 Set方法 class ThreadLocal public void set(T value) { //拿到当前线程 Thread t = Thread.currentThread(); //获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) //如果当前线程的Map已经创建,直接set map.set(this, value); else //没有创建,则创建Map createMap(t, value); } private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //拿到当前数组位,当前数组位是否位null,如果为null,直接赋值,如果不为null,则线性查找一个null,赋值 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; //清除一些失效的Entity if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ThreadLocalMap getMap(Thread t) { //获取当前线程的ThreadLocalMap return t.threadLocals; } void createMap(Thread t, T firstValue) { //当前对象作为Key,和我们的设想一样 t.threadLocals = new ThreadLocalMap(this, firstValue); } Get方法 public T get() { //获取当前线程 Thread t = Thread.currentThread(); //拿到当前线程的Map ThreadLocalMap map = getMap(t); if (map != null) { //获取这个实体 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; //返回 return result; } } return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { //计算数组位 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //如果当前数组有值,且数组位的key相同,则返回value if (e != null && e.get() == key) return e; else //线性探测寻找对应的Key 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) //排除当前为空的Entity expungeStaleEntry(i); else //获取下一个数组位 i = nextIndex(i, len); e = tab[i]; } //如果没有找到直接返回空 return null; } remove public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //拿到当前的数组,判断是否为需要的数组位,如果不是线性查找 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); //清空位NUll的实体 expungeStaleEntry(i); return; } } } 我们可以看到一个现象:在set,get,remove的时候都调用了expungeStaleEntry来将所有失效的Entity移除 看一下这个方法做了什么 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 删除实体的Value tab[staleSlot].value = null; //置空这个数组位 tab[staleSlot] = null; //数量减一 size--; // 重新计算一次哈希,如果当前数组位不为null,线性查找直到一个null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { 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; } 更多原创内容请关注博主

优秀的个人博客,低调大师

AOP编程全解析

AOP是一种编程思想,一套规范。 软件开发经历了面向过程编程时代,以C语言为代表,之后是面向对象编程时代,以Java语言为代表。 在21世纪大牛们又提出了一种新的编程思想面向方面编程,即AOP理念,全称Aspect-Oriented Programming。 AOP是第三代编程思想,到哪免不了都要问下。 发展历史 1997年在面向对象编程大会上Gregor Kiczales等人首次提出了AOP的概念,之后各大公司等分别加入研究。2001年Palo Alto研究中心发布了首个支持AOP的语言AspectJ,同时也是一个规范。 目标定位 在对真实世界抽象的面向对象编程过程中,始终伴随着某写操作的代码无法实现模块化封装,会散落在各个对象中存在,特别是非功能性代码。对于一般的功能开发采取面向对象方式进行抽象是能够很好应付的,但是面向方面(切面)给了一种新的思维方式来考虑编程,能更好的进行全局结构化思考。 所以AOP主要解决两个问题: 代码分散问题,特别是那些非功能性代码。 作为面向对象编程思维的一种补充和完善。 核心知识点 连接点 连接点:join point,程序的一个执行点,如类中的一个方法,方法里面一个代码块。 切入点 切入点:point cut,是一个捕获连接点的代码结构,就是定义一个代码逻辑用来捕获某个连接点的代码。 方面 方面;aspect,是具体被执行的切面逻辑代码,类似于一个类。 通知 通知:advice,是point cut执行的代码,定义在连接点什么时机来执行aspect。 主要运用场景 场景分为2类: 一类是非功能性需求,如日志、异常、安全、事务都可以使用AOP思想编程。 另一类是功能性需求,在原来对象抽象的思维中添加AOP思维,这里是一种结构化思维,在定义类时考虑多个类的切面共性。 主流AOP语言实现 对AOP实现除了AspectJ外,已知的还有JBoss AOP、Spring AOP等。 这里只介绍AspectJ和SpringAOP,重点是他们不同点。 AspcetJ AspectJ采用静态织入方式进行切面织入原代码,提供独立的编译器把切面和原代码的java文件编织成一个新的class文件。提供了详细的编译日志和调试工具,编译时间长但是运行效率高。 连接点的支持范围: 方法和构造器调用 方法和构造器执行 属性访问 异常处理 类初始化,是static代码块 语法结构 控制流 对象及参数类型 条件测试 关联连接点通知方式: before,连接点执行前运行 after,连接点执行后运行 around,连接点的整个外侧,整个包住,能够绝的连接点执行和修改上下文环境 Spring AOP Spring AOP没有完全实现AspectJ语言,它更多的是对Spring framwork进行Aop能力的扩展实现,补全Spring framework的不足并让Aop与Spring framwork融合。 连接点只支持方法拦截调用。 连接点通知方式在aspect的before、after、around的基础上增加throw对异常的触发的拦截。 Spring AOP与Spring IoC体系融合,对于aspect类统一交由Spring beans管理,并且提供ProxyFactoryBean的AOP代理工厂类,还有自动代理的BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator的强大工具。 Spring AOP是动态织入,在运行时完成AOP的aspect代码织入原代码逻辑中。其底层默认采用JDK的动态代理实现AOP代理,当对象没有实现接口时,CGLIB会默认使用。 优缺点 优点:解决代码散乱问题、代码逻辑解偶、易于维护、提供扩展性和可重用性。 缺点:切面越多系统越复杂难懂、工程师学习成本增加(业务不再是线型,变成了跳跃式) AOP编程要慎重使用,作为面向对象编程的一种补充。 作者:Owen Jia 关注他的博客:https://blog.shareworld.vip

优秀的个人博客,低调大师

ReentrantLock 核心源码解析

学习完 AQS,本文我们就来研究第一个 AQS 的实现类:ReentrantLock。 1 基本设计 ReentrantLock 可重入锁,可重入表示同一个线程可以对同一个共享资源重复的加锁或释放锁。 具有与使用 synchronized 方法和语句访问的隐式监视器锁相同的基本行为和语义的可重入互斥锁,但具有扩展功能。 ReentrantLock 由最后成功锁定但尚未解锁的线程所拥有。当另一个线程不拥有该锁时,调用该锁的线程将成功返回该锁。如果当前线程已经拥有该锁,则该方法将立即返回。可以使用 isHeldByCurrentThread 和getHoldCount 方法进行检查。 此类的构造函数接受一个可选的 fairness 参数。设置为true时,在争用下,锁倾向于授予给等待时间最长的线程。否则,此锁不能保证任何特定的访问顺序。使用多线程访问的公平锁的程序可能会比使用默认设置的程序呈现较低的总吞吐量(即较慢;通常要慢得多),但获得锁并保证没有饥饿的时间差异较小。但是请注意,锁的公平性不能保证线程调度的公平性。因此,使用公平锁的多个线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。还要注意,未定时的 tryLock 方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它将成功。 建议的做法是始终立即在调用后使用try块进行锁定,最常见的是在构造之前/之后,例如: class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } } 除了实现Lock接口之外,此类还定义了许多用于检查锁状态的 public 方法和 protected 方法。 其中一些方法仅对检测和监视有用。 此类的序列化与内置锁的行为相同:反序列化的锁处于解锁状态,而不管序列化时的状态如何。 此锁通过同一线程最多支持2147483647个递归锁。 尝试超过此限制会导致锁定方法引发错误。 2 类架构 ReentrantLock 本身不继承 AQS,而是实现了 Lock 接口 Lock 接口定义了各种加锁,释放锁的方法,比如 lock() 这种不响应中断获取锁,在ReentrantLock 中实现的 lock 方法是通过调用自定义的同步器 Sync 中的的同名抽象方法,再由两种模式的子类具体实现此抽象方法来获取锁。 ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这些方法,这些方法的底层实现都是交给 Sync 实现。 3 构造方法 无参数构造方法相当于 ReentrantLock(false),默认为非公平的锁 有参构造方法,可以选择锁的公平性 可以看出 公平锁依靠 FairSync 实现 非公平锁依靠 NonfairSync 实现 4 Sync 同步器 结构图 继承体系 可见是ReentrantLock的抽象静态内部类 Sync 继承了 AbstractQueuedSynchronizer ,所以ReentrantLock依靠 Sync 就持有了锁的框架,只需要 Sync 实现 AQS 规定的非 final 方法即可,只交给子类 NonfairSync 和 FairSync 实现 lock 和 tryAcquire 方法 4.1 NonfairSync - 非公平锁 Sync 对象的非公平锁 4.1.1 lock 非公平模式的 lock 方法 若 CAS(已经定义并实现在 AQS 中的 final 方法)state 成功,即获取锁成功并将当前线程设置为独占线程 若 CAS state 失败,即获取锁失败,则进入 AQS 中已经定义并实现的 Acquire 方法善后 这里的 lock 方法并没有直接调用 AQS 提供的 acquire 方法,而是先试探地使用 CAS 获取了一下锁,CAS 操作失败再调用 acquire 方法。这样设计可以提升性能。因为可能很多时候我们能在第一次试探获取时成功,而不需要再经过 acquire => tryAcquire => nonfairAcquire 的调用链。 4.1.2 tryAcquire 其中真正的实现 nonfairTryAcquire 就定义在其父类 Sync 中。下一节分析。 4.2 FairSync - 公平锁 只实现 lock 和 tryAcquire 两个方法 4.2.1 lock 公平模式的 lock 直接调用 acquire,而没有像非公平模式先试图获取,因为这样可能导致违反“公平”的语义:在已等待在队列中的线程之前获取了锁。acquire 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待,详情见本专栏的上一文 4.2.2 tryAcquire 公平模式的 tryAcquire。不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 该方法是 AQS 在 acquire 方法中留给子类去具体实现的 话不多说,看源码: protected final boolean tryAcquire(int acquires) { // 获取当前的线程 final Thread current = Thread.currentThread(); // 获取 state 锁的状态 int c = getState(); // state == 0 => 尚无线程获取锁 if (c == 0) { // 判断 AQS 的同步对列里是否有线程等待,若没有则直接 CAS 获取锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 获取锁成功,设置独占线程 setExclusiveOwnerThread(current); return true; } } // 判断已经获取锁是否为当前的线程 else if (current == getExclusiveOwnerThread()) { // 锁的重入, 即 state 加 1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } 和 Sync 的 nonfairTryAcquire 方法实现类似,唯一不同的是当发现锁未被占用时,使用 hasQueuedPredecessors 确保了公平性。 hasQueuedPredecessors 会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点) 如果是(返回false),符合FIFO,可以获得锁 如果不是(返回true),则继续等待 public final boolean hasQueuedPredecessors() { // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 Node t = tail; // 按反初始化顺序读取字段 Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } 5 nonfairTryAcquire 执行非公平的 tryLock。 tryAcquire 是在子类中实现的,但是都需要对trylock 方法进行非公平的尝试。 final boolean nonfairTryAcquire(int acquires) { // 获取当前的线程 final Thread current = Thread.currentThread(); // 获取 AQS 中的 state 字段 int c = getState(); // state 为 0,表示同步器的锁尚未被持有 if (c == 0) { // CAS state 获取锁(这里可能有竞争,所以可能失败) if (compareAndSetState(0, acquires)) { // 获取锁成功, 设置获取独占锁的线程 setExclusiveOwnerThread(current); // 直接返回 true return true; } } // 判断现在获取独占锁的线程是否为当前线程(可重入锁的体现) else if (current == getExclusiveOwnerThread()) { // state 计数加1(重入获取锁) int nextc = c + acquires; if (nextc < 0) // 整型溢出 throw new Error("Maximum lock count exceeded"); // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } return false; } 无参的 tryLock 调用的就是此方法 6 tryLock 6.1 无参 Lock 接口中定义的方法。 仅当锁在调用时未被其他线程持有时,才获取锁 如果锁未被其他线程持有,则获取锁,并立即返回值 true,将锁持有计数设置为1。即使这个锁被设置为使用公平的排序策略,如果锁可用,调用 tryLock() 也会立即获得锁,不管其他线程是否正在等待锁。这种妥协行为在某些情况下是有用的,虽然它破坏了公平。如果想为这个锁执行公平设置,那么使用 tryLock(0, TimeUnit.SECONDS),这几乎是等价的(它还可以检测到中断)。 如果当前线程已经持有该锁,那么持有计数将增加1,方法返回true。如果锁被另一个线程持有,那么这个方法将立即返回值false。 典型的使用方法 Lock lock = ...; if (lock.tryLock()) { try { // manipulate protected state } finally { lock.unlock(); } } else { // 执行可选的操作 } 6.2 有参 提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false 其中的 doAcquireNanos 已经实现好在 AQS 中。 7 tryRelease 释放锁,对于公平和非公平锁都适用 protected final boolean tryRelease(int releases) { // 释放 releases (由于可重入,这里的 c 不一定直接为 0) int c = getState() - releases; // 判断当前线程是否是获取独占锁的线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 锁已被完全释放 if (c == 0) { free = true; // 无线程持有独占锁,所以置 null setExclusiveOwnerThread(null); } setState(c); return free; } 8 总结 AQS 搭建了整个锁架构,子类锁的实现只需要根据场景,实现 AQS 对应的方法即可。

优秀的个人博客,低调大师

ThreadLocal 核心源码解析

# 1 前言 此类提供线程本地变量。这些变量与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 ThreadLocal 实例通常是期望将状态与线程(例如,用户ID或事务ID)关联的类中的 private static 字段。 例如,下面的类生成每个线程本地的唯一标识符。线程的ID是在第一次调用ThreadId.get() 时赋值的,并且在以后的调用中保持不变。 ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585021172479_C478AA1A06FECA1567F29CCF948C1245 "图片标题") 只要线程是活跃的并且 ThreadLocal 实例是可访问的,则每个线程都对其线程本地变量的副本持有隐式的引用。线程消失后,线程本地实例的所有副本都会被 GC(除非存在对这些副本的其他引用)。 # 2 继续体系 - 继承?不存在的,这其实也是 java.lang 包下的工具类 ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585033878314_CBF921C0352B45E6E7BDDA2A13D174F9 "图片标题") - 但是 ThreadLocal 定义带有泛型,说明可以储存任意格式的数据. ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585030744135_9E087D203EA7418ACDD0B8DB51B48F4B "图片标题") # 3 属性 - ThreadLocal 依赖于附加到每个线程(Thread.threadLocals和InheritableThreadLocals)的线程线性探测哈希表. ThreadLocal 对象充当键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见的情况下仍然表现良好。 一句话总结: ThreadLocal 通过这样的 hashCode,计算当前 ThreadLocal 在 ThreadLocalMap 中的索引 ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585033954555_EC15B568153F5227211EFE4BE8EA269C "图片标题") ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585037145014_6433D86909F472C44933A277CB24F5E1 "图片标题") - 连续生成的哈希码之间的差值,关于该值的设定,可参考文章[ThreadLocal的hash算法(关于 0x61c88647)](https://juejin.im/post/5cced289f265da03804380f2) ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585037195533_D4EDF4BB15420CD3CD0A413647C49E23 "图片标题") - 注意 static 修饰,ThreadLocalMap 会被 set 多个 ThreadLocal ,而多个 ThreadLocal 就根据 threadLocalHashCode 区分 ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585039473385_011B3B54168C0B11A7CEF11FFAA4B4A8 "图片标题") # 4 ThreadLocalMap ThreadLocalMap 是自定义的哈希表,仅适用于维护线程本地的值。没有操作导出到ThreadLocal类之外。 该类是包私有的,允许在 Thread 类中的字段声明。为了帮助处理非常长的使用寿命,哈希表节点使用 WeakReferences 作为键。但是,由于不使用引用队列,因此仅在表空间不足时,才保证删除过时的节点。 ```java static class ThreadLocalMap { /** * 此哈希表中的节点使用其主引用字段作为键(始终是一个 ThreadLocal 对象) * 继承了 WeakReference。 * 请注意,空键(即entry.get()== null)意味着不再引用该键,因此可以从表中删除该节点。 * 在下面的代码中,此类节点称为 "stale entries" */ static class Entry extends WeakReference> { /** 与此 ThreadLocal 关联的值 */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * 初始容量 -- 必须是 2 的幂 */ private static final int INITIAL_CAPACITY = 16; /** * table 数组,必要时扩容 * table.length 必须是 2 的幂 */ private Entry[] table; /** * table 中的节点个数 */ private int size = 0; /** * 下一次扩容的阈值 */ private int threshold; // 默认为 0 ``` ## 特点 - key 是 ThreadLocal 的引用 - value 是 ThreadLocal 保存的值 - 数组的数据结构 # 5 set ## 5.1 ThreadLocal#set 将此线程本地变量的当前线程副本设置为指定值。大多数子类将不需要重写此方法,而仅依靠initialValue方法来设置线程本地变量的值。 ![](https://uploadfiles.nowcoder.com/images/20200324/5088755_1585042632088_456A49E4B894B791C01A12C128A2B662 "图片标题") ### 执行流程 1. 获取当前线程 2. 获取线程所对应的ThreadLocalMap,从这可以看出每个线程都是独立的,所以此方法天然线程安全 3. 判断 map 是否为 null - 否,则 K.V 对赋值,k 为this,即当前的 ThreaLocal 对象 - 是,则初始化一个 ThreadLocalMap 来维护 K.V 对 来具体看看ThreadLocalMap中的 set ## 5.2 ThreadLocalMap#set ```java private void set(ThreadLocal<?> key, Object value) { // 新引用指向 table Entry[] tab = table; int len = tab.length; // 获取对应 ThreadLocal 在table 中的索引,注意这里是 hashCode 与 2 幂次长度-1(想起来为什么这样计算更好了吗?) int i = key.threadLocalHashCode & (len-1); /** * 从该下标开始循环遍历 * 1、如遇相同key,则直接替换value * 2、如果该key已经被回收失效,则替换该失效的key */ for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到内存地址一样的 ThreadLocal,直接替换 if (k == key) { e.value = value; return; } // 若 k 为 null,说明 ThreadLocal 被清理了,则替换当前失效的 k if (k == null) { replaceStaleEntry(key, value, i); return; } } // 找到空位,创建节点并插入 tab[i] = new Entry(key, value); // table内元素size自增 int sz = ++size; // 达到阈值(数组大小的三分之二)时,执行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ``` 注意通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。 # 6 get ```java public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果map不为空 if (map != null) { // 取得当前ThreadLocal对象对应的Entry ThreadLocalMap.Entry e = map.getEntry(this); // 如果不为空,读取当前 ThreadLocal 中保存的值 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 否则都执行 setInitialValue return setInitialValue(); } ``` ### ```java private T setInitialValue() { // 获取初始值,一般是子类重写 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果map不为null if (map != null) // 调用ThreadLocalMap的set方法进行赋值 map.set(this, value); // 否则创建个ThreadLocalMap进行赋值 else createMap(t, value); return value; } ``` 接着我们来看下 ## ThreadLocalMap#getEntry ```java // 得到当前 thradLocal 对应的值,值的类型是由 thradLocal 的泛型决定的 // 由于 thradLocalMap set 时解决数组索引位置冲突的逻辑,导致 thradLocalMap get 时的逻辑也是对应的 // 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止 private Entry getEntry(ThreadLocal<?> key) { // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getEntryAfterMiss 方法找 if (e != null && e.get() == key) return e; else // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的 return getEntryAfterMiss(key, i, e); } // 自旋 i+1,直到找到为止 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的 while (e != null) { ThreadLocal<?> k = e.get(); // 内存地址一样,表示找到了 if (k == key) return e; // 删除没用的 key if (k == null) expungeStaleEntry(i); // 继续使索引位置 + 1 else i = nextIndex(i, len); e = tab[i]; } return null; } ``` # 6 扩容 ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下扩容的逻辑: ```java private void resize() { // 拿出旧的数组 Entry[] oldTab = table; int oldLen = oldTab.length; // 新数组的大小为老数组的两倍 int newLen = oldLen * 2; // 初始化新数组 Entry[] newTab = new Entry[newLen]; int count = 0; // 老数组的值拷贝到新数组上 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { // 计算 ThreadLocal 在新数组中的位置 int h = k.threadLocalHashCode & (newLen - 1); // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置 while (newTab[h] != null) h = nextIndex(h, newLen); // 给新数组赋值 newTab[h] = e; count++; } } } // 给新数组初始化下次扩容阈值,为数组长度的三分之二 setThreshold(newLen); size = count; table = newTab; } ``` 源码注解也比较清晰,我们注意两点: 扩容后数组大小是原来数组的两倍; 扩容时是绝对没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性,一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。 # 7 总结 ThreadLocal 是非常重要的 API,我们在写一个中间件的时候经常会用到,比如说流程引擎中上下文的传递,调用链ID的传递等等,非常好用,但坑也很多。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册