java.util.ConcurrentHashMap (JDK 1.8)
1.1 java.util.ConcurrentHashMap继承结构
ConcurrentHashMap和HashMap的实现有很大的相似性,建议先看HashMap源码,再来理解ConcurrentHashMap。
1.2 java.util.ConcurrentHashMap属性
这里仅展示几个关键的属性
// ConcurrentHashMap核心数组
transient volatile Node<K,V>[] table; // 扩容时才会用的一个临时数组
private transient volatile Node<K,V>[] nextTable; /**
* table初始化和resize控制字段
* 负数表示table正在初始化或resize。-1表示正在初始化,-N表示有N-1个线程正在resize操作
* 当table为null的时候,保存初始化表的大小以用于创建时使用,或者直接采用默认值0
* table初始化之后,保存下一次扩容的的大小,跟HashMap的threshold = loadFactor*capacity作用相同
*/
private transient volatile int sizeCtl; // resize的时候下一个需要处理的元素下标为index=transferIndex-1
private transient volatile int transferIndex; // 通过CAS无锁更新,ConcurrentHashMap元素总数,但不是准确值
// 因为多个线程同时更新会导致部分线程更新失败,失败时会将元素数目变化存储在counterCells中
private transient volatile long baseCount; // resize或者创建CounterCells时的一个标志位
private transient volatile int cellsBusy; // 用于存储元素变动
private transient volatile CounterCell[] counterCells;
1.3 java.util.ConcurrentHashMap方法
1.3.1 Unsafe.compareAndSwapXXX方法
Unsafe.compareAndSwapXXX方法是sun.misc.Unsafe类中的方法,因为在ConcurrentHashMap中大量使用了这些方法。其声明如下:
public final native boolean compareAndSwapXXX(type1 object, type2 offset, type4 expect, type5 update);
object为待修改的对象,offset为偏移量(数组可以理解为下标),expect为期望值,update为更新值。这个方法执行的逻辑伪代码如下:
if (object[offset].value equal expect) {
object[offset].value = update;
return true;
} else {
return false
}
object[offset].value 等于expect更新value值并返回true,否则不更新并且返回false。之所以不更新是因为多线程执行时有其它线程已经修改该值,expect已经不是最新的值,如果强行修改必然会覆盖之前的修改,造成脏数据。
CAS方法都是native方法,可以保证原子性,并且效率比synchronized高。
1.3.2 hash方法
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
int hash = spread(key.hashCode()); static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
上面源码为计算hash算法,h ^ (h >>> 16)在计算hash的时候key.hashCode()的高位也参与运算,这部分跟HashMap计算方法一致,不同的是h ^ (h >>> 16)计算结果“与”上 0x7fffffff,从而保证结果一定为正整数。获得hash之后,通过hash & (n -1)计算下标。
ConcurrentHashMap中的元素节点总结一下有这么几种可能:
(1) null 暂无元素
(2) Node<K, V> 普通节点,可以组成单向链表,hash > 0
(3) TreeBin<K,V> 红黑树节点,TreeBin是对TreeNode的封装,其hash为TREEBIN = -2。
HashMap和ConcurrentHashMap的TreeNode实现并不相同。
在HashMap中TreeNode封装了红黑树所有的操作方法,而ConcurrentHashMap中红黑树操作的方法都封装在TreeBin中,TreeBin相当于一个红黑树容器,容器中的红黑树节点为TreeNode。
HashMap可以直接在tab[i]存入TreeNode,而ConCurrentHashMap只能在tab[i]存入TreeBin。
(4) ForwardingNode<K,V> key和value都为null的一个特殊节点,用于resize操作填充已经完成迁移操作的节点。FrowardingNode的hash在初始化的时候被置成MOVED = -1
在resize过程中当发现tab[i]上是ForwardingNode的时候(通过hash判断)就可知tab[i]已经迁移完了,直接跳过该节点去处理其它节点。
ConcurrentHashMap禁止node的key或value为null或许跟该节点的存在也是有一定关系的。
(5)ReservationNode<K,V>只在compute和computeIfAbsent中使用,其hash为RESERVED = -3
从上面的总结可以看出普通节点hash为正整数是有意义的,hash > 0是判断该节点是否为链表节点(普通节点)的一个重要依据。
1.3.3 get/set/update tab[i] 方法
// 获取tab[i]节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
} // compare and swap tab[i],期望值是c,tab[i].value == c ? tab[i] = v : return false
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
} // 设置tab[i] = v
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
1.3.4 size() 方法
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
} final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
// 除了baseCount以外,部分元素变化存储在counterCells数组中
if (as != null) {
// 遍历数组累加获得结果
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
ConcurrentHashMap中baseCount用于保存tab中元素总数,但是并不准确,因为多线程同时增删改,会导致baseCount修改失败,此时会将元素变动存储于counterCells数组内。
当需要统计当前的size的时候,除了要统计baseCount之外,还需要统计counterCells中的元素变化。
值得一提的是即使如此,统计出来的依旧不是当前tab中元素的准确值,在多线程环境下统计前后并不能stop the world暂停线程操作,因此无法保证准确性。
1.3.5 put/putIfAbsent方法
public V put(K key, V value) {
// 核心是调用putVal方法
return putVal(key, value, false);
} public V putIfAbsent(K key, V value) {
// 如果key存在就不更新value
return putVal(key, value, true);
} /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key或value 为null都是不允许的,因为Forwarding Node就是key和value都为null,是用作标志位的。
if (key == null || value == null) throw new NullPointerException();
// 根据key计算hash值,有了hash就可以计算下标了
int hash = spread(key.hashCode());
int binCount = 0;
// 可能需要初始化或扩容,因此一次未必能完成插入操作,所以添加上for循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 表还没有初始化,先初始化,lazily initialized
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 根据hash计算应该插入的index,该位置上还没有元素,则直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// static final int MOVED = -1; // hash for forwarding nodes
// 说明f为ForwardingNode,只有扩容的时候才会有ForwardingNode出现在tab中,因此可以断定该tab正在进行扩容
else if ((fh = f.hash) == MOVED)
// 协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 节点上锁,hash值相同的节点组成的链表头结点
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 是链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 遍历链表查找是否包含该元素
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val; // 保存旧的值用于当做返回值
if (!onlyIfAbsent)
e.val = value; // 替换旧的值为新值
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 遍历链表,如果一直没找到,则新建一个Node放到链表结尾
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 是红黑树节点
Node<K,V> p;
binCount = 2;
// 去红黑树查找该元素,如果没找到就添加,找到了就返回该节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
// 保存旧的value用于返回
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value; // 替换旧的值
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 链表长度超过阈值(默认为8),则需要将链表转为一棵红黑树
treeifyBin(tab, i);
if (oldVal != null)
// 如果只是替换,并未带来节点的增加则直接返回旧的value即可
return oldVal;
break;
}
}
}
// 元素总数加1,并且判断是否需要扩容
addCount(1L, binCount);
return null;
}
1.3.6 addCount方法
在putVal方法中调用了若干其它方法,下面来看下addCount方法。
// check<0不检查resize, check<=1只在没有线程竞争的情况下检查resize
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// counterCells数组不为null
if ((as = counterCells) != null ||
// CAS更新BASECOUNT失败(有其它线程更新了BASECOUNT,baseCount已经不是最新值)
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// counterCells为null
if (as == null || (m = as.length - 1) < 0 ||
// counterCells对应位置为null,这里不是很懂,有没有大神解答下?
// ThreadLocalRandom.getProbe() 获得线程探测值,什么用途?
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// 更新CELLVALUE失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 初始化counterCells
fullAddCount(x, uncontended);
return;
}
// counterCells != null 或者 BASECOUNT CAS更新失败都是因为有线程竞争,因此不检查resize
if (check <= 1)
return;
// 统计下ConcurrentHashMap元素总数
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// 元素总数大于sizeCtl
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
// 获取一个resize标志位
int rs = resizeStamp(n);
// sizeCtl < 0 表示table正在初始化或者resize
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 当前线程是第一个发起扩容操作
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
1.3.7 resize相关方法:resizeStamp、helpTransfer、transfer
// 返回一个标志位,该标志位经过RESIZE_STAMP_SHIFT左移必定为负数
static final int resizeStamp(int n) {
// Integer.numberOfLeadingZeros返回n对应32位二进制数左侧0的个数,如9(1001)返回28
// 1 << (RESIZE_STAMP_BITS - 1) = 2^15,其中RESIZE_STAMP_BITS固定为16
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
helpTransfer方法:辅助扩容方法,直接进入transfer方法的迁移元素阶段
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 在tab中发现了ForwardingNode,在ForwardingNode初始化的时候保存了nextTable引用
// 因此可以通过f找到nextTable,并且可以断定nextTable!=null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
transfer方法:resize的核心操作。基本思路是先new一个double capacity的nextTable数组,然后将tab中的元素一个一个迁移到nextTable中。迁移完成后将tab = nextTable操作替换掉tab。
// 扩容操作方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 刚开始resize,需要初始化nextTab
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 扩容为两倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n; // 倒序transfer tab
}
int nextn = nextTab.length; // 扩容后表的length
// 预先定义一个头节点ForwardingNode,其hash被置成MOVED=-1
// 当线程发现某个元素hash==MOVED则表明该节点已经被处理过
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
// 是否完成元素迁移的标志
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 这个while循环是为了找到下一个准备处理的下标
while (advance) {
int nextIndex, nextBound;
// --i还未越界,准备处理tab[i]
// finishing==true,resize完成,可能处于提交前的检查阶段,检查tab[--i]
if (--i >= bound || finishing)
advance = false;
// 下一个准备处理的元素下标为transferIndex-1<0, 可以断定tab已经完成了transfer操作
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1; // 下一个准备处理的index
advance = false;
}
}
// i越界,可能已经完成元素迁移操作
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 扩容完成,替换table
// 扩容完成nextTable置空
nextTable = null;
// 替换table为扩容后的nextTab
table = nextTab;
// sizeCtl设置为0.75 * capacity,即为下一次需要扩容的阈值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// CAS更新sizeCtl,sc-1表示新加入一个线程参与扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
// 处理完成后重新遍历一遍,以免多线程操作带来遗漏
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
// tab[i] == null则置一个ForwardingNode
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
// ForwardingNode的hash为MOVED,说明tab[i]已经被置成ForwardingNode,已经处理过
advance = true; // already processed
else {
// 对tab[i]节点加锁,锁住了tab[i]节点上所有的Node
synchronized (f) {
// 如果AB两个线程先后执行到这里,A线程获取锁,执行完迁移之后释放锁;B线程获取锁,此时tab[i]是ForwardingNode,不等于f
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// fh >= 0说明是链表节点。TreeBin的hash在初始化的时候被置成TREEBIN=-2
if (fh >= 0) {
// (fh = f.hash) & n 决定Node应该迁移到原下标i还是应该迁移到i+n位置
// 这种扩容方法参考HashMap的resize思想 http://www.cnblogs.com/snowater/p/7742287.html
int runBit = fh & n;
Node<K,V> lastRun = f;
// 遍历链表找到最后一个与链表头结点runBit不同的Node,并且将runBit置为该节点的 p.hash & n
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
// runBit == 0 表明该Node还应迁移到下标i的位置
ln = lastRun;
hn = null;
}
else {
// runBit != 0 表明该Node应迁移到下标i + n的位置
hn = lastRun;
ln = null;
}
// 遍历链表,拆分之,拆分后基本是原链表的倒序(最后一段链表除外,它还是以顺序的方式处于链表末尾)
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0) // 该Node应该迁移到下标i位置
ln = new Node<K,V>(ph, pk, pv, ln);
else // 该Node应该迁移到下标i+n位置
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 处理完后将tab[i]设置为ForwardingNode,其它线程发现tab[i] == ForwardingNode则会跳过tab[i]继续往后执行
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) { // TreeBin的hash为-2
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果长度小于UNTREEIFY_THRESHOLD=8,则将树转换为链表,否则将lo和hi重建为红黑树
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
1.3.8 get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 根据key计算得到hash
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) // 红黑树,从红黑树中查找
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
参考博客:
http://www.importnew.com/23610.html
http://blog.csdn.net/u010723709/article/details/48007881
http://blog.csdn.net/qq924862077/article/details/74530103
http://www.cnblogs.com/mickole/articles/3757278.html
http://www.techsite.cn/?p=5520
http://blog.csdn.net/dfdsggdgg/article/details/51538601
java.util.ConcurrentHashMap (JDK 1.8)的更多相关文章
- java.util.logging jdk日志详解
jdk自带的日志,结构并不复杂,功能也能满足绝大部分功能.日志写入位置是开放的,只要继承了handler都可以接收日志的写入.handler本身依赖于LogRecord对象,该对象代表一个日志.Han ...
- JDK的帧--java.util包装工具库
题词 JDK,Java Development Kit. 首先,我们必须认识到,,JDK但,但设置Java只有基础类库.它是Sun通过基础类库开发,这是唯一的.JDK书写总结的类库.从技术含量来说,还 ...
- dubbox部署到jdk1.7环境,启动:java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()
本地用jdk1.8编译的服务提供端war包,部署到环境报错了: INFO: Initializing Spring root WebApplicationContext [16/08/17 05:14 ...
- at java.util.concurrent.ConcurrentHashMap.hash(ConcurrentHashMap.java:333)
at java.util.concurrent.ConcurrentHashMap.hash(ConcurrentHashMap.java:333) 原因: null request
- 【并发编程】【JDK源码】JDK的(J.U.C)java.util.concurrent包结构
本文从JDK源码包中截取出concurrent包的所有类,对该包整体结构进行一个概述. 在JDK1.5之前,Java中要进行并发编程时,通常需要由程序员独立完成代码实现.当然也有一些开源的框架提供了这 ...
- JDK源码(1.7) -- java.util.Collection<E>
java.util.Collection<E> 源码分析(JDK1.7) -------------------------------------------------------- ...
- 一点一点看JDK源码(二)java.util.List
一点一点看JDK源码(二)java.util.List liuyuhang原创,未经允许进制转载 本文举例使用的是JDK8的API 目录:一点一点看JDK源码(〇) 1.综述 List译为表,一览表, ...
- 一点一点看JDK源码(三)java.util.ArrayList 前偏
一点一点看JDK源码(三)java.util.ArrayList liuyuhang原创,未经允许禁止转载 本文举例使用的是JDK8的API 目录:一点一点看JDK源码(〇) 1.综述 ArrayLi ...
- 一点一点看JDK源码(四)java.util.ArrayList 中篇
一点一点看JDK源码(四)java.util.ArrayList 中篇 liuyuhang原创,未经允许禁止转载 本文举例使用的是JDK8的API 目录:一点一点看JDK源码(〇) 1.综述 在前篇中 ...
随机推荐
- java多线程系列(九)---ArrayBlockingQueue源码分析
java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...
- [译]漫画SELinux概念
h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h ...
- C# winform 实现图片轮播
作为一个C#winform刚刚入门的我 觉得这可能是初学者都会遇到的 我自己也觉得很神奇 首先 窗体里有一个Button按钮,和一个pictureBox 图片框 定义一个线程方法 /// < ...
- Git详解及 github与gitlab使用
1.1 关于版本控制 1.1.1 本地版本控制 本地版本控制系统 许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别.这么做唯一的 好处就是简单,但是特别容易犯错.有 ...
- TCP/IP 协议 ----- 协议栈
文章是作者对tcp/ip协议族的一些看法,借鉴TCP/IP详解卷一的内容,进行总结归纳,并阐述自己的一些看法. TCP/IP协议栈· : 整个协议栈被分为了四层,每一层协议负责不同的功能: 链路层:负 ...
- Webpack飞行手册
前言 在学习 Webpack 之前,我们需要了解一个概念:模块. 何为模块? 如果你曾学过 Java , C# 之类的语言,一定会知道 Java 中的 import 或 C# 中的 using 吧? ...
- HDU5804--Price List
Price List Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 262144/131072 K (Java/Others) Tot ...
- PAT 1008. Elevator (20)
1008. Elevator (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue The highest ...
- win10解决乱码问题
Unicode是Unicode.org制定的编码标准,目前得到了绝大部分操作系统和编程语言的支持.Unicode.org官方对Unicode的定义是:Unicode provides a unique ...
- Android ViewPager实现图片标题轮播和点击事件
我看别人的文章,最喜欢看有图的.然后先找图看. 那我就先上效果图: 实现了图片自动轮播,手动滑动,轮播标题,以及点击事件.下面开始: 一.资源文件: 首先是布局: <?xml version=& ...