• JUC


    JUC

    回顾

    1 NIO主要内容:Buffer、Channel
    2 非阻塞式网络编程
    

    今天任务

    1 volatile的使用
    2 原子变量和CAS算法
    3 Lock接口
    4 并发集合
    5 同步工具类
    

    第一节 JUC 概述

    	在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,
    用于定义类似于线程的自定义子系统,包括线程池,异步IO和轻量级任务框架;还提供了用于多线程上下文中的 Collection实现等。
    

    第二节 volatile

    volatile:易变的,不稳定的
    在并发编程中的三个特性:
    (1)互斥性(原子性)
    (2)内存可见性
    (3)指令重排序
    	int b=20;
    	int a=10;
    	int c=a+b;
    
    • volatile 关键字: 当多个线程进行操作共享数据时,可以保证内存中的数据是可见的;相较于 synchronized 是一种较为轻量级的同步策略;
    • volatile 不具备"互斥性";
    • volatile 不能保证变量的"原子性";
    synchronized和volatile的区别:
    (1)synchronized可以实现互斥性和内存可见性,不能禁止指令重排序。
    (2)volatile可以实现内存可见性,禁止指令重排序,不能保证原子性。
    

    案例演示:内存可见性

    public class MyThread extends Thread{
    	//标记
    	public volatile  boolean flag=false;
    	@Override
    	public void run() {
    		System.out.println("子线程开始执行了.......");
    		while(true) {
    			if(flag) {
    				break;
    			}
    		}
    		System.out.println("子线程结束了.....");
    	}
    }
    
    public static void main(String[] args) throws Exception {
    		MyThread myThread=new MyThread();
    		myThread.start();
    		System.out.println("请输入任意字符结束子线程");
    		System.in.read();		
    		myThread.flag=true;//把变量改成true
    		System.out.println(myThread.flag);
    		System.out.println("主线程结束了....");
    }
    

    案例演示:懒汉式单例

    package com.qf.juc;
    /**
     * 单例三个步骤
     * 懒汉式
     * (1)私有化构造方法
     * (2)类内部创建对象
     * (3)添加公开的方法,返回这个对象
     * @author wgy
     */
    public class SingleTon {
    	private SingleTon() {
    	}
    	private volatile static SingleTon instance;
    	public static SingleTon getInstance() {
    		if(instance==null) {  //双重检查 double check
    			synchronized (SingleTon.class) {
    				if(instance==null) {
    					instance=new SingleTon();
    					//实例化过程
    					//(1) 堆中开辟空间
    					//(2) 调用构造方法初始化对象
    					//(3) 把对象的地址赋值给变量
    				}
    			}
    		}
    		return instance;
    	}
    	
    }
    

    单例其他写法

    静态内部写法

    package com.qf.juc;
    /**
     * 单例静态内部类写法
     * (1)私有化构造方法
     * (2)创建静态内部类 , 在静态内部类中创建常量
     * (3)添加公开的方法,返回这个对象
     * @author wgy
     * 好处:(1)节省内存空间
     * 	    (2)解决了线程安全问题
     *
     */
    public class SingleTon2 {
    	private SingleTon2() {
    	}
    	private static class SingleTon2Holder{
    		private static final SingleTon2 INSTANCE=new SingleTon2();
    	}
    	public static SingleTon2 getInstance() {
    		return SingleTon2Holder.INSTANCE;
    	}
    }	
    

    第三节 i++的原子性问题

    (1) i++的操作实际上分为三个步骤: "读-改-写";
    	i++可拆分为:
    		int temp1=i;
    		int temp2=temp+1;
    		i=temp2;
    	使用 javap -c Demo.class 可查看字节码	
    (2) 原子性: 就是"i++"的"读-改-写"是不可分割的三个步骤;
    (3) 原子变量: JDK1.5 以后, java.util.concurrent.atomic包下,提供了常用的原子变量;
    		3.1 原子变量中的值,使用volatile 修饰,保证了内存可见性;
    		3.2 CAS(Compare-And-Swap) 算法保证数据的原子性;
    

    案例演示:

    public class AtomicDemo implements Runnable{
    
    	private int num=0;
    	@Override
    	public void run() {
    		try {
    			Thread.sleep(200);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println(++num);
    	}
    }
    
    public static void main(String[] args) {
    		
    		AtomicDemo atomicDemo=new AtomicDemo();
    		for(int i=0;i<10;i++) {
    			new Thread(atomicDemo).start();
    		}
    }		
    
    3.1 使用原子变量
    public class AtomicThread implements Runnable{
    
    	//private int num=0;
    	private AtomicInteger atomicinteger=new AtomicInteger(0);
    	
    	@Override
    	public void run() {
    		System.out.println(atomicinteger.getAndIncrement()); //i++
    	}
    
    }
    
    3.2 CAS 算法
    • CAS(Compare-And-Swap) 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
    • CAS 是一种无锁的非阻塞算法(属于乐观锁)的实现;
    • CAS 包含了三个操作数:
      • 进行比较的旧预估值: A
      • 需要读写的内存值: V
      • 将写入的更新值: B
      • 当且仅当 A == V 时, V = B, 否则,将不做任何操作,并且这个比较交换过程属于原子操作;

    模拟CAS算法

    public class CompareAndSwapDemo {
    	public static void main(String[] args) {
    		CompareAndSwap compareAndSwap = new CompareAndSwap();
    		for (int i = 0; i < 10; i++) {
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    
    					int expect = compareAndSwap.get();
    					boolean b = compareAndSwap.compareAndSwap(expect, new Random().nextInt(101));
    					System.out.println(b);
    				}
    			}).start();
    		}
    	}
    }
    
    class CompareAndSwap {
    	private int value;
    
    	/**
    	 * 获取值
    	 * 
    	 * @return
    	 */
    	public synchronized int get() {
    		return value;
    	}
    
    	public synchronized boolean compareAndSwap(int expect, int newValue) {
    		if (this.value == expect) {
    			this.value = newValue;
    			return true;
    		}
    		return false;
    	}
    }
    

    ABA问题:

    在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。
    假设如下事件序列:
    线程 1 从内存位置V中取出A。
    线程 2 从位置V中取出A。
    线程 2 进行了一些操作,将B写入位置V。
    线程 2 将A再次写入位置V。
    线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。
    
    尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。
    
    public class AbaDemo {
    	private static AtomicStampedReference<Integer> integer=new AtomicStampedReference<Integer>(0, 0);
    	public static void main(String[] args) throws Exception{
    		for(int i=0;i<100;i++) {		
    			//Thread.sleep(10);
    			new Thread(new Runnable() {
    				@Override
    				public void run() {
    					while(true) {
    						int stamp = integer.getStamp();
    						Integer reference = integer.getReference();
    						if(integer.compareAndSet(reference, reference+1, stamp, stamp+1)) {
    							System.out.println(reference+1);
    							break;
    						}
    					}
    				}
    			}).start();
    		}
    	}
    }
    

    第四节 Lock接口

    synchronized的缺陷:
    	(1)获取锁的线程如果由于某种原因,不能及时释放锁(除非发生异常),其他线程只能等待
    	(2)使用同一个锁会进入同一个等待队列,所以需要唤醒所有线程
    	(3)无法实现读写锁操作	
    	
    

    案例:使用Lock实现三个线程交替输出20遍A、B、C

    package com.qf.day20_12;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    import com.qf.day20_10.Consume;
    
    /**
     * 多线程实现交替输出 A B C ,连续输出20遍
     * @author wgy
     *
     */
    public class Alternative {
    	
    	private Lock lock=new ReentrantLock();
    	//三个对象
    	Condition conditionA=lock.newCondition();
    	Condition conditionB=lock.newCondition();
    	Condition conditionC=lock.newCondition();
    	private int num=1;// 1 a  2 b  3 c
    	private int count=1;
    	
    	public void outputA() {
    		lock.lock();//上锁
    		try {
    			if(num!=1) {
    				try {
    					conditionA.await();
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    			}
    			
    			System.out.println("A");
    			num=2;
    			conditionB.signal();
    			
    			
    		} finally {
    			lock.unlock();
    		}
    	}
    	public void outputB() {
    		lock.lock();//上锁
    		try {
    			if(num!=2) {
    				try {
    					conditionB.await();
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    			}
    			
    			System.out.println("B");
    			num=3;
    			conditionC.signal();
    			
    			
    		} finally {
    			lock.unlock();
    		}
    	}
    	public void outputC() {
    		lock.lock();//上锁
    		try {
    			if(num!=3) {
    				try {
    					conditionC.await();
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    			}
    			
    			System.out.println("C");
    			System.out.println("------"+count+"------");
    			count++;
    			num=1;
    			conditionA.signal();
    			
    			
    		} finally {
    			lock.unlock();
    		}
    	}
    }
    

    第五节 并发集合

    ​ 前面学习了List(ArrayList|LinkedList)、Set(HashSet|TreeSet)、Map(HashMap|TreeMap)集合,这些集合只适合在单线程情况下使用。在Collecionts工具类中有synchronized开头方法可以把单线程集合转成支持并发的集合,但是效率不高,很少使用。

    问题演示:

    package com.qf;
    
    import java.util.ArrayList;
    import java.util.List;
    /*
     * wgy 2019/8/11 19:05
     * 佛祖保佑,永无BUG!
     */
    public class Demo3 {
        public static void main(String[] args) {
            List<String> all = new ArrayList<String>() ;
            for (int x = 0; x < 20; x++) {
                int temp = x;
                new Thread(() -> {
                    for (int y = 0; y < 30; y++) {
                        all.add(Thread.currentThread().getName()
                                + " - " + temp + " - " + y);
                        System.out.println(all);
                    }
                }).start();
            }
        }
    }
    
    

    原因是当你保存的容量个数和你的实际操作数可能不匹配的时候就会出现此异常。

    为了更好的实现集合的高并发访问处理,创建了一组新的集合工具类。

       ➣ List和Set集合:
            ➣ CopyOnWriteArrayList相当于线程安全的ArrayList,实现了List接口。
                CopyOnWriteArrayList是支持高并发的;
            ➣ CopyOnWriteArraySet相当于线程安全的HashSet,它继承了AbstractSet类,
                CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,
                它是通过CopyOnWriteArrayList实现的。
       ➣ Map集合:
            ➣ ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);
                它继承于AbstractMap类,并且实现ConcurrentMap接口。
                ConcurrentHashMap是通过“锁分段”来实现的,它支持并发;
            ➣ ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap);
                它继承于AbstactMap类,并且实现ConcurrentNavigableMap接口。
                ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发;
            ➣ ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);
                它继承于AbstractSet,并实现了NavigableSet接口。
                ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发;
      ➣ Queue队列:
           ➣ ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列;
           ➣ LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按FIFO(先进先出)排序元素;
           ➣ LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,
              该阻塞队列同时支持FIFO和FILO两种操作方式;
           ➣ ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按FIFO(先进先出)排序元素。
           ➣ ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
    
    

    案例一:使用CopyOnWriteArrayList实现多线程异步访问

     List<String> all = new CopyOnWriteArrayList<String>() ;
            for (int x = 0; x < 20; x++) {
                int temp = x ;
                new Thread(()->{
                    for (int y = 0; y < 30; y++) {
                        all.add(Thread.currentThread().getName()
                                + " - " + temp + " - " + y) ;
                        System.out.println(all);
                    }
                }).start();
            }
    

    案例二:使用CopyOnWriteArraySet实现多线程异步访问

        Set<String> all = new CopyOnWriteArraySet<String>() ;
            for (int x = 0; x < 20; x++) {
                int temp = x ;
                new Thread(()->{
                    for (int y = 0; y < 30; y++) {
                        all.add(Thread.currentThread().getName()
                                + " - " + temp + " - " + y) ;
                        System.out.println(all);
                    }
                }).start();
            }
    

    ConcurrentHashMap的使用

    ConcurrentHashMap是HashMap的多线程版本,在并发情况下使用。
    

    基本使用:

    Map<String, String> all =
                    new ConcurrentHashMap<String,String>() ;
            for (int x = 0; x < 20; x++) {
                int temp = x ;
                new Thread(()->{
                    for (int y = 0; y < 10; y++) {
                        all.put(Thread.currentThread().getName(),
                                "x = " + temp + "、y = " + y);
                        System.out.println(all);
                    }
                }).start();  ;
            }
    
    Map集合的主要特征是做数据的查询处理操作,所以在ConcurrentHashMap设计的时候考虑到了数据更新的安全性与数据查询的并发性。
    JDK1.7之前
    ConcurrentHashMap采用锁分段机制,默认并发级别为16。
    特点是写的时候同步写入,使用独占锁,读的时候为了保证性能使用了共享锁。
    JDK1.8以后
    ConcurrentHashMap写的时候采用CAS无锁算法进一步提高写入效率。
    

    ArrayBlockingQueue

    ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,可以作为线程通信同步工具类使用。
    

    案例:使用ArrayBlockingQueue实现生产者消费者

    Producer.java

    package com.qf.demo;
    
    import java.util.concurrent.ArrayBlockingQueue;
    
    /**
     * wgy 2019/7/14 21:52
     */
    public class Producer extends Thread {
    
        private ArrayBlockingQueue<String> bq;
        public Producer(ArrayBlockingQueue<String> bq){
            this.bq=bq;
        }
    
        @Override
        public void run() {
    
            for(int i=0;i<30;i++){
                try {
                    bq.put(Thread.currentThread().getName()+"生产"+(i+1)+"号产品");
                    System.out.println(Thread.currentThread().getName()+"生产"+(i+1)+"号产品");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
    
        }
    }
    
    

    Consumer.java

    package com.qf.demo;
    
    import java.util.concurrent.ArrayBlockingQueue;
    
    /**
     * wgy 2019/7/14 21:56
     */
    public class Consumer extends  Thread {
        private ArrayBlockingQueue<String> bq;
        public Consumer(ArrayBlockingQueue<String> bq){
            this.bq=bq;
        }
    
        @Override
        public void run() {
            for(int i=0;i<30;i++){
                try {
                    String take = bq.take();
                    System.out.println(Thread.currentThread().getName()+"消费了"+take);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
    
        }
    }
    
    
     public static void main(String[] args) {
            ArrayBlockingQueue<String> bq=new ArrayBlockingQueue<>(1);
            new Producer(bq).start();
            new Consumer(bq).start();
        }
    

    第六节 同步工具类 CountDownLatch、CyclicBarrier、Semaphore

    CountDownLatch类

    	CountDownLatch(闭锁)是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
    	闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才能继续执行:
    	(1)确保某个计算在其需要的所有资源都被初始化后才能继续执行。
    	(2)确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
    	(3)等待直到某个操作所有参与者都执行完毕其他线程才能继续执行。
    

    案例一:计算所有子线程执行完毕所用时间

    package com.qf.demo;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * wgy 2019/7/12 10:49
     * 计算线程执行的时间
     */
    public class Demo1 {
    
        public static void main(String[] args) {
            CountDownLatch countDownLatch=new CountDownLatch(5);
            long start =System.currentTimeMillis();
            for(int i=0;i<5;i++){
                new MyThread(countDownLatch).start();
            }
            try {
                System.out.println("等待执行结束");
                countDownLatch.await();
                //System.out.println("执行结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end=System.currentTimeMillis();
            System.out.println("最终用时:"+(end-start));
        }
    
    
        public static class MyThread extends  Thread{
    
            private CountDownLatch countDownLatch;
            public MyThread(CountDownLatch countDownLatch){
                this.countDownLatch=countDownLatch;
            }
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"开始执行.....");
                int sum=0;
                for(int i=0;i<99999;i++){
                    sum+=i;
                    for(int j=99999;j>0;j--){
                        sum-=j;
                    }
                }
                System.out.println(Thread.currentThread().getName()+"执行完毕"+sum);
                countDownLatch.countDown();
            }
        }
    }
    

    案例二:所有员工都到达,老板开始开始开会

    package com.qf.demo;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * wgy 2019/7/12 14:43
     * 所有员工都到达,老板开始开始开会
     */
    public class Demo2 {
        private static CountDownLatch countDownLatch = new CountDownLatch(5);
        public static void main(String[] args) {
            //Boss线程启动
            new BossThread().start();
    
            for(int i = 0 ; i < countDownLatch.getCount() ; i++){
                new EmpleoyeeThread().start();
            }
        }
    
        /**
         * Boss线程,等待员工到达开会
         */
        static class BossThread extends Thread{
            @Override
            public void run() {
                System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会...");
                try {
                    //Boss等待
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println("所有人都已经到齐了,开会吧...");
            }
        }
    
        //员工到达会议室
        static class EmpleoyeeThread  extends Thread{
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ",到达会议室....");
                //员工到达会议室 count - 1
                countDownLatch.countDown();
            }
        }
    }
    

    CyclicBarrier类

    	CyclicBarrier和CountDownLatch类似(屏障)表面意思理解为可循环使用的屏障,作用是让一组线程在到达一个屏障时被阻塞,等到最后一个线程到达屏障点,才会运行被拦截的线程继续运行。
    	(1)构造函数 CyclicBarrier(int parties) 屏障拦截的线程数量
    	(2)await() 调用该方法时表示线程已经到达屏障,随即阻塞
    

    案例一:简单使用,实现多个线程同时执行

    package com.qf.demo;
    
    import java.util.concurrent.CyclicBarrier;
    
    /**
     * wgy 2019/7/12 15:37
     * 5个线程同时执行
     */
    public class Demo3 {
        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier=new CyclicBarrier(5);
    
            for(int i=0;i<5;i++){
                new MyThread(cyclicBarrier).start();
            }
    
        }
    
        static class MyThread extends Thread{
    
            private CyclicBarrier cyclicBarrier;
            public MyThread(CyclicBarrier cyclicBarrier){
                this.cyclicBarrier=cyclicBarrier;
            }
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"准备好....");
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
                System.out.println(Thread.currentThread().getName()+"要执行了....");
    
            }
        }
    
    }
    

    案例二:等待所有运动员都到位才开始赛跑

    package com.qf.demo;
    
    import java.util.concurrent.CyclicBarrier;
    
    /**
     * wgy 2019/7/12 15:15
     * 等待所有运动员都到位才开始赛跑
     */
    public class Demo4 {
    
        private static CyclicBarrier cyclicBarrier;
    
        public static void main(String[] args) {
            cyclicBarrier=new CyclicBarrier(5, new Runnable() {
                @Override
                public void run() {
                    System.out.println("所有运动员已经准备完毕");
                }
            });//5个线程表示5个远动员
    
            for(int i=0;i<5;i++){
                new SportsMan(cyclicBarrier).start();;
            }
        }
    
        static class SportsMan extends  Thread{
    
            private CyclicBarrier cyclicBarrier;
            public SportsMan(CyclicBarrier cyclicBarrier){
                this.cyclicBarrier=cyclicBarrier;
            }
    
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"远动员准备好");
                try {
                    cyclicBarrier.await();
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+"已经跑完...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    CountDownLatch和CyclicBarrier的区别
    1. CountDownLatch只能用一次,CyclicBarrier可以reset(),且适合处理更复杂的业务 
    2. CyclicBarrier还有getNumberWaiting 获取当前阻塞的线程数量,isBroken()判断阻塞线程是否被中断。
    

    Semaphore信号量

     Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。
     用来控制同时访问特定资源的线程数量,通过协调保证合理的使用公共资源。
     比作控制车流的红绿灯,如马路要控制流量,只限制100辆车通行,其他必须在路口处等待,不能行驶在马路上,当其中有5辆离开马路,那么允许后面5辆进入马路。
    

    案例一:使用Semaphore信号量控制并发的个数

    package com.qf.demo;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.Semaphore;
    
    /**
     * wgy 2019/7/12 15:44
     * Semaphore信号量的使用
     *
     */
    public class Demo5 {
        public static void main(String[] args) {
            Semaphore semaphore=new Semaphore(2);//同步关键类,构造方法传入的数字是多少,则同一个时刻,只允许多少个线程同时运行指定代码
            for(int i=0;i<10;i++){
                new MyThread(semaphore).start();
            }
        }
    
        static class MyThread extends  Thread{
    
            private SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
            public Semaphore semaphore;
            public MyThread(Semaphore semaphore){
                this.semaphore=semaphore;
            }
            @Override
            public void run() {
                try {
                    semaphore.acquire(); //获取一个锁
                    System.out.println(Thread.currentThread().getName() + ":doSomething start-" + getTime());
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + ":doSomething   end-" + getTime());
                    semaphore.release();
    
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
    
            public String getTime(){
    
                return sf.format(new Date());
    
            }
        }
    
    }
    

    总结

    volatile的使用
    i++原子性问题
    	原子变量
    	CAS算法(比较交换算法)
    Lock接口的使用
    并发集合
    并发工具类
    

    课前默写

    作业

    面试题

    1 HashMap和ConcurrentHashMap的区别
    
  • 相关阅读:
    [TimLinux] myblog 创建第一个app
    [TimLinux] MySQL InnoDB的外键约束不支持set default引用选项
    [TimLinux] 养成一个习惯
    [TimLinux] myblog 页面Axure设计
    [TimLinux] MySQL 中的CASE/WHEN语法
    [TimLinux] Python Django myblog启动
    [TimLinux] Python 模块
    [TimLinux] JavaScript 获取元素节点的5种方法
    堆和栈的一点知识
    OpenCV2基础操作----直线、矩形、圆、椭圆函数的使用
  • 原文地址:https://www.cnblogs.com/Zzzxb/p/11397162.html
Copyright © 2020-2023  润新知