精华内容
下载资源
问答
  • JAVA多线程使用场景和注意事项

    千次阅读 2019-03-15 17:59:58
    我曾经对自己的小弟说,如果你实在搞不清楚什么时候用HashMap,什么时候用...多线程生来就是复杂的,也是容易出错的。一些难以理解的概念,规避。本文不讲基础知识,因为你手里就有jdk的源码。 线程 Threa...

    我曾经对自己的小弟说,如果你实在搞不清楚什么时候用HashMap,什么时候用ConcurrentHashMap,那么就用后者,你的代码bug会很少。

    他问我:ConcurrentHashMap是什么? -.-

    编程不是炫技。大多数情况下,怎么把代码写简单,才是能力。

    多线程生来就是复杂的,也是容易出错的。一些难以理解的概念,要规避。本文不讲基础知识,因为你手里就有jdk的源码。

    线程

    Thread

    第一类就是Thread类。大家都知道有两种实现方式。第一可以继承Thread覆盖它的run方法;第二种是实现Runnable接口,实现它的run方法;而第三种创建线程的方法,就是通过线程池。

    我们的具体代码实现,就放在run方法中。

    我们关注两种情况。一个是线程退出条件,一个是异常处理情况。

    线程退出

    有的run方法执行完成后,线程就会退出。但有的run方法是永远不会结束的。结束一个线程肯定不是通过Thread.stop()方法,这个方法已经在java1.2版本就废弃了。所以我们大体有两种方式控制线程。

    定义退出标志放在while中

    代码一般长这样。

    private volatile boolean flag= true;
    public void run() {
        while (flag) {
        }
    }
    

    标志一般使用volatile进行修饰,使其读可见,然后通过设置这个值来控制线程的运行,这已经成了约定俗成的套路。

    使用interrupt方法终止线程

    类似这种。

    while(!isInterrupted()){……}
    

    对于InterruptedException,比如Thread.sleep所抛出的,我们一般是补获它,然后静悄悄的忽略。中断允许一个可取消任务来清理正在进行的工作,然后通知其他任务它要被取消,最后才终止,在这种情况下,此类异常需要被仔细处理。

    interrupt方法不一定会真正”中断”线程,它只是一种协作机制。interrupt方法通常不能中断一些处于阻塞状态的I/O操作。比如写文件,或者socket传输等。这种情况,需要同时调用正在阻塞操作的close方法,才能够正常退出。

    interrupt系列使用时候一定要注意,会引入bug,甚至死锁。

    异常处理

    java中会抛出两种异常。一种是必须要捕获的,比如InterruptedException,否则无法通过编译;另外一种是可以处理也可以不处理的,比如NullPointerException等。

    在我们的任务运行中,很有可能抛出这两种异常。对于第一种异常,是必须放在try,catch中的。但第二种异常如果不去处理的话,会影响任务的正常运行。

    有很多同学在处理循环的任务时,没有捕获一些隐式的异常,造成任务在遇到异常的情况下,并不能继续执行下去。如果不能确定异常的种类,可以直接捕获Exception或者更通用的Throwable。

    while(!isInterrupted()){
        try{
            ……
        }catch(Exception ex){
            ……
        }
    }
    

    同步方式

    java中实现同步的方式有很多,大体分为以下几种。

    • synchronized 关键字
    • wait、notify等
    • Concurrent包中的ReentrantLock
    • volatile关键字
    • ThreadLocal局部变量

    生产者、消费者是wait、notify最典型的应用场景,这些函数的调用,是必须要放在synchronized代码块里才能够正常运行的。它们同信号量一样,大多数情况下属于炫技,对代码的可读性影响较大,不推荐。关于ObjectMonitor相关的几个函数,只要搞懂下面的图,就基本ok了。

    使用ReentrantLock最容易发生错误的就是忘记在finally代码块里关闭锁。大多数同步场景下,使用Lock就足够了,而且它还有读写锁的概念进行粒度上的控制。我们一般都使用非公平锁,让任务自由竞争。非公平锁性能高于公平锁性能,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。非公平锁还会造成饿死现象:有些任务一直获取不到锁。

    synchronized通过锁升级机制,速度不见得就比lock慢。而且,通过jstack,能够方便的看到其堆栈,使用还是比较广泛。

    volatile总是能保证变量的读可见,但它的目标是基本类型和它锁的基本对象。假如是它修饰的是集合类,比如Map,那么它保证的读可见是map的引用,而不是map对象,这点一定要注意。

    synchronized和volatile都体现在字节码上(monitorenter、monitorexit),主要是加入了内存屏障。而Lock,是纯粹的java api。

    ThreadLocal很方便,每个线程一份数据,也很安全,但要注意内存泄露。假如线程存活时间长,我们要保证每次使用完ThreadLocal,都调用它的remove()方法(具体来说是expungeStaleEntry),来清除数据。

    关于Concurrent包

    concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。

    线程池

    最全的线程池大概有7个参数,想要合理使用线程池,肯定不会不会放过这些参数的优化。

    线程池参数

    concurrent包最常用的就是线程池,平常工作建议直接使用线程池,Thread类就可以降低优先级了。我们常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类创建。

    newSingleThreadExecutor可以用于快速创建一个异步线程,非常方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。

    我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。

    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) 
    

    假如我的任务可以预估,corePoolSize,maximumPoolSize一般都设成一样大的,然后存活时间设的特别的长。可以避免线程频繁创建、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不一样的,一般I/O密集型的应用线程就可以开的多一些。

    threadFactory我一般也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,能够直观的看到我所创建的线程。

    监控

    高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。

    通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。

    线程池饱和策略

    最容易被遗忘的可能就是线程的饱和策略了。也就是线程和缓冲队列的空间全部用完了,新加入的任务将如何处置。jdk默认实现了4种策略,默认实现的是AbortPolicy,也就是直接抛出异常。下面介绍其他几种。

    DiscardPolicy 比abort更加激进,直接丢掉任务,连异常信息都没有。

    CallerRunsPolicy 由调用的线程来处理这个任务。比如一个web应用中,线程池资源占满后,新进的任务将会在tomcat线程中运行。这种方式能够延缓部分任务的执行压力,但在更多情况下,会直接阻塞主线程的运行。

    DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。

    很多情况下,这些饱和策略可能并不能满足你的需求,你可以自定义自己的策略,比如将任务持久化到一些存储中。

    阻塞队列

    阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提高的编码的灵活性,非常方便。在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。

    ArrayBlockingQueue对访问者的调用默认是不公平的,我们可以通过设置构造方法参数将其改成公平阻塞队列。

    LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。

    SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身不存储任何元素,吞吐量非常高。对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。它更像是一个管道,在一些通讯框架中(比如rpc),通常用来快速处理某个请求,应用较为广泛。

    DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象需要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式通常能够比大多数非阻塞的while循环更加节省cpu资源。

    另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜测它的用途。在线程池的构造参数中,我们使用的队列,一定要注意其特性和边界。比如,即使是最简单的newFixedThreadPool,在某些场景下,也是不安全的,因为它使用了无界队列。

    CountDownLatch

    假如有一堆接口A-Y,每个接口的耗时最大是200ms,最小是100ms。

    我的一个服务,需要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?

    此类问题典型的还有赛马问题,只有通过并行计算才能完成问题。归结起来可以分为两类:

    • 实现任务的并行性
    • 开始执行前等待n个线程完成任务

    在concurrent包出现之前,需要手工的编写这些同步过程,非常复杂。现在就可以使用CountDownLatch和CyclicBarrier进行便捷的编码。

    CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
    CyclicBarrier与其类似,可以实现同样的功能。不过在日常的工作中,使用CountDownLatch会更频繁一些。

    信号量

    Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽量少用。

    信号量可以实现限流的功能,但它只是常用限流方式的一种。其他两种是漏桶算法、令牌桶算法。

    hystrix的熔断功能,也有使用信号量进行资源的控制。

    Lock && Condition

    在Java中,对于Lock和Condition可以理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。

    但这些类和函数对于初中级码农来说,难以理解,容易产生bug,应该在业务代码中严格禁止。但在网络编程、或者一些框架类工程中,这些功能是必须的,万不可将这部分的工作随便分配给某个小弟。

    End

    不管是wait、notify,还是同步关键字或者锁,能不用就不用,因为它们会引发程序的复杂性。最好的方式,是直接使用concurrent包所提供的机制,来规避一些编码方面的问题。

    concurrent包中的CAS概念,在一定程度上算是无锁的一种实现。更专业的有类似disruptor的无锁队列框架,但它依然是建立在CAS的编程模型上的。近些年,类似AKKA这样的事件驱动模型正在走红,但编程模型简单,不代表实现简单,背后的工作依然需要多线程去协调。

    golang引入协程(coroutine)概念以后,对多线程加入了更加轻量级的补充。java中可以通过javaagent技术加载quasar补充一些功能,但我觉得你不会为了这丁点效率去牺牲编码的可读性。

    展开全文
  • 多线程开发中需要注意的问题

    千次阅读 2017-03-21 23:04:34
    多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,...

    多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。
    线程函数列表(左为linux,右为windows)
    线程
    创建 pthread_create CreateThread
    退出 pthread_exit ThreadExit
    等待 pthread_join WaitForSingleObject
    互斥锁
    创建 pthread_mutex_init CreateMutex
    销毁 pthread_mutex_destroy CloseHandle
    加锁 pthread_mutex_lock WaitForSingleObject
    解锁 pthread_mutex_unlock ReleaseMutex
    条件
    创建 pthread_cond_init CreateEvent
    销毁 pthread_cond_destroy CloseHandle
    触发 pthread_cond_signal SetEvent
    广播 pthread_cond_broadcast SetEvent/ResetEvent
    等待 pthread_cond_wait/ pthead_cond_timedwait SingleObjectAndWait
    1、明确目的,为什么要使用多线程?如果是由于单线程读写或者网络访问(例如HTTP访问互联网)的瓶颈,可以考虑使用线程池。如果是对不同的资源(例如SOCKET连接)进行管理,可以考虑多个线程。
    2、线程使用中要注意,如何控制线程的调度和阻塞,例如利用事件的触发来控制线程的调度和阻塞,也有用消息来控制的。
    3、线程中如果用到公共资源,一定要考虑公共资源的线程安全性。一般用LOCK锁机制来控制线程安全性。一定要保证不要有死锁机制。
    4、合理使用sleep,何时Sleep,Sleep的大小要根据具体项目,做出合理安排。一般原则非阻塞状态下每个循环都要有SLeep,这样保证减少线程对CPU的抢夺。每次线程的就绪和激活都会占用一定得资源,如果线程体如果有多个循环,多处使用SLEEP将导致性能的下降。
    5、线程的终止一般要使线程体在完成一件工作的情况下终止,一般不要直接使用抛出线程异常的方式终止线程。
    6、线程的优先级一定根据程序的需要要有个整体的规划。
    7、尽量设置 recursive 属性以初始化 Linux 的互斥变量
    互斥锁是多线程编程中基本的概念,在开发中被广泛使用。其调用次序层次清晰简单:建锁,加锁,解锁,销毁锁。但是需要注意的是,与诸如 Windows 平台的互斥变量不同,在默认情况下,Linux 下的同一线程无法对同一互斥锁进行递归加速,否则将发生死锁。所谓递归加锁,就是在同一线程中试图对互斥锁进行两次或两次以上的行为。

    // 通过默认条件建锁
        pthread_mutex_t *theMutex = new pthread_mutex_t; 
        pthread_mutexattr_t attr; 
        pthread_mutexattr_init(&attr); 
        pthread_mutex_init(theMutex,&attr); 
        pthread_mutexattr_destroy(&attr); 
    
        // 递归加锁
        pthread_mutex_lock (theMutex); 
        pthread_mutex_lock (theMutex); 
        pthread_mutex_unlock (theMutex); 
        pthread_mutex_unlock (theMutex);

    在以上代码场景中,问题将出现在第二次加锁操作。由于在默认情况下,Linux 不允许同一线程递归加锁,因此在第二次加锁操作时线程将出现死锁。Linux 互斥变量这种奇怪的行为或许对于特定的某些场景会所有用处,但是对于大多数情况下看起来更像是程序的一个 bug 。毕竟,在同一线程中对同一互斥锁进行递归加锁在尤其是二次开发中经常会需要。这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起 recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。

    pthread_mutexattr_init(&attr); 
        // 设置 recursive 属性
        pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP); 
        pthread_mutex_init(theMutex,&attr);

    因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。
    8、注意 Linux 平台上触发条件变量的自动复位问题
    条件变量的置位和复位有两种常用模型:第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。其状态变化如图所示:
    这里写图片描述
    第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。其状态变化如图所示:
    这里写图片描述
    具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。而对于 Windows,当调用 SetEvent 触发 Auto-reset 的 Event 条件时,如果没有被条件阻塞的线程,那么条件将维持在触发状态,直到有新的线程被条件阻塞并被释放为止。
    这种差异性对于那些熟悉 Windows 平台上的条件变量状态模型而要开发 Linux 平台上多线程的程序员来说可能会造成意想不到的尴尬结果。试想要实现一个旅客坐出租车的程序:旅客在路边等出租车,调用条件等待。出租车来了,将触发条件,旅客停止等待并上车。一个出租车只能搭载一波乘客,于是我们使用单一触发的条件变量。这个实现逻辑在第一个模型下即使出租车先到,也不会有什么问题,其过程如图所示:
    这里写图片描述
    然而如果按照这个思路来在 Linux 上来实现,代码看起来可能是这样:

    ……
     // 提示出租车到达的条件变量
     pthread_cond_t taxiCond; 
    
     // 同步锁
     pthread_mutex_t taxiMutex; 
    
     // 旅客到达等待出租车
     void * traveler_arrive(void * name) { 
        cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl; 
        pthread_mutex_lock(&taxiMutex); 
        pthread_cond_wait (&taxiCond, &taxtMutex); 
        pthread_mutex_unlock (&taxtMutex); 
        cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl; 
        pthread_exit( (void *)0 ); 
     } 
    
     // 出租车到达
     void * taxi_arrive(void *name) { 
        cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl; 
        pthread_cond_signal(&taxtCond); 
        pthread_exit( (void *)0 ); 
     } 
    
     void main() {  
        // 初始化
        taxtCond= PTHREAD_COND_INITIALIZER; 
        taxtMutex= PTHREAD_MUTEX_INITIALIZER; 
        pthread_t thread; 
        pthread_attr_t threadAttr; 
        pthread_attr_init(&threadAttr); 
    
        pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” )); 
        sleep(1); 
        pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” )); 
        sleep(1); 
        pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” )); 
        sleep(1); 
    
        return 0; 
     }
    

    运行结果如下:

    Taxi Jack arrives. 
        Traveler Susan needs a taxi now! 
        Taxi Mike arrives. 
        Traveler Susan now got a taxi.

    采用 Linux 条件变量模型的出租车实例流程
    这里写图片描述
    通过对比结果,你会发现同样的逻辑,在 Linux 平台上运行的结果却完全是两样。对于在 Windows 平台上的模型一, Jack 开着出租车到了站台,触发条件变量。如果没顾客,条件变量将维持触发状态,也就是说 Jack 停下车在那里等着。直到 Susan 小姐来了站台,执行等待条件来找出租车。 Susan 搭上 Jack 的出租车离开,同时条件变量被自动复位。
    但是到了 Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。
    条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后的linux代码如下所示:
    Linux 出租车案例代码实例:

    ……
     // 提示出租车到达的条件变量
     pthread_cond_t taxiCond; 
    
     // 同步锁
     pthread_mutex_t taxiMutex; 
    
     // 旅客人数,初始为 0 
     int travelerCount=0; 
    
     // 旅客到达等待出租车
     void * traveler_arrive(void * name) { 
        cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl; 
        pthread_mutex_lock(&taxiMutex); 
    
        // 提示旅客人数增加
        travelerCount++; 
        pthread_cond_wait (&taxiCond, &taxiMutex); 
        pthread_mutex_unlock (&taxiMutex); 
        cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl; 
        pthread_exit( (void *)0 ); 
     } 
    
     // 出租车到达
     void * taxi_arrive(void *name) 
     { 
        cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl; 
    
     while(true) 
     { 
            pthread_mutex_lock(&taxiMutex); 
    
            // 当发现已经有旅客在等待时,才触发条件变量
            if(travelerCount>0) 
            { 
                pthread_cond_signal(&taxtCond); 
                pthread_mutex_unlock (&taxiMutex); 
                break; 
            } 
            pthread_mutex_unlock (&taxiMutex); 
        } 
    
        pthread_exit( (void *)0 ); 
     }

    因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。
    9、注意条件返回时互斥锁的解锁问题
    在 Linux 调用 pthread_cond_wait 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单所示:

    void * traveler_arrive(void * name) { 
        cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl; 
        pthread_mutex_lock(&taxiMutex); 
        pthread_cond_wait (&taxiCond, &taxtMutex); 
        pthread_mutex_unlock (&taxtMutex); 
        cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl; 
        pthread_exit( (void *)0 ); 
     }

    这一点对于熟悉 Windows 平台多线程开发的开发者来说尤为重要。 Windows 上的 SignalObjectAndWait() 函数是常与 Linux 平台上的 pthread_cond_wait() 函数被看作是跨平台编程时的一对等价函数。但是需要注意的是,两个函数退出时的状态是不一样的。在 Windows 平台上,SignalObjectAndWait(HANDLE a, HANDLE b, …… ) 方法在调用结束返回时的状态是 a 和 b 都是置位(signaled)状态,在普遍的使用方法中,a 经常是一个 Mutex 变量,在这种情况下,当返回时,Mutex a 处于解锁状态(signaled),Event b 处于置位状态(signaled), 因此,对于 Mutex a 而言,我们不需要考虑解锁的问题。而且,在 SignalObjectAndWait() 之后,如果需要对临界区数据进行重新访问,都需要调用 WaitForSingleObject() 重新加锁。这一点刚好与 Linux 下的 pthread_cond_wait() 完全相反。
    Linux 对于 Windows 的这一点额外解锁的操作区别很重要,一定得牢记。否则从 Windows 移植到 Linux 上的条件等待操作一旦忘了结束后的解锁操作,程序将肯定会发生死锁。
    10、等待的绝对时间问题
    超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。但是如果你是第一次使用这个 API 时,首先你需要了解的就是这个 API 当中超时参数的特殊性(就如本节标题所提示的那样)。我们首先来看一下这个 API 的定义。
    pthread_cond_timedwait() 函数定义

    int pthread_cond_timedwait(pthread_cond_t *restrict cond, 
                  pthread_mutex_t *restrict mutex, 
                  const struct timespec *restrict abstime);

    参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。这对于拥有 Windows 平台线程开发经验的人来说可能尤为困惑。因为 Windows 平台下所有的 API 等待参数(如 SignalObjectAndWait,等)都是相对时间,假设我们指定相对的超时时间参数如 dwMilliseconds (单位毫秒)来调用和超时相关的函数,这样就需要将 dwMilliseconds 转化为 Linux 下的绝对时间参数 abstime 使用。
    相对时间到绝对时间转换实例

    /* get the current time */ 
        struct timeval now; 
        gettimeofday(&now, NULL); 
    
        /* add the offset to get timeout value */ 
        abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000; 
        abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

    Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。
    11、正确处理 Linux 平台下的线程结束问题
    在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义如清单

    pthread_join 函数定义

    调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。
    如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。其申明如清单
    pthread_detach 函数定义

    int pthread_detach(pthread_t th);

    另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如清单
    创建 detach 线程代码实例

    ………………………………… .. 
        pthread_t       tid; 
        pthread_attr_t  attr; 
        pthread_attr_init(&attr); 
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 
        pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

    总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。
    总结与补充
    1. Boost 库
    Boost 库来自于由 C++ 标准委员会类库工作组成员发起,致力于为 C++ 开发新的类库的 Boost 组织。虽然该库本身并不是针对多线程而产生,但是发展至今,其已提供了比较全面的多线程编程的 API 支持。 Boost 库对于多线程支持的 API 风格上更类似于 Linux 的 Pthread 库,差别在于其将线程,互斥锁,条件等线程开发概念都封装成了 C++ 类,以方便开发调用。 Boost 库目前对跨平台支持的很不错,不仅支持 Windows 和 Linux ,还支持各种商用的 Unix 版本。如果开发者想使用高稳定性的统一线程编程接口减轻跨平台开发的难度, Boost 库将是首选。
    2. ACE
    ACE 全称是 ADAPTIVE Communication Environment,它是一个免费的,开源的,面向对象的工具框架,用以开发并发访问的软件。由于 ACE 最初是面向网络服务端的编程开发,因此对于线程开发的工具库它也能提供很全面的支持。其支持的平台也很全面,包括 Windows,Linux 和各种版本 Unix 。 ACE 的唯一问题是如果仅仅是用于线程编程,其似乎显得有些过于重量级。而且其较复杂的配置也让其部署对初学者而言并非易事。

    展开全文
  • 为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等 多线程的好处: 1.使用线程可以...

    首先说下多线程出现的原因:

    为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等

    多线程的好处:

    1.使用线程可以把占据时间长的程序中的任务放到后台去处理

    2.用户界面更加吸引人,这样比如用户点击了一个按钮去触发某件事件的处理,可以弹出一个进度条来显示处理的进度

    3.程序的运行效率可能会提高

    4.在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了.

    多线程的缺点:

    1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.

    2.更多的线程需要更多的内存空间

    3.线程中止需要考虑对程序运行的影响.

    4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

    第三部分---------------Java中的线程------------------

    1、        什么是进程?

    答案:进程其实就是一个静态的概念。在机器上实际上运行的都是线程(线程,进程的一部分。)

    2、        什么是线程?

    答案:线程是一个程序内部的顺序控制流。一个进程里面有一个主方法叫main方法。是一个进程里面不同的执行路径。

    3、        什么时候会用到线程?

    答案:当系统中或者开发中。遇到高并发    并行的情况下为了解决负载均衡的问题,就会使用到线程。线程可以提高cpu的利用率。

    4、        在一个时间点上。Cpu只能支持一个线程的运行(由于windows在一个进程中将多个线程执行速度非常的快。所以好多人认为是那是多线程。其实在同一个时间点上只有一个线程在运行)。

    5、        注意:真正的多线程就是当你的机器为双cpu或者是双核的。那么这个时候确实是真正的多线程在运行。

    6、        Java的线程是通过java.long.thread类来实现的。

    7、        一个新的线程需要两步来执行@创建线程@启动线程

    8、        如何创建一个新的新的线程呢?

    答案:Thread  t=new Thread();每创建一个新的thread对象就相当于创建了一个新的线程。

    A)       Thread里面一个特殊的方法run();这个方法就是为执行一个线程而做准备的(当你创建了一个新的线程以后,所有实现的业务逻辑全部在run()方法里面),也就是说在run()方法里面写啥业务。线程就执行实现啥业务。

    B)       启动一个线程用start()方法,也就是说当调用Start()方法线程准备就绪以后。才能去启动执行run()方法里面的所有业务逻辑。、

    C)       总结:进程有独立的运行内存和空间。而线程是进程的一个执行单元。相同的线程是共享内存空间的。所以进程运行上对内存的开销比较大。而线程比较节省内存。

    D)       Therad线程类提供了好多方法。最常用的有sleep()方法,调用此方法是让一个线程处于睡眠状态。它是Thread类的一个静态方法。

    E)        注意:sleep()在使用过程中会抛出异常。当在处理异常的时候用try{}catch{}。当重写的方法无法用throws来处理异常时。就必须用try{}catch{}来处理异常。

    F)        如何结束一个线程?调用方法shutDown()方法。可以定义一个变量。比如:boolean flag=true;将变量变为false不就停止了吗?

    G)       Thread类的join()方法——合并某个线程。

    H)       Yield()方法,让出cpu让其他线程执行的机会。

    I)          线程的优先级:setPriority();用此方法就是设置线程的优先级。

     

    9、        线程同步:多个线程同时访问同一资源时。线程与线程之间协调的这一过程就叫做线程同步。

    A)  synchronized ()方法,指同步方法的意思。锁定线程。同步方法实际就是在执行方法的时候当前的对象呗锁定。(锁定方法实际就是锁定了当前方法的对象。必须等当前线程执行完才能执行下一个线程),线程的同步也就是使线程处于安全化状态。

    B)    线程的死锁:两个线程相互等待的状态就形成了死锁。

    C)   如何解决死锁:加大锁的粒度




    展开全文
  • java 多线程开发注意事项

    千次阅读 2018-08-22 21:55:28
    多线程开发的三大特性 有序性 可见性 对修改后的数据可以看到拥有可见性 原子性 代码在执行的时候必须一次执行完,一次成功或者是一次失败,一次线程对一段代码有掌控,就像事务里面的原子一样 线程,本地内存你...

    多线程开发的三大特性

    • 有序性
    • 可见性
      • 对修改后的数据可以看到拥有可见性
    • 原子性
      • 代码在执行的时候必须一次执行完,一次成功或者是一次失败,一次线程对一段代码有掌控,就像事务里面的原子一样

    线程,本地内存你,驻村交互图

    线程,内存交互图

    在jvm中的三大特性

    有序性
    编译器会对代码以及指令进行编译重排.
    1. 编译器优化重排
    2. 指令集重排
    3. 内存系统重排
    经过三次重新排序经生成最终的指令集.
    在单线程的执行过程中,编译器的重排不会导致结果发生变化.

    有序性编排

    可见性
    jvm在执行多线程的时候会将驻村中的变量拿到线程中的内存中,如果在线程不安全的情况下,A线程在对变量进行更改的时候B线程并不知道,B线程会执行自己的更改,在这种情况下就是操作不可见.

    方法可见性

    原子性
    一个操作是不可终端的,及时在多线程的环境下,一个操作开始就不会被其他线程影响.
    下图中只有语句一是原子性操作

    原子性操作

    通过加锁的形式来保证线程安全

    正常情况下两个线程是交替的执行,如果我们加锁两个线程在执行的时候就是串行的.

    JMM定义内存访问规范

    通过JMM定义内存访问规范,实现有序性可见性,原子性.

    • 程序顺序原则
    • 锁规则
    • volatile变量规则
    • 传递性规则,A先发生于B,B先发生于C那么A必然先于C
    • 线程启动规则
      • 会先调用线程启动中的内容
    • 线程的终止
    • 线程中断规则
    • 对象中介规则

    JMM规范

    通过满足上面的条件来避免加锁

    线程安全的基本概念

    什么是线程安全?
    当过多个线程访问某个类是,不管运行时环境采用何种类,调度方式或者这些线程将如何交替执行,并且在主要调用代码中不需要任何额外的同步活协同,这个类都能表现出正确的行为,那么这个类就是线程安全的.
    通过加锁而实现的线程安全不能叫做线程安全.

    同步机制

    • 监视器所(synchronized)
    • 显示锁(ReentrantLock,ReadWriteLock)
    • 原子变量(atomicInteger,atomicLong,AtomicBollean)
    • Volatile
      • 在使用的时候一定要注意使用原子操作

    线程同步机制

    note
    经过jvm的优化在1.5版本之后synchronized性能已经高于显示锁了
    - 增加了自旋锁等优化模式
    自旋锁实现原理

    问题:遇到同步问题如何选择具体的实现方式
    舞曲:Synchronized是解决一切并发问题的最佳选择

    共享的和可变的变量

    保证共享使用的变量在一个线程中就可以保证线程安全.

    线程封闭技术

    当访问共享的可变数据时,通常需要同步.一种避免同步的方式就是不共享数据.如果仅在单线程内访问数据,就不需要同步,这种技术成为线程封闭.

    将属性设置为不可变变量

    当对象满足一下条件是才是不可变的:
    对象创建后不可修改
    对象所有的与都是final
    最佳方案:使用线程安全对象是实现线程安全的最佳方法.java.util.concurrent

    展开全文
  • 什么多线程?如何实现多线程

    万次阅读 多人点赞 2019-04-09 09:53:36
    【转】什么线程安全?怎么实现线程安全?什么是进程?什么线程什么线程安全?添加一个状态呢?如何确保线程安全?synchronizedlock 转自:https://blog.csdn.net/csdnnews/article/details/82321777 什么是...
  • 众所周知,Java中有一些被称为是线程安全的集合容器,但是这里的线程安全会让人误以为在多线程环境中去使用这些容器就可以放心使用,包打天下。但是事实上并不是如此,在多线程使用这些类仍然会存在问题。这就让人...
  • C++ 在类里面使用多线程技术

    万次阅读 2017-05-16 07:24:13
    前言有很多时候,我们希望可以在C++类里面对那些比较耗时的函数使用多线程技术,但是熟悉C++对象语法的人应该知道,C++类的成员函数的函数指针不能直接做为参数传到pthread_create,主要因为是C++成员函数指针带有类...
  • linux下使用多线程注意事项

    千次阅读 2010-10-09 10:52:00
    根据我的经验,linux下使用pthread库写多线程程序时,在调用系统调用/库函数方面,应注意至少如下几点:1、创建了线程后,不要再使用fork()/vfork()创建子进程2、尽量不使用signal机制3、...关于1,有个具体的教训。...
  • java中为什么要多线程

    千次阅读 2016-09-22 12:56:36
    我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括个独立运行的线程(Thread...这时候,我们需要引入线程“同步”机制,即各位线程之间有个先来后到,不能一窝蜂挤上去抢作一团。 同步这个词是
  • 什么在Python里推荐使用多进程而不是多线程

    万次阅读 多人点赞 2016-04-25 16:19:09
    转载地址  最近在看Python的多线程,经常我们会听到老手说:“Python下多线程是鸡肋,推荐使用多进程!”,但是为什么这么说呢?   知其然,更知其所以然。所以有了下面的深入研究: 首先强调背景:1. GIL是...
  • Qt多线程方法1 继承QThread 1写一个继承于QThread的线程 2 QThread的几个函数quitexitterminate函数 3 正确的终止一个线程 4 如何正确启动一个线程 41正确的启动一个全局线程和UI一直存在的线程 42 如何启动一...
  • python使用多线程查询数据库

    万次阅读 热门讨论 2019-02-01 17:04:39
     当数据量过大时,一个程序的执行时间就会主要花费在等待单次查询返回结果,在这个过程中cpu无疑是处于等待io的空闲状态的,这样既浪费了cpu资源,又花费了大量时间(当然这里主要说多线程,批量查询不在考虑范围,...
  • Python 实现多线程爬虫

    千次阅读 多人点赞 2019-06-19 15:48:44
    什么要爬虫使用多线程? 为了提高抓取数据效率 有些网站对访问速度有限制, 这样网站可以可以开启多个线程, 每一个线程使用一个代理,去提取页面的一部分内容 1.多线程的方法使用 在python3中,主线程主进程结束,...
  • .NET使用多线程编程

    千次阅读 2019-04-20 13:34:50
    本文介绍了如何使用System.Threading命名空间编写多线程应用程序。应用程序中的多线程可能会导致并发问题,例如竞争条件和死锁。 最后,本文讨论了各种同步技术,如Locks,Mutexes和Semaphores,以处理并发问题并...
  • 使用多线程提高代码运行速度

    千次阅读 2017-03-29 15:23:41
    第一次写博客,心情还是比较激动的,写博客的目的主要还是积累下工作中遇到的各种问题以及解决问题的思路。。。 在工作中,难免会遇到业务逻辑比较复杂的情况,这时候...多线程在单核计算机中是不能起到提高代码速度
  • Spring Boot 2.x多线程使用@Async开启多线程,配置类+启动类注解,搞定多线程任务。 配置类TaskPoolConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context....
  • python使用多线程实例讲解

    千次阅读 2017-07-05 17:14:17
    说起多线程,你需要知道多进程和多线程的区别,了解多线程的概念,至于这两点,我不打算在本片文章中详述了,不错,因为我懒得整理。。。不了解的请先问度娘 1、普通的单线程: 打个比方,我想做听音乐和敲代码...
  • Android中使用多线程的各种姿势

    千次阅读 2016-12-04 15:49:17
    1)为什么需要多线程处理? 解决耗时任务 文件IO、联网请求、数据库操作、RPC 提高并发能力 同一时间处理更多事情 防止ANR InputDispatching Timeout:输入事件分发超时5s(触摸或按键) Service Timeout:服务...
  • 多线程编程注意事项

    千次阅读 2016-12-06 11:04:21
    sleep(0)将该线程剩余时间片作废,请勿在主线程中使用,这会减慢消息的处理。 (2)如果消息队列中一条消息处理的时间超过100毫秒,建议另建线程处理。 (3)主线程用于处理用户输入,其生成的二级线程处理与用户...
  • 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的...什么多线程多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务, 也...
  • 在数据处理中,多线程用到的场景很多,在满足计算机CPU处理能力的情况下,使用多线程可以明显提高程序运行效率,缩短大数据处理的能力。作为java程序开发,离不开spring,那么在spring中怎么创建多线程并将注册到...
  • QT多线程编程详解

    万次阅读 多人点赞 2019-04-24 22:08:20
    一、线程基础 1、GUI线程与工作线程 每个程序启动后拥有的第一个线程称为主线程,即GUI线程。QT中所有的组件类和几个相关的类只能工作在GUI线程,不能工作在次...二、QT多线程简介 QT通过三种形式提供了对线程...
  • Java多线程wait()方法注意事项

    千次阅读 2019-05-02 11:40:38
    注意事项一: 当前线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。 下面举个栗子: private static volatile Object resourceA = new ...
  • libcurl 多线程使用注意事项

    万次阅读 2011-05-23 14:00:00
    1、问题来源,多线程使用Libcurl导致程序跑一段时间后自己退出,没有明显的异常。找不到合适的BUG。   最后通过查看资料和网上找的一些文章,发现,原来是信号处理的问题:     CURLOPT_NOSIGNAL Pass ...
  • 使用单线程还是多线程的问题

    千次阅读 2016-06-01 22:05:26
    对于处理时间短的服务或者启动频率高的用单线程,相反用多线程!  不论什么时候只要能用单线程就不用多线程,只有在需要响应时间要求比较高的情况下用多线程 某此操作允许并发而且该操作有可能阻塞时, 用多线程....
  • 现在Qt官方并不是很推荐继承QThread来实现多线程方法,而是极力推崇继承QObject的方法来实现,当然用哪个方法实现视情况而定,别弄错了就行,估计Qt如此推崇继承QObject的方法可能是QThread太容易用错的原因。...
  • 使用pyhton采用多线程方式ping IP

    千次阅读 2016-11-30 22:28:07
    使用ping命令ping多个网络地址时,一般是一个一个的ping,等待前一个结果出来后再ping后一个IP地址,本文使用python多线程写了一个简单的小程序,它支持同时ping多个IP地址。需要注意的有以下几点: 1、本代码...
  • 1, 多线程的作用:可以解决负载均衡问题,充分利用CPU的资源,为了提高Cpu的使用,采用多线程的方法去同时完成几件事情而互不干扰 2. 大多数的情况下, 使用多线程 主要是需要处理大量的IO操作或处理的情况需要花大量...
  • Qt中多线程使用(一)

    万次阅读 2018-07-19 07:55:00
    我们实现的是读取大文件qtgui.index的内容加入文本框中。 很容易想到的方法: QFile* file = new QFile("E:\qtgui.index"); file-&gt;open(QIODevice::ReadOnly); QTextStream *stream = ...
  • Java多线程超详解

    万次阅读 多人点赞 2019-06-11 01:00:30
    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 957,705
精华内容 383,082
关键字:

使用多线程要注意什么