main函数由哪个在程序中调用fork创建子进程创建?

3.2 进程等待的方法

3.5 进程的阻塞和非阻塞等待

4.4 替换函数命名理解

4.5.2 程序替换的原理

4.5.4 替换自己写的可执行程序

五、进程控制应用场景:模拟 shell命令行解释器

5.3 内建/内置命令


fork 的返回值有两个

  1. 创建子进程失败返回 -1

进程调用 fork函数,当控制转移到内核中的 fork代码后,内核做

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程(第一点和第二点在进程地址空间已经详细解释)
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

fork 之后,父子进程代码共享

pid,分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而 fork之后父子两个执行流分别执行,也就是父子进程代码共享

注意:fork之后,父进程和子进程谁先执行完全由调度器决定

小提示:在编写 makefile 的时候,目标文件的依赖方法中,可以用 “$@” 表示要形成的目标文件,即依赖关系中 “:” 左边的内容;用 “$^” 表示目标文件的依赖文件,即依赖关系中 “:” 右边的内容

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

        一个子进程永远只有一个父进程,但父进程可以拥有多个子进程。比如,一个孩子只有一个父亲,而父亲可以有多个孩子。

        进程多了就要有进程的标识符,没有事不行的。就好比一个父亲他有三个孩子,父亲想叫其中的一个孩子,得叫孩子的名字吧,不叫孩子怎么知道叫哪一个孩子,总不能说:孩子,你过来一下。这样叫哪知道是哪一个,同比进程也是如此,得有一个认得出你的标识符。给子进程返回 0,给父进程返回子进程的

为什么fork函数有两个返回值? 

        因为存在两个进程(父进程和子进程),那么 fork 自然也就会被返回两次,每一个进程都要 return,所以 fork 函数有两个返回值。(这里在地址空间也有介绍,这里简单说一下)

        通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本

写时拷贝在进程地址空间也有详细介绍

        当我们不修改数据时,父子进程的虚拟内存所对应的物理内存都是同一块物理地址(内存),当子进程的数据被修改,那么就会将子进程修改所对应数据的物理内存出进行写时拷贝,在物理内存中拷贝一份放在物理内存的另一块空间,将子进程虚拟内存与这个新的地址通过页表进行关联 

为什么数据要进行写时拷贝?

        进程具有独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

  1. 实际用户的进程数超过了限制

        进程有创建,进程也有结束的时候,进程结束我们称为进程终止在C/C++中,在 main 函数最后基本都会写上 return 0,对于这个返回值 0 我们称它为进程退出码

        进程退出码有很多,每个进程退出码都有着自己的意义,进程退出码代表了进程为什么会退出,比如进程退出码 0 代表的意义就是进程正常退出,也就是代码正常执行完成

程序运行完了,怎么查看进程退出码?

当进程执行之完成可以通过一个命令查看具体的进程退出码,? 就是环境变量中的一个名字,@?就是获取相应的环境变量

我们可以修改进程的退出码,进程退出码的意义也可以自己定义,不使用操作系统的那一套进程退出码

如何设定 main函数的返回值?

        如果不关心进程退出码,return 0 就行,如果要关心进程退出码,要返回特定的数据表明进程退出的情况和特定的错误(进程是正常退出还是非正常退出)

进程退出码一般使用0表示成功,!0表示错误,!0具体是多少,就标定特定的错误

strerror 这个函数就是把进程的退出码转换成文字描述

进程退出的场景分三类:

  1. 代码运行完毕,结果正确(进程退出码为 0)
  2. 代码运行完毕,结果不正确(进程退出码 !0)
  3. 代码没有跑完,异常终止(退出码无意义)

进程如何退出呢?接下来就来解释一下(前两种情况)

(1)main 函数的 return 退出,这是最常用的一种方式

(2)通过 exit 函数退出

man exit 查看一下,exit 是C语言的一个库函数,参数 status 就是当前进程的退出码

运行结果,到exit语句就会将进程结束,后面的代码也就不会再去执行了

(3)通过 _exit 系统调用退出(了解)

二者的区别在刷新缓冲区上,将换行符去掉进行测试

进程结束后,会刷新缓冲区,打印的结果暂停2秒也会显示出来,下面看 _exit()

