Jiahui's Blog


  • 首页

  • 归档

  • 标签

《学会提问》读书笔记

发表于 2017-10-16

图书信息

书名(中文):《学会提问》(第10版)

书名(英文):《Asking The Right Questions : A Guide to Critical Thinking》

作者:(美国)尼尔•布朗(Neil Browne) 斯图尔特•基利(Stuart M.Keeley)

译者:吴礼敬

出版社:机械工业出版社

目录

学会提出好问题

1.1 批判性思维涉及:
(1)有一套相互关联、环环相扣的关键问题的意识;(2)恰如其分地提出和回答关键问题的能力;(3)积极主动地利用关键问题的强烈愿望。

1.2 思维的两种风格:
1)海绵式思维(强调知识的获得);2)淘金式思维(强调与知识积极的互动)

淘金式思维清单:

  • 我有没有问“为什么”别人要我相信他的观点
  • 在我想到别人的说法可能有问题时有没有把它记下来
  • 我对别人说过的话有没有进行客观评价
  • 针对某一特定主题我有没有在别人合理说法的基础上形成自己的结论

1.3 弱批判性思维:利用批判性思维来捍卫自己现有的立场和看法;强批判性思维:利用批判性思维技能来评估所有断言和看法,尤其是自己的看法。

1.4 批判性思考的人拥有的主要价值观:1)自主性;2)好奇心;3)谦恭有礼;4)以理服人者逢之必敬

1.5 警惕感情用事,一厢情愿。

论题和结论是什么

2.1 论题:引发讨论的问题或争议

1)描述性论题(descriptive issues):针对有关过去、现在、未来的描述是否正确提出的问题(引起高血压的原因是什么?提高销售税的决定是谁做出的?)

2)规定性论题(prescriptive issues):针对我们应当怎样做及对与错、好与坏提出的问题(死刑应该被废除吗?我们必须禁止SUV吗?)

2.2 结论就是演讲者或作者希望你接受的信息。没有证据支撑的断言:纯观点。

找到结论有据可循:

  • 论题是什么
  • 寻找指示词
  • 开头结尾
  • 这些不属于结论:例子、数据、定义、背景资料、证据
  • 交流的语境、作者的背景
  • 问一问“所以呢?”

2.3 一些建议:写作之前先将论题的范围尽量缩小。引导读者得出你的结论。

理由是什么

3.1 理由是对我们为什么要相信某个结论的解释说明或逻辑依据。

3.2 理由+结论=论证(推理)

3.3 结论站不站得住脚主要取决于理由的扎实与否。理由是模具,结论据此得以成型和修改。

3.4 写作的一些建议:在作出结论前要探究可能存在的种种理由;找到涵盖你的论题的主要刊物;帮助读者确定你的理由。

哪些词语意思不明确

4.1 只有理解了关键术语和词组的意思(直接或含蓄),才能对一个论证进行评价。

4.2 找准关键词。这里的词或词组,指的是在论题上下文语境里可能有多种解释,假如选择了不同含义,其理由支撑结论的效度会大受影响,需要作者做出更清晰的定义。

查找关键词的线索

  • 检查论题看有没有关键词
  • 在理由和结论中寻找关键词或短语
  • 留意抽象的词或短语
  • 通过反串来判断别人怎样给特定的词或短语下不同的定义

4.3 克服两个障碍:自认为和作者表达的是同一个意思;认为术语只存在一个明显的定义。

4.4 只有出现在分析推理过程中,意思不明确的词才最为关键。

4.5 如何理解模糊不清的陈述:依靠上下文语境;词典里的定义(不一定适合文章里的情境)

4.6 注意饱含情感色彩的词语,对术语引发你怎样的感情保持警惕。

4.7 写作建议:时刻留意歧义;一旦判断自己的论证里有个词意思不明确,就一定要解释清楚。

SSDB杂谈

发表于 2017-05-18

前言

