首页 文章 精选 留言 我的

精选列表

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

Java并发面试题精选

1,什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。 2,线程和进程有什么区别? 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。 3,如何在Java中实现线程? 两种方式:java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。 4,Java 关键字volatile 与 synchronized 作用与区别? 1,volatile 它所修饰的变量不保留拷贝,直接访问主内存中的。 在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变 量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。 2,synchronized 当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。 四、当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。 五、以上规则对其它对象锁同样适用。 5,有哪些不同的线程生命周期? 当我们在Java程序中新建一个线程时,它的状态是New。当我们调用线程的start()方法时,状态被改变为Runnable。线程调度器会为Runnable线程池中的线程分配CPU时间并且讲它们的状态改变为Running。其他的线程状态还有Waiting,Blocked 和Dead。 6,你对线程优先级的理解是什么? 每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。 7,什么是死锁(Deadlock)?如何分析和避免死锁? 死锁是指两个以上的线程永远阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资源。 分析死锁,我们需要查看Java应用程序的线程转储。我们需要找出那些状态为BLOCKED的线程和他们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁。 避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。 8,什么是线程安全?Vector是一个线程安全类吗? 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。 9,Java中如何停止一个线程? Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程 10,什么是ThreadLocal? ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。 每个线程都会拥有他们自己的Thread变量,它们可以使用get()set()方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal实例通常是希望它们同线程状态关联起来是private static属性。 11,Sleep()、suspend()和wait()之间有什么区别? Thread.sleep()使当前线程在指定的时间处于“非运行”(Not Runnable)状态。线程一直持有对象的监视器。比如一个线程当前在一个同步块或同步方法中,其它线程不能进入该块或方法中。如果另一线程调用了interrupt()方法,它将唤醒那个“睡眠的”线程。 注意:sleep()是一个静态方法。这意味着只对当前线程有效,一个常见的错误是调用t.sleep(),(这里的t是一个不同于当前线程的线程)。即便是执行t.sleep(),也是当前线程进入睡眠,而不是t线程。t.suspend()是过时的方法,使用suspend()导致线程进入停滞状态,该线程会一直持有对象的监视器,suspend()容易引起死锁问题。 object.wait()使当前线程出于“不可运行”状态,和sleep()不同的是wait是object的方法而不是thread。调用object.wait()时,线程先要获取这个对象的对象锁,当前线程必须在锁对象保持同步,把当前线程添加到等待队列中,随后另一线程可以同步同一个对象锁来调用object.notify(),这样将唤醒原来等待中的线程,然后释放该锁。基本上wait()/notify()与sleep()/interrupt()类似,只是前者需要获取对象锁。 12,什么是线程饿死,什么是活锁? 当所有线程阻塞,或者由于需要的资源无效而不能处理,不存在非阻塞线程使资源可用。JavaAPI中线程活锁可能发生在以下情形: 1,当所有线程在程序中执行Object.wait(0),参数为0的wait方法。程序将发生活锁直到在相应的对象上有线程调用Object.notify()或者Object.notifyAll()。 2,当所有线程卡在无限循环中。 13,什么是Java Timer类?如何创建一个有特定时间间隔的任务? java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。 java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。 14,Java中的同步集合与并发集合有什么区别? 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。 在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。 Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和 内部分区等现代技术提高了可扩展性。 15,同步方法和同步块,哪个是更好的选择? 同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 16,什么是线程池? 为什么要使用它? 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。 为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。 从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。 17,Java中invokeAndWait 和 invokeLater有什么区别? 这两个方法是Swing API 提供给Java开发者用来从当前线程而不是事件派发线程更新GUI组件用的。InvokeAndWait()同步更新GUI组件,比如一个进度条,一旦进度更新了,进度条也要做出相应改变。如果进度被多个线程跟踪,那么就调用invokeAndWait()方法请求事件派发线程对组件进行相应更新。而invokeLater()方法是异步调用更新组件的。 18,多线程中的忙循环是什么? 忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存。 在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。 19,Java内存模型是什么? Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了: 线程内的代码能够按先后顺序执行,这被称为程序次序规则。 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。 一个线程的所有操作都会在线程终止之前,线程终止规则。 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。 可传递性 更多介绍可以移步并发编程交流群:628134587 20,Java中interrupted 和isInterruptedd方法的区别? interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。 非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态都有可能被其它线程调用中断来改变。 21,Java中的同步集合与并发集合有什么区别? 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。 不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。 同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。 比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。 同样的,CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。 如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。 22,什么是线程池? 为什么要使用它? 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池) 线程池的作用,就是在调用线程的时候初始化一定数量的线程,有线程过来的时候,先检测初始化的线程还有空的没有,没有就再看当前运行中的线程数是不是已经达到了最大数,如果没有,就新分配一个线程去处理。 就像餐馆中吃饭一样,从里面叫一个服务员出来;但如果已经达到了最大数,就相当于服务员已经用尽了,那没得办法,另外的线程就只有等了,直到有新的“服务员”为止。 线程池的优点就是可以管理线程,有一个高度中枢,这样程序才不会乱,保证系统不会因为大量的并发而因为资源不足挂掉。 23,Java中活锁和死锁有什么区别? 活锁:一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。 死锁:两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候,死锁会让你的程序挂起无法完成任务。 24,如何避免死锁? 死锁的发生必须满足以下四个条件: 互斥条件:一个资源每次只能被一个进程使用。 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 三种用于避免死锁的技术: 加锁顺序(线程按照一定的顺序加锁) 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁) 死锁检测 (死锁原因及如何避免更深理解移步:628134587) 25,notify()和notifyAll()有什么区别? 1,notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。 2,void notify(): 唤醒一个正在等待该对象的线程。 3,void notifyAll(): 唤醒所有正在等待该对象的线程。 两者的最大区别在于: notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。 notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。 26,什么是可重入锁(ReentrantLock)? Java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为Java 类,而不是作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上。) Reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续)synchronized块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,才释放锁。 27,读写锁可以用于什么应用场景? 读写锁可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作 ReadWriteLock对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。 ReadWriteLock 对程序性能的提高主要受制于如下几个因素: 1,数据被读取的频率与被修改的频率相比较的结果。 2,读取和写入的时间 3,有多少线程竞争 4,是否在多处理机器上运行

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

