面试之多线程与并发

一、Java中的同步容器类和缺陷java

在Java中,同步容器主要包括2类:编程

  1)Vector、HashTable。数组

  2)Collections类中提供的静态工厂方法建立的类。Collections.synchronizedXXX()。缓存

缺陷:安全

  1)性能问题。多线程

  在有多个线程进行访问时,若是多个线程都只是进行读取操做,那么每一个时刻就只能有一个县城进行读取,其余线程便只能等待,这些线程必须竞争同一把锁。并发

  2)ConcurrentModificationException异常。工具

  在对Vector等容器进行迭代修改时,可是在并发容器中(如ConcurrentHashMap,CopyOnWriteArrayList等)不会出现这个问题。性能

二、为何说ConcurrentHashMap是弱一致性的?以及为什么多个线程并发修改ConcurrentHashMap时不会报ConcurrentModificationException?优化

1)ConcurrentHashMap #get()

  正是由于GET操做几乎全部时候都是一个无锁操做(GET中有一个readValueUnderLock调用,不过这句执行到的概率极小),使得同一个Segment实例上的PUT和GET能够同时进行,这就是GET操做是弱一致的根本缘由。

2)ConcurrentHashMap #clear()

public void clear(){
    for(int i=0;i<segments.length;++i)
        segments[i].clear;
}  

  由于没有全局的锁,在消除完一个segment以后,正在清理下一个segment的时候,已经清理的segment可能又被加入了数据,所以clear返回的时候,ConcurrentHashMap中是可能存在数据的。所以,clear方法是弱一致的。

ConcurrentHashMap中的迭代器

  在遍历过程当中,若是已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。若是未遍历的数组上的内容发生了变化,则有可能反映到迭代过程当中。这就是ConcurrentHashMap迭代器弱一致的表现。

  在这种迭代方式中,当iterator被建立后,集合再发生改变就再也不是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可使用原来老的数据,而写线程也能够并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提高的关键。

  总结,ConcurrentHashMap的弱一致性主要是为了提高效率,是一致性与效率之间的一种权衡。要成为强一致性,就获得处使用锁,甚至是全局锁,这就与HashTable和同步的HashMap同样了。

三、CopyOnWriteArrayList的实现原理

  CopyOnWrite容器即写时复制的容器,也就是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器(改变引用的指向)。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此CopyOnWrite容器也是一种读写分离的思想,读和写在不一样的容器上进行,注意,写的时候须要加锁。

  1)一下代码是向CopyOnWriteArrayList中add方法的实现,能够发如今添加的时候是须要加锁的,不然多线程写的时候会Copy出N个副本。

public boolean add(E e){ final ReentrantLock lock = this.lock;//加的是lock锁
 lock.lock(); try{ Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements,len+1); newElements[len]=e; setArray(newElements);//将原容器的引用指向新的容器;
        return true; }finally{ lock.unlock(); } }

  在CopyOnWriteArrayList里处理写操做(包括add,remove,set等)是先将原始的数据经过Arrays.copyof()来生成一份新的数据,而后再新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上。而后读的时候就是在引用的当前对象上进行读(包括get,iterator等),不存在加锁和阻塞。

  CopyOnWriteArrayList中写操做须要大面积复制数组,因此性能确定不好,可是读操做由于操做的对象和写操做不是同一个对象,读之间也不须要加锁,读和写之间的同步处理只是在写完后经过一个简单的“=”将引用指向新的数组对象上来,这个几乎不须要时间,这样读操做就很快很安全,适合在多线程里使用。

  2)读的时候不须要加锁,若是读的时候有线程正在向CopyOnWriteArrayList添加数据,读仍是会读到旧的数据(在原容器中进行读)。

public E get(int index){ return get(getArray(),index); }

  CopyOnWriteArrayList在读上效率很高,因为,写的时候每次都要将源数组复制到一个新的数组中,因此写的效率不高。

