转自:https://my.oschina.net/hosee/blog/607677

其他资料:

https://my.oschina.net/zhupanxin/blog/269037


摘要: 本系列基于炼数成金课程,为了更好的学习,做了系列的记录。 本文主要介绍: 1.各种同步控制工具的使用 2.并发容器及典型源码分析

在[高并发Java 二] 多线程基础中,我们已经初步提到了基本的线程同步操作。这次要提到的是在并发包中的同步控制工具。

1. 各种同步控制工具的使用

1.1 ReentrantLock

ReentrantLock感觉上是synchronized的增强版,synchronized的特点是使用简单,一切交给JVM去处理,但是功能上是比较薄弱的。在JDK1.5之前,ReentrantLock的性能要好于synchronized,由于对JVM进行了优化,现在的JDK版本中,两者性能是不相上下的。如果是简单的实现,不要刻意去使用ReentrantLock。

相比于synchronized,ReentrantLock在功能上更加丰富,它具有可重入、可中断、可限时、公平锁等特点。

首先我们通过一个例子来说明ReentrantLock最初步的用法:

package test;import java.util.concurrent.locks.ReentrantLock;public class Test implements Runnable
{public static ReentrantLock lock = new ReentrantLock();public static int i = 0;@Overridepublic void run(){for (int j = 0; j < 10000000; j++){lock.lock();try{i++;}finally{lock.unlock();}}}public static void main(String[] args) throws InterruptedException{Test test = new Test();Thread t1 = new Thread(test);Thread t2 = new Thread(test);t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}

有两个线程都对i进行++操作,为了保证线程安全,使用了 ReentrantLock,从用法上可以看出,与 synchronized相比,ReentrantLock就稍微复杂一点。因为必须在finally中进行解锁操作,如果不在 finally解锁,有可能代码出现异常锁没被释放,而synchronized是由JVM来释放锁。

那么ReentrantLock到底有哪些优秀的特点呢?

1.1.1 可重入

单线程可以重复进入,但要重复退出

lock.lock();
lock.lock();
try
{i++;}
finally
{lock.unlock();lock.unlock();
}

由于ReentrantLock是重入锁,所以可以反复得到相同的一把锁,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放(重入锁)。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁。

public class Child extends Father implements Runnable{final static Child child = new Child();//为了保证锁唯一public static void main(String[] args) {for (int i = 0; i < 50; i++) {new Thread(child).start();}}public synchronized void doSomething() {System.out.println("1child.doSomething()");doAnotherThing(); // 调用自己类中其他的synchronized方法}private synchronized void doAnotherThing() {super.doSomething(); // 调用父类的synchronized方法System.out.println("3child.doAnotherThing()");}@Overridepublic void run() {child.doSomething();}
}
class Father {public synchronized void doSomething() {System.out.println("2father.doSomething()");}
}

我们可以看到一个线程进入不同的 synchronized方法,是不会释放之前得到的锁的。所以输出还是顺序输出。所以synchronized也是重入锁

输出:

1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
1child.doSomething()
2father.doSomething()
3child.doAnotherThing()
...

1.1.2.可中断

与synchronized不同的是,ReentrantLock对中断是有响应的。中断相关知识查看[高并发Java 二] 多线程基础

普通的lock.lock()是不能响应中断的,lock.lockInterruptibly()能够响应中断。

我们模拟出一个死锁现场,然后用中断来处理死锁

package test;import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.locks.ReentrantLock;public class Test implements Runnable
{public static ReentrantLock lock1 = new ReentrantLock();public static ReentrantLock lock2 = new ReentrantLock();int lock;public Test(int lock){this.lock = lock;}@Overridepublic void run(){try{if (lock == 1){lock1.lockInterruptibly();try{Thread.sleep(500);}catch (Exception e){// TODO: handle exception}lock2.lockInterruptibly();}else{lock2.lockInterruptibly();try{Thread.sleep(500);}catch (Exception e){// TODO: handle exception}lock1.lockInterruptibly();}}catch (Exception e){// TODO: handle exception}finally{if (lock1.isHeldByCurrentThread()){lock1.unlock();}if (lock2.isHeldByCurrentThread()){lock2.unlock();}System.out.println(Thread.currentThread().getId() + ":线程退出");}}public static void main(String[] args) throws InterruptedException{Test t1 = new Test(1);Test t2 = new Test(2);Thread thread1 = new Thread(t1);Thread thread2 = new Thread(t2);thread1.start();thread2.start();Thread.sleep(1000);//DeadlockChecker.check();}static class DeadlockChecker{private final static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();final static Runnable deadlockChecker = new Runnable(){@Overridepublic void run(){// TODO Auto-generated method stubwhile (true){long[] deadlockedThreadIds = mbean.findDeadlockedThreads();if (deadlockedThreadIds != null){ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds);for (Thread t : Thread.getAllStackTraces().keySet()){for (int i = 0; i < threadInfos.length; i++){if(t.getId() == threadInfos[i].getThreadId()){t.interrupt();}}}}try{Thread.sleep(5000);}catch (Exception e){// TODO: handle exception}}}};public static void check(){Thread t = new Thread(deadlockChecker);t.setDaemon(true);t.start();}}}

上述代码有可能会发生死锁,线程1得到lock1,线程2得到lock2,然后彼此又想获得对方的锁。

我们用jstack查看运行上述代码后的情况

的确发现了一个死锁。

DeadlockChecker.check();方法用来检测死锁,然后把死锁的线程中断。中断后,线程正常退出。

1.1.3.可限时

超时不能获得锁,就返回false,不会永久等待构成死锁

使用lock.tryLock(long timeout, TimeUnit unit)来实现可限时锁,参数为时间和单位。

举个例子来说明下可限时:

package test;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;public class Test implements Runnable
{public static ReentrantLock lock = new ReentrantLock();@Overridepublic void run(){try{if (lock.tryLock(5, TimeUnit.SECONDS)){Thread.sleep(6000);}else{System.out.println("get lock failed");}}catch (Exception e){}finally{if (lock.isHeldByCurrentThread()){lock.unlock();}}}public static void main(String[] args){Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();}}

使用两个线程来争夺一把锁,当某个线程获得锁后,sleep6秒,每个线程都只尝试5秒去获得锁。

所以必定有一个线程无法获得锁。无法获得后就直接退出了。

输出:

get lock failed

1.1.4.公平锁

使用方式:

public ReentrantLock(boolean fair) public static ReentrantLock fairLock = new ReentrantLock(true);

一般意义上的锁是不公平的,不一定先来的线程能先得到锁,后来的线程就后得到锁。不公平的锁可能会产生饥饿现象。

公平锁的意思就是,这个锁能保证线程是先来的先得到锁。虽然公平锁不会产生饥饿现象,但是公平锁的性能会比非公平锁差很多。

1.2 Condition

Condition与ReentrantLock的关系就类似于synchronized与Object.wait()/signal()

await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()时或者signalAll()方法时,线 程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似。

awaitUninterruptibly()方法与await()方法基本相同,但是它并不会再等待过程中响应中断。 singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。

这里就不再详细介绍了。举个例子来说明:

package test;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class Test implements Runnable
{public static ReentrantLock lock = new ReentrantLock();public static Condition condition = lock.newCondition();@Overridepublic void run(){try{lock.lock();condition.await();System.out.println("Thread is going on");}catch (Exception e){e.printStackTrace();}finally{lock.unlock();}}public static void main(String[] args) throws InterruptedException{Test t = new Test();Thread thread = new Thread(t);thread.start();Thread.sleep(2000);lock.lock();condition.signal();lock.unlock();}}

上述例子很简单,让一个线程await住,让主线程去唤醒它。condition.await()/signal只能在得到锁以后使用。

1.3.Semaphore

对于锁来说,它是互斥的排他的。意思就是,只要我获得了锁,没人能再获得了。

而对于Semaphore来说,它允许多个线程同时进入临界区。可以认为它是一个共享锁,但是共享的额度是有限制的,额度用完了,其他没有拿到额度的线程还是要阻塞在临界区外。当额度为1时,就相等于lock

下面举个例子:

package test;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;public class Test implements Runnable
{final Semaphore semaphore = new Semaphore(5);@Overridepublic void run(){try{semaphore.acquire();Thread.sleep(2000);System.out.println(Thread.currentThread().getId() + " done");}catch (Exception e){e.printStackTrace();}finally {semaphore.release();}}public static void main(String[] args) throws InterruptedException{ExecutorService executorService = Executors.newFixedThreadPool(20);final Test t = new Test();for (int i = 0; i < 20; i++){executorService.submit(t);}}}

有一个20个线程的线程池,每个线程都去 Semaphore的许可,Semaphore的许可只有5个,运行后可以看到,5个一批,一批一批地输出。

当然一个线程也可以一次申请多个许可

public void acquire(int permits) throws InterruptedException

1.4 ReadWriteLock

ReadWriteLock是区分功能的锁。读和写是两种不同的功能,读-读不互斥,读-写互斥,写-写互斥。

这样的设计是并发量提高了,又保证了数据安全。

使用方式:

private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();

详细例子可以查看 Java实现生产者消费者问题与读者写者问题,这里就不展开了。

1.5 CountDownLatch

倒数计时器
一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。 只有等所有检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程
,等待所有检查线程全部完工后,再执行

使用方式:

static final CountDownLatch end = new CountDownLatch(10);
end.countDown();
end.await();

示意图:

一个简单的例子:

package test;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Test implements Runnable
{static final CountDownLatch countDownLatch = new CountDownLatch(10);static final Test t = new Test();@Overridepublic void run(){try{Thread.sleep(2000);System.out.println("complete");countDownLatch.countDown();}catch (Exception e){e.printStackTrace();}}public static void main(String[] args) throws InterruptedException{ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++){executorService.execute(t);}countDownLatch.await();System.out.println("end");executorService.shutdown();}}

主线程必须等待10个线程全部执行完才会输出"end"。

1.6 CyclicBarrier

和CountDownLatch相似,也是等待某些线程都做完以后再执行。与CountDownLatch区别在于这个计数器可以反复使用。比如,假设我们将计数器设置为10。那么凑齐第一批1 0个线程后,计数器就会归零,然后接着凑齐下一批10个线程

使用方式:

public CyclicBarrier(int parties, Runnable barrierAction) barrierAction就是当计数器一次计数完成后,系统会执行的动作await()

示意图:

下面举个例子:

package test;import java.util.concurrent.CyclicBarrier;public class Test implements Runnable
{private String soldier;private final CyclicBarrier cyclic;public Test(String soldier, CyclicBarrier cyclic){this.soldier = soldier;this.cyclic = cyclic;}@Overridepublic void run(){try{//等待所有士兵到齐cyclic.await();dowork();//等待所有士兵完成工作cyclic.await();}catch (Exception e){// TODO Auto-generated catch blocke.printStackTrace();}}private void dowork(){// TODO Auto-generated method stubtry{Thread.sleep(3000);}catch (Exception e){// TODO: handle exception}System.out.println(soldier + ": done");}public static class BarrierRun implements Runnable{boolean flag;int n;public BarrierRun(boolean flag, int n){super();this.flag = flag;this.n = n;}@Overridepublic void run(){if (flag){System.out.println(n + "个任务完成");}else{System.out.println(n + "个集合完成");flag = true;}}}public static void main(String[] args){final int n = 10;Thread[] threads = new Thread[n];boolean flag = false;CyclicBarrier barrier = new CyclicBarrier(n, new BarrierRun(flag, n));System.out.println("集合");for (int i = 0; i < n; i++){System.out.println(i + "报道");threads[i] = new Thread(new Test("士兵" + i, barrier));threads[i].start();}}}

打印结果:

集合
0报道
1报道
2报道
3报道
4报道
5报道
6报道
7报道
8报道
9报道
10个集合完成
士兵5: done
士兵7: done
士兵8: done
士兵3: done
士兵4: done
士兵1: done
士兵6: done
士兵2: done
士兵0: done
士兵9: done
10个任务完成

1.7 LockSupport

提供线程阻塞原语

和suspend类似

LockSupport.park();
LockSupport.unpark(t1);

与suspend相比 不容易引起线程冻结

LockSupport的思想呢,和 Semaphore有点相似,内部有一个许可,park的时候拿掉这个许可,unpark的时候申请这个许可。所以如果unpark在park之前,是不会发生线程冻结的。

下面的代码是[高并发Java 二] 多线程基础中suspend示例代码,在使用suspend时会发生死锁。

package test;import java.util.concurrent.locks.LockSupport;public class Test
{static Object u = new Object();static TestSuspendThread t1 = new TestSuspendThread("t1");static TestSuspendThread t2 = new TestSuspendThread("t2");public static class TestSuspendThread extends Thread{public TestSuspendThread(String name){setName(name);}@Overridepublic void run(){synchronized (u){System.out.println("in " + getName());//Thread.currentThread().suspend();LockSupport.park();}}}public static void main(String[] args) throws InterruptedException{t1.start();Thread.sleep(100);t2.start();
//        t1.resume();
//        t2.resume();LockSupport.unpark(t1);LockSupport.unpark(t2);t1.join();t2.join();}
}

而使用 LockSupport则不会发生死锁。

另外

park()能够响应中断,但不抛出异常。中断响应的结果是,park()函数的返回,可以从Thread.interrupted()得到中断标志。

在JDK当中有大量地方使用到了park,当然LockSupport的实现也是使用unsafe.park()来实现的。

public static void park() {unsafe.park(false, 0L);}

1.8 ReentrantLock 的实现

下面来介绍下ReentrantLock的实现,ReentrantLock的实现主要由3部分组成:

  • CAS状态
  • 等待队列
  • park()

ReentrantLock的父类中会有一个state变量来表示同步的状态

/*** The synchronization state.*/private volatile int state;

通过CAS操作来设置state来获取锁,如果设置成了1,则将锁的持有者给当前线程

final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}

如果拿锁不成功,则会做一个申请

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

首先,再去申请下试试看tryAcquire,因为此时可能另一个线程已经释放了锁。

如果还是没有申请到锁,就addWaiter,意思是把自己加到等待队列中去

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}

其间还会有多次尝试去申请锁,如果还是申请不到,就会被挂起

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

同理,如果在unlock操作中,就是释放了锁,然后unpark,这里就不具体讲了。

2. 并发容器及典型源码分析

2.1 ConcurrentHashMap

我们知道HashMap不是一个线程安全的容器,最简单的方式使HashMap变成线程安全就是使用Collections.synchronizedMap,它是对HashMap的一个包装

public static Map m=Collections.synchronizedMap(new HashMap());

同理对于List,Set也提供了相似方法。

但是这种方式只适合于并发量比较小的情况。

我们来看下synchronizedMap的实现

private final Map<K,V> m;     // Backing Mapfinal Object      mutex;        // Object on which to synchronizeSynchronizedMap(Map<K,V> m) {if (m==null)throw new NullPointerException();this.m = m;mutex = this;}SynchronizedMap(Map<K,V> m, Object mutex) {this.m = m;this.mutex = mutex;}public int size() {synchronized (mutex) {return m.size();}}public boolean isEmpty() {synchronized (mutex) {return m.isEmpty();}}public boolean containsKey(Object key) {synchronized (mutex) {return m.containsKey(key);}}public boolean containsValue(Object value) {synchronized (mutex) {return m.containsValue(value);}}public V get(Object key) {synchronized (mutex) {return m.get(key);}}public V put(K key, V value) {synchronized (mutex) {return m.put(key, value);}}public V remove(Object key) {synchronized (mutex) {return m.remove(key);}}public void putAll(Map<? extends K, ? extends V> map) {synchronized (mutex) {m.putAll(map);}}public void clear() {synchronized (mutex) {m.clear();}}

它会将HashMap包装在里面,然后将HashMap的每个操作都加上synchronized。

由于每个方法都是获取同一把锁(mutex),这就意味着,put和remove等操作是互斥的,大大减少了并发量。

下面来看下ConcurrentHashMap是如何实现的

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegments = ensureSegment(j);return s.put(key, hash, value, false);}

在 ConcurrentHashMap内部有一个Segment段,它将大的HashMap切分成若干个段(小的HashMap),然后让数据在每一段上Hash,这样多个线程在不同段上的Hash操作一定是线程安全的,所以只需要同步同一个段上的线程就可以了,这样实现了锁的分离,大大增加了并发量。

在使用ConcurrentHashMap.size时会比较麻烦,因为它要统计每个段的数据和,在这个时候,要把每一个段都加上锁,然后再做数据统计。这个就是把锁分离后的小小弊端,但是size方法应该是不会被高频率调用的方法。

在实现上,不使用synchronized和lock.lock而是尽量使用trylock,同时在HashMap的实现上,也做了一点优化。这里就不提了。

2.2 BlockingQueue

BlockingQueue不是一个高性能的容器。但是它是一个非常好的共享数据的容器。是典型的生产者和消费者的实现。

示意图:

具体可以查看Java实现生产者消费者问题与读者写者问题


系列:[高并发Java 一] 前言[高并发Java 二] 多线程基础[高并发Java 三] Java内存模型和线程安全[高并发Java 四] 无锁[高并发Java 五] JDK并发包1[高并发Java 六] JDK并发包2[高并发Java 七] 并发设计模式[高并发Java 八] NIO和AIO[高并发Java 九] 锁的优化和注意事项[高并发Java 十] JDK8对并发的新支持

高并发基础之Java并发包相关推荐

  1. 多线程与高并发基础一(超发--多线程悲观锁,乐观锁、类数据库悲观锁乐观锁)

    PS:看完文章后对自己以前所做过的并发和锁机制有了深入原理的了解. 知其然和知其所以然! 遂以记之! 关键词: 线程,同步,单例,高并发,高访问,死锁 一.大规模并发带来的挑战 在过去的工作中,我曾经 ...

  2. java vanish 缓存_高并发基础、思路以及普遍的处理方式

    何为高并发 在同时或者极短时间内,有大量的请求到达服务端,每个请求都需要服务端消耗资源去进行处理.同时开启的进程数.能同时运行的线程数.网络连接数.cpu.io.内存均为服务端资源,由于服务端资源是有 ...

  3. 一个生成全局唯一Sequence ID的高并发工厂类 (Java)

    为什么80%的码农都做不了架构师?>>>    Sequence是数据库系统按照一定规则自动增加的数字序列.这个序列一般作为代理主键(因为不会重复),没有其他任何意义. Sequen ...

  4. lambda 高并发_玩Java 8 – Lambda和并发

    lambda 高并发 因此Java 8不久前发布,具有大量功能和更改. 我们所有的Java狂热者一直在等待这一点,从他们最初宣布Java 7的所有强大功能开始一直到最终被取消. 我最近才有时间实际开始 ...

  5. java 抽奖 高并发_【java】如何设计高并发下的抽奖?

    我写的伪代码如下,但出现了个bug,抽奖现在要限制每日抽奖结果出现的次数,但实际运行是在并发时不能限制住,如何解决? resultDayLimitTimes = { resultA => 2 # ...

  6. 「高并发秒杀」java课程设计报告模板

    正文 我在做技术面试官的时候,在问完问题后,照例会问一句:你期望的工资是多少?对此,我只会记录下候选人的回答然后上报,没有同意权,更没有批驳权. 判断候选人能否通过面试,主要看候选人能力和岗位的匹配度 ...

  7. java 内存模型 多线程_Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这 ...

  8. Java并发指南1:并发基础与Java多线程

    什么是并发 在过去单CPU时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的"同一时间点",而是多个 ...

  9. 【多线程高并发】查看Java代码对应的汇编指令教程

    1.首先下载hsdis-amd64.dll文件 零积分下载地址:https://download.csdn.net/download/Kevinnsm/54740576 2.解压放到java11\bi ...

最新文章

  1. python尝试不同的随机数进行数据划分、使用卡方检验依次计算不同随机数划分下训练接和测试集所有分类特征的卡方检验的p值,如果所有p值都大于0.05则训练集和测试集都具有统计显著性、数据划分合理
  2. 把2018年所有踩过的坑都记在这里。
  3. [hdu5203]计数水题
  4. 每日一题(35)—— heap与stack的差别
  5. AE开发 创建Feature后,需要进行拓扑检查
  6. python 空值(NoneType)
  7. 入行数据科学,仅需6步
  8. 【漏洞分析】dedecms有前提前台任意用户密码修改
  9. 操作系统学习笔记:文件系统接口
  10. PyTorch绘制训练过程的accuracy和loss曲线
  11. 科研 | 中英文期刊分区介绍及查询方法
  12. 2020年面向前端开发人员的10个很棒的 JS 库
  13. [转]2015年数学建模C题–基于无线通信基站的室内三维定位问题
  14. 手把手教你从零开始腾讯云服务器部署
  15. java中switch的类型_java中switch()括号中不能放什么类型?
  16. 如何使用思维导图做计划的6种模板类型
  17. 91、储存物品的火灾危险性分类
  18. Java编程入门与应用 P104—例4-12(学生管理系统——密码的重复验证)
  19. HTML5应用——欢乐老虎机
  20. VS2017 -error LNK1104: 无法打开文件“msvcprtd.lib”

热门文章

  1. mybatis动态sql模糊查询方法
  2. 腾讯高级工程师带你完整体验Node.js开发实战
  3. 关于SV的一些知识1
  4. Error creating bean with name ‘ribbonLoadBalancingHttpClient‘
  5. 超像素分割(Superpixel Segmentation)发展
  6. oracle中怎样判断数据是否为空,为空赋值,不为空也赋值
  7. RocketMQ原理剖析
  8. CrossCompiler And Auto tools
  9. SDSoC和SDAccel和SDNet和HLS工具介绍
  10. IKEv2与IKEv1的差异