结论先行,细节在下面

jdk1.7是如何解决并发问题的以及完整流程

一.首先new一个concurrentHashMap

调用默认构造方法

二.初始化

初始化initialCapacity(默认是16,指一个segment内Entry的数量),loadFactor(默 认0.75f,负载因子),初始化concurrentLevel(默认是16,segment数量)。
1.校验传入的参数是否符合规定
2.计算concurrentLevel、segementMask(掩码)和segementShift(移位数)
3.计算每个segment中的Entry数组大小,默认且最小为2
4.此时你得到了一个segment对象,调用UNSAFE.putOrderedObject方法,利用CAS将 此segment对象放在segment数组下标为0的位置,其余15个位置为null

三.初始化完开始使用。先put一个键值对进去

1.判断value是否为空,为空直接报错
2.计算hash值。int j = (hash >>> segmentShift) & segmentMask
先用segementShift将32位的hash右移28位,剩4位,再与segmentMask(二进制码,具体数值为1111)进行与运算,得到j,此时segment[j]还是null,不像segment[0]已经初始化,那么调用ensureSegment(j)初始化segment[j]
3.上来第一步先 tryLock() ? null : scanAndLockForPut(key, hash, value);
如果tryLock失败,也就是没拿到独占锁,将调用scanAndLockForPut方法,这个方法大概是循环尝试tryLock(),尝试次数到一定后,将调用lock()进行阻塞,直到拿到锁
4.获取锁成功后,hash计算entry下标,int index = (tab.length - 1) & hash
5.遍历链表,有数据就覆盖,没数据就头插
6.判断是否需要扩容
7.释放锁

四.扩容

1.定义threshold = (int)(newCapacity * loadFactor),只要threshold小于map中实际存入的元素大小,就开始扩容;entry数组一次扩容成原来的两倍
2.用rehash方法,计算新的掩码segmentMask,然后遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置(原理HashMap那里说过)
3.最后插入新节点

五.get方法

第一次计算hash定位segment,第二次hash定位entry,然后返回。

六.并发问题的解决

注意到,get没有加锁,put和remove都加上了独占锁,需要考虑的问题就是 get 的时候在同一个 segment 中发生了 put 或 remove 操作,会发生什么
1.对于put
第一个问题是:初始化segment是用CAS将segment对象放入segment数组index为0的位置的;
第二个问题是:put进entry是头插,如果此时get操作已经遍历到链表中间,无影响。但是还需要保证put之后get要找的到刚被插入的头节点,这个依赖于 setEntryAt 方法中使用的 UNSAFE.putOrderedObject;
第三个问题是:扩容也有并发。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table,get操作会在旧table上进行,不影响,如果put先行,扩容后行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。

jdk1.8是如何解决并发问题的以及完整流程

一.首先new一个concurrentHashMap

调用默认构造方法,需要注意的是,1.8摒弃了segment这个概念,引入了红黑树这个数据结构,加锁则采用CAS和synchronized实现

二.构造函数内部操作。

维护一个sizeCtl = (1.5 * initialCapacity + 1) 再向上取最近的2的倍数。比如initialCapacity = 10,则sizeCtl = 16。sizeCtl的使用场景很多。
构造函数只是计算值而已,初始化操作延迟到真正操作数据的时候。

三.put过程分析

1.key或value==null直接抛错误。
2.hash = spread(key.hashCode()),得到hash值,定义binCount记录链表长度。
3. if 数组为空,初始化数组(这里才真正初始化数组);如果已经初始化,找出该hash值对应的数组下标,得到第一个节点
else if 该位置尚未有任何节点,利用CAS将新节点放入。put逻辑基本结束。
else if hash == MOVED,说明在扩容,转而帮助其数据迁移。
else 此时节点存在,也不为空。
在这个 else 下,又有两个判断:
如果hash >= 0,说明是链表
如果节点f instanceof TreeBin,说明是红黑树
对应不同的插入逻辑
4.进行完以上判断,开始进入判断是否将链表转化成红黑树的阶段
if(binCount >= TREEIFY_THRESHOLD) 也就是第三步的第二小步定义的binCount记录着本链表的长度,大于等于8就转红黑树

四.真正对数组的初始化

initTable方法
初始化一个合适大小的数组,然后会设置 sizeCtl。
初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的
U.compareAndSwapInt(this, SIZECTL, sc, -1),将sizeCtl改成-1,代表抢到锁
接下来就是各种赋初值,比如数组长度什么的。

五.链表转红黑树

treeifyBin方法
treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容
如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容,而不是转化为红黑树。
如果需要转化,那么用synchronized加锁,将链表变成红黑树,然后返回头结点,设置 到数组相应的位置上。