_exit 没有打印出结果,也就是说 _exit 并没有刷新缓冲区

  1. exit终止进程,主动刷新缓冲区
  2. _exit终止进程,不会刷新缓冲区

前面的三点都是进程的正常退出,最后一点是异常退出

(4)异常退出:通过 ctrl + c 终止进程,信号终止,如 kill -9

进程等待的必要性: 

  1. 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  2. 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息  

        总的来说,进程等待的意义就是:回收子进程资源,获取子进程退出信息,即通过进程等待的方式解决僵尸进程的问题

3.2 进程等待的方法

man 2 wait 查看 wait,wait 是一个系统调用,输出型参数,获取子进程退出状态,不关心则可以设置成为NULL,下面先使用第一个接口

返回值,等待成功返回子进程的PID,失败返回 -1

 测试代码,让子进程处于 Z状态5秒,父进程 10秒后醒来回收子进程

        右侧执行脚本,左侧同时运行 mytest,发现当子进程正在执行时,子进程和父进程都处于 S 状态,当子进程执行完毕,没有被父进程回收时的那 5秒,子进程就变成了 Z 状态,当父进程执行时,通过调用 wait 将子进程回收,子进程就结束了,最后的5秒只剩下父进程处于S+状态,这就是父进程通过进程等待回收了僵尸进程(子进程)

返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数:pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。 options:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

  1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  2. 如果传递NULL,表示不关心子进程的退出状态信息
  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  4. status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

怎么获取这些有用的信息?答案是通过位操作符

再运行程序,就可以获取子进程的信息了

会等待父进程来取走子进程退出信息

来获取子进程的退出信息的

3.5 进程的阻塞和非阻塞等待

        上面的测试代码就是阻塞等待,所谓的阻塞等待就是:当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待,也叫轮询阻塞等待

        父进程不做任何事,一直等待子进程的退出,在此期间父进程会一直询问:子进程,你好了没?这种询问会一直询问到子进程忙完,也就是子进程退出,父进程的一直询问这种方式称为轮询检测

        而父进程不是一直等到子进程退出,而是间隔一定时间去询问子进程,父进程在子进程未退出时可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,这种等待方式叫做非阻塞等待,也叫非轮询阻塞等待

下面进行非阻塞等待代码测试

非阻塞等待有什么好处?

  1. 想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码中的一部分)

  2. 想让子进程执行一个全新的程序(让子进程想办法加载磁盘是指定的程序,执行新程序的代码和数据,这就是进程的程序替换)

        替换函数有六种以exec开头的函数,它们统称为exec函数,这六种都是库函数,这些函数的作用是:将指定的程序加载到内存中,让指定的进程执行

        第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量

        第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量

  1. 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  2. 如果调用出错则返回-1
  3. 所以 exec 系列函数只有出错的返回值而没有成功的返回值

4.4 替换函数命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记

  • e(env) : 表示自己维护环境变量

        第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,... 是可变参数列表。这些函数作用是将指定的程序加载到内存中,让指定的进程执行

这是由第一个参数决定的,通过环境变量找到指定的程序

这个是由第二个参数决定的,通过相应的命令执行程序

下面假设替换 ls 这个程序,execl 这个函数第一个参数要带路径

        我们发现,程序确实被替换了,执行了 ls 这个程序,而且最后一句打印没有打印出来,对比 ls 命令执行的结果,二者无差异,只不过没有把颜色带上,加上颜色的参数就可以了

exec 系列的函数为什么没有成功返回值呢?

程序执行完成后,最后一句为什么没有被打印?下面解释原理

4.5.2 程序替换的原理

        以上面代码为例,代码执行时,进程地址空间与物理内存与页表就会形成映射关系,当执行原有的代码时,执行第一个printf会照常打印,到了execl 函数时,就会发生进程的程序替换,也就是说,我们所编写的代码会被 execl 函数所调用对应磁盘内部的代码和数据覆盖,即将指定程序的代码和数据覆盖原有的代码和数据,然后执行这个新的代码和数据,所以 execl 后面的printf没有打印

当进行进程程序替换时,有没有创建新的进程?

        进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的 pid 并没有改变

        程序替换一般都是用 fork 生成子进程,让子进程进行程序替换,上面的单进程例子是为了方便演示