前端高频面试题 JavaScript篇

以下问题都来自于互联网前端面经分享,回答为笔者通过查阅资料加上自身理解总结,不保证解答的准确性,有兴趣讨论的同学可以留言或者私信讨论。 1.JS的异步机制? 2.闭包如何实现? 3.原型链、继承? 4.实现订阅者发布者模式? 5.数组的方法有哪些? 1.JS的异步机制? JS使用一个任务队列记录异步任务的回调函数,当异步任务(或者事件被激发,如鼠标点击)完成后,其回调函数会被添加到该任务队列的末尾,JS主线程在将所有的同步任务执行完毕后,会无限循环地去检查任务队列,如果任务队列不为空,则主线程回去执行任务队列中的任务。 关于异步机制的详细解答,可以参考:Javascript异步机制 2.闭包如何实现? 在计算机科学中,闭包是引用了自由变量的函数。在Javascript中,在一个函数中定义一个内部函数,并且内部函数引用了外部函数作用域的变量,然后将这个内部函数作为外部函数的返回值,这样就构成了一个闭包。如下代码: function wrapper() { var milk = '特仑苏' function drink() { console.log('我喝了' + milk) } return drink } var result = wrapper() result() //我喝了特仑苏 关于闭包的详细解答,可以参考:Javascript闭包 3.原型链、继承? 在Javascript中,每当我们定义一个函数,Javascript引擎就会自动为这个函数对象添加一个prototype属性(也被称作原型),每当我们使用这个函数new创建一个对象时,Javascript引擎就会自动为这个对象中添加一个__ proto __ 属性,并让其指向其类的prototype。当我们访问一个对象的属性时,首先回去寻找该对象本身的属性,如果找不到的话回去寻找该对象的 __ proto __ 作用域下的属性,一直到寻找到该属性或者没有__ proto __为止。这种寻找实例属性的方式我们称作原型链。 当我们让子类的prototype指向父类的实例时,便实现了原型链继承。 // 原型链实现继承的关键代码 Son.prototype = new Father() 当然Javascript的继承方式还有 构造函数继承 、 组合继承 、 寄生继承,更详尽的解答,可以参考:对Javascript 类、原型链、继承的理解 4.实现订阅/发布者模式? var publisher = {}; // 定义发布者 publisher.list = []; // 缓存列表 存放订阅者回调函数 // 增加订阅者 publisher.listen = function(fn) { publisher.list.push(fn); // 订阅消息添加到缓存列表 } // 发布消息 publisher.trigger = function(){ for(var i = 0,fn; fn = this.list[i++];) { var that = this fn.apply(null, arguments); } } 详细答案可以参考:Javascript中理解发布--订阅模式 5.数组的方法有哪些? 这个题属于开放题,答案就比较多了,下面我列举一下比较常用的数组方法:遍历方法: 包括 map、foreach、filter let arr = [{ name:"西瓜", type:"水果" },{ name:"芒果", type:"水果" },{ name:"小龙虾", type:"夜宵" }] arr.forEach((item, index) => { console.log(item.name) }) //分别打印 西瓜 芒果 小龙虾 arr.map((item, index) => { return item.name }) //返回数组["西瓜", "芒果", "小龙虾"] arr.filter((item, index) => { if (item.type == "水果") return true; else return false; }) //返回数组[{ name: 西瓜, type : 水果 }, { name: 芒果, type : 水果 }] forEach:用于遍历数组,无返回值; map:遍历数组之后,对每一项返回一个值,并将这些返回值都推入一个数组,最后返回这个新的数组; filter:对数据进行过滤,回调函数返回值为false的项将被过滤掉,最后返回过滤后的数组。操作方法: 包括 concat、push、pop、unshift、shift、splice let listA = ["西瓜", "芒果"] let listB = ["小龙虾"] let listC = listA.concat(listB) // 返回值:[西瓜, 芒果, 小龙虾] listC.push("鸡腿") // 返回值:4 // 数组值:[西瓜, 芒果, 小龙虾, 鸡腿] listC.pop() // 返回值:"鸡腿" // 数组值:[西瓜, 芒果, 小龙虾] listC.unshift("鸡腿") // 返回值:4 // 数组值:[鸡腿, 西瓜, 芒果, 小龙虾] listC.shift() // 返回值:"鸡腿" // 数组值:[西瓜, 芒果, 小龙虾] listC.splice(1, 1, "冰激凌", "奶茶") // 返回值:[芒果] // 数组值:[西瓜, 冰激凌, 奶茶, 小龙虾] concat:拼接数组,将参数数组拼接到调用数组末尾,并返回这个新数组;值得一提的是,这个方法并不会改变调用数组和参数数组,即上例中的listA、listB); push:在数组尾部插入新的元素,返回插入元素之后的数组长度; pop:从数组尾部删除元素,返回删除的元素; unshift:在数组头部插入新的元素,返回插入元素之后的数组长度; shift:从数组头部删除元素,返回删除的元素; splice:删除元素并插入元素,第一个参数为操作位置X,第二个参数为需要从操作位置X删除元素数量,后面的参数为需要从操作位置X插入的元素,返回删除的元素组成的数组。

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

java并发面试常识之copyonwrite

