Java多线程面试题

三、多线程:

35.并行和并发有什么区别?

答:

  • 并发:同一时间应对多件事情的能力被称为并发,同一时刻轮流使用cpu的做法,单核cpu下,线程实际还是串行能够执行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片分给不同的线程使用,只是由于cpu在线程间的切换十分快,人感觉是同时运行的,微观串行,宏观并行。Concurrent。
  • 并行:真正同时运行能力,同一时间动手做多件事情的能力
36.线程和进程的区别

答:

  • 进程:资源分配的最小单位,负责加载指令,管理内存,管理IO,当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程,进程可以视为程序的一个实例,大部分程序可以同时运行多个实例进程,也有程序只能启动一个实例进程。
  • 线程:
    一个进程之内可以分为一到多个线程
    一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
    Java中,线程作为最小调度单位,进程作为资源分配的最小单位,在windows中进程 是不活动的,只是作为进程的容器。
  • 二者对比:
    进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集
    进程拥有共享资源,如内存空间等,供其内部的线程共享
    进程间通信较为复杂
    同一台计算机的进程通信成为IPC
    线程通信相对简单,因为它们共享进程的内存,一个例子是多个线程可以访问同一个共享变量
    线程更轻量,线程上下文切换成本一般上要比进程上下切换低。
37.守护线程是什么?

答:

  • 如果 JVM 中没有一个正在运行的非守护线程,这个时候,JVM 会退出。换句话说,守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
    当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。
  • 通常来说,守护线程经常被用来执行一些后台任务,但是呢,又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是首选。
38.创建线程的方法有哪些?

答:

  • 方法一:直接使用Thread
// 创建线程对象
Thread t = new Thread("t1"){
	public void run(){
		// 要执行的任务
	}
};
t1.start();
  • 方法二:使用Runnable配合Thread
    把【线程】和【任务】(要执行的代码)分开
    Thread代表线程
    Runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable(){
	public void run(){
		// 要执行的任务
	}
};
// 创建线程对象
Thread t = new Thread( runnable );
//启动线程
t.start();
  • 方法三:
// 创建任务对象 
FutureTask<Integer> task3 = new FutureTask<>(() -> { 
	log.debug("hello"); 
	return 100; 
}); 

// 参数1 是任务对象; 参数2 是线程名字,推荐 
new Thread(task3, "t3").start(); 

// 主线程阻塞,同步等待 task 执行完毕的结果 
Integer result = task3.get(); 
log.debug("结果是:{}", result);
39.说一下runnable和callable有什么区别?

答:

  • Runnable接口run方法无返回值;Callable 接口 call 方法有返回值,支持泛型
  • Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
40.线程有哪些状态?

答:
这6个状态分别是:

  • New(初始化状态)刚刚new出来的时候
  • Runnable(可运行状态)
  • Blocked(阻塞状态)
  • Waiting(无限时等待)
  • Timed_Waiting(有限时等待)
  • Terminated(终止状态)在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态(休眠状态)。即只要 Java 线程处于这三种状态之一,就永远没有 CPU 的使用权。

转换:
假设有线程 Thread t

  • 情况 1 NEW --> RUNNABLE
    当调用 t.start() 方法时,由 NEW --> RUNNABLE
  • 情况 2 RUNNABLE <–> WAITING
    t 线程用 synchronized(obj) 获取了对象锁后
    调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
    调用 obj.notify(),obj.notifyAll(),t.interrupt() 时
    竞争锁成功,t 线程从 WAITING --> RUNNABLE
    竞争锁失败,t 线程从 WAITING --> BLOCKED
  • 情况 3 RUNNABLE <–> WAITING
    当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    注意是当前线程在t 线程对象的监视器上等待 ,t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
  • 情况 4 RUNNABLE <–> WAITING
    当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
    调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
  • 情况 5 RUNNABLE <–> TIMED_WAITING
    t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING ,t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
    竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
  • 情况 6 RUNNABLE <–> TIMED_WAITING
    当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    注意是当前线程在t 线程对象的监视器上等待当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE
  • 情况 7 RUNNABLE <–> TIMED_WAITING
    当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
    当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
  • 情况 8 RUNNABLE <–> TIMED_WAITING
    当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
  • 情况 9 RUNNABLE <–> BLOCKED
    t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
    持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
    成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
  • 情况 10 RUNNABLE <–> TERMINATED
    当前线程所有代码运行完毕,进入 TERMINATED
41.sleep()和wait()有什么区别

答:

  • sleep() 和 wait() 的区别就是调用sleep方法的线程不会释放对象锁,而调用wait() 方法会释放对象锁。
  • sleep()方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
    因为sleep()是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep()方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
  • wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程。
42.notify()和notifyAll()有什么区别?

答:

  • notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。notify可能会导致死锁,而notifyAll则不会的例子也就好解释。
43.线程的run()和start()有什么区别?

答:

  • 调用 start()方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
    一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。
44.Java程序中怎么保证多线程的运行安全

答:

  • 线程的安全性问题体现在:
    原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性
    可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
    有序性:程序执行的顺序按照代码的先后顺序执行

  • 导致原因:
    缓存导致的可见性问题
    线程切换带来的原子性问题
    编译优化带来的有序性问题

  • 解决办法:
    JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    synchronized、volatile、LOCK,可以解决可见性问题
    Happens-Before 规则可以解决有序性问题

45.多线程锁的升级原理是什么?

答:

  • 锁的级别从低到高:
    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

  • 锁分级别原因:
    没有优化以前,synchronized是重量级锁(悲观锁),使用 wait和notify、notifyAll来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁状态。

  • 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

  • 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。也就是只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
    偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
    如果线程处于活动状态,升级为轻量级锁的状态。

  • 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程B所访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
    当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

  • 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

46.什么是死锁?

答:

  • 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁,易发生死锁。
47.怎么防止死锁?

答:

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测
48.ThreadLocal是什么?有哪些使用场景?

答:

  • ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
    经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection;还有 Session管理等问题。
49.说一下synchronized底层实现原理

答:

  • 同步代码块是通过 monitorenter 和 monitorexit 指令获取线程的执行权
    如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  • 同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制
    方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
50.Synchronized和volatile的区别是什么?

答:

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
51.Synchronized和ReentrantLock(可重入锁)区别是什么?

答:

  • synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果,可以被打断
  • synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
  • synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
  • synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
  • synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
  • synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放

你可能感兴趣的:(Java,知识回顾,多线程,java)