六.扩容机制

tryPresize方法
这个方法的核心在于对 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)
所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚
总的来说,肯定是得把老数组的东西拷贝到新数组里面,然后引用指向新数组,这样就行了,怎么拷贝呢?用transfer方法
原理太复杂,大概意思就是将一个大数组分割成很多个小部分,可以令每个线程负责转移一部分数据,转移数据的时候,会锁头节点或者根节点,转移后一个位置,就会在那个位置放置一个特殊的节点,该节点hash值为-1,表示该位置已经转移

七.get 过程分析

计算hash,利用hash定位。
如果为null,返回null;
如果刚好是需要的,那就返回;
如果hash < 0,说明正扩容,用find方法找;
如果上面都不满足,说明是链表,直接往后遍历即可。

八.并发问题的解决

1.初始化时:在initTable方法内可以看到,通过CAS判断当前是否有其他线程在初始化,如果有,那么当前线程会被阻塞,一直CAS自旋等到数组初始化成功。
2.扩容时:将数组分割成若干份,允许多个线程一起扩容,一起转移数据,每个线程在负责自己那一part的数据转移时,会对头结点加锁。
3.插入时:位置为空时,CAS插入;不为空时,对头结点加锁,再插入。

上面是总结,速度过一遍;下面是细节,仔细看一遍

正式绪论

JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap;本文将分别介绍这两种方式的实现方案及其区别。

请带着这些问题学习。

为什么HashTable慢

Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。

JDK1.7版本

在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap. 简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。接下来分析JDK1.7版本中ConcurrentHashMap的实现原理。

segment

整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个 segment。
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。


concurrentLevel:是一个int数值,命名为并发数,默认是16。也就是说,一个map中有16个segment,于是map支持16个线程并发写,只要他们分别操作这16个segment。
可以人为在初始化时设置成其他值,一旦指定,不可扩容。

segment的内部

在使用之前先初始化map,调用上图的方法,initialCapacity是初始容量,loadFactor是负载因子,concurrentLevel是并发数,也是segment的数量。
如果调用无参构造方法,那么我将得到:

segmentMask要等于数组长度减一,比如16 - 1 = 15,二进制码是1111,可以更好地保证散列的均匀性;
segmentShift是移位数,由于hash是32位的,它设为28的话,可以使hash无符号右移28位,剩下4个高位数,而这四位再和1111(也就是segmentMask)做一次与运算就可以转换为segment数组的下标,因为4位二进制数可以表示数字0~15,segment数组下标也是从0到15。

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();// 1. 计算 key 的 hash 值int hash = hash(key);// 2. 根据 hash 值找到 Segment 数组中的位置 j//    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下高 4 位,//    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的高 4 位,也就是槽的数组下标int j = (hash >>> segmentShift) & segmentMask;// 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,// ensureSegment(j) 对 segment[j] 进行初始化if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegments = ensureSegment(j);// 3. 插入新值到 槽 s 中return s.put(key, hash, value, false);
}

这里主要是为了计算出segment的下标,也就是该存到哪个segment下。
之后会进入segment内部获取锁,然后正式插入数据。

PUT方法的细节

初始化槽: ensureSegment(int k)方法

ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。
这里需要考虑并发,因为很可能会有多个线程同时进来初始化同一个槽 segment[k],不过只要有一个成功了就可以。

初始化第一个槽的原因


拿segment[0]这个最先被初始化且被操作的当做榜样,利用[0]去初始化[k]。
总的来说,ensureSegment(int k) 比较简单,对于并发操作使用 CAS 进行控制。

获取写入锁方法scanAndLockForPut(K key, int hash, V value)

在往某个 segment 中 put 的时候,首先会调用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是说先进行一次 tryLock() 快速获取该 segment 的独占锁,如果失败,那么进入到 scanAndLockForPut 这个方法来获取锁。

这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。 这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。

扩容:rehash

扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容,扩容后,容量为原来的 2 倍。
注意到,在put方法里,会判断该值插入后是否会导致超出阈值,超了就先扩容再插。

get方法

计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
槽中也是一个数组,根据 hash 找到数组中具体的位置
到这里是链表了,顺着链表进行查找即可

并发问题分析

JDK1.8版本

写在前面

在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

数据结构

构造函数

// 这构造函数里,什么都不干
public ConcurrentHashMap() {}
public ConcurrentHashMap(int initialCapacity) {if (initialCapacity < 0)throw new IllegalArgumentException();int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));this.sizeCtl = cap;
}

sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。