今天在网上看到一个问题,问除了加锁之外有没有其他方法来保证线程安全。楼下很多回答copyonwrite机制。这个问题回答有很多,但是copyonwrite的回答有点误导人。 copyonwrite机制 和单词描述的一样,他的实现就是写时复制, 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。 java中的copyonwrite java中提供了两个利用这个机制实现的线程安全集合。copyonwritearraylist,copyonwritearrayset。看名字就大概猜到他们之间的关系,copyonwritearrayset的底层实现是copyonwritearraylist。我们接下来看看java的实现。 public E get(int index) { return get(getArray(), index); } get的方法就是普通集合的get没有什么特殊的地方,但是成员变量的声明还是有讲究的,是个用volatile声明的数组,这样就保证了读取的那一刻读取的是最新的数据。 private transient volatile Object[] array; 接下来重点就是add方法了。下面的代码可以明显看出是明显需要reentrantlock加锁的,接下来就是复制数据和添加数据的过程,在setArray的过程中,把新的数组赋值给成员变量array(这里是引用的指向,java保证赋值的过程是一个原子操作)。 public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } finally { lock.unlock(); } } 关于迭代,他采取的是获取传递给迭代器的数组值进行迭代,中间就算加入新的值也迭代不到。在构造函数中就直接赋值给final的成员变量。 private final Object[] snapshot; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } 适用场景 copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据也是不及时的,数据量少还好说,数据太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)。

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

java并发面试常识之LinkedBlockingQueue

谈到ArrayBlockingQueue的特色就是循环队列,然后一把锁,2个条件,完成了功能。本来以为LinkedBlockingQueue也是这样的,结果和预期不一样,LinkedBlockingQueue利用了链表的特点,使用了两把锁,两个条件来控制。是一个锁分离的应用,下面就说说,他的实现,以及为什么ArrayBlockingQueue就不适合锁分离。 主要成员变量 private final ReentrantLock takeLock = new ReentrantLock(); private final Condition notEmpty = takeLock.newCondition(); private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition(); private final AtomicInteger count = new AtomicInteger(); 除了两个锁,两个条件外,我这里专门列举了计数器。这个计数器很重要,重要到锁分离要依赖他才能正常运行。 锁分离 使用双锁分离就得注意一点,那就是防止线程夯死。生产线程要唤醒生产线程,消费线程也要唤醒生产线程,消费线程唤醒消费线程,消费线程也要唤醒生产线程。 public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); //唤醒标记 int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { while (count.get() == capacity) { //阻塞生产线程 notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) //唤醒生产线程 notFull.signal(); } finally { putLock.unlock(); } //唤醒消费线程 if (c == 0) signalNotEmpty(); } 生产线程唤醒消费线程 基于上面的介绍,我们来看代码,唤醒标记就是为了生产唤醒消费的,因为可能出现消费线程全部都已经等待了,此时生产线程运作,但是消费线程并不能自己唤醒自己,于是就有了signalNotEmpty()的操作。这里的c是getAndIncrement的值,就是获取计数之前的值。c==0的满足条件就有1个元素,在这种情况下才去唤醒消费线程。 生产线程唤醒生产线程 在获取锁后,如果发现容量达到上限,就阻塞了,等待被唤醒,如果可以加入,就执行enqueue方法,是个很简单的链表添加节点的方法。就是在原来last节点后加节点,然后更新last节点。 private void enqueue(Node<E> node) { last = last.next = node; } 在计数器自增后,判断唤醒标记,如果还能继续生产,就去唤醒生产线程。 消费的方案思想和生产类似,这里就不说代码了。 删除 public boolean remove(Object o) { if (o == null) return false; fullyLock(); try { for (Node<E> trail = head, p = trail.next; p != null; trail = p, p = p.next) { if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyUnlock(); } } 删除代码相对比较简单,主要是要获取两把锁,才能进行删除操作就是fullyLock()和fullyUnlock(),删除掉元素后,还要唤醒生产线程。 void unlink(Node<E> p, Node<E> trail) { p.item = null; trail.next = p.next; if (last == p) last = trail; if (count.getAndDecrement() == capacity) notFull.signal(); } ArrayBlockingQueue为何不适合锁分离 这个主要是循环队列的原因,主要是数组和链表不同,链表队列的添加和头部的删除,都是只和一个节点相关,添加只往后加就可以,删除只从头部去掉就好。为了防止head和tail相互影响出现问题,这里就需要原子性的计数器,头部要移除,首先得看计数器是否大于0,每个添加操作,都是先加入队列,然后计数器加1,这样保证了,队列在移除的时候,长度是大于等于计数器的,通过原子性的计数器,双锁才能互不干扰。数组的一个问题就是位置的选择没有办法原子化,因为位置会循环,走到最后一个位置后就返回到第一个位置,这样的操作无法原子化,所以只能是加锁来解决。 适用场景 LinkedBlockingQueue的优点是锁分离,那就很适合生产和消费频率差不多的场景,这样生产和消费互不干涉的执行,能达到不错的效率,尽量不使用remove操作,获取两把锁的效率更低,可以使用size方法(就是计数器直接返回),这个还是比较重要的,有些集合不适合使用size,例如ConcurrentLinkedQueue,正确应该使用isEmpty()。

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

java并发面试常识之ArrayBlockingQueue