CopyOnWriteArrayList容器有不少优势,可是同时也存在两个问题,即内存占用问题和数据一致性的问题。

  1)内存占用问题。由于CopyOnWrite的写时复制机制,因此在进行写操做的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。针对内存占用问题,能够

    a. 经过压缩容器中的元素的方法来减小大对象的内存消耗,好比,若是元素全是10进制的数字,能够考虑把它压缩成36进制或64进制。

    b. 不使用CopyOnWrite容器,而使用其余的并发容器,如ConcurrentHashMap。

  2)数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。因此若是你但愿写入的数据,立刻能读到,请不要使用CopyOnWrite容器!!

四、Java中堆和栈有什么不一样?

  栈是一块和线程紧密相关的内存区域。每一个线程都有本身的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其余线程是不可见的。而堆是全部线程共享的一片公用内存区域。对象都在堆里建立,为了提高效率,县城会从堆中弄一个缓存到本身的栈,若是多个线程使用该变量就可能引起问题,这时volatile变量就能够发挥做用了。它要求线程从主存中读取变量的值。

五、Java中的活锁、死锁、饥饿有什么区别?

  死锁:是指两个或两个以上的进程在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去,此时称系统处于死锁状态或系统产生了死锁。

  饥饿:考虑一台打印机分配的例子,当有多个进程须要打印文件时,系统按照短文件优先的策略排序,该策略具备平均等待时间短的优势,彷佛很是合理,但当短文件打印任务源源不断时,长文件的打印任务将被无限期地推迟,致使饥饿以致饿死。

  活锁:与饥饿相关的另一个概念称为活锁,在忙式等待条件下发生的饥饿,称为活锁。

  不进入等待状态的等待称为忙式等待。另外一种等待方式是阻塞式等待,进程得不到共享资源时将进入阻塞状态,让出CPU给其余进程使用。忙等待和阻塞式等待的相通之处在于进程都不具有继续向前推动的条件,不一样之处在于忙等待的进程不主动放弃CPU,尽管CPU可能被剥夺,于是是低效的;而处于阻塞状态的进程主动放弃CPU,于是是高效的。

  活锁的例子:若是事务T1封锁了数据R,事务T2又请求封锁R,因而T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。而后T4又请求封锁R,当T3释放了R上的封锁以后,系统有批准了T4的请求......T2可能永远等待(在整个过程当中,事务T2在不断的重复尝试获取锁R)。

  活锁的时候,进程是不会阻塞的,这会致使耗尽CPU资源,这是与死锁最明显的区别。

  活锁指的是任务或执行者没有被阻塞,因为某些条件没有知足,致使一直重复尝试,失败,尝试,失败。活锁和死锁的区别在于,处于活锁的实体是在不断地改变状态,所谓的“活”,而处于死锁的实体表现为等待;活锁有必定概率解开,而死锁是没法解开的。

  避免活锁的简单方法是采用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求封锁的前后次序对事务排队,数据对象上的锁一旦释放就批准申请队列中第一个事务得到锁。

六、实现线程之间的通讯?

当线程间是能够共享资源时,线程间通讯是协调它们的重要的手段。

1)Object 类中wait()、notify()、notifyAll()方法。

2)用Condition接口。

  Condition是被绑定到Lock上的,要建立一个Lock的Condition对象必须用newCondition()方法。在一个Lock对象里面能够建立多个Condition对象,线程能够注册在指定的Condition对象中,从而能够有选择性地进行线程通知,在线程调度上更加灵活。

  在Condition中,用await()替换wait(),用signal替换notify(),用signalAll()替换notifyAll(),传统线程的通讯方式,Condition均可以实现。调用Condition对象中的方法中,须要被包含在lock()和unlock()之间。