PUT方法

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.htmlpublic V put(K key, V value) {return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();// 得到 hash 值int hash = spread(key.hashCode());// 用于记录相应链表的长度int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// 如果数组"空",进行数组初始化if (tab == null || (n = tab.length) == 0)// 初始化数组,后面会详细介绍tab = initTable();// 找该 hash 值对应的数组下标,得到第一个节点 felse if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 如果数组该位置为空,//    用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了//          如果 CAS 失败,那就是有并发操作,进到下一个循环就好了if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}// hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容else if ((fh = f.hash) == MOVED)// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了tab = helpTransfer(tab, f);else { // 到这里就是说,f 是该位置的头节点,而且不为空V oldVal = null;// 获取数组该位置的头节点的监视器锁synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) { // 头节点的 hash 值大于 0,说明是链表// 用于累加,记录链表的长度binCount = 1;// 遍历链表for (Node<K,V> e = f;; ++binCount) {K ek;// 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了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) {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) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {// 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8if (binCount >= TREEIFY_THRESHOLD)// 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,// 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树//    具体源码我们就不看了,扩容部分后面说treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}// addCount(1L, binCount);return null;
}

初始化数组: initTable

这个比较简单,主要就是初始化一个合适大小的数组,然后会设置 sizeCtl。

初始化方法中的并发问题是通过对 sizeCtl 进行一个 CAS 操作来控制的。

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {// 初始化的"功劳"被其他线程"抢去"了if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {// DEFAULT_CAPACITY 默认初始容量是 16int n = (sc > 0) ? sc : DEFAULT_CAPACITY;// 初始化数组,长度为 16 或初始化时提供的长度Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];// 将这个数组赋值给 table,table 是 volatile 的table = tab = nt;// 如果 n 为 16 的话,那么这里 sc = 12// 其实就是 0.75 * nsc = n - (n >>> 2);}} finally {// 设置 sizeCtl 为 sc,我们就当是 12 吧sizeCtl = sc;}break;}}return tab;
}

数组转红黑树

前面我们在 put 源码分析也说过,treeifyBin 不一定就会进行红黑树转换,也可能是仅仅做数组扩容。我们还是进行源码分析吧。

private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {// MIN_TREEIFY_CAPACITY 为 64// 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容if ((n = tab.length) < MIN_TREEIFY_CAPACITY)// 后面我们再详细分析这个方法tryPresize(n << 1);// b 是头节点else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {// 加锁synchronized (b) {if (tabAt(tab, index) == b) {// 下面就是遍历链表,建立一颗红黑树TreeNode<K,V> hd = null, tl = null;for (Node<K,V> e = b; e != null; e = e.next) {TreeNode<K,V> p =new TreeNode<K,V>(e.hash, e.key, e.val,null, null);if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}// 将红黑树设置到数组相应位置中setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}

扩容: tryPresize

如果说 Java8 ConcurrentHashMap 的源码不简单,那么说的就是扩容操作和迁移操作。 这个方法要完完全全看懂还需要看之后的 transfer 方法,读者应该提前知道这点。 这里的扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍。

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {// c: size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :tableSizeFor(size + (size >>> 1) + 1);int sc;while ((sc = sizeCtl) >= 0) {Node<K,V>[] tab = table; int n;// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码if (tab == null || (n = tab.length) == 0) {n = (sc > c) ? sc : c;if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if (table == tab) {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = nt;sc = n - (n >>> 2); // 0.75 * n}} finally {sizeCtl = sc;}}}else if (c <= sc || n >= MAXIMUM_CAPACITY)break;else if (tab == table) {// 我没看懂 rs 的真正含义是什么,不过也关系不大int rs = resizeStamp(n);if (sc < 0) {Node<K,V>[] nt;if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;// 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法//    此时 nextTab 不为 nullif (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}// 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)//     我是没看懂这个值真正的意义是什么? 不过可以计算出来的是,结果是一个比较大的负数//  调用 transfer 方法,此时 nextTab 参数为 nullelse if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);}}
}

这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。 所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),这里怎么结束循环的需要看完 transfer 源码才清楚。

transfer数据迁移方法

太麻烦了

get方法

get 方法从来都是最简单的,这里也不例外:
计算 hash 值 根据 hash 值找到数组对应位置: (n - 1) & h
根据该位置处结点性质进行相应查找
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
如果以上 3 条都不满足,那就是链表,进行遍历比对即可

两个版本的区别

参考资料

https://www.pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html