ArrayBlockingQueue是常用的线程集合,在线程池中也常常被当做任务队列来使用。使用频率特别高。他是维护的是一个循环队列(基于数组实现),循环结构在数据结构中比较常见,但是在源码实现中还是比较少见的。 线程安全的实现 线程安全队列,基本是离不开锁的。ArrayBlockingQueue使用的是ReentrantLock,配合两种Condition,实现了集合的线程安全操作。这里稍微说一个好习惯,下面是成员变量的声明。 private static final long serialVersionUID = -817911632652898426L; final Object[] items; int takeIndex; int putIndex; int count; final ReentrantLock lock; private final Condition notEmpty; private final Condition notFull; transient Itrs itrs = null; 赋值的操作基本都是在构造函数里做的。这样有个好处,代码执行可控。成员变量的初始化也是会合并在构造方法里执行的,但是在执行顺序上需要好好斟酌,如果写在构造方法里初始化,则没有相关问题。 阻塞队列的常用场所就是生产者消费者。一般都是生产者放入,消费者从头取数据。下面重点说这两个操作。 这两个操作都是依靠锁来保证线程安全的。 生产操作 public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } } put等放入操作,首先是获取锁,如果发现数据满了,就通过notFull的condition,来阻塞线程。这里的条件判定一定是用while而不是if,多线程情况下,可以被唤醒后发现又满了。 private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); } 这个是入队列的操作。首先获取维护的数组。putindex就是放入操作的标志。这个操作会一直加。达到预定的长度后就变成0从头开始计数。这样插入的操作就是一个循环的操作了,count就是用来做计数的,作为能否插入数据的一个标准,插入数据后就通过notEmpty的condition发出一个信号唤醒消费线程。 消费操作 public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } } 消费的方法也是这样。先获取锁,然后进行条件判断,如果没有数据,则阻塞线程。注意点和put一样。 private E dequeue() { final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); notFull.signal(); return x; } 取数据的时候,也依靠takeIndex,这是一个标志,这个数值也会一直增加,表示取的第一个数据的位置。如果这个标志走到最后,然后变成0,从头再来。这样保证取出的数据都是fifo的顺序。删除的时候如果发现迭代中,则会修改迭代器的遍历。然后通过notFull的condition来唤醒生产线程。 移除操作 public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { if (count > 0) { final int putIndex = this.putIndex; int i = takeIndex; do { if (o.equals(items[i])) { removeAt(i); return true; } if (++i == items.length) i = 0; } while (i != putIndex); } return false; } finally { lock.unlock(); } } 对于remove操作就比较麻烦了,首先获取锁之后,把两个标志位本地化,然后找到要删除的元素的位置。调用removeAt,这里删除需要对标志位做改变。 void removeAt(final int removeIndex) { final Object[] items = this.items; if (removeIndex == takeIndex) { items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); } else { final int putIndex = this.putIndex; for (int i = removeIndex;;) { int next = i + 1; if (next == items.length) next = 0; if (next != putIndex) { items[i] = items[next]; i = next; } else { items[i] = null; this.putIndex = i; break; } } count--; if (itrs != null) itrs.removedAt(removeIndex); } notFull.signal(); } 如果删除的元素是位置和takeindex一样。那就可以直接删除,然后让删除标志位向后移动。如果不是,则从删除的位置开始,进行后面向前面的数据覆盖的操作。直到遇到putindex的前一个位置。然后把那个位置的数据设置为null。并且把putindex的位置往前移动一格,正在迭代的时候要删除数据并且唤醒生产线程。

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

面试问题总结 2018/05/16

Redis与Memcache与什么区别? 只说特点,不说谁好谁坏: Redis是单线程的,支持多种数据类型,定制订阅/发布模式,支持Lua脚本,事务,包括持久化存储。 Memcache也是内存存储,不过memcache还可用于缓存其他东西,例如图片、视频等等,Memcache无法持久化。 个人觉得Redis更加强大,效率上两者差别不大,Memcache能做的东西,Redis基本上也能做,所以就选Redis吧。如果需要持久化存储的选用Redis。 MongoDB有什么优缺点? 特点:MongoDB在海量数据下性能优越,文档数据存储结构,可以快速的实现故障转移。 缺点:不支持事务,占用空间大。 如何做MySQL数据库的优化? 硬件优化,提升硬件 MySQL服务器优化,优化my.ini文件,增加调整buffer,cache 表结构优化,恰当的索引 SQL语句优化,专业的人员来做 MySQL的Select语句实现纵表转横表。 参考:https://blog.csdn.net/fysuccess/article/details/40789869 分布式中的生产中与消费者,说说他们的关系。项目中那些是生产者,那些是消费者。 接口与抽象类的区别,什么时候用接口,什么时候用抽象类 参考:https://www.jianshu.com/p/fa651f8d3180 这些技术你再实际生产环境中是如何应用的 惭愧,很多知识没只懂概念,没实践过,以后要学有所用。 说说JDK8的新特性,JDK10呢 JDK8: Lambda 表达式,直接方法引用, 接口默认方法 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。 Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。 Date Time API − 加强对日期与时间的处理。 Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。 Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。 JDK10新特性:https://www.oschina.net/translate/109-new-features-in-jdk-10 常见的设计模式,你经常用那些设计模式 Spring Bean的生命周期 参考:https://blog.csdn.net/lisongjia123/article/details/52091013 AOP了解多少,在项目中是如何使用的 服务器硬件配置如何,数据存储量有多少 你们现有项目中的瓶颈在哪里,能容纳多大的并发量 惭愧,上面的问题表面我的技术在实际应用中并没怎么发挥。 在使用Redis,MongoDB,等遇到过哪些问题,然后是如何解决的。 项目的用户量有多少,并发量多少,数据量有多少 接下来有什么规划 都用过哪些数据库 大数据的场景下,谈谈以空间换取时间的做法。 Spring的事务 JVM性能优化上,常用的JVM参数。关于新生代,老年代的。 先记录这么多吧,水平还是很有限的。主要问题在于我线上实操经验太少。

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

Java 高级面试知识点汇总!

