首页 文章 精选 留言 我的

精选列表

搜索[面试],共5086篇文章
优秀的个人博客,低调大师

java-面试-Java并发容器大合集

概述 java.util包中的大部分容器都是非线程安全的,若要在多线程中使用容器,你可以使用Collections提供的包装函数:synchronizedXXX,将普通容器变成线程安全的容器。但该方法仅仅是简单地给容器使用同步,效率很低。因此并发大师Doug Lea提供了java.util.concurrent包,提供高效的并发容器。并且为了保持与普通的容器的接口一致性,仍然使用util包的接口,从而易于使用、易于理解。 PS:问题:synchronizedXXX究竟对容器做了什么从而能达到线程安全的目的? 类图 List和Set JUC包中List接口的实现类:CopyOnWriteArrayList CopyOnWriteArrayList是线程安全的ArrayList JUC包中Set接口的实现类:CopyOnWriteArraySet、ConcurrentSkipListSet CopyOnWriteArraySet是线程安全的Set,它内部包含了一个CopyOnWriteArrayList,因此本质上是由CopyOnWriteArrayList实现的。 ConcurrentSkipListSet相当于线程安全的TreeSet。它是有序的Set。它由ConcurrentSkipListMap实现。 Map ConcurrentHashMap:线程安全的HashMap。采用分段锁实现高效并发。 ConcurrentSkipListMap:线程安全的有序Map。使用跳表实现高效并发。 Queue ConcurrentLinkedQueue:线程安全的无界队列。底层采用单链表。支持FIFO。 ConcurrentLinkedDeque:线程安全的无界双端队列。底层采用双向链表。支持FIFO和FILO。 ArrayBlockingQueue:数组实现的阻塞队列。 LinkedBlockingQueue:链表实现的阻塞队列。 LinkedBlockingDeque:双向链表实现的双端阻塞队列。 CopyOnWrite容器(写时复制容器) CopyOnWrite容器包括:CopyOnWriteArrayList和CopyOnWriteArraySet。 PS:CopyOnWriteArraySet有CopyOnWriteArrayList实现。 特性 适用于读操作远远多于写操作,并且数据量较小的情况。 修改容器的代价是昂贵的,因此建议批量增加addAll、批量删除removeAll。 CopyOnWrite容器是如何实现线程安全的? 使用volatile修饰数组引用:确保数组引用的内存可见性。 对容器修改操作进行同步:从而确保同一时刻只能有一条线程修改容器(因为修改容器都会产生一个新的容器,增加同步可避免同一时刻复制生成多个容器,从而无法保证数组数据的一致性) 修改时复制容器:确保所有修改操作都作用在新数组上,原本的数组在创建过后就用不变化,从而其他线程可以放心地读。 新增方法 CopyOnWriteArrayList: // 添加集合中不存在的元素 int addAllAbsent(Collection<? extends E> c) // 该元素若不存在则添加 boolean addIfAbsent(E e) CopyOnWriteArraySet:木有新增! 迭代 CopyOnWriteArrayList拥有内部类:COWIterator,它是ListIterator的子类。 当调用iterator函数时返回的是COWIterator对象。 COWIterator不允许修改容器,你若调用则会抛出UnsupportedOperationException。 优点 读操作无需加锁,从而高效。 缺点 数据一致性问题 由于迭代的是容器当前的快照,因此在迭代过程中容器发生的修改并不能实时被当前正在迭代的线程感知。 内存占用问题 由于修改容器都会复制数组,从而当数组超大时修改容器效率很低。 PS:因此写时复制容器适合存储小容量数据。 ConcurrentHashMap java.util包中提供了线程安全的HashTable,但这家伙只是通过简单的同步来实现线程安全,因此效率低。只要有一条线程获取了容器的锁之后,其他所有的线程访问同步函数都会被阻塞。因此同一时刻只能有一条线程访问同步函数。而ConcurrentHashMap采用了分段锁机制实现高效的并发访问。 分段锁原理 ConcurrentHashMap由多个Segment构成,每个Segment都包含一张哈希表。每次操作只将操作数据所属的Segment锁起来,从而避免将整个锁住。 数据结构 ConcurrentHashMap内部包含了Segment数组,而每个Segment又继承自ReentrantLock,因此它是一把可重入的锁。 Segment内部拥有一个HashEntry数组,它就是一张哈希表。HashEntry是单链表的一个节点,HashEntry数组存储单链表的表头节点。 新增API V putIfAbsent(K key, V value) ConcurrentSkipListMap 它是一个有序的Map,相当于TreeMap。 TreeMap采用红黑树实现排序,而ConcurrentHashMap采用跳表实现有序。 跳表的由来 作用:存储有序序列,并且实现高效的查找与插入删除。 存储有序序列最简单的办法就是使用数组,从而查找可以采用二分搜索,但插入删除需要移动元素较为低效。 因此出现了二叉搜索树,用来解决插入删除移动元素的问题。但二叉搜索树在最坏情况下会退化成一条单链表,搜索的效率降为O(n)。 为了避免二叉搜索树的退化,出现了二叉平衡树,它在每次插入删除节点后都会重新调整树形,使得它仍然保持平衡,从而保证了搜索效率,也保证了插入删除的效率。 此外,根据平衡算法的不同,二叉平衡树又分为:B+树、B-树、红黑树。 但平衡算法过于复杂,因此出现跳表。 跳表介绍 跳表是条有序的单链表,它的每个节点都有多个指向后继节点的引用。 它有多个层次,上层都是下层的子集,从而能跳过不必要的节点,提升搜索速度。 它通过空间来换取时间。 如查找19的过程: ConcurrentSkipListSet 它是一个有序的、线程安全的Set,相当于线程安全的TreeSet。 它内部拥有ConcurrentSkipListMap实例,本质上就是一个ConcurrentSkipListMap,只不过仅使用了Map中的key。 ArrayBlockingQueue 概要 ArrayBlockingQueue是一个 数组实现的 线程安全的 有限 阻塞队列。 数据结构 ArrayBlockingQueue继承自AbstractQueue,并实现了BlockingQueue接口。 ArrayBlockingQueue内部由Object数组存储元素,构造时必须要指定队列容量。 ArrayBlockingQueue由ReentrantLock实现队列的互斥访问,并由notEmpty、notFull这两个Condition分别实现队空、队满的阻塞。 ReentrantLock分为公平锁和非公平锁,可以在构造ArrayBlockingQueue时指定。默认为非公平锁。 新增API // 在队尾添加指定元素,若队已满则等待指定时间 boolean offer(E e, long timeout, TimeUnit unit) // 获取并删除队首元素,若队为空则阻塞等待 E take() // 添加指定元素,若队已满则一直等待 void put(E e) // 获取队首元素,若队为空,则等待指定时间 E poll(long timeout, TimeUnit unit) 队满、队空阻塞唤醒的原理 队满阻塞:当添加元素时,若队满,则调用notFull.await()阻塞当前线程;当移除一个元素时调用notFull.signal()唤醒在notFull上等待的线程。 队空阻塞:当删除元素时,若队为空,则调用notEmpty.await()阻塞当前线程;当队首添加元素时,调用notEmpty.signal()唤醒在notEmpty上等待的线程。 LinkedBlockingQueue 概要 LinkedBlockingQueue是一个 单链表实现的、线程安全的、无限 阻塞队列。 数据结构 LinkedBlockingQueue继承自AbstractQueue,实现了BlockingQueue接口。 LinkedBlockingQueue由单链表实现,因此是个无限队列。但为了方式无限膨胀,构造时可以加上容量加以限制。 LinkedBlockingQueue分别采用读取锁和插入锁控制读取/删除 和 插入过程的并发访问,并采用notEmpty和notFull两个Condition实现队满队空的阻塞与唤醒。 队满队空阻塞唤醒的原理 队满阻塞:若要插入元素,首先需要获取putLock;在此基础上,若此时队满,则调用notFull.await(),阻塞当前线程;当移除一个元素后调用notFull.signal()唤醒在notFull上等待的线程;最后,当插入操作完成后释放putLock。 队空阻塞:若要删除/获取元素,首先要获取takeLock;在此基础上,若队为空,则调用notEmpty.await(),阻塞当前线程;当插入一个元素后调用notEmpty.signal()唤醒在notEmpty上等待的线程;最后,当删除操作完成后释放takeLock。 PS:API和ArrayBlockingQueue一样。 LinkedBlockingDeque 概要 它是一个 由双向链表实现的、线程安全的、 双端 无限 阻塞队列。 数据结构 ConcurrentLinkedQueue 概述 它是一个由单链表实现的、线程安全的、无限 队列。 数据结构 它仅仅继承了AbstractQueue,并未实现BlockingQueue接口,因此它不是阻塞队列,仅仅是个线程安全的普通队列。 特性 head、tail、next、item均使用volatile修饰,保证其内存可见性,并未使用锁,从而提高并发效率。 PS:它究竟是怎样在不使用锁的情况下实现线程安全的?

