Java学习手册:Java并发与多线程面试问题

1、Java学习手册:Java基础知识点
2、Java学习手册:Java面向对象面试问题
3、Java学习手册:Java集合、泛型面试问题
4、Java学习手册:Java并发与多线程面试问题
5、Java学习手册:Java虚拟机面试问题
6、Java学习手册:Java IO面试问题
7、Java学习手册:Java反射机制面试问题
8、Java学习手册:Java网络编程面试问题
9、Java学习手册:Java异常面试问题
10、Java学习手册:Java设计模式面试问题
11、Java学习手册:Java数据库面试问题


------------------------------模块一:Java多线程面试题--------------------------------

一、线程与进程

进程是指一段正在执行的程序,是操作系统中运行的一个任务(一个应用程序运行在一个进程中)。而线程是程序执行的最小单元,有时也被称为轻量级进程。一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)以及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间。进程是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程所包含的一个或多个执行单元称为线程。进程还拥有一个私有的虚拟的地址空间,该空间仅能被它所包含的线程访问。线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。

线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有5种状态,分别是New(新建状态)、Runnable(就绪状态)、Block(阻塞状态)、Running(运行状态)、Dead(死亡状态)。一个线程是进程的一个顺序执行流,同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程可以包含多个线程(一个进程至少有一个线程)。

不同:一个进程是一个独立的运行环境,它可以被看做一个程序或一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类和程序的单一进程。线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。

获取线程信息:

long getId()//返回该线程的标识符
String getName()//返回该线程的名称
int getPriority()//返回该线程的优先级
Thread.state getState()//获取线程的状态
boolean isAlive()//测试线程是否处于活动状态
boolean isDaemon()//测试线程是否为守护线程
boolean isInterrupted()//测试线程是否已经中断

注:进程与线程的区别

  • 1、地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 2、资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
  • 3、一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 4、进程切换时,消耗的资源大,效率不高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
  • 5、执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 6、线程是处理器调度的基本单位,但是进程不是。

二、为什么要使用多线程?

(1)使用多线程可以减少程序的响应时间。
(2)与进程相比,线程的创建和切换开销更小。
(3)在多CPU计算机上使用多线程能提高CPU的利用率。
(4)使用多线程能简化程序的结构,使程序便于理解和维护。


三、简述线程的状态及其转换

在Java语言中,线程有5种状态,分别是New(新建状态)、Runnable(就绪状态)、Block(阻塞状态)、Running(运行状态)、Dead(死亡状态)
线程之间的状态转换如下:
(1)New,创建一个线程,但是线程并没有进行任何操作。
(2)Runnable,新线程从New状态,调用start方法转换到Runnable状态。线程调用start方法向线程调度程序(JVM或操作系统)注册一个线程,这时一切就绪只等cpu的时间。
(3)Running,从Runnable状态到Running状态,线程调度根据调度策略的不同调用不同的线程,被调度执行的线程进入Running状态,执行Run方法。
(4)Dead,从Running状态到Runnable,run方法运行完毕后,线程就会被抛弃,线程就进入Dead状态。
(5)Block,从Running状态到Block状态,如果线程在运行的状态中因为I/O阻塞,调用了线程的sleep方法以及调用对象的wait方法,则线程将进入阻塞状态,直到这些阻塞原因被结束,线程进入到Runnable状态。

当我们在Java程序中新建一个线程时,它的状态是New。当我们调用线程的start()方法时,状态被改变为Runnable。线
程调度器会为Runnable线程池中的线程分配CPU时间并且将它们的状态改变为Running。其他的线程状态还有BlockDead

下附线程状态转换图
Java学习手册:Java并发与多线程面试问题_第1张图片
注:阻塞状态的三种情况
(1)位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,Java虚拟机就会把这线程放到这个对象的等待池中。
(2)位于对象锁中的阻塞状态:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的锁池中。
(3)其他的阻塞状态:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态中。


四、Java程序每次运行时至少启动几个线程?

答:两个线程,一个是main线程,另外一个是垃圾收集线程。


五、如何实现Java多线程?

在Java中,多线程的实现一般有以下三种方法:

(1)继承Thread类,并重写run()方法

该方法的不足之处
①由于需要继承Thread类,当前类就不能扩展其他类了。
②由于线程内部重写了run方法,决定了当前线程要执行的任务,导致当前线程只会做这件事,任务与线程有了强耦合关系,不利于线程重用。

package com.haobi;
/*
 * 继承Thread类,并重写run()方法
 */
class MyThread extends Thread{
     
	public void run() {
     
		System.out.println("Thread body");
	}
}
public class Test1 {
     