1、常用设计模式 单例模式:懒汉式、饿汉式、双重校验锁、静态加载,内部类加载、枚举类加载。保证一个类仅有一个实例,并提供一个访问它的全局访问点。 代理模式:动态代理和静态代理,什么时候使用动态代理。 适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 装饰者模式:动态给类加功能。 观察者模式:有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 命令模式:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 创建者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。 抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 2、基础知识 Java基本类型哪些,所占字节和范围 Set、List、Map的区别和联系 什么时候使用Hashmap 什么时候使用Linkedhashmap、Concurrenthashmap、Weakhashmap 哪些集合类是线程安全的 为什么Set、List、map不实现Cloneable和Serializable接口 Concurrenthashmap的实现,1.7和1.8的实现 Arrays.sort的实现 什么时候使用CopyOnArrayList volatile的使用 synchronied的使用 reentrantlock的实现和Synchronied的区别 CAS的实现原理以及问题 AQS的实现原理 接口和抽象类的区别,什么时候使用 类加载机制的步骤,每一步做了什么,static和final修改的成员变量的加载时机 双亲委派模型 反射机制:反射动态擦除泛型、反射动态调用方法等 动态绑定:父类引用指向子类对象 JVM内存管理机制:有哪些区域,每个区域做了什么 JVM垃圾回收机制:垃圾回收算法 垃圾回收器 垃圾回收策略 jvm参数的设置和jvm调优 什么情况产生年轻代内存溢出、什么情况产生年老代内存溢出 内部类:静态内部类和匿名内部类的使用和区别 Redis和memcached:什么时候选择redis,什么时候选择memcached,内存模型和存储策略是什么样的 MySQL的基本操作 主从数据库一致性维护 mysql的优化策略有哪些 mysql索引的实现 B+树的实现原理 什么情况索引不会命中,会造成全表扫描 java中bio nio aio的区别和联系 为什么bio是阻塞的 nio是非阻塞的 nio是模型是什么样的 Java io的整体架构和使用的设计模式 Reactor模型和Proactor模型 http请求报文结构和内容 http三次握手和四次挥手 rpc相关:如何设计一个rpc框架,从io模型 传输协议 序列化方式综合考虑 Linux命令 统计,排序,前几问题等 StringBuff 和StringBuilder的实现,底层实现是通过byte数据,外加数组的拷贝来实现的 cas操作的使用 内存缓存和数据库的一致性同步实现 微服务的优缺点 线程池的参数问题 ip问题 如何判断ip是否在多个ip段中 判断数组两个中任意两个数之和是否为给定的值 乐观锁和悲观锁的实现 synchronized实现原理 你在项目中遇到的困难和怎么解决的 你在项目中完成的比较出色的亮点 消息队列广播模式和发布/订阅模式的区别 生产者消费者代码实现 死锁代码实现 线程池:参数,每个参数的作用,几种不同线程池的比较,阻塞队列的使用,拒绝策略 Future和ListenableFuture 异步回调相关 算法相关:判断能否从数组中找出两个数字和为给定值,随机生成1~10000不重复并放入数组,求数组的子数组的最大和,二分查找算法的实现及其时间复杂计算 3、其它 算法:常用排序算法,二分查找,链表相关,数组相关,字符串相关,树相关等 常见序列化协议及其优缺点 memcached内存原理,为什么是基于块的存储 搭建一个rpc需要准备什么 如果线上服务器频繁地出现full gc ,如何去排查 如果某一时刻线上机器突然量变得很大,服务扛不住了,怎么解决 LUR算法的实现 LinkedHashMap实现LRU 定义栈的数据结构,请在该类型中实现一个能够找到栈最小元素的min函数 海量数据处理的解决思路 reactor模型的演变 阻塞、非阻塞、同步、异步区别 Collection的子接口 jvm调优相关 zookeeper相关,节点类型,如何实现服务发现和服务注册 nginx负载均衡相关,让你去实现负载均衡,该怎么实现 linux命令,awk、cat、sort、cut、grep、uniq、wc、top等 压力测试相关,怎么分析,单接口压测和多情况下的压测 你觉得你的有点是什么,你的缺点是什么 spring mvc的实现原理 netty底层实现,IO模型,ChannelPipeline的实现和原理 缓存的设计和优化 缓存和数据库一致性同步解决方案 你所在项目的系统架构,谈谈整体实现 消息队列的使用场景 ActiveMQ、RabbitMQ、Kafka的区别 扫描关注我们的微信公众号,干货每天更新。 image

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

蛇皮的Python面试题目

1.小易有一些彩色的砖块。每种颜色由一个大写字母表示。各个颜色砖块看起来都完全一样。现在有一个给定的字符串s,s中每个字符代表小易的某个砖块的颜色。小易想把他所有的砖块排成一行。如果最多存在一对不同颜色的相邻砖块,那么这行砖块就很漂亮的。请你帮助小易计算有多少种方式将他所有砖块排成漂亮的一行。(如果两种方式所对应的砖块颜色序列是相同的,那么认为这两种方式是一样的。) 例如: s = “ABAB”,那么小易有六种排列的结果: “AABB”,”ABAB”,”ABBA”,”BAAB”,”BABA”,”BBAA” 其中只有”AABB”和”BBAA”满足最多只有一对不同颜色的相邻砖块。 . 输入描述: 1.输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s中的每一个字符都为一个大写字母(A到Z)。 输出描述: 2.输出一个整数,表示小易可以有多少种方式。 输入例子:ABAB 输出例子:2 代码: a = raw_input("") new = [] for i in range(len(a)): new.append(a[i]) f len(set(new)) == 2: #set(new) 返回有序不重复 eg:输入ABAB 那么set(new) = ['A','B'] print 2 elif len(set(new)) == 1:#当输入的字符串是 一个 字符时 那么就只有一种方法了 print 1 else: print 0 #当有ABC三种颜色的地板时 那么 无论如何都超过 相邻的不同颜色对数 超过 1 因此排序方式为0 print '这怎一个狗字了的,蛇皮,皮这一下网易你就很快乐吗?'

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

java面试-数据库事务详解