没读过SSDB的源码,也没读过LevelDB,但好歹也是用了很久的工具,就算没用好没用到位也有必要总结一下,说不定以后用不到了。。。

先了解一下leveldb的原理别人整理的blog,记得当时我还看过郎格科技的原文,如今已无法访问,世事无常啊。

SSDB在LevelDB的基础上提供了网络支持,各种语言客户端api,封装了一些数据结构。

以下只谈使用不谈原理。

特点

  • 耗CPU不耗内存
  • 天生单节点,说实话主从同步之类的感觉不好用,分布式要自己实现
  • 读写速度和redis一个数量级的
  • 写单线程,读多线程
  • 删除有点蛋疼
  • 单单从api角度来看,功能对比redis单薄很多
  • 单节点的写操作只会影响CPU一个核的使用

使用场景

举两个例子,公司生产中用到的,都是当持久化数据库而非缓存使用。

  1. 使用的数据结构为kv(只有kv支持过期机制),配置了双主(为了容错),数据量不大,用得还行。但项目开始时由于没有控制数据量,因为删除动作(过期)相当于写入,影响到了实际数据写入效率。得出结论:得分多个节点,充分利用cpu;慎用过期/删除。

  2. 另个项目用hashmap,示意一下:’城市_版本号’,’key’,’value’。每周批量写一个版本的数据(共数十G),而老版本只需保留一个已防万一,之前的就没用了。由于前文提到的原因万万不能做删除动作,目前采用了临时的解决方案:切换节点,老节点数据手动删除。

遇到的大坑

  1. 删除/过期算一个
  2. 主从同步,主节点使用multihset批量插入,从节点同步。实际从节点数据是一条条插入的,延时严重,log日志量爆表。还必须注意binlog_capacity这个参数,当主从差距超过这个参数会触发OUT_OF_SYNC,slave节点会由sync状态改用copy同步全量数据,简直爆炸。

Java8 HashMap 源码阅读笔记

发表于 2017-04-10

简介

散列(英语:Hashing)是电脑科学中一种对资料的处理方法,通过某种特定的函数/算法(称为散列函数/算法)将要检索的项与用来检索的索引(称为散列,或者散列值)关联起来,生成一种便于搜索的数据结构(称为散列表)。

HashMap是Java程序中使用频率很高的类,用于处理映射/键值对,是散列表ADT的一种实现。

根据官方文档的介绍,HashMap有如下几个特点:

  • 允许键为null
  • 允许映射的值为null
  • 不保证映射的顺序
  • 非同步

结构概览

HashMap采用数组+链表+红黑树的设计

Imgur

根据书上和网上的一些资料,为了使key均匀分布,表的大小最好是素数,不过HashMap不是这样设定的,后文会看到。

当两个关键词散列到了同一个值(把这种情况称为冲突),采用分离链接法解决冲突

实现

属性

哈希桶数组,长度为2^n

1
2
3
4
5
6
7
8
9
10
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}

1
2
3
transient int size;
int threshold;
final float loadFactor;

size:key-value映射的数量
load factor(装填因子):bucket装填程度的最大比例,默认0.75
threshold: capacity * load factor

hash

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash函数看起来很简单,将hashcode与自身高位异或

计算index = (n - 1) & hash

分析HashMap的put方法

put大致的实现思路为:\
对于key的hashCode(),算出hash值;\
根据hash值和数组长度算出index;\
如果没碰撞直接放到bucket里;\
如果节点已经存在就覆盖value;\
如果碰撞了,以链表的形式存在buckets后;\
如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),以红黑树的形式存在;\
如果size超过threshold,就要resize

resize

MyBatis学习笔记

发表于 2017-04-10

入门

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

我们就可以通过 SqlSessionFactory 获得 SqlSession 的实例。SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。

可以通过 SqlSession 实例来直接执行已映射的 SQL 语句

1
2
3
4
5
6
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
session.close();
}

新版本更推荐使用对于给定语句能够合理描述参数和返回值的接口(Mapper)