优秀的个人博客,低调大师

java-面试-Java并发编程(七)——Executors

Executors框架简介 Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逸出。 Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。 Executor类 Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。 ExecutorService类 ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。 Executors类 Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。 public static ExecutorServicenewFixedThreadPool(int nThreads) 创建固定数目线程的线程池。 newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程; 任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子; 和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器; 从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:fixed池线程数固定,并且是0秒IDLE(无IDLE)cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE。 public static ExecutorServicenewCachedThreadPool() 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。 缓存型池子通常用于执行一些生存期很短的异步型任务; 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。 缺省timeout是60s。 public static ExecutorServicenewSingleThreadExecutor() 创建一个单线程化的Executor。 用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE) public staticScheduledExecutorServicenewScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。Timer类存在以下缺陷: Timer类不管启动多少定时器,但它只会启动一条线程,当有多个定时任务时,就会产生延迟。如:我们要求一个任务每隔3S执行,且执行大约需要10S,第二个任务每隔5S执行,两个任务同时启动。若使用Timer我们会发现,第而个任务是在第一个任务执行结束后的5S才开始执行。这就是多任务的延时问题。 若多个定时任务中有一个任务抛异常,那所有任务都无法执行。 Timer执行周期任务时依赖系统时间。若系统时间发生变化,那Timer执行结果可能也会发生变化。而ScheduledExecutorService基于时间的延迟,并非时间,因此不会由于系统时间的改变发生执行变化。综上所述,定时任务要使用ScheduledExecutorService取代Timer。 Executor执行任务 在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。 Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。 当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。 Executor执行Runnable任务 通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上执行。 // 获取ExecutorService实例 ExecutorService executorService = Executors.newCachedThreadPool(); // 提交任务 executorService.execute( new Runnable(){ public void run(){ //…… } } ); 1 2 3 4 5 6 7 8 9 Executor执行Callable任务 // 创建线程池 ExecutorService executorService = Executors.newCachedThreadPool(); // 提交任务 Future<String> future = executorService.submit( new Callable<String>{ public String call(){ // …… } } ); // 获取执行结果 if ( future.isDone ) { String result = future.get(); } // 关闭线程池 executorService.shutdown(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Callable<String>表示call函数返回值为String类型; 如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。 ThreadPoolExecutor类 该类用于构造自定义的线程池。构造方法如下: public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) 1 corePoolSize:线程池中所保存的核心线程数,包括空闲线程。线程池认为这是一个最合理的值,它会尽量使得线程数量维持在这个值上下。 maximumPoolSize:池中允许的最大线程数。 keepAliveTime:线程池中的空闲线程所能持续的最长时间。 unit:持续时间的单位。 workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。 当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理: 如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务; 如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则不再创建新的线程,并将新任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行); 如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务; 如果线程池中的线程数量等于了maximumPoolSize,有4种才处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。 另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