什么是『事务』? 事务就是一组具有原子性的操作,这一组操作要么全都正确执行,要么全都不执行。事务能保证数据库从一种一致性状态转换为另一种一致性状态。 事务的四大特性ACID 1. 原子性 原子性指的是事务是一个不可分割的操作,要么全都正确执行,要么全都不执行。 2. 一致性 事务开始前和事务结束后,数据库的完整性约束没有被破坏。 3. 隔离性 事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另一个正在运行过程中的事务的数据。 4. 持久性 事务结束后,事务的结果必须是永久保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。PS:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。 事务的分类 1. 扁平事务 它是实际生产环境中最常用、最简单的事务类型。 事务从BEGIN WORK开始,从COMMIT WORK或ROLLBACK WORK结束。 缺点:发生错误时回滚到事务的起始位置,无法回滚部分操作。而回滚所有的操作开销太大。 2. 带有保存点的扁平事务 这种事务能设置多个保存点,当发生错误时可以回滚到事务中指定的保存点,而不需要将整个事务回滚。 3. 链事务 4. 嵌套事务 5. 分布式事务 数据库并发访问会出现的问题 1. 更新丢失 当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另一个事务的更新覆盖掉。当数据库没有加任何锁操作的情况下会发生。 2. 脏读 一个事务读到另一个尚未提交的事务中的数据。该数据可能会被回滚从而失效。如果第一个事务拿着失效的数据去处理那就发生错误了。 3. 不可重复读 不可重复度的含义:一个事务对同一行数据读了两次,却得到了不同的结果。它具体分为如下两种情况:1. 虚读:在事务1两次读取同一记录的过程中,事务2对该记录进行了修改,从而事务1第二次读到了不一样的记录。2. 幻读:事务1在两次查询的过程中,事务2对该表进行了插入、删除操作,从而事务1第二次查询的结果发生了变化。 与『脏读』的区别?脏读读到的是尚未提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程中数据被另一个事务改过了。 事务的隔离级别 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。 1. Read uncommitted 读未提交 在该级别下,一个事务对一行数据修改的过程中,不允许另一个事务对该行数据进行修改,但允许另一个事务对该行数据读。因此本级别下,不会出现更新丢失,但会出现脏读、不可重复读。 2. Read committed 读提交 在该级别下,未提交的写事务不允许其他事务访问该行,因此不会出现脏读;但是读取数据的事务允许其他事务的访问该行数据,因此会出现不可重复读的情况。 3. Repeatable read 重复读 在该级别下,读事务禁止写事务,但允许读事务,因此不会出现同一事务两次读到不同的数据的情况(不可重复读),且写事务禁止其他一切事务。 4. Serializable 序列化 该级别要求所有事务都必须串行执行,因此能避免一切因并发引起的问题,但效率很低。 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。 使用事务的注意事项 1. 不要在循环中提交事务 2. 不要使用自动提交 3. 不要使用自动回滚 4. 不要使用长事务

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

java面试-彻底搞懂红黑树

红黑树性质 1、每个结点或是红色的,或是黑色的2、根节点是黑色的3、每个叶结点(NIL)是黑色的4、如果一个节点是红色的,则它的两个儿子都是黑色的。5、对于每个结点,从该结点到其叶子结点构成的所有路径上的黑结点个数相同。 和AVL树的比较 AVL树是一棵严格的平衡树,它所有的子树都满足二叉平衡树的定义。因此AVL树高被严格控制在XXX,因此AVL树的查找比较高效。但AVL树插入、删除结点后旋转的次数比红黑树多。 红黑树用非严格的平衡来降低插入删除时旋转的次数。 因此,如果你的业务中查找远远多于插入、删除,那选AVL树;如果查找、插入、删除频率差不多,那么选择红黑树。 插入过程 默认插入的结点为红色。为何?因为红黑树中黑节点至少是红节点的两倍,因此插入节点的父节点为黑色的概率较大,而此时并不需要作任何调整,因此效率较高。 1. 父为黑 插入后无需任何操作。由于黑节点个数至少为红节点的两倍,因此父为黑的情况较多,而这种情况在插入后无需任何调整,这就是红黑树比AVL树插入效率高的原因! 2. 父为红 父为红的情况破坏了红黑树的性质,此时需要根据叔叔的颜色来做不同的处理。 叔叔为红此时很简单,只需交换爸爸、叔叔和爷爷的颜色即可。此时若爷爷节点和太爷爷节点颜色相同,再以爷爷节点为起始节点,进行刚才相同的操作,即:根据爷爷的兄弟颜色做相应的操作。 叔叔为黑此时较为复杂,分如下四种情况:a)爸爸在左、叔叔在右、我在左以爸爸为根节点,进行一次R旋转。b)爸爸在左、叔叔在右、我在右先以我为根节点,进行一次L旋转;再以我为根节点,进行一次R旋转。c)叔叔在左、爸爸在右、我在左先以我为根节点,进行一次R旋转;再以我为根节点,进行一次L旋转。d)叔叔在左、爸爸在右、我在右以爸爸为根节点,进行一次L旋转。 删除过程 二叉搜索树的删除 若删除二叉搜索树的节点A,实际上删除的是二叉搜索树中序遍历的前驱节点,注意:1. 这个被删除节点要么就是一个叶子节点,2. 要么有且仅有一个左孩子然后将孩子顶替它原来的位置,最后将被删的节点值覆盖待删除的那个节点A。 红黑树按照二叉搜索树的方式删除节点,之后再进行相应的旋转操作,使得删除后的树仍然是一棵红黑树。 定义 待删除节点:要删除的那个节点 实际删除节点:待删除节点的中序遍历前驱 红黑树实际删除节点的性质 实际删除节点要么是叶子节点,要么有且仅有一个左孩子; 若为叶子节点,必为红色; 若实际删除节点还有孩子,则该必为左孩子;a)若左孩子为红色,则实际删除节点必为黑色;b)若左孩子为黑色,则实际删除节点红黑均可以。 约定 蓝色箭头:表示判定点 在删除操作开始前,蓝色箭头首先指向实际删除节点。 『实际删除节点』在图中以『父』表示。 旋转过程开始: 1. 父为红色(待删节点为叶子) 直接删除父节点即可: 2. 父为黑 子为红(待删节点为黑、待删节点子节点为红+左孩子) 用子节点覆盖父节点,并保持父节点的颜色: 3. 父为黑 子为黑(待删节点和子节点均为黑) 3.1. 叔叔为红 PS:叔叔为红,则爷爷必为黑! 父在左 叔在右a)子节点覆盖父节点b)进行一次左旋 父在右 叔在左a)子节点覆盖父节点b)进行一次右旋 3.2. 叔叔为黑 PS:叔叔、爸爸都为黑,那爷爷颜色就不确定了! 祖父红 两个侄子黑以下两种情况操作一致:1.子覆盖父(删除)2.交换祖父和叔叔的颜色。 a)父在左 叔在右b)父在右 叔在左同上。 祖父黑 两个侄子黑以下两种情况操作一致:1. 祖父染成子节点的颜色;2. 子节点染成黑色;3. 叔叔染成红色a)父在左 叔在右b)父在右 叔在左 祖父颜色随意 至少有一个红侄a)红侄为左左(叔左、红侄左)1. 红侄进行一次右旋2. 红侄染成黑色3. 交换叔叔和祖父的颜色b)红侄为左右(叔左、红侄右)1. 红侄进行一次右旋+左旋2. 红侄染成父节点颜色;3. 父节点染成黑色c)红侄为右左(叔右、红侄左)1. 红侄进行一次右旋+左旋2. 红侄染成父节点颜色;3. 父节点染成黑色;d)红侄为右右(叔右、红侄右)1. 红侄进行一次左旋2. 叔叔染成父节点颜色;3. 红侄染成黑色;

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