下面使用子进程进行程序替换(双进程(父子进程)),函数依旧是 execl

        还是一致的,用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

        进行程序替换时会发生写时拷贝,保证进程的独立性,不让子进程影响父进程

4.5.4 替换自己写的可执行程序

随便创建一个源文件:test.c

  • e(env) : 表示自己维护环境变量

        第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量

直接使用 4.5.4 上面的代码,修改一下

        结果发现,系统内部的环境变量使用不了,我们自定义的就可以使用。这是因为我们的 execle 函数的最后一个参数的原因,最后的一个参数就是传入的环境变量,没有传入就不会使用,因此如果我们在 exec.c 中将最后一个位置的参数改成 environ(前面添加extern char** environ)的话,就会反过来:我们自定义的环境变量就不会生效,只有系统的才会生效。

man putenv 查看,putenv 是一个库函数,作用是把你自定义的环境变量导入环境变量中,让自定义的环境和系统的环境变量让两者同时生效

这样就可以让自定义的环境和系统的环境变量让两者同时生效

对于execle函数和main函数,在进程调用的时候是谁先被调用?

程序是先加载呢?还是先执行main呢?

main 也作为函数,也需要被传参,exec 系列的函数和 main函数的参数有什么关联呢?

main 函数本身自带三个参数,不过平时我们都不传参数

下图exec函数族 一个完整的例子

其实 shell需要执行的逻辑非常简单,其只需循环执行以下步骤:

  1. 创建子进程(fork)
  2. 替换子进程(execvp)
  3. 等待子进程退出(wait)

 运行结果,一个简易的 shell 就完成了

但是这个简易的 shell命令行解释器还有一个问题:就是返回上一级路径时,路径没有发生变化

执行这个程序并新建窗口进行观察

在 Linux 中,我们可以使用 chdir 系统调用来改变进程的工作目录

回到上面,为什么我们自己写的shell,cd 的时候路径没有变化呢?

        myshell 是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd 命令是由子进程去执行的,那么自然被改变也是子进程的工作目录,父进程的工作目录不受影响

 运行结果,可以使用 cd 命令改变路径了

5.3 内建/内置命令

Linux 中的命令一共分为两种 – 内建(内置)命令和外部命令

文章到这里就结束了,进程控制这个篇章也完结了,下篇进入基础IO

  • 进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行程序是一个进程从创建到消亡的过程。在java中,当我们启动main函数,其实就是启动了一个jvm的进程。main函数所在的线程就是这个进程中的一个线程,这个线程也加主线程。

  • 线程是一个比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

请简要描述线程与进程的关系,区别及优缺点?

  • 一个进程中可以有多个线程,多个线程共享进程的堆、方法区(jdk1.8之后的元空间),但是每个线程有自己的程序计数器,虚拟机栈和本地方法栈。

  • 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

程序计数器为什么是私有的?

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

  • 如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

  • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

一句话简单了解堆和方法区?

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、以及编译器编译后的代码等数据。

说说并发与并行的区别?

并发:某一个时间段同时进行多个事情,但是同一时刻可能并没有同时进行 在计算机科学中,多个线程或者是多个进程同时在单核或者多核cpu上执行时,执行路径具有不确定性的这种情形。

并行:同一时刻,同时进行多个事情。

为什么要使用多线程呢?

从计算机底层来说: 单核时代,主要是为了提高单线程利用CPU和IO系统的效率,当线程被IO阻塞时,能充分利用CPU。多核时代,主要是为了提高单线程对多核CPU能力的利用,例如,假设在多核CPU上只有一个线程执行,其他CPU资源就会被浪费掉。

使用多线程可能带来什么问题?

并发编程是为了提高程序的执行效率和运行速度(多核多线程),但是多线程不一定都能提高运行速度(单核多线程)。并且多线程可能会遇到内存泄漏、死锁、线程不安全等问题。

说说线程的生命周期和状态?

  • 线程在执行过程中,会有自己的运行条件和状态,例如程序计数器,栈信息等。当出现如下条件时,线程会从CPU占用状态退出。

  • 调用了阻塞类系统中断,比如IO,线程被阻塞、被终止或结束运行

  • 前面三种情况会发生线程上下文切换