1
2
3
4
5
6
7
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
} finally {
session.close();
}

源码解读

第一种写法

目前公司采用这种写法,在DAO层调用SqlSessionTemplate的方法。好处是可以将一些简单的逻辑写在dao实现类,比如设置创建、更新信息,对sql结果简单的处理。

1
sqlSessionTemplate.selectOne("com.jh.dao.BlogDao.selectBlog", id);

SqlSessionTemplate 是 MyBatis-Spring 的核心。 这个类负责管理 MyBatis 的 SqlSession, 调用 MyBatis 的 SQL 方法, 翻译异常。 SqlSessionTemplate 是线程安全的, 可以被多个 DAO 所共享使用。
当调用 SQL 方法时, 包含从映射器 getMapper()方法返回的方法, SqlSessionTemplate 将会保证使用的 SqlSession 是和当前 Spring 的事务相关的。此外,它管理 session 的生命 周期,包含必要的关闭,提交或回滚操作。

SqlSessionTemplate中的属性SqlSession采用了动态代理的方式,在执行sqlSessionProxy的方法时,会被SqlSessionInterceptor拦截到。在该拦截器的invoke方法中,会从Spring事务管理器获取sqlsession,管理sqlsession的生命周期。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
......
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
......
@Override
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.<T> selectOne(statement);
}
......
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}

继续通过断点跟踪,实际调用的还是SqlSession的一个实现类DefaultSqlSession,其中属性executor默认为CachingExecutor,Executor负责执行数据库操作。

1
2
3
4
5
6
7
8
9
10
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
private boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
...
}

CachingExecutor先尝试从内存中获取数据,若没有的话通过代理调用其它Executor实现类的query方法,这里应是mybatis二级缓存,实际没有用过不做研究了。

1
2
3
4
5
public class CachingExecutor implements Executor {
private Executor delegate;
private TransactionalCacheManager tcm = new TransactionalCacheManager();
...
}

BaseExecutor有三个子类,分别对应于SqlSessionTemplate中ExecutorType的3个枚举类型。在公司项目中,指定了ExecutorType为REUSE,根据sql缓存了jdbc的statement

1
2
3
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}

BaseExecutor有个属性PerpetualCache是mybatis一级缓存,本质是一个HashMap,Key根据StatementId(com.jh.dao.BlogDao.selectBlog)、RowBounds分页的参数(limit&offset)、sql语句(select * from blog where id = ?)、查询条件(1)这几个条件生成。

1
2
3
public int update(Blog blog) {
return sqlSessionTemplate.update("com.jh.dao.BlogDao.update", blog);
}

update操作(包括update(),delete(),insert())在执行到BaseExecutor.update()时清空一级缓存,再执行数据库查询操作。

第二种写法

1
2
3
4
5
6
7
@Mapper
public interface CityMapper {
@Select("select * from city where state = #{state}")
City findByState(@Param("state") String state);
}

用到了动态代理,Mapper接口实际调用MapperProxy.invoke,底层同样调用SqlSession的方法

1
2
3
4
5
6
7
8
9
10
11
12
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
...
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
}

之后的流程与第一种写法一样。

小结

本文只是简单记录一下mybatis大体流程,之后如有机会将深入研究具体细节。

mybatis源码中用到的动态代理和一些设计模式,值得进一步学习。

Java8 LinkedList 源码阅读笔记

发表于 2017-03-30

概述

LinkedList提供了List ADT的双链表实现,关于链表的概念参见wiki。
在之前的文章中介绍了基于数组的ArrayList,有一个重大缺点是插入和删除一个元素,会影响插入/删除位置之后的所有元素,而链表可以轻松完成插入删除动作。

给出java中LinkedList类图

Imgur

实现

属性

头节点、尾节点、表的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;

Node是一个静态内部类,静态内部类的作用是把一个类隐藏在另一个类的内部,并不引用外部类对象。可以看做是一个独立的类。

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