java面试-Java并发编程(三)——volatile

1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行;但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 与 同步。 通信通信是指消息在两条线程之间传递。既然要传递消息,那接收线程 和 发送线程之间必须要有个先后关系,此时就需要用到同步。通信和同步是相辅相成的。 同步同步是指,控制多条线程之间的执行次序。 2. 通信的方式 2.1 通信方式的种类 线程之间的通信一共有两种方式:共享内存 和 消息传递。 共享内存共享内存指的是多条线程共享同一片内存,发送者将消息写入内存,接收者从内存中读取消息,从而实现了消息的传递。但这种方式有个弊端,即需要程序员来控制线程的同步,即线程的执行次序。 这种方式并没有真正地实现消息传递,只是从结果上来看就像是将消息从一条线程传递到了另一条线程。 消息传递顾名思义,消息传递指的是发送线程直接将消息传递给接收线程。由于执行次序由并发机制完成,因此不需要程序员添加额外的同步机制,但需要声明消息发送和接收的代码。 综上所述:对于共享内存的通信方式,需要进行显示的同步,隐式的通信;而对于消息传递的通信方式,需要隐式的同步,显示的通信。 2.2 Java使用的通信方式 Java使用共享内存的方式实现多线程之间的消息传递。因此,程序员需要写额外的代码用于线程之间的同步。 PS:其实共享内存的方式从实现过程来看,跟消息传递一点关系都没有:一条线程将消息存入共享内存,另一条线程从共享内存中读这条消息。但从结果来看,整个过程就好像是一条消息被从线程A传递到了线程B。这种方式之所以能实现消息传递,依托于两点: 必须有一片共享的内存 必须要实现多线程的同步 3. Java多线程的内存模型(简化版) 所有线程都共享一片内存,用于存储共享变量;此外,每条线程都有各自的存储空间,存储各自的局部变量、方法参数、异常对象。 4. volatile是什么? Java采用共享内存的方式实现消息传递,而共享内存需要依托于同步。Java提供了synchronized、volatile关键字实现同步。此外volatile关键字还拥有一些额外的功能。 5. volatile的使用 在成员变量前加上该关键字即可。 public volatile boolean flag; 1 6. volatile的特性 6.1 重排序 重排序是计算机为了提高程序执行效率而对代码的执行顺序进行调整。你以为代码是一行行顺序执行的,但实际并非如此,重排序详解请移步至:Java并发编程的艺术(二)——重排序 若两行指令之间没有依赖关系,那么计算机可以对他们的顺序进行重排序,但若两行之间的某个变量被volatile修饰后,重排序规则会发生变化。 在以下情况下,即使两行代码之间没有依赖关系,也不会发生重排序: volatile读 若volatile读操作的前一行为volatile读/写,则这两行不会发生重排序 volatile读操作和它后一行代码都不会发生重排序 volatile写 volatile写操作和它前一行代码都不会发生重排序; 若volatile写操作的后一行代码为volatile读/写,则这两行不会发生重排序。 6.2 可见性 什么是内存可见性? “内存可见性”指的是一条线程修改完一个共享变量后,另一个线程若访问这个变量将会访问到修改后的值。即:一条线程对共享变量的修改,对其他线程立即可见。 但如果未对共享变量采用同步机制,那么共享变量的修改不会对其他线程立即可见。 为什么会出现内存不可见的情况? 通过上文可知,在Java中每条线程都有各自独立的存储空间,此外还有一个所有线程共享的内存空间。当开启线程时,系统会将共享内存中的所有共享变量拷贝一份到线程专属的存储空间中。接下来该线程在结束前的所有操作都是基于自己的存储空间进行的。因此,若一条线程改变了一个共享变量,仅仅改变的是这条线程专属存储空间中的变量值;此时若其他线程访问这个变量,访问的仍然是先前从共享存储空间读出来的值。然而我们希望一条线程将某个共享变量修改后,其他线程能立即访问到这个最新的值,而不是失效值。这时就需要同步机制来解决这个问题。 如何确保共享变量的可见性? 要确保所有共享变量对所有线程是可见的,就需要给所有共享变量使用同步。在Java中你可以选择将共享变量用同步代码块包裹或用volatile修饰共享变量。 为什么volatile能保证共享变量的内存可见性? volatile修饰了一个成员变量后,这个变量的读写就会比普通变量多一些步骤。 volatile变量写当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。 volatile变量读当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。 通过对volatile变量读写的限制,就能保证线程每次读到的都是最新的值,从而确保了该变量的内存可见性。 volatile变量赠送的附加功能 进行volatile写操作时,不仅会将volatile变量写入共享内存,系统还会将当前线程专属空间中的所有共享变量写入共享内存。进行volatile读操作时,系统也会一次性将共享内存中所有共享变量读入线程专属空间。这就意味着,如果普通变量在volatile写操作之前被修改,那么在volatile读操作之后就能正确读到他们。但是,在volatile写操作之后被修改的普通变量 和 在volatile读操作之前被访问的普通变量 都不具有内存可见性。 6.3 原子性 什么是原子性? 原子性指的是一组操作必须一起完成,中途不能被中断。 volatile能确保long、double读写的原子性 在Java中的所有类型中,有long、double类型比较特殊,他们占据8字节(64比特),其余类型都小于64比特。在32位操作系统中,CPU一次只能读取/写入32位的数据,因此对于64位的long、double变量的读写会进行两步。在多线程中,若一条线程只写入了long型变量的前32位,紧接着另一条线程读取了这个只有“一半”的变量,从而就读到了一个错误的数据。为了避免这种情况,需要在用volatile修饰long、double型变量。 在内存可见性与原子性上,volatile就相当于是同步的setter和getter函数。但并不具有volatile的重排序规则,同步块只确保同步块内部的指令不发生重排序,并不确保同步块以外的指令的重排序。 PS1:Java中的byte竟然是字节,bit才是比特(位)。PS2:char和short-2字节、int和float-4字节、long和double-8字节、byte-1字节 QA:在同步块中调用wait函数是否会破坏原子性?

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