	public static void main(String[] args) {
     
		MyThread my = new MyThread();
		my.start();//开启线程
	}
}
//程序输出结果如下:
Thread body

:调用start()方法后并不是立即执行多线程代码,而是使得该线程变为可运行状态(Runnable),什么时候运行多线程代码是由操作系统决定的。

(2)实现Runnable接口,并实现该接口的run()方法
package com.haobi;
/*
 * 实现Runnable接口,并实现该接口的run()方法
 */
class MyThread implements Runnable{
     
	@Override
	public void run() {
     
		System.out.println("Thread Body");
	}
}
public class Test2 {
     
	public static void main(String[] args) {
     
		MyThread my = new MyThread();
		Thread t = new Thread(my);
		t.start();//开启线程
	}
}
//程序输出结果如下:
Thread Body

:其实,不管是通过Thread类还是通过使用Runnable接口来实现多线程的方法,最终还是通过Thread的对象API来控制线程的。

(3)实现Callable接口(ExecutorService、Callable、Future),重写call()方法

Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能。主要表现为:①Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。②Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。③运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。

注:ExecutorService、Callable、Future对象实际上都属于Executor框架中的功能类。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get()即可获取到Callable任务返回的Object,再结合线程池接口ExecutorService即可实现传说中有返回结果的多线程。

package com.haobi;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/*
 * 实现Callable接口,重写call()方法
 */
public class Test3 {
     
	//创建线程类
	public static class CallableTest implements Callable<String>{
     
		public String call() throws Exception{
     
			return "Hello World!";
		}
	}
	public static void main(String[] args) {
     
		ExecutorService threadPool = Executors.newSingleThreadExecutor();
		//启动线程
		Future<String> future = threadPool.submit(new CallableTest());
		try {
     
			System.out.println("waiting thread to finish");
			System.out.println(future.get());//等待线程结束,并获取返回结果
		} catch (Exception e) {
     
			e.printStackTrace();
		}
	}
}
//程序输出额结果如下:
waiting thread to finish
Hello World!
(4)由线程池创建并管理线程
(5)前三种方法对比

前三个方中,前两种方式线程执行完成后都没有返回值,只有最后一种是带返回值的。当需要实现多线程时,一般推荐实现Runnable接口的方式。此外,实现Callable和实现Runnable类似,但是功能更强大,具体表现在:
1、可以在任务结束后提供一个返回值,Runnable不行;
2、call方法可以抛出异常,Runnable的run方法不行;
3、可以通过运行Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值;


六、run()方法与start()方法有什么区别?

通常,系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run()方法来完成实际的操作,当run()方法结束后,此线程就会终止。如果直接调用线程类的run()方法,这会被当作一个普通的函数调用,程序中仍然只有主线程这一个线程,也就是说,start()方法能够异步的调用run()方法,但是直接调用run()方法确是同步的,因此也就无法达到多线程的目的。由此可见,只有通过调用线程类的start()方法才能真正达到多线程的目的
(1)当你调用start()方法时,将会创建新的线程,并且执行run()方法里的代码。
(2)当直接调用run()方法,它不会创建新的线程,也不会执行调用线程的代码。


七、多线程同步的实现方法有哪些?

多个线程并发操作同一数据时,由于线程切换的不确定性,会导致出现混乱。严重时可能导致系统崩溃。为例避免这种情况的产生,我们要将各干各的(互相抢)变为排队干(同步的,一个线程做完了另一个线程再做)。
Java主要提供了3种实现同步机制的方法:

(1)synchronized关键字
//1、synchronized方法,在方法的声明前加入synchronized关键字,示例如下:
public synchronized void mutiThreadAccess();
//当一个方法被synchronized修饰后,该方法变为同步方法,意思是多个线程不能同时访问该方法内部。
//synchronized被修饰在方法上时,上锁对象为该方法所属的对象。若两个线程看到的是同一个对象,
//则一个线程上锁后,另一个线程只能等待该锁释放后方可执行调用方法。(同步范围大)


//2、synchronized块
//有效的减少同步范围可以在保证安全的前提下提高并发效率,控制同步的代码范围可以使用“同步块”。
synchronized(syncObject){
     
	//访问syncObject的代码
}

由于Java的每个对象都有一个内置锁,用此关键字修饰时,内置锁会保护整个方法。在调用该方法前,需获得内置锁,否则就处于阻塞状态。

同步块与同步方法的区别:
①同步块力度小,同步块更加细。
同步方法获取的锁是当前对象的锁,同步块获取的锁可以是任意对象的锁

