进程和线程分别是什么?为什么引入线程

一、进程的概念
进程是程序的一次执行过程,是系统进行资源分配的一个独立单位。内存中可以有多个进程实体,操作系统为了方便管理各进程,会为每个进程提供一个进程控制块PCB,而PCB,程序段和数据段则共同构成了进程实体。由于各进程以不可预知的速度向前推进,可能导致运行结果的不确定性

二、线程的概念
线程和进程相似,但线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。与进程不同的是同一进程的不同线程间共享进程的堆和方法区资源。所以系统在产生一个线程或是在各个线程之间做切换工作时,负担要比进程小得多。但每个线程有自己的程序计数器,虚拟机栈和本地方法栈。

三、程序计数器为什么是私有的
程序技术器主要有两个作用:
1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制(如:顺序执行,循环,异常处理等)
2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了

四、虚拟机栈和本地方法栈为什么是私有的?
虚拟机栈:每个java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在java虚拟机栈中入栈和出栈的过程。
本地方法栈:和虚拟机栈所发挥的作用非常相似。区别是:虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。
\color{red}{所以为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。}

五、为什么引入线程
引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务。并且引入线程之后,进程只作为除CPU之外的系统资源的分配单元(像打印机、内存地址空间等都是分配给进程的)

六、为什么要使用多线程
引入多线程最主要的原因是为了提高资源的利用率。现在的cpu都是多核心的这意味着多个线程可以同时运行,从而可以减少了线程上下文切换的开销。第二点是可以防止阻塞。如果单核cpu使用单线程,那么只要这个线程被阻塞了,那么整个程序也就被阻塞。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

:Tomcat是以多线程去响应请求的,我们可以在server.xml中配置连接池。Tomcat处理每一个用户请求都会从连接池里边用一个线程去处理,然后这个请求线程找到对应的servlet资源执行service方法

七、那线程是不是越多越好呢
1.线程在java中是一个对象,每一个java线程都需要一个操作系统线程支持,此处涉及到用户态和内核态切换。所以大量的线程创建和销毁需要花费大量的时间。如果创建时间+销毁时间>执行任务时间就很不合算。
2.java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小为1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗更多内存。
3.线程一多,cpu需要频繁切换线程上下文,影响性能。

八、什么是上下文切换
多线程编程中一般线程的个数都大于cpu核心的个数,而一个cpu核心在任意时刻只能被一个线程使用,为了让这些线程能够得到有效执行,cpu采取的策略是为每个线程分配时间片并轮转的形式,当当前任务执行完cpu时间片切换到另一个任务的时候为了方便下次再切换会这个任务时会先保存自己的状态。而任务状态从保存再加载的过程就是一次上下文切换

九、进程的通信方式
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。


image.png

进程的同信方式:
1.管道通信:是指在一个时间段内,只能由一个进程单向的通信。如果要双向通信,需要创建两个管道

:所谓的管道,就是内核里面的一串缓存。从管道的一端写入的数据,实际上是缓存在内核中的,另一端读取。这种方式:简单但是效率低。并且在需要双向通信的时候,往往需要通过创建两个管道来避免两个进程同时写入造成的混乱。所以管道不适合进程间频繁的交换数据。

2.:消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体。所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。这种方式存在不足的地方:
一是通信不及时;
二是附件也有大小限制。消息队列不适合比较大数据的传输;
三:消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

3.:消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式就很好的解决了这一方式。
对于内存管理机制,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以即使进程A和进程B的虚拟地址是一样的,其实访问的是不同的的物理内存地址,对于数据的增删改查互不影响。

这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

image.png

4.信号量
用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改用一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
\color{red}{信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。}

5.Socket
管道,消息队列,共享内存,信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要Socket通信了。

总结:消息队列克服了管道通信的数据是无格式的字节流的问题。消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问。带来新的问题当多进程竞争同个共享资源会造成数据的错乱。

那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步

前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。

以上是进程间通信的主要机制,那么线程通信的方式呢?
同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步

十、线程的通信方式
1.volatile
2.等待/通知方式wait/notify
3.join方式
4.threadLocal

十一、谈谈对线程安全的理解
当多个线程访问同一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么可以认为这个对象是线程安全的。

