多线程 订阅
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1]  。 展开全文
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1]  。
信息
外文名
multithreading
对    象
计算机
作    用
提升整体处理性能
用    途
实现多个线程并发执行的技术
含    义
从软件或者硬件上实现多个线程并发执行的技术
中文名
多线程
多线程简介
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些掌握机器低级语言的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。中断对那些实时性很强的任务来说是很有必要的。但对于其他许多问题,只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求 [2]  。最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个线程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源 [2]  。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的 [2]  。 多线程(2张)
收起全文
精华内容
参与话题
问答
  • C++11多线程编程

    千人学习 2018-09-06 14:34:58
    本课程,讲解的重点定位在c++11新标准中的多线程开发部分,同时,老师还会结合自己的经验把多线程的讲解进一步拓展到一个比较大的范畴,因为无论是c++11多线程开发还是各种其他的多线程开发实现方法,都有很多类似的...
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47题(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...

    史上最强多线程面试47题(含答案),建议收藏

    金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~

    1、并发编程三要素?

    1)原子性

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    2)可见性

    可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

    3)有序性

    有序性,即程序的执行顺序按照代码的先后顺序来执行。

    2、实现可见性的方法有哪些?

    synchronized或者Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

    3、多线程的价值?

    1)发挥多核CPU的优势

    多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情而不互相干扰。

    2)防止阻塞

    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

    3)便于建模

    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    4、创建线程的有哪些方式?

    1)继承Thread类创建线程类

    2)通过Runnable接口创建线程类

    3)通过Callable和Future创建线程

    4)通过线程池创建

    5、创建线程的三种方式的对比?

    1)采用实现Runnable、Callable接口的方式创建多线程。

    优势是:

    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

    劣势是:

    编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

    2)使用继承Thread类的方式创建多线程

    优势是:

    编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

    劣势是:

    线程类已经继承了Thread类,所以不能再继承其他父类。

    3)Runnable和Callable的区别

    Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
    Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    Call方法可以抛出异常,run方法不可以。
    运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

    6、线程的状态流转图

    线程的生命周期及五种基本状态:

    7、Java线程具有五中基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就
    绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    8、什么是线程池?有哪几种创建方式?

    线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

    java 提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

    9、四种线程池的创建:

    1)newCachedThreadPool创建一个可缓存线程池

    2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。

    3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。

    10、线程池的优点?

    1)重用存在的线程,减少对象创建销毁的开销。

    2)可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

    3)提供定时执行、定期执行、单线程、并发数控制等功能。

    11、常用的并发工具类有哪些?

    CountDownLatch
    CyclicBarrier
    Semaphore
    Exchanger

    12、CyclicBarrier和CountDownLatch的区别

    1)CountDownLatch简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用countDown()方法发出通知后,当前线程才可以继续执行。

    2)cyclicBarrier是所有线程都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行!

    3)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

    4)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。

    13、synchronized的作用?

    在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。

    synchronized既可以加在一段代码上,也可以加在方法上。

    14、volatile关键字的作用

    对于可见性,Java提供了volatile关键字来保证可见性。

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见java.util.concurrent.atomic包下的类,比如AtomicInteger。

    15、什么是CAS

    CAS是compare and swap的缩写,即我们所说的比较交换。

    cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

    java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的( AtomicInteger,AtomicBoolean,AtomicLong)。

    16、CAS的问题

    1)CAS容易造成ABA问题

    一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

    2) 不能保证代码块的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    3)CAS造成CPU利用率增加

    之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

    17、什么是Future?

    在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

    Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

    18、什么是AQS

    AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

    19、AQS支持两种同步方式:

    1)独占式

    2)共享式

    这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。

    20、ReadWriteLock是什么

    首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。

    因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    21、FutureTask是什么

    这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。

    22、synchronized和ReentrantLock的区别

    synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

    1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

    2)ReentrantLock可以获取各种锁的信息

    3)ReentrantLock可以灵活地实现多路通知

    另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

    23、什么是乐观锁和悲观锁

    1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    24、线程B怎么知道线程A修改了变量

    volatile修饰变量
    synchronized修饰修改变量的方法
    wait/notify
    while轮询

    25、synchronized、volatile、CAS比较

    synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
    volatile提供多线程共享变量可见性和禁止指令重排序优化。
    CAS是基于冲突检测的乐观锁(非阻塞)

    26、sleep方法和wait方法有什么区别?

    这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

    27、ThreadLocal是什么?有什么用?

    ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

    简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

    28、为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁

    29、多线程同步有哪几种方法?

    Synchronized关键字,Lock锁实现,分布式锁等。

    30、线程的调度策略

    线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

    1)线程体中调用了yield方法让出了对cpu的占用权利

    2)线程体中调用了sleep方法使线程进入睡眠状态

    3)线程由于IO操作受到阻塞

    4)另外一个更高优先级线程出现

    5)在支持时间片的系统中,该线程的时间片用完

    31、ConcurrentHashMap的并发度是什么

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?

    32、Linux环境下如何查找哪个线程使用CPU最长

    1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

    2)top -H -p pid,顺序不能改变

    33、Java死锁以及如何避免?

    Java中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或更多资源。

    Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。

    34、死锁的原因

    1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环。

    例如:线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。

    2)默认的锁申请操作是阻塞的。

    所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。

    35、怎么唤醒一个阻塞的线程

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

    36、不可变对象对多线程有什么帮助

    前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

    37、什么是多线程的上下文切换

    多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

    38、如果你提交任务时,线程池队列已满,这时会发生什么

    这里区分一下:

    1)如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

    2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy

    39、Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

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

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

    41、什么是自旋

    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

    42、Java
    Concurrency API中的Lock接口(Lock
    interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    可以使锁更公平
    可以使线程在等待锁的时候响应中断
    可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    可以在不同的范围,以不同的顺序获取和释放锁

    43、单例模式的线程安全性

    老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

    1)饿汉式单例模式的写法:线程安全

    2)懒汉式单例模式的写法:非线程安全

    3)双检锁单例模式的写法:线程安全

    44、Semaphore有什么作用

    Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    45、Executors类是什么?

    Executors为Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable类提供了一些工具方法。

    Executors可以用于方便的创建线程池

    46、线程类的构造方法、静态块是被哪个线程调用的

    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

    1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

    2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的

    47、同步方法和同步块,哪个是更好的选择?

    同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

    48、Java线程数过多会造成什么异常?

    1)线程的生命周期开销非常高

    2)消耗过多的CPU资源

    如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。

    3)降低稳定性

    JVM在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM的启动参数、Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError异常。

    展开全文
  • 多进程与多线程区别

    万次阅读 多人点赞 2016-04-21 10:01:16
    在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一...

    在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一个执行线程), Java的Web容器Tomcat、Websphere等都是多线程的(每客户连接对应一个线程,所有线程都在一个进程中)。

    从Unix发展历史看,伴随着Unix的诞生进程就出现了,而线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特点,也就是各自的优缺点如下:

    进程优点:编程、调试简单,可靠性较高。
    进程缺点:创建、销毁、切换速度慢,内存、资源占用大。
    线程优点:创建、销毁、切换速度快,内存、资源占用小。
    线程缺点:编程、调试复杂,可靠性较差。

    上面的对比可以归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程可以十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,因此比较可靠。我相信这个观点可以被大部分人所接受,因为和我们所接受 的知识概念是相符的。

    在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通讯程序中,因时间紧总是采用多进程并发技术,而且是比较简单的现场为 每客户fork()一个进程,当时总是担心并发量增大时负荷能否承受,盘算着等时间充裕了将它改为多线程形式,或者改为预先创建进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我自己也做了实验,结论和论文作者的相似,但对大部分人可以说是颠覆性的。

    下面是得出结论的实验步骤和过程,结论究竟是怎样的? 感兴趣就一起看看吧。

     

    实验代码使用周丽论文中的代码样例,我做了少量修改,值得注意的是这样的区别:

    论文实验和我的实验时间不同,论文所处的年代linux内核是2.4,我的实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

    论文实验和我用的机器不同,论文描述了使用的环境:单 cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。我的环境是我的工作本本:单cpu单核celeron(R) M 1.5 GZ,1.5G内存,ubuntu10.04 desktop,内核2.6.32。

    进程实验代码(fork.c):

    1. #include <stdlib.h>
    2. #include <stdio.h>
    3. #include <signal.h>
    4.  
    5. #define P_NUMBER 255    /* 并发进程数量 */
    6. #define COUNT 100       /* 每进程打印字符串次数 */
    7. #define TEST_LOGFILE "logFile.log"
    8. FILE *logFile = NULL;
    9.  
    10. char *s = "hello linux\0";
    11.  
    12. int main()
    13. {
    14.     int i = 0,j = 0;
    15.     logFile = fopen(TEST_LOGFILE, "a+")/* 打开日志文件 */
    16.     for(i = 0; i < P_NUMBER; i++)
    17.     {
    18.         if(fork() == 0) /* 创建子进程,if(fork() == 0){}这段代码是子进程运行区间 */
    19.         {
    20.             for(j = 0;j < COUNT; j++)
    21.             {
    22.                 printf("[%d]%s\n", j, s)/* 向控制台输出 */
    23.                 fprintf(logFile,"[%d]%s\n", j, s)/* 向日志文件输出 */
    24.             }
    25.             exit(0)/* 子进程结束 */
    26.         }
    27.     }
    28.  
    29.     for(i = 0; i < P_NUMBER; i++) /* 回收子进程 */
    30.     {
    31.         wait(0);
    32.     }
    33.  
    34.     printf("OK\n");
    35.     return 0;
    36. }

    进程实验代码(thread.c):

    1. #include <pthread.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <stdio.h>
    5.  
    6. #define P_NUMBER 255    /* 并发线程数量 */
    7. #define COUNT 100       /* 每线程打印字符串次数 */
    8. #define Test_Log "logFIle.log"
    9. FILE *logFile = NULL;
    10.  
    11. char *s = "hello linux\0";
    12.  
    13. print_hello_linux() /* 线程执行的函数 */
    14. {
    15.     int i = 0;
    16.     for(i = 0; i < COUNT; i++)
    17.     {
    18.         printf("[%d]%s\n", i, s)/* 向控制台输出 */
    19.         fprintf(logFile, "[%d]%s\n", i, s)/* 向日志文件输出 */
    20.     }
    21.     pthread_exit(0)/* 线程结束 */
    22. }
    23.  
    24. int main()
    25. {
    26.     int i = 0;
    27.     pthread_t pid[P_NUMBER]/* 线程数组 */
    28.     logFile = fopen(Test_Log, "a+")/* 打开日志文件 */
    29.  
    30.     for(i = 0; i < P_NUMBER; i++)
    31.         pthread_create(&pid[i]NULL(void *)print_hello_linux, NULL)/* 创建线程 */
    32.  
    33.     for(i = 0; i < P_NUMBER; i++)
    34.         pthread_join(pid[i],NULL)/* 回收线程 */
    35.  
    36.     printf("OK\n");
    37.     return 0;
    38. }

    两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:

    diaoyf@ali:~/tmp1$ gcc -o fork fork.c
    diaoyf@ali:~/tmp1$ gcc -lpthread -o thread thread.c

    实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

    time ./fork
    time ./thread

    每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

    一、重复周丽论文实验步骤

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m1.277s 0m1.175s 0m1.227s 0m1.245s 0m1.228s 0m1.230s
    多线程 0m1.150s 0m1.192s 0m1.095s 0m1.128s 0m1.177s 0m1.148s

    进程线程数:255 / 打印次数:100

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m6.341s 0m6.121s 0m5.966s 0m6.005s 0m6.143s 0m6.115s
    多线程 0m6.082s 0m6.144s 0m6.026s 0m5.979s 0m6.012s 0m6.048s

    进程线程数:255 / 打印次数:500

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m12.155s 0m12.057s 0m12.433s 0m12.327s 0m11.986s 0m12.184s
    多线程 0m12.241s 0m11.956s 0m11.829s 0m12.103s 0m11.928s 0m12.011s

    进程线程数:255 / 打印次数:1000

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 1m2.182s 1m2.635s 1m2.683s 1m2.751s 1m2.694s 1m2.589s
    多线程 1m2.622s 1m2.384s 1m2.442s 1m2.458s 1m3.263s 1m2.614s

    进程线程数:255 / 打印次数:5000

    本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行10 次,50 次,100 次,200 次……900 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

    虽然我的实验直到5000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。

    当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过1分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。

    二、增加并发数量的实验

    下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后三组(线程数500,800,1000)的测试中都出现了“段错误”,出现错误的原因和线程栈的大小有关。

    实验中的计算机CPU是32位的赛扬,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所 有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间),对于进程而言只有一个栈,这个栈可以用尽这3GB空间(计算时需要排除程序文本、数据、 共享库等占用的空间),所以它的大小通常不是问题。但对线程而言每个线程有一个线程栈,这3GB空间会被所有线程栈摊分,线程数量太多时,线程栈累计的大 小将超过进程虚拟内存空间大小,这就是实验中出现的“段错误”的原因。

    Linux2.6的默认线程栈大小是8M,可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024*1024*1024*3) / (1024*1024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀 罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问题, guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。

    有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一 种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:

    diaoyf@ali:~/tmp1$ ulimit -s 1024
    diaoyf@ali:~/tmp1$ time ./thread

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m4.958s 0m5.032s 0m5.181s 0m4.951s 0m5.032s 0m5.031s
    多线程 0m4.994s 0m5.040s 0m5.071s 0m5.113s 0m5.079s 0m5.059s

    进程线程数:100 / 打印次数:1000

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m12.155s 0m12.057s 0m12.433s 0m12.327s 0m11.986s 0m12.184s
    多线程 0m12.241s 0m11.956s 0m11.829s 0m12.103s 0m11.928s 0m12.011s

    进程线程数:255 / 打印次数:1000 (这里使用了第一次的实验数据)

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m17.686s 0m17.569s 0m17.609s 0m17.663s 0m17.784s 0m17.662s
    多线程 0m17.694s 0m17.976s 0m17.884s 0m17.785s 0m18.261s 0m17.920s

    进程线程数:350 / 打印次数:1000

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m23.638s 0m23.543s 0m24.135s 0m23.981s 0m23.507s 0m23.761s
    多线程 0m23.634s 0m23.326s 0m23.408s 0m23.294s 0m23.980s 0m23.528s

    进程线程数:500 / 打印次数:1000 (线程栈大小更改为1M)

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m38.517s 0m38.133s 0m38.872s 0m37.971s 0m38.005s 0m38.230s
    多线程 0m38.011s 0m38.049s 0m37.635s 0m38.219s 0m37.861s 0m37.995s

    进程线程数:800 / 打印次数:1000 (线程栈大小更改为1M)

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m48.157s 0m47.921s 0m48.124s 0m48.081s 0m48.126s 0m48.082s
    多线程 0m47.513s 0m47.914s 0m48.073s 0m47.920s 0m48.652s 0m48.014s

    进程线程数:1000 / 打印次数:1000 (线程栈大小更改为1M)

    出现了线程栈的问题,让我特别关心Java线程是怎样处理的,因此用Java语言写了同样的实验程序,Java程序加载虚拟机环境比较耗时,所以没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也可以用这个程序去对比理解上面的C语言试验程序。

    1. import java.io.File;
    2. import java.io.FileNotFoundException;
    3. import java.io.FileOutputStream;
    4. import java.io.IOException;
    5.  
    6. public class MyThread extends Thread
    7. {
    8.     static int P_NUMBER = 1000;     /* 并发线程数量 */
    9.     static int COUNT = 1000;        /* 每线程打印字符串次数 */
    10.  
    11.     static String s = "hello linux\n";
    12.        
    13.     static FileOutputStream out = null/* 文件输出流 */
    14.     @Override
    15.     public void run()
    16.     {
    17.         for (int i = 0; i < COUNT; i++)
    18.         {
    19.             System.out.printf("[%d]%s", i, s)/* 向控制台输出 */
    20.            
    21.             StringBuilder sb = new StringBuilder(16);
    22.             sb.append("[").append(i).append("]").append(s);
    23.             try
    24.             {
    25.                 out.write(sb.toString().getBytes());/* 向日志文件输出 */
    26.             }
    27.             catch (IOException e)
    28.             {
    29.                 e.printStackTrace();
    30.             }
    31.         }
    32.     }
    33.  
    34.     public static void main(String[] args) throws FileNotFoundExceptionInterruptedException
    35.     {
    36.         MyThread[] threads = new MyThread[P_NUMBER]/* 线程数组 */
    37.        
    38.         File file = new File("Javalogfile.log");
    39.         out = new FileOutputStream(file, true);  /* 日志文件输出流 */
    40.        
    41.         System.out.println("开始运行");
    42.         long start = System.currentTimeMillis();
    43.  
    44.         for (int i = 0; i < P_NUMBER; i++) //创建线程
    45.         {
    46.             threads[i] = new MyThread();
    47.             threads[i].start();
    48.         }
    49.  
    50.         for (int i = 0; i < P_NUMBER; i++) //回收线程
    51.         {
    52.             threads[i].join();
    53.         }
    54.        
    55.         System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
    56.         return;
    57.     }
    58.  
    59. }
      第1次 第2次 第3次 第4次 第5次 平均
    Java 65664 毫秒 66269 毫秒 65546 毫秒 65931 毫秒 66540 毫秒 65990 毫秒

    线程数:1000 / 打印次数:1000

    Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次测试都平稳完成,可以用下面的ps指令获得java进程中线程的数量:

    diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l
    1010

    用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,比如用于GC。我不知道Java创建线程时默认栈的大 小是多少,很多资料说法不统一,于是下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到需要的信息。对于jvm的运行,java提供了控制参数,因此再次测试时,通过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

    diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

    出乎意料的是并没有出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序 运行的时间远小于估计值也证明了这个判断。程序虽然没有抛出异常,但运行的并不正常,另一个问题是最后并没有打印出“用时 xxx毫秒”信息。

    这次测试更加深了我的一个长期的猜测:Java的Web容器不稳定。因为我是多年编写B/S的Java程序员,WEB服务不稳定常常挂掉也是司空见 惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直猜测还有更深层的原因,如果就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠 覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

    这次测试还打破了以前的一个说法:单CPU上并发超过6、7百,线程或进程间的切换就会占用大量CPU时间,造成服务器效率会急剧下降。但从上面的实验来看,进程/线程数到1000时(这差不多是非常繁忙的WEB服务器了),仍具有很好的线性。

    三、增加每进程/线程的工作强度的实验

    这次将程序打印数据增大,原来打印字符串为:

    1. char *s = "hello linux\0";

    现在修改为每次打印256个字节数据:

    1. char *s = "1234567890abcdef\
    2. 1234567890abcdef\
    3. 1234567890abcdef\
    4. 1234567890abcdef\
    5. 1234567890abcdef\
    6. 1234567890abcdef\
    7. 1234567890abcdef\
    8. 1234567890abcdef\
    9. 1234567890abcdef\
    10. 1234567890abcdef\
    11. 1234567890abcdef\
    12. 1234567890abcdef\
    13. 1234567890abcdef\
    14. 1234567890abcdef\
    15. 1234567890abcdef\
    16. 1234567890abcdef\0";
      第1次 第2次 第3次 第4次 第5次 平均
    多进程 0m28.149s 0m27.993s 0m28.094s 0m27.657s 0m28.016s 0m27.982s
    多线程 0m28.171s 0m27.764s 0m27.865s 0m28.041s 0m27.780s 0m27.924s

    进程线程数:255 / 打印次数:100

      第1次 第2次 第3次 第4次 第5次 平均
    多进程 2m20.357s 2m19.740s 2m19.965s 2m19.788s 2m19.796s 2m19.929s
    多线程 2m20.061s 2m20.462s 2m19.789s 2m19.514s 2m19.479s 2m19.861s

    进程线程数:255 / 打印次数:500

      第1次 第2次
    多进程 9m39s 9m17s
    多线程 9m31s 9m22s

    进程线程数:255 / 打印次数:2000 (实验太耗时,因此只进行了2轮比对)

    【实验结论】

    从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

    在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

    四、多进程和多线程在创建和销毁上的效率比较

    预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

    行号 服务器描述 进程控制CPU时间(秒,与基准之差)
    Solaris2.5.1 Digital Unix4.0b BSD/OS3.0
    0 迭代服务器(基准测试,无进程控制) 0.0 0.0 0.0
    1 简单并发服务,为每个客户请求fork一个进程 504.2 168.9 29.6
    2 预先派生子进程,每个子进程调用accept   6.2 1.8
    3 预先派生子进程,用文件锁保护accept 25.2 10.0 2.7
    4 预先派生子进程,用线程互斥锁保护accept 21.5    
    5 预先派生子进程,由父进程向子进程传递套接字 36.7 10.9 6.1
    6 并发服务,为每个客户请求创建一个线程 18.7 4.7  
    7 预先创建线程,用互斥锁保护accept 8.6 3.5  
    8 预先创建线程,由主线程调用accept 14.5 5.0  

    stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

    第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线程), stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建15个 线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

    从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

    1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

    2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

    因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

    创建10万个进程(forkcreat.c):

    1. #include <stdlib.h>
    2. #include <signal.h>
    3. #include <stdio.h>
    4. #include <unistd.h>
    5. #include <sys/stat.h>
    6. #include <fcntl.h>
    7. #include <sys/types.h>
    8. #include <sys/wait.h>
    9.  
    10. int count;  /* 子进程创建成功数量 */
    11. int fcount; /* 子进程创建失败数量 */
    12. int scount; /* 子进程回收数量 */
    13.  
    14. /* 信号处理函数–子进程关闭收集 */
    15. void sig_chld(int signo)
    16. {
    17.    
    18.     pid_t chldpid; /* 子进程id */
    19.     int stat; /* 子进程的终止状态 */
    20.  
    21.     /* 子进程回收,避免出现僵尸进程 */
    22.     while ((chldpid = wait(&stat)) > 0)
    23.     {
    24.         scount++;
    25.     }
    26. }
    27.  
    28. int main()
    29. {
    30.     /* 注册子进程回收信号处理函数 */
    31.     signal(SIGCHLD, sig_chld);
    32.  
    33.     int i;
    34.     for (i = 0; i < 100000; i++) //fork()10万个子进程
    35.     {
    36.         pid_t pid = fork();
    37.         if (pid == -1) //子进程创建失败
    38.         {
    39.             fcount++;
    40.         }
    41.         else if (pid > 0) //子进程创建成功
    42.         {
    43.             count++;
    44.         }
    45.         else if (pid == 0) //子进程执行过程
    46.         {
    47.             exit(0);
    48.         }
    49.     }
    50.  
    51.     printf("count: %d fcount: %d scount: %d\n", count, fcount, scount);
    52. }

    创建10万个线程(pthreadcreat.c):

    1. #include <stdio.h>
    2. #include <pthread.h>
    3.  
    4. int count = 0/* 成功创建线程数量 */
    5.  
    6. void thread(void)
    7. {
    8.     /* 线程啥也不做 */
    9. }
    10.  
    11. int main(void)
    12. {
    13.     pthread_t id; /* 线程id */
    14.     int i,ret;
    15.  
    16.     for (i = 0; i < 100000; i++) /* 创建10万个线程 */
    17.     {
    18.         ret = pthread_create(&id, NULL(void *)thread, NULL);
    19.         if(ret != 0)
    20.         {
    21.             printf ("Create pthread error!\n");
    22.             return (1);
    23.         }
    24.  
    25.         count++;
    26.  
    27.         pthread_join(id, NULL);
    28.     }
    29.    
    30.     printf("count: %d\n", count);
    31.  
    32. }

    创建10万个线程的Java程序:

    1. public class ThreadTest
    2. {
    3.     public static void main(String[] ags) throws InterruptedException
    4.     {
    5.         System.out.println("开始运行");
    6.         long start = System.currentTimeMillis();
    7.         for(int i = 0; i < 100000; i++) //创建10万个线程
    8.         {
    9.             Thread athread = new Thread();  //创建线程对象
    10.             athread.start();                //启动线程
    11.             athread.join();                 //等待该线程停止
    12.         }
    13.        
    14.         System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
    15.     }
    16. }

    在我的赛扬1.5G的CPU上测试结果如下(仍采用测试5次后计算平均值):

    创建销毁10万个进程 创建销毁10万个线程 创建销毁10万个线程(Java)
    0m18.201s 0m3.159s 12286毫秒

    从数据可以看出,多线程比多进程在效率上有5~6倍的优势,但不能让我们在使用那种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优 越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销 毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

    预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优 势。因此我对这一节实验下的结论是:

    预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

    我想,这是fork迷们最愿意看到的结论了。

    五、并发服务的不可测性

    看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

    这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

     

    南二环交通图一

     

    南二环交通图二

    为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

    从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

    那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

    现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

    结束语

    本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

    展开全文
  • optimization: { splitChunks: { chunks: 'async', //对同步,异步,所有的模块有效 minSize: 30000, //当模块大于 30kb maxSize: 0, //对模块进行二次分割时使用,不推荐使用 minChunks: 1, //打包生成的 ...
    optimization: {
      splitChunks: {
        chunks: 'async', //对同步,异步,所有的模块有效 
        minSize: 30000, //当模块大于 30kb 
        maxSize: 0, //对模块进行二次分割时使用,不推荐使用 
        minChunks: 1, //打包生成的 chunk 文件最少有几个 chunk 引用了这个模块 
        maxAsyncRequests: 5, //模块请求 5 次 
        maxInitialRequests: 3, //入口文件同步请求 3 次 
        automaticNameDelimiter: '~',
        name: true,
        cacheGroups: {
          vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10 ,//优先级 数字越大,优先级越高 
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true
            }
          }
        }
      },
      minimize: true,
      minimizer: [
        new TerserPlugin({
          cache: true, // 是否缓存
          parallel: 4 // 是否并行打包,多线程 // parallel: 4,
        }),
      ],
    },
    
    展开全文
  • 多线程

    万次阅读 2018-02-28 16:55:11
    Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。 其中前两种方式线程...

    Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。

    其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。

    1、继承Thread类创建线程
    Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

    public class MyThread extends Thread {  
      public void run() {  
       System.out.println("MyThread.run()");  
      }  
    }  
     
    MyThread myThread1 = new MyThread();  
    MyThread myThread2 = new MyThread();  
    myThread1.start();  
    myThread2.start();  
    

    2、实现Runnable接口创建线程
    如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口,如下:

    public class MyThread extends OtherClass implements Runnable {  
      public void run() {  
       System.out.println("MyThread.run()");  
      }  
    }  
    

    为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

    MyThread myThread = new MyThread();  
    Thread thread = new Thread(myThread);  
    thread.start();  
    

    事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

    public void run() {  
      if (target != null) {  
       target.run();  
      }  
    }  
    

    3、实现Callable接口通过FutureTask包装器来创建Thread线程

    Callable接口(也只有一个方法)定义如下:

    public interface Callable<V>   { 
      V call() throws Exception;   } 
    
    public class SomeCallable<V> extends OtherClass implements Callable<V> {
    
        @Override
        public V call() throws Exception {
            // TODO Auto-generated method stub
            return null;
        }
    
    }
    
    Callable<V> oneCallable = new SomeCallable<V>();   
    //由Callable<Integer>创建一个FutureTask<Integer>对象:   
    FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
    //注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
      //由FutureTask<Integer>创建一个Thread对象:   
    Thread oneThread = new Thread(oneTask);   
    oneThread.start();   
    //至此,一个线程就创建完成了。
    

    4、使用ExecutorService、Callable、Future实现有返回结果的线程

    ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。

    可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。

    执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。

    注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。

    再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

    下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

    import java.util.concurrent.*;  
    import java.util.Date;  
    import java.util.List;  
    import java.util.ArrayList;  
      
    /** 
    * 有返回值的线程 
    */  
    @SuppressWarnings("unchecked")  
    public class Test {  
    public static void main(String[] args) throws ExecutionException,  
        InterruptedException {  
       System.out.println("----程序开始运行----");  
       Date date1 = new Date();  
      
       int taskSize = 5;  
       // 创建一个线程池  
       ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
       // 创建多个有返回值的任务  
       List<Future> list = new ArrayList<Future>();  
       for (int i = 0; i < taskSize; i++) {  
        Callable c = new MyCallable(i + " ");  
        // 执行任务并获取Future对象  
        Future f = pool.submit(c);  
        // System.out.println(">>>" + f.get().toString());  
        list.add(f);  
       }  
       // 关闭线程池  
       pool.shutdown();  
      
       // 获取所有并发任务的运行结果  
       for (Future f : list) {  
        // 从Future对象上获取任务的返回值,并输出到控制台  
        System.out.println(">>>" + f.get().toString());  
       }  
      
       Date date2 = new Date();  
       System.out.println("----程序结束运行----,程序运行时间【"  
         + (date2.getTime() - date1.getTime()) + "毫秒】");  
    }  
    }  
      
    class MyCallable implements Callable<Object> {  
    private String taskNum;  
      
    MyCallable(String taskNum) {  
       this.taskNum = taskNum;  
    }  
      
    public Object call() throws Exception {  
       System.out.println(">>>" + taskNum + "任务启动");  
       Date dateTmp1 = new Date();  
       Thread.sleep(1000);  
       Date dateTmp2 = new Date();  
       long time = dateTmp2.getTime() - dateTmp1.getTime();  
       System.out.println(">>>" + taskNum + "任务终止");  
       return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
    }  
    }  
    

    代码说明:
    上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
    public static ExecutorService newFixedThreadPool(int nThreads)
    创建固定数目线程的线程池。
    public static ExecutorService newCachedThreadPool()
    创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
    public static ExecutorService newSingleThreadExecutor()
    创建一个单线程化的Executor。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

    ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

    展开全文
  • 多线程(一):创建线程和线程的常用方法

    万次阅读 多人点赞 2018-09-01 19:14:23
    一:为什么要学多线程 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也...
  • 本文将带领你与多线程作第一次亲密接触,并深入分析CreateThread与_beginthreadex的本质区别,相信阅读本文后你能轻松的使用多线程并能流畅准确的回答CreateThread与_beginthreadex到底有什么区别,在实际的编程中...
  • 我说我懂多线程,面试官立马给我发了offer

    万次阅读 多人点赞 2020-04-07 09:24:52
    不小心拿了几个offer,有点烦
  • if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
  • if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2015-03-14 13:13:17
    本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
  • 信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该...
  • 什么是多线程?如何实现多线程

    万次阅读 多人点赞 2019-04-09 09:53:36
    【转】什么是线程安全?怎么实现线程安全?什么是进程?什么是线程?...电脑中时会有很单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。比如下图中的QQ、酷狗播放器、电脑...
  • C++ 多线程 API 简介

    万次阅读 2020-10-04 20:31:59
    本文主要介绍了一些C++中的多线程 API 的使用和原理,以及多线程类的封装,包括 线程的创建、挂起、恢复、销毁、使用等等
  • 爬虫养成记--千军万马来相见(详解多线程

    万次阅读 多人点赞 2020-03-23 10:38:32
    爬虫养成记–千军万马来相见(详解多线程) 本文由图雀社区成员 灿若星空 写作而成,欢迎加入图雀社区,一起创作精彩的免费技术教程,予力编程行业发展。 如果您觉得我们写得还不错,记得 点赞 + 关注 + 评论 三...
  • Callable是jdk提供的多线程工具类 来自 java.util.concurrent.Callable; Callable的使用方式比较简单,对于一般的多线程使用Callable接口就可以完成。 DeferredResult是由spring提供的一个多线程工具类,使用...
  • 秒杀多线程第一篇 多线程笔试面试题汇总

    万次阅读 多人点赞 2012-04-05 09:35:49
    系列中不但会详细讲解多线程同步互斥的各种“招式”,而且会进一步的讲解多线程同步互斥的“内功心法”。有了“招式”和“内功心法”,相信你也能对多线程挥洒自如,在笔试面试中顺利的秒杀多线程试题。 ----------...
  • 秒杀多线程第六篇 经典线程同步 事件Event

    万次阅读 多人点赞 2012-04-11 09:06:57
    阅读本篇之前推荐阅读以下姊妹篇:《秒杀多线程第四篇 一个经典的多线程同步问题》《秒杀多线程第五篇 经典线程同步关键段CS》 上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性...
  • 3-04多线程相关

    万次阅读 2019-02-17 14:24:57
    多线程是指一个进程(执行中的程序)同时运行多个线程(进程中负责程序执行的执行单元),多线程可以协作完成进程工作,其目的是更好的利用 CPU 资源 Java线程具有五中基本状态 新建状态(New):当线程对象对创建...
  • 多线程的作用以及什么地方用到多线程?

    万次阅读 多人点赞 2016-07-31 13:09:12
    多线程的作用以及什么地方用到多线程?
  • Java多线程:彻底搞懂线程池

    万次阅读 多人点赞 2019-07-09 19:27:00
    熟悉Java多线程编程的同学都知道,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。 目录 1 线程池的优势 2 线程池的使用 3 线程池的工作原理 4 线程池的参数 4.1 任务队列...
  • 秒杀多线程第五篇 经典线程同步 关键段CS

    万次阅读 多人点赞 2012-04-11 09:06:40
    上一篇《秒杀多线程第四篇 一个经典的多线程同步问题》提出了一个经典的多线程同步互斥问题,本篇将用关键段CRITICAL_SECTION来尝试解决这个问题。本文首先介绍下如何使用关键段,然后再深层次的分析下关键段的实现...
  • python多线程详解(超详细)

    万次阅读 多人点赞 2019-09-28 08:33:31
    python中的多线程是一个非常重要的知识点,今天为大家对多线程进行详细的说明,代码中的注释有多线程的知识点还有测试用的实例。 import threading from threading import Lock,Thread import time,os ''' python...
  • 【2】多线程线程安全

    万次阅读 2019-11-22 20:58:01
    目录 知识点1:什么是线程安全? 1、为什么有线程安全问题? 知识点2:线程安全解决办法 ...知识点3:多线程死锁 1、什么是多线程死锁? 知识点4:Threadlocal 1、什么是Threadlocal 2、ThreadL...
  • Java多线程之初识线程

    万次阅读 2020-09-10 23:49:26
    文章目录实现多线程的两种方式区别继承Thread示例实现Runnable接口示例start()的执行步骤 实现多线程的两种方式 1、继承Thread类; 2、实现Runnable接口。 区别 Java语言是单继承的,使用实现Runnable方式创建线程,...
  • 秒杀多线程第三篇 原子操作 Interlocked系列函数

    万次阅读 多人点赞 2012-04-09 09:08:16
    上一篇《多线程第一次亲密接触 CreateThread与_beginthreadex本质区别》中讲到一个多线程报数功能。为了描述方便和代码简洁起见,我们可以只输出最后的报数结果来观察程序是否运行出错。这也非常类似于统计一个网站...
  • 多线程(一):初识多线程

    万次阅读 多人点赞 2016-03-06 16:24:11
    在这次研究过程中自己也对多线程以及相关的知识进行了深入的探索,总的来说还是蛮不错的一次探索之旅吧! 【前世今缘】 说起我和多线程的渊源,还得追溯到两年前的考试维护,记得当时自己还是一个小兵,看...
  • 秒杀多线程第八篇 经典线程同步 信号量Semaphore

    万次阅读 多人点赞 2012-05-03 09:30:00
    阅读本篇之前推荐阅读以下姊妹篇:《秒杀多线程第四篇一个经典的多线程同步问题》《秒杀多线程第五篇经典线程同步关键段CS》《秒杀多线程第六篇经典线程同步事件Event》《秒杀多线程第七篇经典线程同步互斥量Mutex》...

空空如也

1 2 3 4 5 ... 20
收藏数 447,359
精华内容 178,943
关键字:

多线程