注:同一个类里面两个synchronized方法,两个线程同时访问的问题
①如果synchronized修饰的是静态方法,锁的是当前类的class对象,进入同步代码前要获得当前类对象的锁;
②普通方法,锁的是当前实例对象,进入同步代码前要获得的是当前实例的锁;
③同步代码块,锁的是括号里面的对象,对给定的对象加锁,进入同步代码块库前要获得给定对象锁;
④如果两个线程访问同一个对象的synchronized方法,会出现竞争,如果是不同对象,则不会相互影响。

(2)wait()方法与notify()方法

这两个方法是在Object上定义的,也就是说所有对象都具有着两个方法。当一个线程调用一个对象的wait()方法后,该线程进入阻塞状态,直到这个对象的notify()方法别调用后,当前线程方可解除。这样的好处在于协调两个工作时可以更加灵活。

  • wait():释放obj的锁,导致当前的线程等待
  • notify():唤醒在此对象监视器上等待的单个线程
  • notifyAll():通知所有等待该竞争资源的线程

当要调用wait()或notify()/notifyAll()方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)代码中。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。

(3)Lock

JDK1.5新增加了Lock接口以及它的一个实现类RenntrantLock(重入锁),Lock也可以用来实现多线程的同步,具体而言,它提供了如下一些方法来实现多线程的同步:

  • 1、lock()
    以阻塞的方式获取锁,也就是说,如果获取到了锁,立即返回;如果别的线程持有锁,当前线程等待,直到获取锁后返回。

  • 2、tryLock()
    以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,立即返回false。也就说这个方法无论如何都会立即返回。tryLock()方法是有返回值的。

  • 3、tryLock(long timeout,TimeUnit unit)
    如果获取了锁,立即返回true,否则会等待参数给定地时间单元,在等待地过程中,如果获取了锁,就返回true,如果等待超时,返回false。
    tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定时间,在时间限制之内如果还是拿不到锁,就返回false。如果一开始就拿到锁或者在等待期间内拿到了锁,则就返回true。

  • 4、lockInterruptibly()
    如果获取了锁,立即返回;如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断(会收到InterruptedExeption异常)。它与lock()方法最大的区别在于如果lock()方法获取不到锁,会一直处于阻塞状态,且会忽略interrupt()方法。
    lockinterruptibly()方法比较特殊,当通过这个方法区获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,
    即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockinterruputibly()方法获取某个锁时,假如此时线程A获取
    到了锁,而线程B只有等待,那么对线程调用threadB.interrupt()方法能够中断线程B的等待过程。

注:在Java中Lock接口比Synchronized块的优势是什么?
Lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足有条件的阻塞。

(4)volatile修饰变量

保证变量在线程间的可见性,每次线程要访问volatile修饰的变量时都从内存中读取,而不缓存中,这样每个线程访问到的变量都是一样的。

(5)ReentrantLock重入锁

创建一个ReentrantLock实例,lock()获得锁,unlock()释放锁。

(6)使用局部变量ThreadLocal实现线程同步

每个线程都会保存一份该变量的副本,副本之间相互独立,这样每个线程都可以随意修改自己的副本,而不影响其他线程。常用方法ThreadLocal()创建一个线程本地变量;get()返回此线程局部的当前线程副本变量;initialValue()返回此线程局部变量的当前线程的初始值;set(T value)将此线程变量的当前线程副本中的值设置为value。

(7)使用原子变量

如AtomicInteger,常用方法AtomicInteger(int value)创建个有给定初始值的AtomicInteger整数;addAndGet(int data)以原子方式将给定值与当前值相加。

(8)使用阻塞队列

使用阻塞队列实现线程同步LinkedBlockingQueue


八、sleep()方法与wait()方法有什么区别?

sleep()是使线程暂停执行一段时间的方法wait()也是一种使线程暂停执行的方法。具体而言,二者的主要区别主要表现在以下几个方面:

(1)原理不同

sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,但是监视状态依然保持,等到计时时间一到,此线程会自动“苏醒”。而wait()方法是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程调用notify()方法(或notifyAll方法)时才“醒”来,开发人员也可以给它指定一个时间,自动“醒”来。

(2)对锁的处理机制不同

由于sleep()方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此,调用sleep()方法并不会释放锁。而wait()方法则不同,当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。

由于sleep不会释放“锁标志”,容易导致死锁问题的发生。因此,一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法

(3)使用区域不同

由于wait()方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而sleep()方法则可以放在任何地方使用。

(4)使用方式不同

sleep()方法必须捕获异常,而wait()、notify()以及notifyall()不需要捕获异常。在sleep过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常。