优秀的个人博客,低调大师

java面试-Java并发编程(二)——重排序

当我们写一个单线程程序时,总以为计算机会一行行地运行代码,然而事实并非如此。 什么是重排序? 重排序指的是编译器、处理器在不改变程序执行结果的前提下,重新排列指令的执行顺序,以达到最佳的运行效率。 重排序分类 重排序分为:编译器重排序 和 处理器重排序。 数据依赖 编译器和处理器并不会随意的改变指令的执行顺序,因为有些指令之间是有依赖关系的,若改变了他们的执行顺序,就会出现错误的结果。因此,编译器和处理器只会对没有依赖关系的指令进行重排序。 数据依赖:若相邻的两条指令访问同一个变量,并且其中有一条指令执行写操作,那么这样的两条指令之间存在数据依赖。对于有数据依赖关系的指令,不会发生重排序。 数据依赖关系总结一下为以下三种情况: 指令 示例 读后写 a=b;b=1; 写后写 a=1;a=2; 写后读 a=1;b=a; as-if-serial 在单线程开发中,程序员不需要知道指令是如何重排序的,只要简单地认为指令是按照顺序依次执行的即可。这就是as-if-serial的语义,即:貌似是串行的。

优秀的个人博客,低调大师

Java继承中的几道面试题