add & remove

add方法将指定元素添加到链表末端

1
2
3
4
5
6
7
8
9
10
11
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

remove方法删除链表中第一次出现的指定元素

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
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

由此可见LinkedList对访问指定位置元素的效率不高,node方法先判断index位于表的前半部或后半部再遍历,复杂度O(n/2)

iterator

下面引用java核心技术中的一段话

由于迭代器是描述集合中位置的,所有依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例如Set其元素完全无序,因此Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法……

ListIterator接口允许正向反向遍历表,在遍历时改变表结构,获取迭代器在表中的当前位置。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

小结

LinkedList这种双向链表设计减少了添加、删除操作的开销,但如果需要对集合进行随机访问,那么使用数组或ArrayList是更好的选择。

记一次应用频繁gc原因分析

发表于 2017-03-28

问题背景

发布应用后,通过gclog日志和jstat命令发现gc频繁,而应用的调用量不多。很奇怪,有必要进一步分析。

应用环境:jdk1.7.0_45 tomcat8.0.32

如何排查jvm相关问题

在JDK1.7u40之后,java内置了一个jdk分析诊断工具平台–JMC,也是Oracle官方推荐使用的工具。

生成jfr文件

tomcat启动参数配置

1
2
-Dcom.sun.management.jmxremote=true -Dcom.sun.management.jmxremote.port=3000 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=127.0.0.1
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder

使用jcmd命令生成记录文件

1
jcmd <pid> JFR.start name=MyRecording settings=/home/appadmin/profile.jfc delay=2s duration=2m filename=myrecording.jfr

注:settings指定的文件在$JAVA_HOME/jre/lie/jfr中会有一份默认配置的,我这里将method-sampling-interval和socket read/write从默认的10ms改为1ms

更多关于jmc jfr jcmd 参见官方文档
Oracle troubleshoot doc

分析jfr文件

内存概览中观察到gc频繁,为TLAB分配的内存超过7G

Imgur

内存-分配-新TLAB中的分配-按线程分配,可以看到占用内存最多的线程ContainerBackgroundProcessor[StandardEngine[Catalina]]

Imgur

事件-图形,根据线程筛选,勾选上事件类型:Allocation in new TLAB

Imgur

图中橙色部分堆栈跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Allocation in new TLAB
...
位于 org.apache.catalina.webresources.AbstractArchiveResourceSet.getResource(String)
位于 org.apache.catalina.webresources.StandardRoot.getResourceInternal(String, boolean)
位于 org.apache.catalina.webresources.Cache.getResource(String, boolean)
位于 org.apache.catalina.webresources.StandardRoot.getResource(String, boolean, boolean)
位于 org.apache.catalina.webresources.StandardRoot.getClassLoaderResource(String)
位于 org.apache.catalina.loader.WebappClassLoaderBase.modified()
位于 org.apache.catalina.loader.WebappLoader.modified()
位于 org.apache.catalina.loader.WebappLoader.backgroundProcess()
位于 org.apache.catalina.core.StandardContext.backgroundProcess()
位于 org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(Container)
位于 org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(Container)
位于 org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(Container)
位于 org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run()
位于 java.lang.Thread.run()

紫色部分

1
2
3
4
Java Thread Sleep
位于 java.lang.Thread.sleep(long)
位于 org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run()
位于 java.lang.Thread.run()

TLAB(Thread Local Allocation Buffer)
位于Eden区,线程私有

问题定位&解决

通过上面的信息可以定位到问题出在ContainerBackgroundProcessor.run()方法上,这个线程会定期检查webapps下是否有变动,将jar加载到内存中。

但理论上不会有这么大量的内存产生啊

将tomcat -> conf/server.xml -> reloadable = false,可以解决这个问题。

Java8 ArrayList 学习笔记

发表于 2017-03-23

概述

java.util.Collection是Java集合类的基本接口,存储和操作一组类型相同的对象。而ArrayList是java语言最常用的集合类,是表ADT的一种实现。