(5)使用目的不同

wait()方法通常被用于线程间交互,sleep()方法通常被用于暂停执行。


九、sleep()方法与yield()方法有什么区别?

Thread的静态方法sleep→static void sleep(long ms)
可以使得当前线程进入阻塞状态指定毫秒。当超时后,该线程会自动回到Runnable状态,等待再次分配时间片运行。该方法声明时会抛出一个InterruptException,所以在使用时需要捕获这个异常。

Thread的静态方法yield→static void yield()
该方法用于使当前线程主动让出当次cpu时间片回到Runnable状态,等待时间片分配。(将一个线程的操作暂时让给其他线程执行)

sleep():让当前正在执行的线程休眠,有一种用法可以代替yield方法,就是sleep(0)。

yield():暂停当前正在执行的线程对象,并执行其他线程,也就是交出CPU使用时间。

区别:
(1)sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
(2)当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间,当超时后,该线程会自动回到就绪状态;当线程执行了yield()方法后,当前线程主动让出CPU执行时间片,转到就绪状态。
(3)sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常。
(4)sleep()方法比yield()方法具有更好的移植性。

注:为什么Thread类的sleep()和yield()方法是静态的?
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。


十、线程终止的方法有哪些?

终止线程的方法有三种,具体如下:
(1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程终止。
(2)使用Thread的interrupt()方法中断线程。(但调用interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程)
(3)使用Thread的stop()方法强行终止线程。(这个方法不推荐使用,因为stop()和suspend()、resume()一样,也可能发生不可预料的结果)

参考:诺瓦科技面试总结(三)


十一、synchronized与lock有什么异同?

Java语言提供了两种锁机制来实现对某个资源的同步(解决线程安全问题):synchronizedLock。其中,Synchronized使用Object对象本身的notify、wait、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,完成synchrinized实现的所有功能。二者的主要区别表现在以下几个方面:

(1)用法不一样

在需要同步的对象中加入Synchronized控制,Synchronized既可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。而Lock需要显式地指定起始位置和终止位置Synchronized是托管给JVM的,而Lock的锁定是通过代码实现的,具有更精确的线程语义

(2)性能不一样

在资源竞争不是很激烈的情况下,Synchronized使用的是轻量级锁或者偏向锁,这两种锁都能有效地减少轮询或者阻塞的发生,与之相比Lock要将未获得锁的线程放入等待队列阻塞,带来上下文切换的开销,此时Synchronized效率会更高
在资源竞争很激烈的情况下,Synchronized会升级为重量级锁,Synchronized的性能会下降得非常块,而Lock的性能基本保持不变。由于Synchronized的出队速度相比Lock要慢,所以Lock的效率会更高些
注:一般对于数据结构设计或者框架的设计都倾向于使用Lock而非Synchronized。

(3)锁机制不一样

Synchronized获得锁和释放的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常而导致锁没有被释放从而引发死锁。而Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。

(4)异常处理不一样

Synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用lock()时需要在finally块中释放锁。

注:
(1)Lock有比Synchronized更精确的线程语义和更好的性能。Lock()还有更强大的功能,例如,它的tryLock()方法可以非阻塞方式去拿锁。
(2)需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
(3)通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。


十二、守护线程(后台线程)

守护线程指的是随着主线程死亡而死亡的子线程。在子线程启动之前,调用setDaemon(true)方法,即可将该线程设置为守护线程。

void setDaemon(boolean)//当参数为true时,该线程为守护线程
//注:这一方法必须在线程启动之前调用

守护线程的特点是:当进程中只剩下守护线程时,所有守护线程强制终止。GC就是运行在一个守护线程上的。

注:如何创建守护线程?
使用Thread类的setDaemon(true)方法可以将线程设置为守护线程,需要注意的是,需要在调用start()方法前调用这个方法,否则会抛出IllegalThreadStateException异常。


十三、join()方法的作用是什么?

在Java语言中,join()方法的作用是让一个线程强制运行(线程强制运行期间,其他线程无法运行),即让调用该方法的线程在执行完run()方法后,再执行join()方法后面的代码。该方法用于等待当前线程结束。简单说,就是将两个线程合并,用于实现同步功能。允许当前线程在另一线程上等待,进入阻塞状态,直到另一个线程运行完毕,当前线程才会结束阻塞,继续执行,通常用于协调两个线程同步工作使用。示例如下:

package com.haobi;

class ThreadImp implements Runnable{
     
	@Override
	public void run() {
     
		try {
     
			System.out.println("Begin ThreadImp");
			Thread.sleep(5000);
			System.out.println("End ThreadImp");
		} catch (Exception e) {
     
			e.printStackTrace();
		}
	}
}
public class JoinTest {
     
	public static void main(String[] args) {
     
		Thread t = new Thread(new ThreadImp());
		t.start();
		try {
     
			t.join(1000);//主线程等待t结束,即等待1秒
			if(t.isAlive()) {
     
				System.out.println("t has not finished");
			}else {
     
				System.out.println("t has finished");
			}
			System.out.println("joinFinish");
		} catch (Exception e) {
     
			e.printStackTrace();
		}
	}
}
//程序输出结果如下:
Begin ThreadImp
t has not finished
joinFinish
End ThreadImp

十四、Java锁分类和特点

Java学习手册:Java锁的分类和特点


十五、死锁

Java学习手册:死锁

注:什么是死锁(Deadlock)?如何分析和避免死锁?

死锁是指两个以上的线程永远阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资源

分析死锁,我们需要查看Java应用程序的线程转储。我们需要找出那些状态为BLOCKED的线程和他们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁。

避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的常用方法。


十六、多线程的好处是什么?

在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存(heap memory),因此创建多个线程去执行一些任务会比创建多个进程好。


十七、用户线程与守护线程的区别?

当我们在Java程序中创建一个线程,它就被称为用户线程。一个守护线程是在后台执行并且不会阻止JVM终止的线程。

当没有用户线程在运行的时候,JVM关闭程序并且退出。一个守护线程创建的子线程依然是守护线程。


十八、线程优先级

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(1-10),1代表最低优先级,10代表最高优先级。


十九、什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。


二十、在多线程中,什么是上下文切换(contextswitching)?

上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。


二十一、如何确保main()方法所在的线程是Java程序最后结束的线程?

我们可以使用Thread类的joint()方法来确保所有程序创建的线程在main()方法退出前结束。


二十二、线程之间是如何通信的?

当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait()、notify()、notifyAll()方法可以用于线程间通信中关于资源的锁的状态。


二十三、为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类里?

Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。


二十四、为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。


二十五、volatile关键字在Java中有什么作用?

Java学习手册:volatile

当我们使用volatile关键字去修饰变量的时候,所以线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的。


二十六、同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。


二十七、什么是ThreadLocal?

ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。
每个线程都会拥有他们自己的Thread变量,它们可以使用get()、set()方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal实例通常是希望它们同线程状态关联起来是private static属性。


二十八、什么是Thread Group?为什么不建议使用它?

ThreadGroup是一个类,它的目的是提供关于线程组的信息。

ThreadGroup API比较薄弱,它并没有比Thread提供了更多的功能。它有两个主要的功能:一是获取线程组中处于活跃状态线程的列表;二是设置为线程设置未捕获异常处理器(Uncaught exception handler)。但在Java 1.5中Thread类也添加了setUncaughtExceptionHandler(UncaughtExceptionHandler eh)方法,所以ThreadGroup是已经过时的,不建议继续使用。


二十九、什么是Java线程转储(Thread Dump),如何得到它?

线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。有很多方法可以获取线程转储——使用Profiler,Kill 3命令,jstack工具等等。我更喜欢jstack工具,因为它容易使用并且是JDK自带的。由于它是一个基于终端的工具,所以我们可以编写一些脚本去定时的产生线程转储以待分析。


三十、什么是Java Timer类?如何创建一个有特定时间间隔的任务?

java.util.Timer是一个工具类,可以用于安排一个线程在未来的某个特定时间执行。Timer类可以用安排一次性任务或者周期任务。

java.util.TimerTask是一个实现了Runnable接口的抽象类,我们需要去继承这个类来创建我们自己的定时任务并使用Timer去安排它的执行。


三十一、什么是线程池?如何创建一个Java线程池?

Android学习笔记:线程池(ThreadPool)

一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行的任务的队列。

java.util.concurrent.Executors提供了一个java.util.concurrent.Executor接口的实现用于创建线程池。


三十二、线程池的作用

线程池,主要解决三个问题:

  • 1、重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
  • 2、能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 3、能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

三十三、在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

监视器和锁在Java虚拟机中是⼀块使⽤的。监视器监视⼀块同步代码块,确保⼀次只有⼀个线程执⾏同步代码块。每⼀个监视器都和⼀个对象引⽤相关联。线程在获取锁之前不允许执⾏同步代码。


三十四、如何确保N个线程可以访问N个资源同时⼜不导致死锁?

使⽤多线程的时候,⼀种⾮常简单的避免死锁的⽅式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。


三十五、什么是线程池,如何使用?

Android学习笔记:线程池(ThreadPool)

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)。


