进程和线程 Java多线程学习(三)---线程的生命周期 15个顶级多线程面试题及答案

一、基本概念:html

1.1定义java

进程是具备必定独立功能的程序关于某个数据集合的一次运行活动,进程是操做系统进行资源分配和调度的一个独立单位;它能够申请和拥有系统资源,是一个动态的概念,是一个活动的实体,它不仅是程序的代码,还包括当前的活动,经过程序计数器的值和处理寄存器的内容来表示。面试

线程是进程的一个实体,是cpu调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程本身基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(程序计数器,一组寄存器和栈),可是它可与同属一个进程的其余的线程共享进程所拥有的所有资源。算法

 1.2关系及区别sql

进程和线程的关系:数据库

1 一个程序至少有一个进程,一个进程至少有一个线程;线程是指进程内的一个执行单元,也是进程内的可调度实体。资源分配给进程,同一进程的全部线程共享该进程的全部资源;编程

2 一个线程能够建立和撤销另外一个线程,同一个进程中的多个线程之间能够并发执行,相对于进程而言,线程是一个更加接近于执行体的概念,它能够与同进程中的其余线程共享数据,但拥有本身的栈空间,拥有独立的执行序列。浏览器

3 线程在执行过程当中,须要协做同步。不一样进程的线程间要利用消息通讯的办法实现同步。缓存

 

进程和线程的区别:安全

1)  资源占用:进程是拥有资源的一个独立单位,进程间相互独立,某进程内的线程在其它进程内不可见;线程不拥有资源,但能够访问隶属于进程的资源。

2)  通讯:进程间通讯IPC,线程间能够直接读写进程数据段(全局变量)来进行通讯—须要进程同步和互斥手段的辅助,以保证数据的一致性。

3)  调度和切换:线程上下文切换比进程上下文切换要快的多。线程做为调度和分配的基本单位,进程做为拥有资源的基本单位;

4)  线程在执行过程当中与进程仍是有区别的。每一个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但多线程不可以独立执行,必须依存在应用程序中,有应用程序提供多个线程执行控制;

5)  从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分能够同时执行,但操做系统并无将多个线程看作多个独立的应用,来实现进程的调度和管理以及资源分配。

进程和线程的主要差异在于它们是不一样的操做系统资源管理的方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径,线程有本身的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉等于整个进程死掉,因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且要共享某些变量的并发操做,只能用线程,不能用进程。

优缺点:

线程执行开销小,但不利于资源的管理和保护,进程正好相反。同时,线程适合于在SMP机器上运行,而进程则能够跨机器迁移。

 1.3另类解释

进程和线程是操做系统的基本概念:

1 计算机的核心是CPU,它承担了全部的计算任务,时刻在运行。CPU相似工厂,假定工厂电力有限,一次只能供给一个车间使用,也就是说,一个车间开工的时候,其余车间都必须停工。含义是:单个cpu一次只能运行一个任务;

2 进程比如工厂的车间,它表明cpu所能处理的单个任务,任一时刻,cpu老是运行一个进程,其余进程处于非运行状态;

3 一个车间里,能够有不少工人,它们协同完成一个任务;线程就比如车间里的工人。一个进程能够包括多个线程。

4 车间的空间是工人们共享的,好比许多房间是每一个工人均可以进出的。这象征一个进程的内存空间是共享的,每一个线程均可以使用这些共享内存。

5 但是,每间房间的大小不一样,有些房间最多只能容纳一我的,好比厕所。里面有人的时候,其余人就不能进去了。这表明一个线程使用某些共享内存时,其余线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

6 还有些房间,能够同时容纳n我的,好比厨房。也就是说,若是人数大于n,多出来的人只能在外面等着。这比如某些内存区域,只能供给固定数目的线程使用。

这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种作法叫作"信号量"(Semaphore),用来保证多个线程不会互相冲突。

不难看出,mutex是semaphore的一种特殊状况(n=1时)。也就是说,彻底能够用后者替代前者。可是,由于mutex较为简单,且效率高,因此在必须保证资源独占的状况下,仍是采用这种设计。

 

操做系统的设计,所以能够归结为三点:

(1)以多进程形式,容许多个任务同时运行;

(2)以多线程形式,容许单个任务分红不一样的部分运行;