第一题: 1 /* 2 看程序写结果: 3 A:访问成员变量的原则:就近原则。 4 B:this和super的问题: 5 this 访问本类的成员 6 super 访问父类的成员(可以理解为的) 7 C:子类的所有构造方法执行前默认先执行父类的无参构造方法。 8 D:一个类的初始化过程: 9 成员变量进行初始化过程如下: 10 默认初始化 11 显示初始化 12 构造方法初始化 13 14 输出的结果是: 15 fu 16 zi 17 30 18 20 19 10 20 */ 21 class Fu { 22 public int num = 10; 23 public Fu() { 24 System.out.println("fu"); 25 } 26 } 27 28 class Zi extends Fu { 29 public int num = 20; 30 public Zi() { 31 System.out.println("zi"); 32 } 33 public void show() { 34 int num = 30; 35 System.out.println(num); //30 36 System.out.println(this.num); //20 37 System.out.println(super.num); //10 38 } 39 } 40 class ExtendsTest { 41 public static void main(String[] args) { 42 Zi z = new Zi(); 43 z.show(); 44 } 45 } 第二题: 1 /* 2 看程序写结果: 3 A:一个类的静态代码块,构造代码块,构造方法的执行流程: 4 静态代码块 > 构造代码块 > 构造方法 5 B:静态的内容是随着类的加载而加载, 6 即:静态代码块的内容会优先执行。 7 C:构造代码块 8 在类中方法外出现(即在类中的成员位置),可以把多个构造方法方法中相同的代码存放到一起,用于对对象进行初始化, 9 每次调用构造方法都执行,并且在构造方法前执行。 10 C:子类的所有的构造方法默认都会去访问父类的无参构造方法。 11 12 输出结果是: 13 静态代码块Fu 14 静态代码块Zi 15 构造代码块Fu 16 构造方法Fu 17 构造代码块Zi 18 构造方法Zi 19 */ 20 class Fu { 21 static { 22 System.out.println("静态代码块Fu"); 23 } 24 25 { 26 System.out.println("构造代码块Fu"); 27 } 28 29 public Fu() { 30 System.out.println("构造方法Fu"); 31 } 32 } 33 34 class Zi extends Fu { 35 static { 36 System.out.println("静态代码块Zi"); 37 } 38 39 { 40 System.out.println("构造代码块Zi"); 41 } 42 43 public Zi() { 44 System.out.println("构造方法Zi"); 45 } 46 } 47 48 class ExtendsTest2 { 49 public static void main(String[] args) { 50 Zi z = new Zi(); 51 } 52 } 第三题: 1 /* 2 看程序写结果: 3 A:成员变量的问题 4 int x = 10; //成员变量x是基本类型 5 Student s = new Student(); //成员变量s是引用类型 6 B:一个类的初始化过程 7 先进行成员变量的初始化: 8 默认初始化 9 显示初始化 10 构造方法初始化 11 C:子父类的初始化(分层初始化) 12 先进行父类初始化,然后进行子类初始化。 13 14 结果: 15 YXYZ 16 17 问题: 18 虽然子类中的构造方法默认有一个 super(); 19 但初始化的时候,不是按照那个顺序进行的。 20 而是按照分层初始化进行的。 21 super(); 它仅仅表示要先初始化父类数据,再初始化子类数据。 22 */ 23 24 class X { 25 //成员变量(引用类型) 26 Y b = new Y(); 27 //无参构造方法 28 X() { 29 System.out.print("X"); 30 } 31 } 32 33 class Y { 34 //无参构造方法 35 Y() { 36 System.out.print("Y"); 37 } 38 } 39 40 public class Z extends X { 41 //成员变量(引用类型) 42 Y y = new Y(); 43 //无参构造方法 44 Z() { 45 //super(); //它仅仅表示要先初始化父类数据,再初始化子类数据。 46 System.out.print("Z"); 47 } 48 public static void main(String[] args) { 49 new Z(); 50 } 51 } 我的GitHub地址: https://github.com/heizemingjun 我的博客园地址: http://www.cnblogs.com/chenmingjun 我的蚂蚁笔记博客地址: http://blog.leanote.com/chenmingjun Copyright ©2018 黑泽明军 【转载文章务必保留出处和署名,谢谢!】

优秀的个人博客,低调大师

精选21道Java后端面试题,看完你也能唬住面试官拿30K

微信公众号:慕容千语的架构笔记。欢迎关注一起进步。 1. 如何用数组实现队列? 用数组实现队列时要注意 溢出 现象,这时我们可以采用循环数组的方式来解决,即将数组收尾相接。使用front指针指向队列首位,tail指针指向队列末位。 2. 内部类访问局部变量的时候,为什么变量必须加上final修饰? 因为生命周期不同。局部变量在方法结束后就会被销毁,但内部类对象并不一定,这样就会导致内部类引用了一个不存在的变量。 所以编译器会在内部类中生成一个局部变量的拷贝,这个拷贝的生命周期和内部类对象相同,就不会出现上述问题。 但这样就导致了其中一个变量被修改,两个变量值可能不同的问题。为了解决这个问题,编译器就要求局部变量需要被final修饰,以保证两个变量值相同。 在JDK8之后,编译器不要求内部类访问的局部变量必须被final修饰,但局部变量值不能被修改(无论是方法中还是内部类中),否则会报编译错误。利用javap查看编译后的字节码可以发现,编译器已经加上了final。 3. long s = 499999999 * 499999999 在上面的代码中,s的值是多少? 根据代码的计算结果,s的值应该是-1371654655,这是由于Java中右侧值的计算默认是int类型。 4. NIO相关,Channels、Buffers、Selectors NIO(Non-blocking IO)为所有的原始类型提供(Buffer)缓存支持,字符集编码解码解决方案。 Channel :一个新的原始I/O 抽象。 支持锁和内存映射文件的文件访问接口。提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O 。 IO NIO 面向流 面向缓冲 阻塞IO 非阻塞IO 无 选择器 流与缓冲 Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。 阻塞与非阻塞IO Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,是线程向某通道发送请求读取数据,仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,当然它不会保持线程阻塞。所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。所以一个单独的线程现在可以管理多个输入和输出通道。 选择器(Selectors) Java NIO 的 选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。 5. 反射的用途 Java反射机制可以让我们在编译期(Compile Time)之外的运行期(Runtime)检查类,接口,变量以及方法的信息。反射还可以让我们在运行期实例化对象,调用方法,通过调用get/set方法获取变量的值。同时我们也可以通过反射来获取泛型信息,以及注解。还有更高级的应用–动态代理和动态类加载(ClassLoader.loadclass())。 下面列举一些比较重要的方法: getFields:获取所有 public 的变量。 getDeclaredFields:获取所有包括 private , protected 权限的变量。 setAccessible:设置为 true 可以跳过Java权限检查,从而访问private权限的变量。 getAnnotations:获取注解,可以用在类和方法上。 获取方法的泛型参数: method = Myclass.class.getMethod("setStringList", List.class); Type[] genericParameterTypes = method.getGenericParameterTypes(); for(Type genericParameterType : genericParameterTypes){ if(genericParameterType instanceof ParameterizedType){ ParameterizedType aType = (ParameterizedType) genericParameterType; Type[] parameterArgTypes = aType.getActualTypeArguments(); for(Type parameterArgType : parameterArgTypes){ Class parameterArgClass = (Class) parameterArgType; System.out.println("parameterArgClass = " + parameterArgClass); } } } 动态代理: //Main.java public static void main(String[] args) { HelloWorld helloWorld=new HelloWorldImpl(); InvocationHandler handler=new HelloWorldHandler(helloWorld); //创建动态代理对象 HelloWorld proxy=(HelloWorld)Proxy.newProxyInstance( helloWorld.getClass().getClassLoader(), helloWorld.getClass().getInterfaces(), handler); proxy.sayHelloWorld(); } //HelloWorldHandler.java public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; //调用之前 doBefore(); //调用原始对象的方法 result=method.invoke(obj, args); //调用之后 doAfter(); return result; } 通过反射获取方法注解的参数: Class aClass = TheClass.class; Annotation[] annotations = aClass.getAnnotations(); for(Annotation annotation : annotations){ if(annotation instanceof MyAnnotation){ MyAnnotation myAnnotation = (MyAnnotation) annotation; System.out.println("name: " + myAnnotation.name()); System.out.println("value: " + myAnnotation.value()); } } 非静态内部类能定义静态方法吗? public class OuterClass{ private static float f = 1.0f; class InnerClass{ public static float func(){return f;} } } 以上代码会出现编译错误,因为只有静态内部类才能定义静态方法。 6. Lock 和 Synchronized 有什么区别? 使用方法的区别 Synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。 Lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 性能的区别 synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。 Synchronized:采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着 其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。 Lock:用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。 ReentrantLock:具有更好的可伸缩性:比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 7. float 变量如何与 0 比较? folat类型的还有double类型的,这些小数类型在趋近于0的时候直接等于0的可能性很小,一般都是无限趋近于0,因此不能用==来判断。应该用|x-0| //用程序表示就是 fabs(x) < 0.00001f 8. 如何新建非静态内部类? 内部类在声明的时候必须是 Outer.Inner a,就像int a 一样,至于静态内部类和非静态内部类new的时候有点区别: Outer.Inner a = new Outer().new Inner()(非静态,先有Outer对象才能 new 内部类) Outer.Inner a = new Outer.Inner()(静态内部类) 9. Java标识符命名规则 可以包含:字母、数字、$、_(下划线),不可用数字开头,不能是 Java 的关键字和保留字。 11. 你知道哪些JDK中用到的设计模式? 装饰模式:java.io 单例模式:Runtime类 简单工厂模式:Integer.valueOf方法 享元模式:String常量池、Integer.valueOf(int i)、Character.valueOf(char c) 迭代器模式:Iterator 职责链模式:ClassLoader的双亲委派模型 解释器模式:正则表达式java.util.regex.Pattern 12. ConcurrentHashMap如何保证线程安全 JDK 1.7及以前: ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。 JDK 1.8: Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。 插入时使用CAS算法:unsafe.compareAndSwapInt(this, valueOffset, expect, update)。 CAS(Compare And Swap)意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。插入时不允许key或value为null 与Java8的HashMap有相通之处,底层依然由“数组”+链表+红黑树; 底层结构存放的是TreeBin对象,而不是TreeNode对象; CAS作为知名无锁算法,那ConcurrentHashMap就没用锁了么?当然不是,当hash值与链表的头结点相同还是会synchronized上锁,锁链表。 13. i++在多线程环境下是否存在问题,怎么解决? 虽然递增操作++i是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。这是一个“读取 - 修改 - 写入”的操作序列,并且其结果状态依赖于之前的状态。所以在多线程环境下存在问题。 要解决自增操作在多线程环境下线程不安全的问题,可以选择使用Java提供的原子类,如AtomicInteger或者使用synchronized同步方法。 14. new与newInstance()的区别 new是一个关键字,它是调用new指令创建一个对象,然后调用构造方法来初始化这个对象,可以使用带参数的构造器 newInstance()是Class的一个方法,在这个过程中,是先取了这个类的不带参数的构造器Constructor,然后调用构造器的newInstance方法来创建对象。 Class.newInstance不能带参数,如果要带参数需要取得对应的构造器,然后调用该构造器的Constructor.newInstance(Object … initargs)方法 15. 你了解哪些JDK1.8的新特性? 接口的默认方法和静态方法,JDK8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可。也可以定义被static修饰的静态方法。 对HashMap进行了改进,当单个桶的元素个数大于6时就会将实现改为红黑树实现,以避免构造重复的hashCode的攻击 多并发进行了优化。如ConcurrentHashMap实现由分段加锁、锁分离改为CAS实现。 JDK8拓宽了注解的应用场景,注解几乎可以使用在任何元素上,并且允许在同一个地方多次使用同一个注解 Lambda表达式 16. 你用过哪些JVM参数? Xms 堆最小值 Xmx 堆最大值 Xmn: 新生代容量 XX:SurvivorRatio 新生代中Eden与Surivor空间比例 Xss 栈容量 XX:PermSize 方法区初始容量 XX:MaxPermSize 方法区最大容量 XX:+PrintGCDetails 收集器日志参数 17. 如何打破 ClassLoader 双亲委托? 重写loadClass()方法。 18. hashCode() && equals() hashcode() 返回该对象的哈希码值,支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改(equals默认返回对象地址是否相等)。如果根据 equals(Object)方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧I。) hashCode的存在主要是用于查找的快捷性,如 Hashtable,HashMap等,hashCode 是用来在散列存储结构中确定对象的存储地址的; 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同; 如果对象的 equals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode 使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第2点; 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。 19. Thread.sleep() & Thread.yield() sleep()和yield()都会释放CPU。 sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。 20. #{}和${}的区别是什么? {}是预编译处理,${}是字符串替换。 Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;Mybatis在处理${}时,就是把${}替换成变量的值。使用#{}可以有效的防止SQL注入,提高系统安全性。 21. 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。 Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。 Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。 欢迎关注微信公众号:慕容千语的架构笔记 一起学习提升

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册