三十六、Java中堆和栈有什么不同?

栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。


三十七、如何控制某个方法允许并发访问线程的个数?

创建Semaphore变量,Semaphore semaphore = new Semaphore(5, true); 当方法进入时,请求一个信号,如果信号被用完则等待,方法运行完,释放一个信号,释放的信号新的线程就可以使用。


三十八、什么导致线程阻塞?

1)线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行。

2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行。

3)线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作。

4)线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。

5)线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。

6)线程执行suspend()使线程进入阻塞态,必须resume()方法被调用,才能使线程重新进入可执行状态。


三十九、Synchronized、Volatile、Lock、ReentrantLock四者的比较

1)volatile:解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。

2)synchronized:互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。

3)lock:lock是一个接口,而synchronized是java中的关键字,synchronized是内置语言的实现。lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

4)ReentrantLock:可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。


四十、Thread为什么不能用stop方法停止线程?

从SUN的官方文档可以得知,调用Thread.stop()方法是不安全的,这是因为当调用Thread.stop()方法时,会发生下面两件事:
1、即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。
2、释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。


四十一、线程同步机制与原理

为什么需要线程同步?当多个线程操作同一个变量的时候,存在这个变量何时对另一个线程可见的问题,也就是可见性。每一个线程都持有主存中变量的一个副本,当他更新这个变量时,首先更新的是自己线程中副本的变量值,然后会将这个值更新到主存中,但是是否立即更新以及更新到主存的时机是不确定的,这就导致当另一个线程操作这个变量的时候,他从主存中读取的这个变量还是旧的值,导致两个线程不同步的问题。线程同步就是为了保证多线程操作的可见性和原子性,比如我们用synchronized关键字包裹一端代码,我们希望这段代码执行完成后,对另一个线程立即可见,另一个线程再次操作的时候得到的是上一个线程更新之后的内容,还有就是保证这段代码的原子性,这段代码可能涉及到了好几部操作,我们希望这好几步的操作一次完成不会被中间打断,锁的同步机制就可以实现这一点。一般说的synchronized用来做多线程同步功能,其实synchronized只是提供多线程互斥,而对象的wait()和notify()方法才提供线程的同步功能。JVM通过Monitor对象实现线程同步,当多个线程同时请求synchronized方法或块时,monitor会设s置几个虚拟逻辑数据结构来管理这些多线程。新请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有锁的线程unlock之后,则排队队列里的线程竞争上岗(synchronized是不公平竞争锁)。如果运行的线程调用对象的wait()后就释放锁并进入wait线程集合那边,当调用对象的notify()或notifyall()后,wait线程就到排队那边。