(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另外一方面容许进程之间和线程之间共享资源。

 

二、 线程的生命周期:

2.1概述:

 

当线程被建立并启动之后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要通过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤为是当线程启动之后,它不可能一直"霸占"着CPU独自运行,CPU须要在多条线程之间切换,因而线程状态也会屡次在运行、阻塞之间切换

1. 新建状态,当程序使用new关键字建立了一个线程以后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

2. 就绪状态,当线程对象调用了start()方法以后,该线程处于就绪状态。Java虚拟机会为其建立方法调用栈和程序计数器,等待调度运行。

3. 运行状态,若是处于就绪状态的线程得到了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

4. 阻塞状态,当处于运行状态的线程失去所占用资源以后,便进入阻塞状态。

5. 在线程的生命周期当中,线程的各类状态的转换过程

2.2新建和就绪状态

当程序使用new关键字建立了一个线程以后,该线程就处于新建状态,此时它和其余的Java对象同样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法以后,该线程处于就绪状态。Java虚拟机会为其建立方法调用栈和程序计数器,处于这个状态中的线程并无开始运行,只是表示该线程能够运行了。至于该线程什么时候开始运行,取决于JVM里线程调度器的调度。

注意:启动线程使用start()方法,而不是run()方法。永远不要调用线程对象的run()方法。调用start0方法来启动线程,系统会把该run()方法当成线程执行体来处理;但若是直按调用线程对象的run()方法,则run()方法当即就会被执行,并且在run()方法返回以前其余线程没法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。须要指出的是,调用了线程的run()方法以后,该线程已经再也不处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,不然将引起IllegaIThreadStateExccption异常。

调用线程对象的start()方法以后,该线程当即进入就绪状态——就绪状态至关于"等待执行",但该线程并未真正进入运行状态。若是但愿调用子线程的start()方法后子线程当即开始执行,程序能够使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,由于在这1毫秒内CPU不会空闲,它会去执行另外一个处于就绪状态的线程,这样就可让子线程当即开始执行。

2.3运行和阻塞状态
2.3.1线程调度

若是处于就绪状态的线程得到了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,若是计算机只有一个CPU。那么在任什么时候刻只有一个线程处于运行状态,固然在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。

当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程当中须要被中断,目的是使其余线程得到执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每一个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其余线程得到执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

全部现代的桌面和服务器操做系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协做式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。

2.3.2 线程阻塞

当发生以下状况时,线程将会进入阻塞状态

① 线程调用sleep()方法主动放弃所占用的处理器资源

② 线程调用了一个阻塞式IO方法,在该方法返回以前,该线程被阻塞

③ 线程试图得到一个同步监视器,但该同步监视器正被其余线程所持有。

④ 线程在等待某个通知(notify)

⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易致使死锁,因此应该尽可能避免使用该方法

当前正在执行的线程被阻塞以后,其余线程就能够得到执行的机会。被阻塞的线程会在合适的时候从新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须从新等待线程调度器再次调度它。

2.3.3 解除阻塞

针对上面几种状况,当发生以下特定的状况时能够解除上面的阻塞,让该线程从新进入就绪状态:

① 调用sleep()方法的线程通过了指定时间。

② 线程调用的阻塞式IO方法已经返回。

③ 线程成功地得到了试图取得的同步监视器。

④ 线程正在等待某个通知时,其余线程发出了个通知。

⑤ 处于挂起状态的线程被调甩了resdme()恢复方法。

 

从上图能够看出,线程从阻塞状态只能进入就绪状态,没法直接进入运行状态。而就绪和运行状态之间的转换一般不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程得到处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可让运行状态的线程转入就绪状态。关于yield()方法后面有更详细的介纽。

2.四、线程死亡

线程会以以下3种方式结束,结束后就处于死亡状态:

① run()或call()方法执行完成,线程正常结束。

② 线程抛出一个未捕获的Exception或Error。

③ 直接调用该线程stop()方法来结束该线程——该方法容易致使死锁,一般不推荐使用。

 

三、建立多线程

1. 经过继承Thread类来建立并启动多线程的方式

2. 经过实现Runnable接口来建立并启动线程的方式

3. 经过实现Callable接口来建立并启动线程的方式

java:

建立Thread子类的一个实例并重写run方法,run方法会在调用start()方法以后被执行。

public class Demo extends Thread {
    int i = 0;
    
    public void run(){
        for(;i< 10;i++){
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int k = 0;k < 20;k++){
            System.out.println(Thread.currentThread().getName() + " : " + k);
            if(k == 5){
                new Demo().start();
            }
        }
    }

}
View Code

编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法能够被线程调用。

public class Demo implements Runnable{
    public void run(){
        for(int i = 0;i< 10;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }
    
    public static void main(String[] args){
        for(int k=0;k< 20;k++){
            System.out.println(Thread.currentThread().getName() + " : " + k);
            if(k == 5){
                Runnable oneRunnable = new Demo();
                Thread oneThread = new Thread(oneRunnable);
                oneThread.start();
            }
        }
        
    }
}
View Code

 

public class Demo implements Callable<Integer>{
    
    @Override
    public Integer call() throws Exception{
        int i = 0;
        for(;i< 10;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
        return i;
    }
    
    public static void main(String[] args){
        Demo demo = new Demo();
        FutureTask<Integer> ft = new FutureTask<>(demo);
        
        for(int k=0;k< 20;k++){
            System.out.println(Thread.currentThread().getName() + " : " + k);
            if(k == 5){
                Thread oneThread = new Thread(ft,"有返回值的线程");
                oneThread.start();
            }
        }
        
        try{
            System.out.println("子线程的返回值:" + ft.get());
        }catch(InterruptedException e){
            e.printStackTrace();
        }catch(ExecutionException e){
            e.printStackTrace();
        }
    }
}
View Code

 

采用实现Runnable、Callable接口的方式创见多线程时,

优点是:线程类只是实现了Runnable接口或Callable接口,还能够继承其余类。在这种方式下,多个线程能够共享同一个target对象,因此很是适合多个相同线程来处理同一份资源的状况,从而能够将CPU、代码和数据分开,造成清晰的模型,较好地体现了面向对象的思想。

劣势是:编程稍微复杂,若是要访问当前线程,则必须使用Thread.currentThread()方法。

 

使用继承Thread类的方式建立多线程时优点是:编写简单,若是须要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this便可得到当前线程。

劣势是:线程类已经继承了Thread类,因此不能再继承其余父类。

四、并发与多线程

4.一、并发

4.1.1 并发与并行

首先介绍一下并发与并行,二者虽然只有一字之差,但实际上却有着本质的区别,其概念以下:

并行性(parallel):指在同一时刻,有多条指令在多个处理器上同时执行;

并发性(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具备多个进程同时执行的效果。

并发的关键是你有处理多个任务的能力,不必定要同时;并行的关键是你有同时处理多个任务的能力;

4.1.2 顺序编程与并发编程

在解决编程问题时,一般使用顺序编程来解决,即程序中的全部事物在任意时刻都只能执行一个步骤。然而对于某些问题,但愿可以并行地执行程序中的多个部分,来达到咱们想要的效果。在单处理器机器中,能够将程序划分为多个部分,而后每一个部分由该处理器并发执行。在多处理器机器中,咱们能够将程序划分多个部分,而后每一个部分分别在多个处理器上并行执行。固然为了更加充分利用CPU资源,咱们也能够在多个处理器上并发执行,那么在这咱们就涉及到了另外一种编程模式了并发编程。

并发编程又叫多线程编程。并发编程使咱们能够将程序划分为多个分离的、独立运行的任务。经过使用多线程机制,每一个独立任务都将由线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,单个进程能够拥有多个"并发执行"的任务。这样使程序的每一个任务,都好像拥有一个本身的CPU同样。但其底层机制仍是是切分CPU时间,CPU都有个时钟频率,表示每秒中能执行CPU指令的次数。在每一个时钟周期内,CPU实际上只能去执行一条也有可能多条指令。操做系统将进程进行管理,轮流分配每一个进程很短的一段是时间但不必定是均分,而后在每一个进程内部,程序代码本身处理该进程内部线程的时间分配,多个线程之间相互的切换去执行,这个切换时间也是很是短的因此一般咱们不须要考虑它。

并发是指"发",不是处理,最多见的状况就是许多人在一小段时间内都点击了你的网站,发出了处理请求。并发编程是对并发情况的应对,在单处理器和多处理器机器上均可对其进行应对,可这个处理方案和架构以及算法有关。CPU通常是分时的,会在极短的时间内不停地切换给不一样的线程使用,不管多少并发都会处理下去,只是时间问题,如何提升处理效率就看采用的技术了。

4.1.3 并发编程的优点

并发编程能够使咱们的程序执行速度获得提升,例如,若是你有一台多处理器的机器,那么就能够在这些处理器之间分布多个任务,从而能够极大地提升吞吐量。这是Web服务器的常见状况,通常Web服务器是一个多处理器机器,将为每一个请求分配到一个线程中,那么就能够将大量的用户请求分布到多个CPU上进行并发处理。

可是,并发一般是提升运行在单处理器上的程序的性能。虽然,在单处理器上运行的并发程序开销确实应该比该程序的全部部分都顺序执行的开销大,由于其中增长了所谓上"下文切换"的代价,即从一个任务切换到另外一个任务。表面上看,将程序的全部部分看成单个的任务运行好像是开销更小一点,而且能够节省上下文切换的代价。可是咱们的程序并不会按咱们设想的那样一直正常运行,它会发生阻塞。若是程序中的某个任务由于某些缘由发生了阻塞,那么该任务将不能继续执行。若是没有并发,则整个程序都将中止下来,直至外部条件发生变化。可是,若是使用并发来编写程序,那么当一个任务阻塞时,程序中的其余任务还能够继续执行,所以这个程序能够保持继续向前执行,这样就提升程序的执行效率和运行性能。

并发须要付出代价,包含复杂性代价,可是这些代价与在程序设计、资源负载均衡以及用户方便使用方面的改进相比,就显得微不足道了。一般,线程使你可以建立更加松散耦合的设计则,你的代码中各个部分都必须显式地关注那些一般能够由线程来处理的任务。

 

4.二、多任务、多进程、多线程

几乎全部的操做系统都支持同时运行多个任务,一个任务一般就是一个程序,每一个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每一个顺序执行流就是一个线程。

4.2.1 多进程

实现并发最直接的方式是在操做系统级别使用进程,进程是运行在它本身的地址空间内的自包容的程序。多任务操做系统能够经过周期性地将CPU从一个进程切换到另外一个进程,来实现同时运行多个进程。 尽管对于一个CPU而言,它在某个时间点只能运行一个进程,但CPU能够在多个进程之间进行轮换执行,而且CPU的切换速度极高,使咱们没法感知其切换的过程,就好像有多个进程在同时执行。

几乎全部的操做系统都支持进程的概念,全部运行中的任务一般对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程当中的程序,而且具备必定的独立功能,进程是系统进行资源分配和调度的一个独立单位。通常而言,进程包含以下3个特征。

■ 独立性:进程是系统中独立存在的实体,它能够拥有本身独立的资源,每个进程都拥有本身私有的地址空间。在没有通过进程自己容许的状况下,一个用户进程不能够直接访问其余进程的地址空间。

■ 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具备本身的生命周期和各类不一样的状态,这些概念在程序中部是不具有的。

■ 并发性:多个进程能够在单个处理器上并发执行,多个进程之间不会互相影响。

现代的操做系统都支持多进程的并发,但在具体的实现细节上可能由于硬件和操做系统的不一样而采用不一样的策略。比较经常使用的方式有:共用式的多任务操做策略,例如Windows 3.1和Mac OS 9。目前操做系统大多采用效率更高的抢占式多任务操做策略,例如 VVindows NT、Windows 2000以及UNIX/Linux等操做系统。但对进程的并发一般会有数量和开销的限制,以免它们在不一样的并发系统之间的可应用性。为了应对该问题,因此在多进程的基础上提出了多线程的概念,下面将详细介绍。

4.2.2 多线程

4.2.2.1 多线程概述

多线程则扩展了多进程的概念。使得同一个进程中也能够同时并发处理多个任务。线程(Thread)也被称做轻量级进程(Lightweight Process)。线程是进程的执行单元,就像进程在操做系统中的地位同样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被建立了。对于绝大多数的应用程序来讲,一般仅要求有一个主线程,但也能够在该进程内建立多条顺序执行流,这些顺序执行流就是线程,每一个线程也是互相独立的。

线程是进程的组成部分,一个进程能够拥有多个线程,一个线程必须有一个父进程。线程能够拥有本身的堆栈、本身的程序计数器和本身的局部变量,但不拥有系统资源,它与父进程的其余线程共享该进程所拥有的所有资源。由于多个线程共享父进程里的所有资源,所以编程更加方便;但必须更加当心,咱们必须确保线程不会妨碍同一进程里的其余线程。

4.2.2.2 多线程机制

线程模型为编程带来了便利,它简化了在单一程序中同时交织在一块儿的多个操做的处理。在使用线程时,CPU将轮流给每一个任务分配其占用时间。每一个任务都以为本身在一直占用CPU,但事实上CPU时间是划分红片断分配给了全部的任务。线程的一大好处是能够使你从这个层次抽身出来,即代码没必要知道它是运行在具备一个仍是多个CPU的机器上。因此,使用线程机制是一种创建透明的、可扩展的程序的方法,若是程序行得太慢,为机器增添一个CPU就能很容易地加快程序的运行速度。多任务和多线程每每是使用多处理器系统的最合理方式。

4.2.2.3 多线程调度

线程能够完成必定的任务,能够与其余线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。线程是独立运行的,它并不知道进程中是否还有其余线程存在,线程的执行是抢占式的,也就是说,当前运行的线程在任什么时候候均可能被挂起,以便另一个线程能够运行。

一个线程能够建立和撤销另外一个线程,同一个进程中的多个线程之间能够并发执行。从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中能够有多个执行部分同时执行,但操做系统无须将多个线程看做多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程自己负责完成。

概括起采能够这样说:操做系统能够同时执行多个任务,每一个任务就是进程;进程能够同时执行多个任务,每一个任务就是线程。简而言之,一个程序运行后至少有一个进程,一个进程里能够包含多个线程,但至少要包含一个线程。

4.2.2.4 多线程的优点

线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程之间的隔离程度要小:

01. 它们共享内存、文件句柄和其余每一个进程应有的状态。由于线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程当中拥有独立的内存单元,而多个线程共享内存,从而极大地提升了程序的运行效率。

02. 线程比进程具备更高的性能,这是因为同一个进程中的线程都有共性----多个线程共享同一个进程虚拟空间。线程共享的环境包括进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通讯。

03. 当操做系统建立一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但建立一个线程则简单得多,所以使用多线程来实现并发比使用多进程实现并发的性能要高得多。

 

总结起来,使用多线程编程具备以下几个优势:

01. 进程之间不能共享内存,但线程之间共享内存很是容易

02. 系统建立进程时须要为该进程从新分配系统资源,但建立线程则代价小得多,所以使用多线程来实现多任务并发比多进程的效率高

03. Java语言内置了多线程功能支持,而不是单纯地做为底层操做系统的调度方式,从而简化了Java的多线程编程

 

在实际应用中,多线程是很是有用的,一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;Java虚拟机自己就在后台提供了一个超级线程来进行垃圾回收;图形用户界面(GUI)应用也须要启动单独的线程从主机环境收集用户界面事件……总之,多线程在实际编程中的应用是很是普遍的。

 

五、多线程和异步操做:

多线程和异步操做二者均可以达到避免调用线程阻塞的目的,从而提升软件的可响应性。甚至有些时候咱们就认为多线程和异步操做是等同的概念。可是,多线程和异步操做仍是有一些区别的。而这些区别形成了使用多线程和异步操做的时机的区别。

 

异步操做的本质

  全部的程序最终都会由计算机硬件来执行,因此为了更好的理解异步操做的本质,咱们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友确定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候能够不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始本身和内存交换数据,在传输完成以后硬件会触发一个中断来通知操做完成。这些无须消耗CPU时间的I/O操做正是异步操做的硬件基础。因此即便在DOS这样的单进程(并且无线程概念)系统中也一样能够发起异步的DMA操做。

 

异步操做的优缺点:

  由于异步操做无须额外的线程负担,而且使用回调的方式进行处理,在设计良好的状况下,处理函数能够没必要使用共享变量(即便没法彻底不用,最起码能够减小共享变量的数量),减小了死锁的可能。固然异步操做也并不是完美无暇。编写异步操做的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思惟方式有些初入,并且难以调试。

线程的本质:线程不是一个计算机硬件的功能,而是操做系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,因此线程须要操做系统投入CPU资源来运行和调度。

 

多线程的优缺点:多线程的优势很明显,线程中的处理程序依然是顺序执行,符合普通人的思惟习惯,因此编程简单。可是多线程的缺点也一样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。而且线程间的共享变量可能形成死锁的出现。

适用范围:

当须要执行I/O操做时,使用异步操做比使用线程+同步I/O操做更合适。I/O操做不只包括了直接的文件、网络的读写,还包括数据库操做、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。

而线程的适用范围则是那种须要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。可是每每因为使用线程编程的简单和符合习惯,因此不少朋友每每会使用线程来执行耗时较长的I/O操做。这样在只有少数几个并发操做的时候还无伤大雅,若是须要处理大量的并发操做时就不合适了。

 

六、多线程提问:

一、在Java中Lock接口比synchronized块的优点是什么?你须要实现一个高效的缓存,它容许多个用户读,但只容许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock接口在多线程和并发编程中最大的优点是它们为读和写分别提供了锁,它能知足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java的读写锁能够实现上述请求。

通常用lock或者 readwritelock时,须要把unlock方法放在一个 fianlly 块中,由于程序运行的时候可能会出现一些咱们人为控制不了的因素,致使锁一直没释放,那其余线程就进不来了。

二、join实现:

  1. public final synchronized void join(long millis)    throws InterruptedException {  
  2.         long base = System.currentTimeMillis();  
  3.         long now = 0;  
  4.   
  5.         if (millis < 0) {  
  6.             throw new IllegalArgumentException("timeout value is negative");  
  7.         }  
  8.           
  9.         if (millis == 0) {  
  10. 10.             while (isAlive()) {  
  11. 11.                 wait(0);  
  12. 12.             }  
  13. 13.         } else {  
  14. 14.             while (isAlive()) {  
  15. 15.                 long delay = millis - now;  
  16. 16.                 if (delay <= 0) {  
  17. 17.                     break;  
  18. 18.                 }  
  19. 19.                 wait(delay);  
  20. 20.                 now = System.currentTimeMillis() - base;  
  21. 21.             }  
  22. 22.         }  
  23. 23.     }  

 

join() method suspends the execution of the calling thread until the object called finishes its execution.

好比在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B;

 

三、在java中wait和sleep方法的不一样?

最大的不一样是在等待时wait会释放锁,而sleep一直持有锁。Wait一般被用于线程间交互,sleep一般被用于暂停执行

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操做将会被阻塞,或者当队列是满时,往队列里添加元素的操做会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其余的线程往空的队列插入新的元素。一样,试图往已满的阻塞队列中添加新元素的线程一样也会被阻塞,直到其余的线程使队列从新变得空闲起来。

 

为何要使用生产者和消费者模式:

在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,若是生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。一样的道理,若是消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,因此便有了生产者和消费者模式。

 

什么是生产者消费者模式:

生产者消费者模式是经过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而经过阻塞队列来进行通信,因此生产者生产完数据以后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就至关于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。

 

四、什么是原子操做,Java中的原子操做是什么?

原子操做是指不会被线程调度机制打断的操做;这种操做一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另外一个线程)

JDK1.5的原子包:java.util.concurrent.atomic

这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具备排他性,即当某个线程进入方法,执行其中的指令时,不会被其余线程打断,而别的线程就像自旋锁同样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另外一个线程进入,这只是一种逻辑上的理解。其实是借助硬件的相关指令来实现的,不会阻塞线程(synchronized 会把别的等待的线程挂起)(或者说只是在硬件级别上阻塞了)。

 

五、什么是竞争条件?你怎样发现和解决竞争?

多个线程或者进程在读写一个共享数据时结果依赖于它们执行的相对时间,这种情形叫作竞争。竞争条件发生在当多个进程或者线程在读写数据时,其最终的的结果依赖于多个进程的指令执行顺序。

当因为事件次序异常而形成对同一资源的竞争,从而致使程序没法正常运行时,就会出现“竞争条件”。

竞争条件的典型解决方案是,确保程序在使用某个资源(好比文件、设备、对象或者变量) 时,拥有本身的专有权。得到某个资源的专有权的过程称为加锁。锁不太容易处理。死锁(“抱死,deadly embrace”)是常见的问题,在这种情形下,程序会因等待对方释放被加锁的资源而没法继续运行。 要求全部线程都必须按照相同的顺序(好比,按字母排序,或者从“largest grain”到“smallest grain”的顺序) 得到锁,这样能够避免大部分死锁。另外一个常见问题是活锁(livelock),在这种状况下,程序至少 成功地得到和释放了一个锁,可是以这种方式没法将程序再继续运行下去。若是一个锁被挂起,顺利地释放它会很难。简言之,编译在任何状况下均可以按须要正确地加锁和释放的程序一般很困难。

有时,能够一次执行一个单独操做来完成一些特殊的操做,从而使您不须要显式地对某个资源 进行加锁然后再解锁。这类操做称为“原子”操做,只要可以使用这类操做,它们一般是最好的解决方案。

 

六、你将如何使用thread dump?你将如何分析Thread dump?

在故障定位(尤为是out of memory)和性能分析的时候,常常会用到一些文件来帮助咱们排除代码问题。这些文件记录了JVM运行期间的内存占用、线程执行等状况,这就是咱们常说的dump文件。经常使用的有heap dump和thread dump(也叫javacore,或java dump)。咱们能够这么理解:heap dump记录内存信息的,thread dump是记录CPU信息的。

heap dump:

heap dump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用状况。HeapDump文件是指定时刻的Java堆栈的快照,是一种镜像文件。Heap Analyzer工具经过分析HeapDump文件,哪些对象占用了太多的堆栈空间,来发现致使内存泄露或者可能引发内存泄露的对象。

thread dump:

thread dump文件主要保存的是java应用中各线程在某一时刻的运行的位置,即执行到哪个类的哪个方法哪个行上。thread dump是一个文本文件,打开后能够看到每个线程的执行栈,以stacktrace的方式显示。经过对thread dump的分析能够获得应用是否“卡”在某一点上,即在某一点运行的时间太长,如数据库查询,长期得不到响应,最终致使系统崩溃。单个的thread dump文件通常来讲是没有什么用处的,由于它只是记录了某一个绝对时间点的状况。比较有用的是,线程在一个时间段内的执行状况。

两个thread dump文件在分析时特别有效,困为它能够看出在前后两个时间点上,线程执行的位置,若是发现前后两组数据中同一线程都执行在同一位置,则说明此处可能有问题,由于程序运行是极快的,若是两次均在某一点上,说明这一点的耗时是很大的。经过对这两个文件进行分析,查出缘由,进而解决问题。

http://blog.csdn.net/rachel_luo/article/details/8920596

https://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html

 

七、为何咱们调用start()方法时会执行run()方法,为何咱们不能直接调用run()方法?

当你调用start()方法时你将建立新的线程,而且执行在run()方法里的代码。可是若是你直接调用run()方法,它不会建立新的线程也不会执行调用线程的代码。

 

start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码:

经过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并无运行。而后经过此Thread类调用方法run()来完成其运行操做的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止,而CPU再运行其它线程。

 

run()方法看成普通方法的方式调用,程序仍是要顺序执行,仍是要等待run方法体执行完毕后才可继续执行下面的代码:

而若是直接用Run方法,这只是调用一个方法而已,程序中依然只有主线程--这一个线程,其程序执行路径仍是只有一条,这样就没有达到写线程的目的。

 

Thread类中run()和start()方法的区别以下:

run()方法:在本线程内调用该Runnable对象的run()方法,能够重复屡次调用;

start()方法:启动一个线程,调用该Runnable对象的run()方法,不能屡次启动一个线程;

 

八、Java中你怎样唤醒一个阻塞的线程?

1. sleep() 方法:sleep() 容许 指定以毫秒为单位的一段时间做为参数,它使得线程在指定的时间内进入阻塞状态,不能获得CPU 时间,指定的时间一过,线程从新进入可执行状态。

典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不知足后,让线程阻塞一段时间后从新测试,直到条件知足为止。不会释放占用锁。

2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,而且不会自动恢复,必须其对应的resume() 被调用,才能使得线程从新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另外一个线程产生的结果的情形:测试发现结果尚未产生后,让线程阻塞,另外一个线程产生告终果后,调用 resume() 使其恢复。

3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,可是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另外一个线程。

4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种容许指定以毫秒为单位的一段时间做为参数,另外一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程从新进入可执行状态,后者则必须对应的 notify() 被调用。

  初看起来它们与 suspend() 和 resume() 方法对没有什么分别,可是事实上它们是大相径庭的。区别的核心在于,前面叙述的全部方法,阻塞时都不会释放占用的锁(若是占用了的话),而这一对方法则相反。

上述的核心区别致使了一系列的细节上的区别。

首先,前面叙述的全部方法都隶属于 Thread 类,可是这一对却直接隶属于 Object 类,也就是说,全部对象都拥有这一对方法。初看起来这十分难以想象,可是实际上倒是很天然的,由于这一对方法阻塞时要释放占用的锁,而锁是任何对象都具备的,调用任意对象的 wait() 方法致使线程阻塞,而且该对象上的锁被释放。而调用 任意对象的notify()方法则致使因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到得到锁后才真正可执行)。

其次,前面叙述的全部方法均可在任何位置调用,可是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁能够释放。一样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁能够释放。所以,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不知足这一条件,则程序虽然仍能编译,但在运行时会出现 IllegalMonitorStateException 异常。

 

wait() 和 notify() 方法的上述特性决定了它们常常和synchronized 方法或块一块儿使用,将它们和操做系统的进程间通讯机制做一个比较就会发现它们的类似性:synchronized方法或块提供了相似于操做系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则至关于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得咱们能够实现操做系统上一系列精妙的进程间通讯的算法(如信号量算法),并用于解决各类复杂的线程间通讯问题。

 

关于 wait() 和 notify() 方法最后再说明两点:

  第一:调用 notify() 方法致使解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,咱们没法预料哪个线程将会被选择,因此编程时要特别当心,避免因这种不肯定性而产生问题。

  第二:除了 notify(),还有一个方法 notifyAll() 也可起到相似做用,惟一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的全部线程一次性所有解除阻塞。固然,只有得到锁的那一个线程才能进入可执行状态。

  谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用均可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,咱们在编程中必须当心地避免死锁。

 

九、在Java中CycliBarriar和CountdownLatch有什么区别?

CyclicBarrier和CountdownLatch是java 1.5中提供的一些很是有用的辅助类来帮助咱们进行并发编程。这两个的区别是CyclicBarrier能够重复使用已经经过的障碍,而CountdownLatch不能重复使用。

CountdownLatch:

一个线程(或者多个),等待另外N个线程完成某个事情以后才能执行。是并发包中提供的一个可用于控制多个线程同时开始某个动做的类,其采用的方法为减小计数的方式,当计数减至零时位于latch.Await()后的代码才会被执行,CountDownLatch是减计数方式,计数==0时释放全部等待的线程;CountDownLatch当计数到0时,计数没法被重置;

CyclicBarrier:

字面意思回环栅栏,经过它能够实现让一组线程等待至某个状态以后再所有同时执行。叫作回环是由于当全部等待线程都被释放之后,CyclicBarrier能够被重用。 即:N个线程相互等待,任何一个线程完成以前,全部的线程都必须等待。CyclicBarrier是当await的数量到达了设置的数量的时候,才会继续往下面执行,CyclicBarrier计数达到指定值时,计数置为0从新开始。

对于CountDownLatch来讲,重点是那个“一个线程”,是它在等待,而另外那N的线程在把“某个事情”作完以后能够继续等待,能够终止。而对于CyclicBarrier来讲,重点是那N个线程,他们之间任何一个没有完成,全部的线程都必须等待。

 

十、什么是不可变对象,它对写并发应用有什么帮助?

不可变对象是指一个对象的状态在对象被建立以后就再也不变化。不可变对象对于缓存是很是好的选择,由于你不须要担忧它的值会被更改。

建立一个不可变类:

将类声明为final,因此它不能被继承;

将全部的成员声明为私有的,这样就不容许直接访问这些成员;

对变量不要提供setter方法;

将全部可变的成员声明为final,这样只能对它们赋值一次;

经过构造器初始化全部成员,进行深拷贝(deep copy);

在getter方法中,不要直接返回对象自己,而是克隆对象,并返回对象的拷贝;

在Java中, String类是不可变的。那么到底什么是不可变的对象呢? 能够这样认为:若是一个对象,在它建立完成以后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其余的对象,引用类型指向的对象的状态也不能改变。

 

区分对象和对象的引用

对于Java初学者, 对于String是不可变对象老是存有疑惑。看下面代码:

String s = "ABCabc"; 

System.out.println("s = " + s); 

 

s = "123456"; 

System.out.println("s = " + s); 

 

打印结果为:

s = ABCabc

s = 123456

 

首先建立一个String对象s,而后让s的值为“ABCabc”, 而后又让s的值为“123456”。 从打印结果能够看出,s的值确实改变了。那么怎么还说String对象是不可变的呢? 其实这里存在一个误区: s只是一个String对象的引用,并非对象自己。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,经过这个地址能够访问对象。

也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过以后,又建立了一个新的对象“123456”, 而引用s从新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并无改变。

 

String类不可变性的好处

String是全部语言中最经常使用的一个类。咱们知道在Java中,String是不可变的、final的。Java在运行时也保存了一个字符串池(String pool),这使得String成为了一个特别的类。

String类不可变性的好处

1.只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现能够在运行时节约不少heap空间,由于不一样的字符串变量都指向池中的同一个字符串。但若是字符串是可变的,那么String interning将不能实现(译者注:String interning是指对不一样的字符串仅仅只保存一个,即不会保存多个相同的字符串。),由于这样的话,若是变量改变了它的值,那么其它指向这个值的变量的值也会一块儿改变。

2.若是字符串是可变的,那么会引发很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来得到数据库的链接,或者在socket编程中,主机名和端口都是以字符串的形式传入。由于字符串是不可变的,因此它的值是不可改变的,不然黑客们能够钻到空子,改变字符串指向的对象的值,形成安全漏洞。

3.由于字符串是不可变的,因此是多线程安全的,同一个字符串实例能够被多个线程共享。这样便不用由于线程安全问题而使用同步。字符串本身即是线程安全的。

4.类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改为了myhacked.Connection,那么会对你的数据库形成不可知的破坏。

5.由于字符串是不可变的,因此在它建立的时候hashcode就被缓存了,不须要从新计算。这就使得字符串很适合做为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键每每都使用字符串。

 

 

引用:

Java多线程学习(三)---线程的生命周期

15个顶级多线程面试题及答案

相关文章
相关标签/搜索