什么是线程死锁?如何避免死锁?

  • 死锁:两个及以上的线程同时阻塞,并互相等待对方释放对方占用的资源,并且会一直等待下去,导致程序无法正常结束。

  • 死锁产生必须具备以下四个条件:

    • 互斥条件:该资源任意一个时刻只由一个线程占用

    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

    • 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源

    • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系

  • 如何预防死锁?破坏死锁的产生的必要条件即可:

    • 破坏请求与保持条件(不释放条件) :一次性申请所有的资源

    • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

    • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。例如要求按照A、B、C的顺序申请A、B、C三个资源,则不会发生死锁

    • 避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

    • 安全状态: 指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列.

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。

  • 两者都可以暂停线程的执行。

  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

  • new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

  • 总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

  • synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

    • 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的,Java 的线程是映射到操作系统的原生线程之上的,而线程切换是需要从用户态切换到内核态,这个状态转换的时间成本比较高。

    • DK1.6 对锁的实现引入了大量的优化,如自旋锁、自适应锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

    • 所以,目前不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁,静态方法锁和实例方法锁可以同时调用,一个锁类,一个锁实例,不会形成互斥

  • 修饰代码块 :指定加锁对象,对给定对象/类加锁

双重校验锁实现对象单例,为什么需要用volatile关键字修饰成员变量?

  • new对象分为三步完成:

    • 将分配的地址空间指向新对象

  • new对象的过程存在指令重拍,如果顺序为1->3->2可能导致其他线程获取到没有正常初始化的对象。

  • 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

构造方法可以使用 synchronized 关键字修饰么?

  • 当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权

  • 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1

  • 在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • 不过两者的本质都是对对象监视器 monitor 的获取。

说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?

  • JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、自适应性锁、锁消除、锁粗化等技术来减少锁操作的开销。

  • 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

    • 线程获取到锁对象,如果此时没有其他线程占用锁对象,就将锁对象头中的标志位置为01,并将自己的线程ID记录在锁对象头的Mark Work的偏向锁线程ID中,同时将偏向锁状态置为1。

    • 当前当前线程再次加锁,直接锁加1,可重入。如果其他线程竞争,判断当前线程是否还需要锁,需要则释放偏向锁,升级为轻量级锁。如果不需要锁,则释放锁,其他线程竞争,继续偏向锁。

    • 当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
    • 当前锁处于轻量级锁时,如果其他线程来竞争锁,此时会进行自旋。自旋锁重试之后如果抢锁依然失败,轻量级锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。
  • 偏向锁:适用于单线程适用锁的情况

    轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似)

    重量级锁:适用于竞争激烈的情况

  • 两者都是可重入锁,是指线程可以再次获取自己已经获取的锁。比如一个线程已经获取到了A对象上的锁,此时这个线程还能再次获取A对象上的锁,只是锁计数加1,释放锁时,依次释放直到锁计数为0,才算释放锁。如果不支持重入的话,会造成死锁。

    • 等待可中断: ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情

    • 可实现选择性通知(锁可以绑定多个条件): 只会唤醒注册在该Condition实例中的所有等待线程

  • 保证共享变量在多线程环境下的可见性

为什么要使用CPU缓存?

  • CPU缓存在寄存器和主存直接设置了一层CPU缓存,寄存器每次都先从寄存器中获取数据,如果获取不到才到主存中读取数据。

  • CPU缓存为了解决CPU处理速度和内存处理速度不匹配的问题,内存缓存是用于解决内存访问太慢的问题。

JMM是java内存模型,模型是说java的线程都有自己的本地内存,可以将数据缓存到本地内存中,在线程执行过程中,可以先去本地内存中读取数据,而不是直接去主存中读取。在多线程的环境下,如果一个线程修改了主存中的数据,而另一个线程从它本地内存中读取到的是没有修改过的缓存数据,从而导致数据不一致。

并发编程的三个重要特征?

  • 原子性: 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。

  • 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。

  • 有序性:代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

  • synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。

  • volatile 关键字能保证数据的可见性,但不能保证原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

  • ThreadLocal是线程保存线程事由变量的对象,在当前线程中对当前线程ThreadLocal中的变量进行修改,只会影响到当前线程的ThreadLocal中的变量,不会影响到其他线程的ThreadLocal变量。