ArrayList继承树

Imgur

实现

速览一下ArrayList的源码,详细介绍一下常用的方法。

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;

ArrayList中的数据存于一个数组Object[],可以理解为ArrayList对数组进行了一层封装,便于使用。

transient标记这个elementData不可序列化,而使用writeObject方法序列化ArrayList对象。

1
protected transient int modCount = 0;

还有个继承于AbstractList的属性很重要,用于记录list结构修改的次数,更详细的说明后文中会有。

get&set

1
2
3
4
5
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
1
2
3
4
5
6
7
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}

get和set方法都是对数组指定位置元素的操作。

add

1
2
3
4
5
6
7
8
9
10
11
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

add方法中需要先检查数组空间是否足够,如有必要则执行扩容,扩容使用Arrays.copyOf方法(底层调用System.arraycopy)

remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}

remove方法实际也使用System.arraycopy

iterator

1
private class Itr implements Iterator<E>

ArrayList有个内部类Itr实现了Iterator接口,提供hasNext(),next(),remove()等方法,使调用者在迭代collection时能做删除操作。

可以用iterator()方法返回一个迭代器,在使用foreach语法实际也用的是这个Itr

1
2
3
public Iterator<E> iterator() {
return new Itr();
}

同理还有个ListIterator

fail-fast

之前提到ArrayList继承了AbstractList的一个非常重要的属性modCount,用于记录list结构改变的次数,在add(),remove()等代码中都有对modCount的自增操作。该属性用于iterator以及list iterator的实现,当modCount的值不符合预期,会抛异常ConcurrentModificationException。

以下代码就是典型的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误的代码 java.util.ConcurrentModificationException
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String s : list) {
if ("2".equals(s)) {
list.remove(s);
}
}
}

然而,如果迭代器调用了它自己的remove方法,那么这个迭代器就仍然是合法的。

RandomAccess

发现ArrayList还实现了一个标记接口RandomAccess,文档中解释说这个接口的作用是使list的实现在随机访问时有更高的效率。

1
2
3
4
5
6
7
8
this loop:
for (int i=0, n=list.size(); i < n; i++)
list.get(i);
runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();

Spliterator

// TODO
java8新特性,有待研究,之后补上

小结

ArrayList提供了List ADT的一种可增长数组的实现,使用ArrayList的优点在于,对get和set调用花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在ArrayList的末端进行。

适合需要快速访问数据,较少插入或删除元素的场景。

变种

concurrent包下提供了CopyOnWriteArrayList

写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

以下代码是向ArrayList里添加元素,可以发现在add方法中是需要加锁的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

get方法则无需加锁

1
2
3
4
5
6
7
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}

当线程A执行修改操作,修改的是copy出来的新数组,其它读线程此时读取的任是原数组。

那么问题来了,修改操作会锁住整个List,是一个并发瓶颈;频繁的copy则会导致内存和gc相关的问题。CopyOnWriteArrayList不是一个通用的并发List,适合读多写少的场景。

用IDEA创建一个基于maven的hello-spark项目

发表于 2017-02-27

环境准备

基于windows环境,安装jdk,maven,scala,spark,配置环境变量

安装idea,scala插件

为什么我不用sbt构建,而选用maven?
公司有maven仓库,jar下载速度快。sbt没有研究过直接使用nexus,在网上找了一个公服repox,但速度不理想。

开始项目

在idea中新建maven project,在pom.xml中引入插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<!-- work-around for https://issues.scala-lang.org/browse/SI-8358 -->
<arg>-nobootcp</arg>
</args>
</configuration>
</plugin>

参考了这个sample

接着引入scala和spark相关jar

在src/main/scala中,参考spark官网copy一段代码,就可以在本机运行了:-D

Jiahui Ma

Jiahui Ma

8 日志
6 标签
© 2017 Jiahui Ma
由 Hexo 强力驱动
主题 - NexT.Mist