java并发系列:一线程常见问题

引子:
本文属于面试题梳理系列,因为java的支持并发的特性,所以实际项目中会牵扯到对并发的开发、调优等。所以多线程相关知识经常被问到。这篇是第一篇,跟线程有关基础概念,通常面试官会从线程基础概念问起,但是只知道这些基础概念是不够的,最终会问到并发编程的诸多问题:如java内存模型,各种锁、死锁,各种j.u.c下面的工具、线程池、线程安全等等。可深入到底层实现的原理,其实梳理过程就是加深自己印象的过程,好了,开始吧。
背景:进程与线程
进程与线程,本质意义上说, 是操作系统的调度单位,可以看成是一种操作系统 “资源” 。Java 作为与平台无关的编程语言,必然会对底层(操作系统)提供的功能进行进一步的封装,以平台无关的编程接口供程序员使用,进程与线程作为操作系统核心概念的一部分无疑亦是如此。在 Java 语言中,对进程和线程的封装,分别提供了 Process 和 Thread 相关的一些类。
关于进程:
在 JDK 的代码中,只提供了 ProcessImpl 类来实现 Process 抽象类。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依赖于操作系统平台的本地方法,它的实现是用 C/C++ 等类似的底层语言实现。 它在实现上就是创建了操作系统的一个进程,也就是每个 JVM 中创建的进程都对应了操作系统中的一个进程。 Java 中对操作系统的进程进行了封装,屏蔽了操作系统进程相关的信息。
关于线程:
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享堆内存。  
1.创建线程有两种方式,如下:

1)扩展java.lang.Thread

2)实现Runnable接口

java并发系列:一线程常见问题_第1张图片

 推荐第二种,避免了单继承的局限性。

2 start方法与run方法的区别。

start 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体, 它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止, 而CPU再运行其它线程,

而如果直接用Run方法, 这只是调用一个方法而已, 程序中依然只有主线程--这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。可以理解为我们需要在run方式处理业务,但是自己不会显示去调用,而是启动线程后通过jvm调用。
阿里的面试官问了这个问题,如果回到到这里,就算是合格吧,按照大神要求的,要想回答为什么给自己加分?还要结合源码去分析。
     
     
     
     
我们可以看下start的源码:
首先会检查线程状态,!=0抛出异常,也就是说一个线程只能被成功启动一次,第二次启动会抛出异常。
加入线程组。
真正的启动是start0方法。这是native方法,jdk看不到源码,网上找了大神的文章分析如下:
 Thread 类有个 registerNatives 本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如 start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的 . 这个方法放在一个 static 语句块中,这就表明,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。
 
  本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,定义了各个操作系统平台都要用到的关于线程的公用数据和操作,如代码清单 2 所示。