ConcurrentHashMap--自用,非教学相关推荐

  1. 数据挖掘的11大算法及python实现(个人笔记整理,非教学用)

    分类算法:C4.5,朴素贝叶斯(Naive Bayes),SVM,KNN,Adaboost,CARTl .聚类算法:K-Means,EMl .关联分析:Aprioril .连接分析:PageRank ...

  2. 数据结构考研复习(自用非408)顺序表

    2.1线性表的基本概念  线性表按存储方式的不同,可以划分为顺序表和链表.线性表是具有相同数据类型的n个数据元素的有限序列,n为表长,当n=0时为空表. 线性表是一种逻辑结构,具有以下特点: 表中的元 ...

  3. 数据结构考研复习知识点梳理(自用非408)第一章

    第一章.绪论 1.1什么是数据结构 1.数据元素是数据的基本单位,在计算机程序中通常作为一个整体考虑和处理 2.一个数据元素可以由若干个数据项构成,数据项是数据的不可分割的最小单位 3.数据对象是性质 ...

  4. 程序设计类课程教学中创造技法的探索

    1 引言    高职院校的计算机软件专业一般都开设了大量的程序设计类课程,从基础的C/C++.Java等语言类课程到可视化开发的Delphi.VB.JBuilder等开发工具类课程.学生对这些课程的学 ...

  5. 中职学校的学生计算机基础较弱,中职学校计算机专业教学的现状分析及对策探究.doc...

    中职学校计算机专业教学的现状分析及对策探究 中职学校计算机专业教学的现状分析及对策探究 摘 要: 随着信息技术的发展,计算机已经成为人们日常学习和生活中不可或缺的工具,这也对中职计算机专业毕业生的能力 ...

  6. 再先进的在线教学,也要回归这个本质

    如何使学习的科学.教育技术和人才培养方案三者有机结合,特别是向教育技术赋予科学的指导思想和教学实践,将成为高校教学信息化改革的"难点". 严峻的疫情防控形势对高校教学提出了挑战,我 ...

  7. 上课用计算机的好处,正确认识多媒体课件在教学中的作用

    正确认识多媒体课件在教学中的作用 时间: 2007-04-01 栏目: 正确认识多媒体课件在教学中的作用 德安二中现代教育技术中心  代玉良 [摘  要]随着现代科学技术的发展,计算机已进入我国的教育 ...

  8. C语言课程建设总体思路,程序设计C语言课程教学资源建设探索

    针对程序设计C语言教学缺乏活力和教学效果不明显的问题,本文介绍了程序设计C语言课程教学的现状,分析了教学改革面临的主要问题,提出了程序设计C语言教学改革的基本方案,并论述了在研究型教学思路下,如何重点 ...

  9. 计算机系统的组成观评课报告,观课报告 精选(15篇)

    <观课报告 精选(15篇)>由会员分享,可在线阅读,更多相关<观课报告 精选(15篇)(40页珍藏版)>请在人人文库网上搜索. 1.观课报告 精选(15篇) 观课报告 精选(1 ...

最新文章

  1. JavaScript函数小集锦
  2. 【ABAP】Field Symbol使用总结
  3. linux下删除乱码文件名的方法
  4. mysqld已删除但仍占用空间的_Windows 10删除备份文件方法
  5. PWN-PRACTICE-CTFSHOW-2
  6. 前端常用 JavaScript 方法封装
  7. 如何低格台式计算机的硬盘,硬盘怎样低级格式化
  8. Maven项目创建后没有resources文件夹
  9. tensorflow之数据集
  10. 魔乐科技 oracle 视频,MLDN魔乐科技JAVA+Oracle数据库视频课程
  11. python卸载pip重新安装_pip的卸载、重装、升级(from pip19.3 to pip20.1)
  12. 支付宝小程序使用 icontfont字体图标
  13. 【转载】从创业者角度看《印度合伙人 Padman》后的一点感受
  14. Layui文件上传样式在ng-dialog不显示的问题处理
  15. 基于微信小程序的在线考试系统【毕业设计源码】
  16. 项目管理(pm)-净现值
  17. java编译器eclipse_Java 调用 Eclipse的编译器 JDT
  18. H5--大概的,没事看看
  19. CityEngine2018正版免费申请试用教程
  20. 智慧小区智慧物业管理系统一体化解决方案

热门文章

  1. CF741C Arpa’s overnight party and Mehrdad’s si
  2. Lost Cows(树状数组)
  3. nssl1478-题【dp】
  4. 二分答案二题-P1182数列分段II,P1873砍树
  5. 2018/7/8-纪中某C组题【jzoj1619,jzoj1620,jzoj1621,jzoj1622】
  6. 【2018.3.31】模拟赛之四-ssl2408 句子【dp,字符串】
  7. codeforces1012 B. Chemical table(并查集+思维)
  8. 牛客练习赛 67——ST表
  9. 网络流及建模专题(下)
  10. 面试官让我讲下线程的TIMED_WAITING状态,我又笑了