四十二、Synchronized和重入锁的区别?

Synchronized是JVM的内置锁,而重入锁是Java代码实现的。重入锁是synchronized的扩展,可以完全代替后者。重入锁可以重入,允许同一个线程连续多次获得同一把锁。其次,重入锁独有的功能有:

  • 可以相应中断,synchronized要么获得锁执行,要么保持等待。而重入锁可以响应中断,使得线程在迟迟得不到锁的情况下,可以不再等待。主要由lockInterruptibly()实现,这是一个可以对中断进行响应的锁申请动作,锁中断可以避免死锁。
  • 锁的申请可以有等待时限,用tryLock()可以实现限时等待,如果超时还未获得锁会返回false,也防止了线程迟迟得不到锁时一直等待,可避免死锁。
  • 公平锁,即锁的获得按照线程先来后到的顺序依次获得,不会产生饥饿现象。synchronized的锁默认是不公平的,重入锁可通过传入构造方法的参数实现公平锁。
  • 重入锁可以绑定多个Condition条件,这些condition通过调用await/singal实现线程间通信。

四十三、Synchronized作了哪些优化?

synchronized对内置锁引入了偏向锁、轻量级锁、自旋锁、锁消除等优化。使得性能和重入锁差不多了。

  • 偏向锁:偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远也不需要再进行同步。偏向锁是在无竞争的情况下把整个同步都消除掉,CAS操作也没有了。适合于同一个线程请求同一个锁,不适用于不同线程请求同一个锁,此时会造成偏向锁失效。
  • 轻量级锁:如果偏向锁失效,虚拟机不会立即挂起线程,会使用一种称为轻量级锁的优化手段,轻量级锁的加锁和解锁都是通过CAS操作完成的。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,表示其他线程抢先得到了锁,轻量级锁将膨胀为重量级锁。
  • 自旋锁:锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力–自旋锁。如果共享数据的锁定状态只有很短的一段时间,为了这段时间去挂起和恢复线程(都需要转入内核态)并不值得,所以此时让后面请求锁的那个线程稍微等待以下,但不放弃处理器的执行时间。这里的等待其实就是执行了一个忙循环,这就是所谓的自旋。虚拟机会让当前线程做几个循环,若干次循环后如果得到了锁,就顺利进入临界区;如果还是没得到,这才将线程在操作系统层面挂起。
  • 锁消除:虚拟机即时编译时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。锁消除的依据来源于“逃逸分析”技术。堆上的所有数据都不会逃逸出去被其他线程访问到,就可以把它们当栈上的数据对待,认为它们是线程私有的,同步加锁就是没有必要的。