JNIEXPORT void JNICALL 
 Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){ 
   (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods)); 
 } 
 static JNINativeMethod methods[] = { 
    {"start0", "()V",(void *)&JVM_StartThread}, 
    {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread}, 
	 {"isAlive","()Z",(void *)&JVM_IsThreadAlive}, 
	 {"suspend0","()V",(void *)&JVM_SuspendThread}, 
	 {"resume0","()V",(void *)&JVM_ResumeThread}, 
	 {"setPriority0","(I)V",(void *)&JVM_SetThreadPriority}, 
	 {"yield", "()V",(void *)&JVM_Yield}, 
	 {"sleep","(J)V",(void *)&JVM_Sleep}, 
	 {"currentThread","()" THD,(void *)&JVM_CurrentThread}, 
	 {"countStackFrames","()I",(void *)&JVM_CountStackFrames}, 
	 {"interrupt0","()V",(void *)&JVM_Interrupt}, 
	 {"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted}, 
	 {"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock}, 
	 {"getThreads","()[" THD,(void *)&JVM_GetAllThreads}, 
	 {"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads}, 
 };
     
     
     
     
到此,可以容易的看出 Java 线程调用 start 的方法,实际上会调用到 JVM_StartThread 方法,那这个方法又是怎样的逻辑呢。实际上,我们需要的是(或者说 Java 表现行为)该方法最终要调用 Java 线程的 run 方法,事实的确如此。
Java 线程的创建调用过程如上图所示,首先 , Java 线程的 start 方法会创建一个本地线程(通过调用 JVM_StartThread),该线程的线程函数是定义在 jvm.cpp 中的 thread_entry(这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数),由其再进一步调用 run 方法。可以看到 Java 线程的 run 方法和普通方法其实没有本质区别,直接调用 run 方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。

3有哪些不同的线程生命周期?


新建状态:用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内存 
就绪状态:当一个线程创建了以后,其他的线程调用了它的start()方法,该线程就进入了就绪状态。处于这个状态的线程位于可运行池中,等待获得CPU的使用权 
运行状态:处于这个状态的线程占用CPU,执行程序的代码 
阻塞状态:当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。 
阻塞状态分为三种情况: 
1、 位于对象等待池中的阻塞状态:当线程运行时,如果执行了某个对象的wait()方法,java虚拟机就回把线程放到这个对象的等待池中 
2、 位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的琐池中。 
3、 其它的阻塞状态:当前线程执行了sleep()方法,或者调用了其它线程的join()方法,或者发出了I/O请求时,就会进入这个状态中。
 4. 如何让正在运行的线程暂停一段时间?

我们可以使用Thread类的Sleep()方法让线程暂停一段时间。需要注意的是,这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为Runnable,并且根据线程调度,它将得到执行。

5.sleep和await()的区别

1)、这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

sleepThread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了bsleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep

2)、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,

要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。

sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。

Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。

3)、使用范围:waitnotifynotifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

   synchronized(x){

      x.notify()

     //或者wait()

   }

4)sleep必须捕获异常,而waitnotifynotifyAll不需要捕获异常

6. 你对线程优先级的理解是什么?

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

7 Java中Runnable和Callable有什么不同?

  Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。

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

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

9A,B两个线程同时启动,但让A结束后B在结束,如何实现

使用thread.join

详情参见另一篇整理:待补充。

10.线程之间是如何通信的?

当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。Object类中wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。当然,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。condition的梳理,待补充。

11.Java中notify 和 notifyAll有什么区别?

 都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notifynotifyAll,它们等待的是被notifynotifyAll,而不是锁。

12.为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

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

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

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

14. 为什么Thread类的sleep()和yield()方法是静态的?

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

15. volatile关键字在Java中有什么作用?

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。
volatile包含以下语义:
(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。
(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

参见深入volatile

16  什么是ThreadLocal?

ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。

当然面试官不会这样直接问,会问你一个适用于threadlocal的场景,让你回答,待整理,详见:***。

17 什么是Java线程转储(Thread Dump),如何得到它?

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

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

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

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

避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁

19如何中断一个线程?线程在那种状态才可以中断?

 中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果. 所以你看到Thread.suspend, Thread.stop等方法都被Deprecated了.

中断是通过调用Thread.interrupt()方法来做的. 这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false.

当然这不是最好的办法,可以自己动手测试下,经常在调用Thread.interrupt()之后,原来的线程不会立即停下,还在运行。推荐方法如下:使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量(尤其在冗余操作期间),然后有秩序地中止任务。

public class BestPractice extends Thread {

private volatile boolean finished = false; // ① volatile条件变量

public void stopMe() {

 finished = true; // ② 发出停止信号

 }

@Override

public void run() {

while (!finished) { // ③ 检测条件变量

 // do dirty work // ④业务代码

 } } }

20、 Java中interrupted 和 isInterruptedd方法的区别?

  interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。


********************

先整理到这里吧,其余在多线程部分整理。

引用:
http://www.ibm.com/developerworks/cn/java/j-lo-processthread/

你可能感兴趣的:(java并发系列:一线程常见问题)