java面试资料—多线程

(1)关于java线程池

预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已。如此反复。

java面试资料—多线程_第1张图片

流程:

a.一个任务提交,如果线程池大小没达到corePoolSize(最小线程数量),则每次都启动一个worker也就是一个线程来立即执行
b.如果来不及执行,则把多余的线程放到workQueue,等待已启动的worker来循环执行
c.如果队列workQueue都放满了还没有执行,则在
maximumPoolSize(最大线程数量)下面启动新的worker来循环执行workQueue
d.如果启动到maximumPoolSize还有任务进来,线程池已达到满负载,此时就执行任务返回RejectedExecutionHandler

使用场景:单个任务处理时间短,需要处理的任务数量巨大的情况(并且多个任务的规模相似,如果有的规模很大有的规模很小,可能会导致拥塞或者死锁)。减少了线程对象的创建、消亡开销;还可以有效控制并发的线程数,避免过多资源竞争。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

四种线程池:

   newCachedThreadPool:调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

   newFixedThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

   newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

   newSingleThreadExecutor:线程池中只有单个线程

线程池调优:必须分析计算环境、资源预算和任务的特性,正确的设置线程池的大小。

对于计算密集型的任务,在拥有NCPU个处理器的系统上,当线程池的大小为  NCPU +1 时,通常能够实现最优的利用率。对于包含I/O操作的或者其他阻塞操作的任务,通常应该设置的更大一点。

通常来说,使用Lock会比synchronized要高效许多,而且synchronized的开销看起来变化范围太大,而Lock相对来说比较一致。但是,Lock所需要的代码量较多且可读性不高,因此只有在性能调优的时候才使用Lock对象。

像vector和hashtable这种老式容器,中间包含很多synchronized方法,虽然线程安全,但是影响了性能,应该使用新版本Java中提供的免锁容器。

这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改时在容器数据结构的某个部分的一个单独副本(有时是完整的数据结构的副本)上执行的,并且这个副本在修改过程是不可视的。只有当修改完成后,被修改的结构才会自动的与主数据结构进行交换,之后读取者就可以看到这个修改了。

ConcurrenthashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,任何修改在完成之前,读取者仍然不能看到他们。ConcurrentHashMap不会抛出ConcurrentModificationException异常。

(2)并发容器:

JDK5新提供的,util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是"最新的、当前的"数据。

ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap把HashMap分成若干个Segmenet,加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能(其实就是降低了锁的粒度)。ConcurrentHashMap也增加了对常用复合操作的支持,比如"若没有则添加":putIfAbsent(),替换:replace()。这2个操作都是原子操作。

CopyOnWriteArrayListCopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象。

写时加锁,当添加一个元素的时候,将原来的容器进行copy,复制出一个新的容器,然后在新的容器里面写,写完之后再将原容器的引用指向新的容器,而读的时候是读旧容器的数据,所以可以进行并发的读,但这是一种弱一致性的策略。 
使用场景:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。

(3) ThreadLocal(线程变量副本)

当使用ThreadLocal维护变量时,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

Synchronized可以实现数据共享

(4)CAS(Compare And Swap) 无锁算法: 
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。


(5)并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue

LinkedBlockingQueue-会阻塞(BlockingQueue为接口,还有一些其他种类)
由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。

ConcurrentLinkedQueue
ConcurrentLinkedQueue是Queue的一个安全实现.Queue中元素按FIFO原则进行排序.采用CAS操作(也就是不会阻塞),来保证元素的一致性。

(6)Volatile变量

与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。

java面试资料—多线程_第2张图片

java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中。每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性)
2)禁止进行指令重排序。

当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存;

当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量;

关于threadlocal、volatile、synchronized区别


(7)线程的阻塞状态

阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

wait(用于线程之间的通信)和sleep区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。(要指定时间)

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。(不指定时间,等待某一条件触发notify)

(8)NIO(no-blocking IO)

Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。


你可能感兴趣的:(java)