在一个Thread的生命周期中,如果Thread中的某个对象持有一个ThreadLocal对象,那么这个线程就有一个线程私有的变量ThreadLocal。

一个ThreadLocal对象被设置到线程中时,至少有两个引用,一个是ThreadLocalMap中的key对它有弱引用,一个是持有ThreadLocal的对象对它有强引用。所以ThreadLocalMap的key被设置成弱引用,当引用ThreadLocal的对象被回收后,即强引用消失后,ThreadLocal对象只剩弱引用,会被回收掉。ThreadLocalMap没有对外提供操作key、value的api也是为了保证数据被回收的数据不能再被操作。

如何让线程持有一个贯穿线程生命周期的ThreadLocal?

  • 将ThreadLocal设置成静态变量(static修饰),这样ThreadLocal对象会被类的Class对象持有,会贯穿线程生命周期中。此时多个线程持有一个相同的ThreadLocal,每个ThreadLocal在各自的Thread中ThreadLocalMap中对应的值是不一样的,所以线程私有变量的实现是依赖线程的ThreadLocalMap来实现的。

  • 设计为非static的,长对象(比如被spring管理的对象)的内部,也不会被回收。

  • 线程池、数据库连接池、Http 连接池等等都是池化技术的应用。池化技术主要是为了减少每次获取资源的消耗,提高对资源的利用率。例如数据库连接,如果每次连接都去创建,开销非常大。

  • 线程池提供了对线程资源的限制和管理。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

  • Runnable 接口 不会返回结果或抛出检查异常。

  • Callable 接口 可以回结果或抛出检查异常。

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。

  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

    • ThreadPoolExecutor一共有三个构造方法,一般使用参数最长的构造方法,自己指定各种参数来创建线程池。
    • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

    • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

    • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

  • 《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险:

  • keepAliveTime:当线程池中的线程数大于corePoolSize核心线程数时,线程不会立即销毁,而是等超过keepAliveTime时间后才会销毁

  • CallerRunsPolicy:直接在调用execute方法的线程中运行被拒绝的任务,这种策略会降低对于新任务的提交速度,影响整体性能。如果程序能够承受延迟,并且要求每个任务都被执行,可以使用这种策略。

  • DiscardPolicy:不处理新任务,直接丢弃掉。

  • 判断corePoolSize是否已经满了,没有满,创建新线程执行,满了交给workQueue。

  • 判断maximumPoolSize是否已经满了,没有满,创建新线程执行,满了交给handler。

  • 判断handler使用的哪种拒绝策略,按照拒绝策略进行拒绝。

  • 原子类就是提供了一系列不可被中断的操作的类。
    • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

  • CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。
  • 另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

为什么会设计AQS同步器类?

  • AQS(AbstractQueuedSynchronizer)同步器的作者在论文中阐明了,几乎任意一个同步器都可以基于它去实现其他类型的同步器。例如我们可以基于ReentrantLock去实现Semaphore,反过来也可以实现。但是这种设计会带来很大的复杂性和不灵活性。所以需要构建一个各种同步器的基础框架,即基于这个基础框架,可以实现各种同步器。
  • 由于java内置的同步锁synchronized关键字,存在潜在的饥饿问题,AQS的主要性能指标之一就是要解决饥饿问题,同时AQS也支持非公平的同步器。

AQS是基于CLH实现的,介绍一下CLH?

CLH是排队式自旋锁论文三个作者的首字母,排队时式自旋锁要解决的问题是如何高效的访问多核CPU的共享资源。无论是一致性内存访问架构(CPU -> 高速缓存 -> 总线 -> 内存),还是非一致性内存访问架构(内存 -> CPU -> 总线)多线程之间的共享资源访问在高并发的情况下,都是系统瓶颈。CLH就是为了解决这个问题而设计的。

AQS使用一个int型的成员变量state来表示同步状态,且这个状态是volatile修饰的,保证线程可见的。所有要获取锁的线程都会被封装成一个PNode添加到一个FIFO的CLH队列中,后一个节点总是自旋监视前一个节点的状态,如果前一个节点的状态是释放锁,则后一个节点可以尝试去获取锁。