十二、可以用什么手段来解决线程的安全性问题
1.使用synchronized关键字
2.使用lock锁
3.使用volatile,它可以保证共享变量在各线程间的可见性,但不能保证数据的原子性
4.jdk1.5并发包中提供的Atomic原子类

十三、谈谈对线程死锁的理解
首先死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源的释放。由于线程被无限期阻塞,所以程序不能正常终止。


DeadLock.png

其次产生死锁必须满足4个条件:
1.互斥条件:该资源任意一个时刻只由一个线程占用
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3.不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

十四、如何避免死锁
我们知道产生死锁有4个必要条件。为了避免死锁,我们只要破坏其中一个就可以成功预防死锁的发生。第一个是互斥条件但一般不会对互斥条件进行破坏。因为某些资源就只能一个人使用,不能同时多人使用。主要对其他三个条件进行破坏。
:每个请求一次性申请所有需要的资源如果无法一次性申请所有的资源就进行等待。
:用部分资源的线程进一步申请其他资源时,如果申请不到,便需要主动释放它占有的资源
:给每个资源都标上一个序号,按序申请,这样线性化后申请资源就不会存在一个循环等待的条件。

十五、谈谈多线程为什么需要加锁?
因为同一进程的不同线程共享进程的堆和方法去资源。如果不加锁,那么会发送数据竞争,读取到脏数据。

十六、什么是乐观锁和悲观锁?
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止

十七、CAS(乐观锁)
CAS是一种无锁算法,它可以实现在不使用锁的情况下实现多线程之间的变量同步,也就是在线程不被阻塞的情况下实现变量的同步。其中在并发包下的原子类例如AtomixBoolean, AtomicInteger, AtomicLong的实现是利用了CAS机制。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值改为B.如果不同,说明已经有其他线程做了更新,则当前线程被告知失败,允许再次尝试或者放弃操作。并且CAS 返回当前 V 的真实值。

CAS的缺点:
1.CPU开销大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复会给CPU带来很大的压力。
2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

十八、
1.说说对与synchronized关键字的理解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。1.6版本之前synchronized 属于 重量级锁,效率低下。1.6版本之后进行了锁升级的优化。

2.说说是怎么使用synchronized关键字
synchronized 关键字最主要的三种使用方式:
<1>.修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
<2>.修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
synchronized void staic method() {
//业务代码
}
<3>.修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。
synchronized(this) {
//业务代码
}

3.构造方法可以使用synchronized关键字修饰么?
构造方法本身就属于线程安全的,不存在同步的构造方法一说。

4.讲一下synchronized 关键字的底层原理
假设利用synchronized来修饰一个语句块或方法,通过javap命令查看字节码信息,可以看到:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法

5.创建多少个线程合适?如何评估?
创建多少个线程我们需要判断目前的程序是cpu密集型程序还是I/O密集型程序。
<1>cpu密集型程序


image.png

如果使用的是单核cpu,所有线程都在等待 CPU 时间片。按照理想情况来看,四个线程执行的时间总和与一个线程5独自完成是相等的,实际上还忽略了四个线程上下文切换的开销
所以,单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程


image.png

如果在 4 核CPU下,每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。理论情况来看效率提升了 4 倍。
所以,如果是多核CPU 处理 CPU 密集型程序,我们完全可以最大化的利用 CPU 核心数,应用并发编程来提高效率
<2>I/O密集型程序
同样是在单核情况下:
image.png

从上图中可以看出,每个线程都执行了相同长度的 CPU 耗时和 I/O 耗时,如果你将上面的图多画几个周期,CPU操作耗时固定,将 I/O 操作耗时变为 CPU 耗时的 3 倍,你会发现,CPU又有空闲了,这时你就可以新建线程 4,来继续最大化的利用 CPU。
综上所述:线程等待时间所占比例越高,需要越多线程;线程cpu时间所占比例越高,需要越少线程。所以对于cpu密集型来说,理论上是线程数量=cpu核数就可以了,但实际上,数量一般会设置为cpu核数+1,目的是防止恰好某个线程停止而另一个额外的线程可以确保在这种情况下cpu周期不会中断工作。对于I/O密集型来说:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时))

你可能感兴趣的:(进程和线程分别是什么?为什么引入线程)