四十四、BIO、NIO、AIO的区别?

在I/O中的同步、异步、阻塞、非阻塞的区别:

  • 同步I/O。由用户进程自己处理I/O的读写,处理过程中不能做其他事。需要主动去询问I/O状态。
  • 异步I/O。由系统内核完成I/O操作,完成后系统会通知用户进程。
  • 阻塞。I/O请求操作需要的条件不满足,请求操作一直等待,直到条件满足。
  • 非阻塞。 I/O请求操作需要的条件不满足,会立即返回一个标志,而不会一直等待。

BIO、NIO、AIO的区别:

  • BIO:同步并阻塞。用户进程在发起一个I/O请求后,必须等待I/O准备就绪,I/O操作也由自己来处理,在IO操作未完成之前,用户进程必须等待。
  • NIO:同步非阻塞。用户进程发起一个I/O请求后可立即返回去做其他任务,当I/O准备就绪时它会收到通知。接着由这个线程自行进行I/O操作,I/O操作本身还是同步的。
  • AIO:异步非阻塞。用户进程发起一个I/O操作以后可立即返回去做其他任务,真正的I/O操作由内核完成后通知用户进程。

NIO和AIO的不同:NIO是操作系统通知用户进程I/O已经准备就绪,由用户进程自行完成I/O操作;AIO是操作系统完成I/O后通知用户进程。

BIO是为每一个客户端连接开启一个线程,简单说就是一个连接一个线程。

NIO主要组件有Seletor、Channel、Buffer,数据需要通过BUffer包装后才能使用Channel进行读取和写入。一个Selector可以由一个线程管理,每一个Channel可看作一个客户端连接。一个Selector可以监听多个Channel,即使用一个或极少数的线程来管理大量的客户端连接。当与客户端连接的数据没有准备好时,Selector处于等待状态,一旦某个Channel的准备好了数据,Selector就能立即得到通知。


四十五、进程间通信的方式?线程间通信的方式?

进程间通信的方式:

  • 管道。分为几种管道。普通管道PIPE:单工,单向传输,只能在父子或者兄弟进程间使用;流管道,半双工,可双向传输,只能在父子或兄弟进程间使用;命名管道:可以在许多并不相关的进程之间进行通讯。
  • 消息队列。消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号。用于通知接收进程某个事件已经发生。
  • 信号量。信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 共享内存。共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC(进程间通信) 方式,它往往与其他通信机制如信号量配合使用,来实现进程间的同步和通信。
  • 套接字。可用于不同机器间的进程通信。

线程间通信的方式:

  • 锁机制。包括互斥锁、条件变量、读写锁。互斥锁以排他方式方式数据被并发修改;读写锁允许多个线程同时读取,对写操作互斥;条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
  • 信号量(Semaphore) 机制。包括无名线程信号量和命名线程信号量。
  • 信号(Signal)机制。类似进程间的信号处理。

四十六、CPU线程调度?

  • 协同式线程调度:线程的执行时间以及线程的切换都是由线程本身来控制,线程把自己的任务执行完后,主动通知系统切换到另一个线程。优点是没有线程安全的问题,缺点是线程执行的时间不可控,可能因为某一个线程不让出CPU,而导致整个程序被阻塞。
  • 抢占式调度模式:线程的执行时间和切换都是由系统来分配和控制的。不过可以通过设置线程优先级,让优先级高的线程优先占用CPU。

Java虚拟机默认采用抢占式调度模型。


四十七、ThreadLocal的作用和实现原理?

对于共享变量,一般采取同步的方式保证线程安全。而ThreadLocal是为每一个线程都提供了一个线程内的局部变量,每个线程只能访问到属于它的副本。

实现原理,下面是set和get的实现:

// set方法
public void set(T value) {
     
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

// 上面的getMap方法
ThreadLocalMap getMap(Thread t) {
     
   return t.threadLocals;
}

// get方法
public T get() {
     
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null) {
     
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
     
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   return setInitialValue();
}

从源码中可以看出:每一个线程拥有一个ThreadLocalMap,这个map存储了该线程拥有的所有局部变量。

set时先通过Thread.currentThread()获取当前线程,进而获取到当前线程的ThreadLocalMap,然后以ThreadLocal自己为key,要存储的对象为值,存到当前线程的ThreadLocalMap中。

get时也是先获得当前线程的ThreadLocalMap,以ThreadLocal自己为key,取出和该线程的局部变量。


四十八、被notify()唤醒的线程可以立即得到执行吗?

被notify唤醒的线程不是立刻可以得到执行的,因为notify()不会立刻释放锁,wait()状态的线程也不能立刻获得锁;等到执行notify()的线程退出同步块后,才释放锁,此时其他处于wait()状态的线程才能获得该锁。


四十九、sleep、wait、yield的区别和联系?

sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。调用sleep后不会释放锁。

yield() 使得线程放弃CPU执行时间,但是不使线程阻塞,线程从运行状态进入就绪状态,随时可能再次分得 CPU 时间。有可能当某个线程调用了yield()方法暂停之后进入就绪状态,它又马上抢占了CPU的执行权,继续执行。

wait() 是Object的方法,会使线程进入阻塞状态,和sleep不同,wait会同时释放锁。wait/notify在调用之前必须先获得对象的锁。


五十、未完待续


------------------------------模块二:Java并发面试题------------------------------------

一、并发操作怎么控制?

Java中可在⽅法名前加关键字syschronized来处理当有多个线程同时访问共享资源时候的问题。syschronized相当于⼀把锁,当有申请者申请该资源时,如果该资源没有被占⽤,那么将资源交付给这个申请者使⽤,在此期间,其他申请者只能申请⽽不能使⽤该资源,当该资源被使⽤完成后将释放该资源上的锁,其他申请者可申请使⽤。

并发控制主要是为了多线程操作时带来的资源读写问题。如果不加以控制,可能会出现死锁读脏数据不可重复读丢失更新等异常。
并发操作可以通过加锁的⽅式进⾏控制,锁⼜可分为乐观锁和悲观锁。

(1)悲观锁:
悲观锁并发模式假定系统中存在⾜够多的数据修改操作,以致于任何确定的读操作都可能会受到由个别的⽤⼾所制造的数据修改的影响。也就是说悲观锁假定冲突总会发⽣,通过独占正在被读取的数据来避免冲突。但是独占数据会导致其他进程⽆法修改该数据,进⽽产⽣阻塞,读数据和写数据会相互阻塞。

(2)乐观锁:
乐观锁假定系统的数据修改只会产⽣⾮常少的冲突,也就是说任何进程都不⼤可能修改别的进程正在访问的数据。乐观并发模式下,读数据和写数据之间不会发⽣冲突,只有写数据与写数据之间会发⽣冲突。即读数据不会产⽣阻塞,只有写数据才会产⽣阻塞。


二、什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误

为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。可以阅读这篇文章来了解Java的atomic类。


三、Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

它的优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

四、什么是Executors框架?

Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。


五、什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

java.util.concurrent.BlockingQueue的特性是:当队列是空的时,从队列中获取或删除元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。

阻塞队列不接受空值,当你尝试向队列中添加空值的时候,它会抛出NullPointerException。

阻塞队列的实现都是线程安全的,所有的查询方法都是原子的并且使用了内部锁或者其他形式的并发控制。

BlockingQueue接口是java collections框架的一部分,它主要用于实现生产者消费者问题。


六、什么是Callable和Future?

Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很相似,但它可以返回一个对象或者抛出一个异常。

Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,我们必须等待它返回的结果。java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它我们可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。


七、什么是FutureTask?

FutureTask是Future的一个基础实现,我们可以将它同Executors使用处理异步任务。通常我们不需要使用FutureTask类,单当我们打算重写Future接口的一些方法并保持原来基础的实现是,它就变得非常有用。我们可以仅仅继承于它并重写我们需要的方法。


八、什么是并发容器的实现?

Java集合类都是快速失败的,这就意味着当集合被改变且一个线程在使用迭代器遍历集合的时候,迭代器的next()方法将抛出ConcurrentModificationException异常。

并发容器支持并发的遍历和并发的更新。

主要的类有ConcurrentHashMap, CopyOnWriteArrayList 和CopyOnWriteArraySet。


九、Executors类是什么?

Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

Executors可以用于方便的创建线程池。


你可能感兴趣的:(Java学习手册,Java)