AQS支持的两种资源共享方式?

  • 独占(Exclusive):只有一个线程执行,又可以分为公平锁和非公平锁

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • ReentrantReadWriteLock可以看做是组合式,因为读锁是多线程的,写锁是单线程的

  • 信号量(Semaphore)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源
  • 倒计时器(CountDownLatch): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行

CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch 。具体场景是下面这样的: 我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 为此我们定义了一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List
  • Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程。
  • 在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机( 即计划任务将不再运行),ScheduledThreadPoolExecutor 不仅捕获运行时异常,还允许在需要时处理它们。抛出异常的任务将被取消,但其他任务将继续正常运行。
  • 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
  • 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上

JDK 提供的并发容器总结?

  • ConcurrentLinkedQueue : 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • BlockingQueue : 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道
  • 使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
  • 在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
  • 读操作时,只有是作为红黑树旋转时,才会加锁。其他时候都不用加锁,且value是volatile类型变量,是线程可见的。
  • 写操作通过分段加锁,没有hash冲突时,使用cas乐观锁,段出现hash冲突升级成synchronized悲观锁。
  • ReentrantReadWriteLock读写锁,解决了读读互斥的问题,但是存在读写互斥
  • CopyOnWriteArrayList让所有的写操作互斥,且写时,先将原来的数组复制一份(只能有一个写操作,只能复制一份副本),将数据添加进去,然后用新数据替换原数组,从而达到读读不互斥,读写不互斥,写写互斥。
  • 源码分析,读不做操作,写全部加上同步一把重入锁
  • Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue。阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。
  • 阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中。
  • BlockingQueue 提供了可阻塞的插入和移除的方法。
  • 当队列容器已满,生产者线程会被阻塞,直到队列未满。
  • 当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
  • ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞
  • ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,即线程访问不准寻FIFO。因此可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue可以在new的时候,通过参数指出
    • 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量
    • 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则
    • PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列,后面插入元素的时候,如果空间不够的话会自动扩容
    • 简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)
  • 跳表又叫跳跃表,是一种可以进行二分查找的有序链表。跳表是在原来的有序链表上添加了多层有序链表实现的。如果严格按照二分查找构建跳表,每次添加数据后都必须重建i>1层的所有链表,会让插入和删除的时间复杂度退化成O(n),所以一般采用插入数据后,随机数据所处在层数,即第1到随机层数都有这个值(只需维护每层这个值的索引关系),从而达到简化跳表插入和删除。
  • 由于满足二分查找,所以跳表的查找复杂度是O(logn)
  • 跳表是一种以时间换空间的算法
  • 线程池、数据库连接池、Http 连接池等等,主要是为了减少每次获取资源的消耗,提高对资源的利用率。
  • 线程池提供了对线程使用的限制和管理。 同时每个线程池还维护一些基本统计信息,例如已完成任务的数量
    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池在实际项目的使用场景?

线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。例如:批量发送邮件。

一般是通过 ThreadPoolExecutor 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。创建线程池时需要注意核心线程数,最大线程数,队列长度,拒绝策略。

  • 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  • 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
  • 总结:使用有界队列,控制线程创建数量。

可以利用 ThreadPoolExecutor 的相关 API做一个的监控。打印线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。

建议不同类别的业务用不同的线程池?

一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。

  • 初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题

CPU 密集型任务(N+1):这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 I/O 密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

  • AQS 就是一个抽象类,主要用来构建锁和同步器。
  • AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  • AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

AQS 对资源的共享方式?

  • Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁

    • 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒
    • 公平锁和非公平锁只有两处不同:
      • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了
      • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面
      • 公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒
      • 相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态
  • Semaphore 有两种模式,公平模式和非公平模式:
    • 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO
    • 非公平模式: 抢占式的

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕,或者允许count线程同时运行。

  • 某一线程在开始运行前等待 n 个线程执行完毕
  • 当count个线程执行完成后,主线程会被唤醒,然后继续执行
  • 实现多个线程开始执行任务的最大并行性

CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用

  • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。

  • CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
支持响应中断、超时、尝试获取锁
必须显示的调用unlock释放锁

线程是指进程内的一个执行单元,也是进程内的可调度实体.