java面试-Java并发编程(五)——中断

什么是中断? 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断。 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断; 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。 中断的相关方法 public void interrupt()将调用者线程的中断状态设为true。 public boolean isInterrupted()判断调用者线程的中断状态。 public static boolean interrupted只能通过Thread.interrupted()调用。它会做两步操作: 返回当前线程的中断状态; 将当前线程的中断状态设为false; 暂停、继续、停止线程(已过时) 以下三个方法都是通过线程对象去调用。 suspend()暂停调用者线程,只释放CPU执行权,不释放锁。由于在不释放资源的情况下进入睡眠状态,容易产生死锁。因此已过时! resume()恢复调用者线程,让他处于就绪状态。 stop()调用stop后,并不会保证资源被正确地释放,它会使程序处于不正确的状态下。 PS:stop和interrupt的区别? 中断的使用 要使用中断,首先需要在可能会发生中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理代码。当需要中断线程时,调用该线程对象的interrupt函数即可。 设置中断监听 Thread t1 = new Thread( new Runnable(){ public void run(){ // 若未发生中断,就正常执行任务 while(!Thread.currentThread.isInterrupted()){ // 正常任务代码…… } // 中断的处理代码…… doSomething(); } } ).start(); 1 2 3 4 5 6 7 8 9 10 11 正常的任务代码被封装在while循环中,每次执行完一遍任务代码就检查一下中断状态;一旦发生中断,则跳过while循环,直接执行后面的中断处理代码。 触发中断 t1.interrupt(); 1 上述代码执行后会将t1对象的中断状态设为true,此时t1线程的正常任务代码执行完成后,进入下一次while循环前Thread.currentThread.isInterrupted()的结果为true,此时退出循环,执行循环后面的中断处理代码。 安全地停止线程 stop函数停止线程过于暴力,它会立即停止线程,不给任何资源释放的余地,下面介绍两种安全停止线程的方法。 循环标记变量 自定义一个共享的boolean类型变量,表示当前线程是否需要中断。 中断标识 volatile boolean interrupted = false; 1 任务执行函数 Thread t1 = new Thread( new Runnable(){ public void run(){ while(!interrupted){ // 正常任务代码…… } // 中断处理代码…… // 可以在这里进行资源的释放等操作…… } } ); 1 2 3 4 5 6 7 8 9 中断函数 Thread t2 = new Thread( new Runnable(){ public void run(){ interrupted = true; } } ); 1 2 3 4 5 循环中断状态 中断标识由线程对象提供,无需自己定义。 任务执行函数 Thread t1 = new Thread( new Runnable(){ public void run(){ while(!Thread.currentThread.isInterrupted()){ // 正常任务代码…… } // 中断处理代码…… // 可以在这里进行资源的释放等操作…… } } ); 1 2 3 4 5 6 7 8 9 中断函数 t1.interrupt(); 1 总结 上述两种方法本质一样,都是通过循环查看一个共享标记为来判断线程是否需要中断,他们的区别在于:第一种方法的标识位是我们自己设定的,而第二种方法的标识位是Java提供的。除此之外,他们的实现方法是一样的。 上述两种方法之所以较为安全,是因为一条线程发出终止信号后,接收线程并不会立即停止,而是将本次循环的任务执行完,再跳出循环停止线程。此外,程序员又可以在跳出循环后添加额外的代码进行收尾工作。 处理中断 上文都在介绍如何获取中断状态,那么当我们捕获到中断状态后,究竟如何处理呢? Java类库中提供的一些可能会发生阻塞的方法都会抛InterruptedException异常,如:BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep。 当你在某一条线程中调用这些方法时,这个方法可能会被阻塞很长时间,你可以在别的线程中调用当前线程对象的interrupt方法触发这些函数抛出InterruptedException异常。 当一个函数抛出InterruptedException异常时,表示这个方法阻塞的时间太久了,别人不想等它执行结束了。 当你的捕获到一个InterruptedException异常后,亦可以处理它,或者向上抛出。 抛出时要注意???:当你捕获到InterruptedException异常后,当前线程的中断状态已经被修改为false(表示线程未被中断);此时你若能够处理中断,则不用理会该值;但如果你继续向上抛InterruptedException异常,你需要再次调用interrupt方法,将当前线程的中断状态设为true。 注意:绝对不能“吞掉中断”!即捕获了InterruptedException而不作任何处理。这样违背了中断机制的规则,别人想让你线程中断,然而你自己不处理,也不将中断请求告诉调用者,调用者一直以为没有中断请求。 QA 为什么catch InterruptedException后会自动清除中断状态?

资源下载

更多资源
腾讯云软件源

腾讯云软件源

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

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应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册