3)管道实现线程间的通讯

 实现方式:一个县城发送数据到输出管道流,另外一个线程从输入管道流中读取数据。

 基本流程:

  1> 建立管道输出流PipedOutputStream pos 和管道输入流 PipedInputStream pis。

  2> 将pos和pis匹配,pos.connect(pis)。

  3> 将pos赋给输入信息的线程,pis赋给获取信息的线程,就能够实现线程间的通信了。

 缺点:

  1> 管道流只能在两个线程之间传递数据。

  线程consumer1 和 consumer2同时从pis中read数据,当线程producer往管道流中写入一段数据(1,2,3,4,5,6)后,每个时刻只有一个线程能获取到数据,并非两个线程都能获取到producer发送来的数据,所以一个管道流只能用于两个线程间的通信。

  2> 管道流只能实现单向发送,若是要两个线程之间互通信,则须要两个管道流。

  线程producer经过管道流向线程consumer发送数据,若是线程consumer想给线程producer发送数据,则须要新建另外一个管道流pos1和pis1,将pos1赋给consumer1,将pis1赋给producer。

4)使用volatile 关键字。

  见之前内容。

七、如何确保线程安全?

  若是多个线程同时运行某段代码,若是每次运行结果和单线程运行的结果是同样的,并且其余变量的值也和预期的是同样的,就是线程安全的

  synchronized,Lock,原子类(如atomicInteger等),同步容器,并发容器,阻塞队列,同步辅助类(好比CountDownLatch,Semaphore,CyclicBarrier)。

八、多线程的优势和缺点?

优势:

  1)充分利用CPU,避免CPU空转。

  2)程序响应更快。

缺点:

  1)上下文切换的开销

  当CPU从执行一个线程切换到执行另一个线程时,它须要先存储当前线程的本地数据,程序指针等,而后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”。CPU会在一个上下文中执行一个线程,而后切换到另一个上下文中执行另一个线程。上下文切换并不廉价。若是没有必要,应该减小上下文切换的发生。

  2)增长资源消耗

  线程在运行时须要从计算机里面获得一些资源。除了CPU,线程还须要一些内存来维持它本地的堆栈。它也须要占用操做系统中一些资源来管理线程。

  3)编程更加复杂

  在多线程访问共享数据时,要考虑线程安全问题。

九、写出3条你遵循的多线程最佳实践

  1)给线程起个有意义的名字。

  2)避免锁定和缩小同步的范围

    相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

  3)多用同步辅助类,少用wait和notify。

    首先,CountDownLatch,Semaphore,CyclicBarrier这些同步辅助类简化了编码操做,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序能够不费吹灰之力得到优化。

  4)多用并发容器,少用同步容器。

    若是下一次你须要用到map,你应该首先想到用ConcurrentHashMap。

十、多线程的性能必定就优于单线程吗?

  不必定,要看具体的任务以及计算机的配置。好比说:

  对于单核CPU,若是是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,由于解压文件须要一直占用CPU资源,若是采用多线程,线程切换致使的开销反而会让性能降低。若是是交互类型的任务,确定是须要使用多线程的。

  对于多核CPU,对于解压文件来讲,多线程确定优于单线程,由于多个线程可以更加充分利用每一个核的资源。

十一、怎么检测一个线程是否拥有锁?

  在java.lang.Thread中有一个方法叫 holdsLock(Object obj),它返回true,若是当且仅当当前线程拥有某个具体对象的锁。

十二、什么是线程调度器?

  线程调度器是一个操做系统服务,它负责为Runnable状态的线程分配CPU时间。一旦咱们建立一个线程并启动它,它的执行便依赖于线程调度器的实现。

1三、Java程序如何中止一个线程?

  建议使用“异常法”来终止线程的继续运行。在想要被中断执行的线程中,调用interrupted()方法,该方法用来检验当前线程是否已经被中断,即该线程是否被打上了中断的标记,并不使得线程当即中止运行,若是返回true,则抛出异常,中止线程的运行。在线程外,调用interrupt()方法,使得该线程打上中断的标记。

相关文章
相关标签/搜索