(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

(3)拥有资源:进程是拥有资源的独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源. 

(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

.C/C++编译器中虚表是如何完成的?

.谈谈COM的线程模型。然后讨论进程内/外组件的差别。

.谈谈IA32下的分页机制

小页(4K)两级分页模式,大页(4M)一级

.给两个变量,如何找出一个带环单链表中是什么地方出现环的?

一个递增一,一个递增二,他们指向同一个接点时就是环出现的地方   ??

.在IA32中一共有多少种办法从用户态跳到内核态?

.如果只想让程序有一个实例运行,不能运行两个。像winamp一样,只能开一个窗口,怎样实现?

用内存映射或全局原子(互斥变量)、查找窗口句柄.. 

FindWindow,互斥,写标志到文件或注册表,共享内存。. 

.如何截取键盘的响应,让所有的‘a’变成‘b’?

.Apartment在COM中有什么用?为什么要引入?

.存储过程是什么?有什么用?有什么优点?

存储过程(Stored Procedure)是一组为了完成特定功能的SQL 语句集,经编译后存储在数据库。中用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

存储过程用于实现频繁使用的查询、业务规则、被其他过程使用的公共例行程序

存储过程在创建时即在服务器上进行编译,所以执行起来比单个 SQL 语句快

.Template有什么特点?什么时候用?

.谈谈Windows DNA结构的特点和优点。



今天群硕笔试,考了好多内容,其中Java占很大部分!

本试卷中最有难度的编程题:给定一个数组,这个数组中既有正数又有负数,找出这个数组中的子数组,此子数组的和最大!

#include<的区别(假期做项目的时候碰到过,嘿嘿)

logic thinking:检测电冰箱(我用软件工程的思想随便写写)

答案:实际上除了“能够让应用程序处理存储于DBMS 中的数据“这一基本相似点外,两者没有太多共同之处。但是ADO 使用OLE DB 接口并基于微软的COM 技术,而 接口并且基于微软的.NET 体系架构。众所周知.NET 体系不同于COM 体系, 和ADO是两种数据访问方式。,看起来好像这些概念都广泛被PHP开发人员所了解。这就说明了PHP实际上到底是多专业。

  对于非常小的项目,它可以是一个十分符合人意的编程语言。但是对于较大的和更为复杂的项目,PHP就显出他的薄弱了。当你不断地摸索之后,你会发现笔者提到的某些问题的解决方案。所以,当解决方案已知之后,为什么不能修正他呢?另外为什么这些修补不在手册中提到呢?

  一个开源的语言十分流行是一件好事。但不幸得是,它不是一个伟大的语言。笔者希望所有的问题能有一天得到解决(也许在PHP6?),然后我们就将拥有一个开源语言,他既开源,又好用。

  到现在,当你要启动一个多于5个脚本页面的项目的时候,你最好考虑C#/ 

  • ThoughtWorks 公司在西邮正式开办的只教女生前端开发的女子卓越实验室已经几个月过去了,这次计划于暑期在西邮内部开展面向所有性别所有专业的前端培训. 具体官方安排请戳:ThoughtWorks ...

  • 技术类面试.笔试题汇总 注:标明*的问题属于选择性掌握的内容,能掌握更好,没掌握也没关系. 下面的参考解答只是帮助大家理解,不用背,面试题.笔试题千变万化,不要梦想着把题覆盖了,下面的题是供大家查漏补 ...

  • 下面这些题目都是我之前准备笔试面试过程中积累的,大部分都是知名公司的笔试题,C++基础薄弱的很容易栽进去.我从中选了10道简单的题,C++初学者可以进来挑战下,C++大牛也可以作为娱乐玩下(比如下面的 ...

    1. BW增强数据源的两种方法 , by SAPBI 前言:我们经常会遇到系统标准的数据源,或者我们自建的数据源无法满足要求的情况,这个时候在数据源中添加几个相关的字段,可能就能满足我们 ...

    2. 写在前面 这里需要介绍的是GSM / GPRS网络测试的一些方法,随着现在硬件设备连网现象的普遍存在,例如智能电表.自动变速箱控制单元(TCU).POS机.报警系统等.这些设备通常需要与网络连接,GS ...

我要回帖

更多关于 在程序中调用fork创建子进程 的文章

 

随机推荐