进程和线程

1、基本概念:

1.1定义

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

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

 1.2关系及区别

进程和线程的关系:

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、 线程的生命周期:

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.4、线程死亡

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

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

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

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

 

3、创建多线程

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、并发

4.1.1 并发与并行

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

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

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

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

4.1.2 顺序编程与并发编程

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

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

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

4.1.3 并发编程的优势

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

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

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

 

4.2、多任务、多进程、多线程

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

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)应用也需要启动单独的线程从主机环境收集用户界面事件……总之,多线程在实际编程中的应用是非常广泛的。

 

5、多线程和异步操作:

多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别。

 

异步操作的本质

  所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对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操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。

 

6、多线程提问:

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

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

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

2、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;

 

3、在java中wait和sleep方法的不同?

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

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

 

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

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

 

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

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

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

 

4、什么是原子操作,Java中的原子操作是什么?

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

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

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

 

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

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

当由于事件次序异常而造成对同一资源的竞争,从而导致程序无法正常运行时,就会出现“竞争条件”。

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

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

 

6、你将如何使用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

 

7、为什么我们调用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()方法,不能多次启动一个线程;

 

8、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 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。

 

9、在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个线程,他们之间任何一个没有完成,所有的线程都必须等待。

 

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

不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可变对象对于缓存是非常好的选择,因为你不需要担心它的值会被更改。

创建一个不可变类:

将类声明为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个顶级多线程面试题及答案

以操作系统的角度述说线程与进程

 

posted @ 2018-01-29 16:27  小项目笔记  阅读(789)  评论(0编辑  收藏  举报

更多文章请关注公众号:小项目笔记

小项目笔记