精华内容
下载资源
问答
  • 问题来了,因为MINA框架是多线程的,所以单单用一个对象去调用自己的方法,有可能会造成程序崩溃。因为当多条线程同时调用这个对象的方法时,就死了。所以怎么才能将这个成员对象转换成为多线程来运行呢?
  • QT多线程编程详解

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

    一、线程基础

    1、GUI线程与工作线程

    每个程序启动后拥有的第一个线程称为主线程,即GUI线程。QT中所有的组件类和几个相关的类只能工作在GUI线程,不能工作在次线程,次线程即工作线程,主要负责处理GUI线程卸下的工作。

    2、数据的同步访问

    每个线程都有自己的栈,因此每个线程都要自己的调用历史和本地变量。线程共享相同的地址空间。

    二、QT多线程简介

        QT通过三种形式提供了对线程的支持,分别是平台无关的线程类、线程安全的事件投递、跨线程的信号-槽连接。

        QT中线程类包含如下:

     QThread 提供了跨平台的多线程解决方案

     QThreadStorage 提供逐线程数据存储
        QMutex 提供相互排斥的锁,或互斥量
        QMutexLocker 是一个辅助类,自动对 QMutex 加锁与解锁
        QReadWriterLock 提供了一个可以同时读操作的锁
        QReadLocker与QWriteLocker 自动对QReadWriteLock 加锁与解锁
        QSemaphore 提供了一个整型信号量,是互斥量的泛化
        QWaitCondition 提供了一种方法,使得线程可以在被另外线程唤醒之前一直休眠。

    三、QThread线程

    1、QThread线程基础

        QThread是Qt线程中有一个公共的抽象类,所有的线程类都是从QThread抽象类中派生的,需要实现QThread中的虚函数run(),通过start()函数来调用run函数。

        void run()函数是线程体函数,用于定义线程的功能。

        void start()函数是启动函数,用于将线程入口地址设置为run函数。

        void terminate()函数用于强制结束线程,不保证数据完整性和资源释放。

        QCoreApplication::exec()总是在主线程(执行main()的线程)中被调用,不能从一个QThread中调用。在GUI程序中,主线程也称为GUI线程,是唯一允许执行GUI相关操作的线程。另外,必须在创建一个QThread前创建QApplication(or QCoreApplication)对象。

        当线程启动和结束时,QThread会发送信号started()和finished(),可以使用isFinished()和isRunning()来查询线程的状态。

        从Qt4.8起,可以释放运行刚刚结束的线程对象,通过连接finished()信号到QObject::deleteLater()槽。 
        使用wait()来阻塞调用的线程,直到其它线程执行完毕(或者直到指定的时间过去)。

        静态函数currentThreadId()和currentThread()返回标识当前正在执行的线程。前者返回线程的ID,后者返回一个线程指针。

        要设置线程的名称,可以在启动线程之前调用setObjectName()。如果不调用setObjectName(),线程的名称将是线程对象的运行时类型(QThread子类的类名)。

    2、线程的优先级

        QThread线程总共有8个优先级

        QThread::IdlePriority   0 scheduled only when no other threads are running.

        QThread::LowestPriority  1 scheduled less often than LowPriority.

        QThread::LowPriority   2 scheduled less often than NormalPriority.

        QThread::NormalPriority  3 the default priority of the operating system.

        QThread::HighPriority   4 scheduled more often than NormalPriority.

        QThread::HighestPriority  5 scheduled more often than HighPriority.

        QThread::TimeCriticalPriority 6 scheduled as often as possible.

        QThread::InheritPriority   7 use the same priority as the creating thread. This is the default.

        void setPriority(Priority priority) 
        设置正在运行线程的优先级。如果线程没有运行,此函数不执行任何操作并立即返回。使用的start()来启动一个线程具有特定的优先级。优先级参数可以是QThread::Priority枚举除InheritPriortyd的任何值。

    3、线程的创建

       void start ( Priority priority = InheritPriority )

        启动线程执行,启动后会发出started ()信号

    4、线程的执行

    int exec() [protected] 
        进入事件循环并等待直到调用exit(),返回值是通过调用exit()来获得,如果调用成功则返回0。

    void run() [virtual protected] 
        线程的起点,在调用start()之后,新创建的线程就会调用run函数,默认实现调用exec(),大多数需要重新实现run函数,便于管理自己的线程。run函数返回时,线程的执行将结束。

    5、线程的退出

    void quit();

    通知线程事件循环退出,返回0表示成功,相当于调用了QThread::exit(0)。

    void exit ( int returnCode = 0 );

    调用exit后,thread将退出event loop,并从exec返回,exec的返回值就是returnCode。通常returnCode=0表示成功,其他值表示失败。

    void terminate ();

        结束线程,线程是否立即终止取决于操作系统。

        线程被终止时,所有等待该线程Finished的线程都将被唤醒。

        terminate是否调用取决于setTerminationEnabled ( bool enabled = true )开关。

        void requestInterruption() 
        请求线程的中断。请求是咨询意见并且取决于线程上运行的代码,来决定是否及如何执行这样的请求。此函数不停止线程上运行的任何事件循环,并且在任何情况下都不会终止它。

        工程中线程退出的解决方案如下:

        通过在线程类中增加标识变量volatile bool m_stop,通过m_stop变量的值判断run函数是否执行结束返回。

     

    #ifndef WORKTHREAD_H
    #define WORKTHREAD_H
    #include <QThread>
    #include <QDebug>
     
    class WorkThread : public QThread
    {
    protected:
      //线程退出的标识量
      volatile bool m_stop;
      void run()
      {
        qDebug() << "run begin";
        while(!m_stop)
        {
            //task handling
            int* p = new int[1000];
            for(int i = 0; i < 1000; i++)
            {
                p[i] = i * i;
            }
            sleep(2);
            delete [] p;
        }
        qDebug() << "run end";
      }
    public:
      WorkThread()
      {
        m_stop = false;
      }
      //线程退出的接口函数,用户使用
      void stop()
      {
        m_stop = true;
      }
    };
     
    #endif // WORKTHREAD_H

     

    6、线程的等待

    bool wait ( unsigned long time = ULONG_MAX )

        线程将会被阻塞,等待time毫秒,如果线程退出,则wait会返回。Wait函数解决多线程在执行时序上的依赖。

    void msleep ( unsigned long msecs )

    void sleep ( unsigned long secs )

    void usleep ( unsigned long usecs )

        sleep()、msleep()、usleep()允许秒,毫秒和微秒来区分,但在Qt5.0中被设为public。

        一般情况下,wait()和sleep()函数应该不需要,因为Qt是一个事件驱动型框架。考虑监听finished()信号来取代wait(),使用QTimer来取代sleep()。

    7、线程的状态

    bool isFinished () const  线程是否已经退出

    bool isRunning () const   线程是否处于运行状态

    8、线程的属性

    Priority priority () const

    void setPriority ( Priority priority )

    uint stackSize () const

    void setStackSize ( uint stackSize )

    void setTerminationEnabled ( bool enabled = true )

    设置是否响应terminate()函数

    9、线程与事件循环

        QThread中run()的默认实现调用了exec(),从而创建一个QEventLoop对象,由QEventLoop对象处理线程中事件队列(每一个线程都有一个属于自己的事件队列)中的事件。exec()在其内部不断做着循环遍历事件队列的工作,调用QThread的quit()或exit()方法使退出线程,尽量不要使用terminate()退出线程,terminate()退出线程过于粗暴,造成资源不能释放,甚至互斥锁还处于加锁状态。

        线程中的事件循环,使得线程可以使用那些需要事件循环的非GUI 类(如,QTimer,QTcpSocket,QProcess)。

        在QApplication前创建的对象,QObject::thread()返回NULL,意味着主线程仅为这些对象处理投递事件,不会为没有所属线程的对象处理另外的事件。可以用QObject::moveToThread()来改变对象及其子对象的线程亲缘关系,假如对象有父亲,不能移动这种关系。在另一个线程(而不是创建它的线程)中delete QObject对象是不安全的。除非可以保证在同一时刻对象不在处理事件。可以用QObject::deleteLater(),它会投递一个DeferredDelete事件,这会被对象线程的事件循环最终选取到。假如没有事件循环运行,事件不会分发给对象。假如在一个线程中创建了一个QTimer对象,但从没有调用过exec(),那么QTimer就不会发射它的timeout()信号,deleteLater()也不会工作。可以手工使用线程安全的函数QCoreApplication::postEvent(),在任何时候,给任何线程中的任何对象投递一个事件,事件会在那个创建了对象的线程中通过事件循环派发。事件过滤器在所有线程中也被支持,不过它限定被监视对象与监视对象生存在同一线程中。QCoreApplication::sendEvent(不是postEvent()),仅用于在调用此函数的线程中向目标对象投递事件。

    四、线程的同步

    1、线程同步基础

        临界资源:每次只允许一个线程进行访问的资源

        线程间互斥:多个线程在同一时刻都需要访问临界资源

        线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护。

        线程死锁:线程间相互等待临界资源而造成彼此无法继续执行。

        产生死锁的条件:

        A、系统中存在多个临界资源且临界资源不可抢占

        B、线程需要多个临界资源才能继续执行

        死锁的避免:

        A、对使用的每个临界资源都分配一个唯一的序号

        B、对每个临界资源对应的线程锁分配相应的序号

        C、系统中的每个线程按照严格递增的次序请求临界资源

        QMutex, QReadWriteLock, QSemaphore, QWaitCondition 提供了线程同步的手段。使用线程的主要想法是希望它们可以尽可能并发执行,而一些关键点上线程之间需要停止或等待。例如,假如两个线程试图同时访问同一个全局变量,结果可能不如所愿。

    2、互斥量QMutex

        QMutex 提供相互排斥的锁,或互斥量。在一个时刻至多一个线程拥有mutex,假如一个线程试图访问已经被锁定的mutex,那么线程将休眠,直到拥有mutex的线程对此mutex解锁。QMutex常用来保护共享数据访问。QMutex类所以成员函数是线程安全的。

       头文件声明:    #include <QMutex>

    互斥量声明:    QMutex m_Mutex;

    互斥量加锁:    m_Mutex.lock();

       互斥量解锁:    m_Mutex.unlock();

        如果对没有加锁的互斥量进行解锁,结果是未定义的。互斥量的加锁和解锁必须在同一线程中成对出现。

        QMutex ( RecursionMode mode = NonRecursive )

        QMutex有两种模式:Recursive, NonRecursive

    A、Recursive

        一个线程可以对mutex多次lock,直到相应次数的unlock调用后,mutex才真正被解锁。

    B、NonRecursive

        默认模式,mutex只能被lock一次。

        如果使用了Mutex.lock()而没有对应的使用Mutex.unlcok()的话就会造成死锁,其他的线程将永远也得不到接触Mutex锁住的共享资源的机会。尽管可以不使用lock()而使用tryLock(timeout)来避免因为死等而造成的死锁( tryLock(负值)==lock()),但是还是很有可能造成错误。

        bool tryLock();

        如果当前其他线程已对该mutex加锁,则该调用会立即返回,而不被阻塞。

        bool tryLock(int timeout);

        如果当前其他线程已对该mutex加锁,则该调用会等待一段时间,直到超时

     

    QMutex mutex;
    int complexFunction(int flag)
     {
         mutex.lock();
         int retVal = 0;
         switch (flag) {
         case 0:
         case 1:
             mutex.unlock();
             return moreComplexFunction(flag);
         case 2:
             {
                 int status = anotherFunction();
                 if (status < 0) {
                     mutex.unlock();
                     return -2;
                 }
                 retVal = status + flag;
             }
             break;
         default:
             if (flag > 10) {
                 mutex.unlock();
                 return -1;
             }
             break;
         }
     
         mutex.unlock();
         return retVal;
     }

     

    3、互斥锁QMutexLocker

        在较复杂的函数和异常处理中对QMutex类mutex对象进行lock()和unlock()操作将会很复杂,进入点要lock(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QMutex的辅助类QMutexLocker来避免lock()和unlock()操作。在函数需要的地方建立QMutexLocker对象,并把mutex指针传给QMutexLocker对象,此时mutex已经加锁,等到退出函数后,QMutexLocker对象局部变量会自己销毁,此时mutex解锁。

    头文件声明:    #include<QMutexLocker>

    互斥锁声明:    QMutexLocker mutexLocker(&m_Mutex);

    互斥锁加锁:    从声明处开始(在构造函数中加锁)

    互斥锁解锁:    出了作用域自动解锁(在析构函数中解锁)

     

    QMutex mutex;
     int complexFunction(int flag)
     {
         QMutexLocker locker(&mutex);
         int retVal = 0;
         switch (flag) {
         case 0:
         case 1:
             return moreComplexFunction(flag);
         case 2:
             {
                 int status = anotherFunction();
                 if (status < 0)
                     return -2;
                 retVal = status + flag;
             }
             break;
         default:
             if (flag > 10)
                 return -1;
             break;
         }
         return retVal;
     }

     

    4、QReadWriteLock

        QReadWriterLock 与QMutex相似,但对读写操作访问进行区别对待,可以允许多个读者同时读数据,但只能有一个写,并且写读操作不同同时进行。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。 QReadWriterLock默认模式是NonRecursive

    QReadWriterLock类成员函数如下:

    QReadWriteLock ( )

    QReadWriteLock ( RecursionMode recursionMode )

    void lockForRead ()

    void lockForWrite ()

    bool tryLockForRead ()

    bool tryLockForRead ( int timeout )

    bool tryLockForWrite ()

    bool tryLockForWrite ( int timeout )

    boid unlock ()

    使用实例:

     

    QReadWriteLock lock;
     void ReaderThread::run()
     {
         lock.lockForRead();
         read_file();
         lock.unlock();
     }
     
     void WriterThread::run()
     {
         lock.lockForWrite();
         write_file();
         lock.unlock();
     }

     

    5、QReadLocker和QWriteLocker

        在较复杂的函数和异常处理中对QReadWriterLock类lock对象进行lockForRead()/lockForWrite()和unlock()操作将会很复杂,进入点要lockForRead()/lockForWrite(),在所有跳出点都要unlock(),很容易出现在某些跳出点未调用unlock(),所以Qt引进了QReadLocker和QWriteLocker类来简化解锁操作。在函数需要的地方建立QReadLocker或QWriteLocker对象,并把lock指针传给QReadLocker或QWriteLocker对象,此时lock已经加锁,等到退出函数后,QReadLocker或QWriteLocker对象局部变量会自己销毁,此时lock解锁。

      

    QReadWriteLock lock;
     QByteArray readData()
     {
         lock.lockForRead();
         ...
         lock.unlock();
         return data;
     }

     

    使用QReadLocker:

     

    QReadWriteLock lock;
     QByteArray readData()
     {
         QReadLocker locker(&lock);
         ...
         return data;
     }

     

    6、信号量QSemaphore

        QSemaphore 是QMutex的一般化,是特殊的线程锁,允许多个线程同时访问临界资源,而一个QMutex只保护一个临界资源。QSemaphore 类的所有成员函数是线程安全的。

        经典的生产者-消费者模型如下:某工厂只有固定仓位,生产人员每天生产的产品数量不一,销售人员每天销售的产品数量也不一致。当生产人员生产P个产品时,就一次需要P个仓位,当销售人员销售C个产品时,就要求仓库中有足够多的产品才能销售。如果剩余仓位没有P个时,该批次的产品都不存入,当当前已有的产品没有C个时,就不能销售C个以上的产品,直到新产品加入后方可销售。

        QSemaphore来控制对环状缓冲的访问,此缓冲区被生产者线程和消费者线程共享。生产者不断向缓冲区写入数据直到缓冲末端,再从头开始。消费者从缓冲不断读取数据。信号量比互斥量有更好的并发性,假如我们用互斥量来控制对缓冲的访问,那么生产者、消费者不能同时访问缓冲区。然而,我们知道在同一时刻,不同线程访问缓冲的不同部分并没有什么危害。

    QSemaphore 类成员函数:

    QSemaphore ( int n = 0 )

    void acquire ( int n = 1 )

    int available () const

    void release ( int n = 1 )

    bool tryAcquire ( int n = 1 )

    bool tryAcquire ( int n, int timeout )

    实例代码:

     QSemaphore sem(5);      // sem.available() == 5

     sem.acquire(3);         // sem.available() == 2

     sem.acquire(2);         // sem.available() == 0

     sem.release(5);         // sem.available() == 5

     sem.release(5);         // sem.available() == 10

     sem.tryAcquire(1);      // sem.available() == 9, returns true

     sem.tryAcquire(250);    // sem.available() == 9, returns false

    生产者-消费者实例:

    #include <QtCore/QCoreApplication>

    #include <QSemaphore>

    #include <QThread>

    #include <cstdlib>

    #include <cstdio>

    const int DataSize = 100000;

    const int BufferSize = 8192;

    char buffer[BufferSize];

    QSemaphore  production(BufferSize);

    QSemaphore  consumption;

    class Producor:public QThread

    {

    public:

        void run();

    };

    void Producor::run()

    {

        for(int i = 0; i < DataSize; i++)

        {

            production.acquire();

            buffer[i%BufferSize] = "ACGT"[(int)qrand()%4];

            consumption.release();

        }

    }

    class Consumer:public QThread

    {

    public:

        void run();

    };

    void Consumer::run()

    {

        for(int i = 0; i < DataSize; i++)

        {

            consumption.acquire();

            fprintf(stderr, "%c", buffer[i%BufferSize]);

            production.release();

        }

        fprintf(stderr, "%c", "\n");

    }

    int main(int argc, char *argv[])

    {

        QCoreApplication a(argc, argv);

        Producor productor;

        Consumer consumer;

        productor.start();

        consumer.start();

        productor.wait();

        consumer.wait();

        return a.exec();

    }

    Producer::run函数:

       当producer线程执行run函数,如果buffer中已满,而consumer线程没有读,producer不能再往buffer中写字符,在 productor.acquire 处阻塞直到 consumer线程读(consume)数据。一旦producer获取到一个字节(资源)就写入一个随机的字符,并调用 consumer.release 使consumer线程可以获取一个资源(读一个字节的数据)。

        Consumer::run函数:

       当consumer线程执行run函数,如果buffer中没有数据,则consumer线程在consumer.acquire处阻塞,直到producer线程执行写操作写入一个字节,并执行consumer.release 使consumer线程的可用资源数=1时,consumer线程从阻塞状态中退出, 并将consumer 资源数-1,consumer当前资源数=0。

    7、等待条件QWaitCondition

        QWaitCondition 允许线程在某些情况发生时唤醒另外的线程。一个或多个线程可以阻塞等待QWaitCondition ,用wakeOne()或wakeAll()设置一个条件。wakeOne()随机唤醒一个,wakeAll()唤醒所有。

    QWaitCondition ()

    bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX )

    bool wait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )

    void wakeOne ()

    void wakeAll ()

    头文件声明:    #include <QWaitCondition>

    等待条件声明:    QWaitCondtion m_WaitCondition;

    等待条件等待:    m_WaitConditon.wait(&m_muxtex, time);

    等待条件唤醒:    m_WaitCondition.wakeAll();

    在经典的生产者-消费者场合中,生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果缓冲区已满,线程停下来等待 bufferNotFull条件。如果没有满,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。使用mutex来保护对numUsedBytes的访问。QWaitCondition::wait() 接收一个mutex作为参数,mutex被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒时,mutex会处于锁定状态,从锁定状态到等待状态的转换是原子操作。当程序开始运行时,只有生产者可以工作,消费者被阻塞等待bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。

    #include <QtCore/QCoreApplication>

    #include <QSemaphore>

    #include <QThread>

    #include <cstdlib>

    #include <cstdio>

    #include <QWaitCondition>

    #include <QMutex>

    #include <QTime>

    const int DataSize = 32;

    const int BufferSize = 16;

    char buffer[BufferSize];

    QWaitCondition bufferNotEmpty;

    QWaitCondition bufferNotFull;

    QMutex mutex;

    int used = 0;

    class Producor:public QThread

    {

    public:

        void run();

    };

    void Producor::run()

    {

        qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

        for(int i = 0; i < DataSize; i++)

        {

            mutex.lock();

            if(used == BufferSize)

                bufferNotFull.wait(&mutex);

            mutex.unlock();

            buffer[i%BufferSize] = used;

            mutex.lock();

            used++;

            bufferNotEmpty.wakeAll();

            mutex.unlock();

        }

    }

    class Consumer:public QThread

    {

    public:

        void run();

    };

    void Consumer::run()

    {

        for(int i = 0; i < DataSize; i++)

        {

            mutex.lock();

            if(used == 0)

                bufferNotEmpty.wait(&mutex);

            mutex.unlock();

            fprintf(stderr, "%d\n", buffer[i%BufferSize]);

            mutex.lock();

            used--;

            bufferNotFull.wakeAll();

            mutex.unlock();

        }

        fprintf(stderr, "%c", "\n");

    }

    int main(int argc, char *argv[])

    {

        QCoreApplication a(argc, argv);

        Producor productor;

        Consumer consumer;

        productor.start();

        consumer.start();

        productor.wait();

        consumer.wait();

        return a.exec();

    }

    8、高级事件队列

    QT事件系统对进程间通信很重要,每个进程可以有自己的事件循环,要在另外一个线程中调用一个槽函数(或任何invokable方法),需要将调用槽函数放置在目标线程的事件循环中,让目标线程在槽函数开始运行之前,先完成自己的当前任务,而原来的线程继续并行运行。

    要在一个事件循环中执行调用槽函数,需要一个queued信号槽连接。每当信号发出时,信号的参数将被事件系统记录。信号接收者存活的线程将运行槽函数。另外,不使用信号,调用QMetaObject::invokeMethod()也可以达到相同的效果。在这两种情况下,必须使用queued连接,因为direct连接绕过了事件系统,并且立即在当前线程中运行此方法。

        当线程同步使用事件系统时,没有死锁风险。然而,事件系统不执行互斥。如果调用方法访问共享数据,仍然需要使用QMutex来保护。

    如果只使用信号槽,并且线程间没有共享变量,那么,多线程程序可以完全没有低级原语。

    五、可重入与线程安全

    可重入reentrant与线程安全thread-safe被用来说明一个函数如何用于多线程程序。

    一个线程安全的函数可以同时被多个线程调用,甚至调用者会使用共享数据也没有问题,因为对共享数据的访问是串行的。一个可重入函数也可以同时被多个线程调用,但是每个调用者只能使用自己的数据。因此,一个线程安全的函数总是可重入的,但一个可重入的函数并不一定是线程安全的。

        一个可重入的类,指的是类的成员函数可以被多个线程安全地调用,只要每个线程使用类的不同的对象。而一个线程安全的类,指的是类的成员函数能够被多线程安全地调用,即使所有的线程都使用类的同一个实例。

    1、可重入

        大多数C++类是可重入的,因为它们典型地仅仅引用成员数据。任何线程可以访问可重入类实例的成员函数,只要同一时间没有其他线程调用这个实例的成员函数。

    class Counter
    {
      public:
          Counter() {n=0;}
          void increment() {++n;}
          void decrement() {--n;}
          int value() const {return n;}
     private:
          int n;
    };

        Counter类是可重入的,但却不是线程安全的。假如多个线程都试图修改数据成员n,结果未定义。

        大多数Qt类是可重入,非线程安全的。有一些类与函数是线程安全的,主要是线程相关的类,如QMutex,QCoreApplication::postEvent()。

    2、线程安全

        所有的GUI类(如QWidget及其子类),操作系统核心类(如QProcess)和网络类都不是线程安全的。

    class Counter
     {
     public:
         Counter() { n = 0; }

    void increment() { QMutexLocker locker(&mutex); ++n; }
         void decrement() { QMutexLocker locker(&mutex); --n; }
         int value() const { QMutexLocker locker(&mutex); return n; }

    private:
         mutable QMutex mutex;
         int n;
     };

     Counter类是可重入和线程安全的。QMutexLocker类在构造函数中自动对mutex进行加锁,在析构函数中进行解锁。mutex使用了mutable关键字来修饰,因为在value()函数中对mutex进行加锁与解锁操作,而value()是一个const函数。

    六、线程与信号槽

    1、线程的依附性

        线程的依附性是对象与线程的关系。默认情况下,对象依附于自身被创建的线程。

        对象的依附性与槽函数执行的关系,默认情况下,槽函数在其所依附的线程中被调用执行。

        修改对象的依附性的方法:QObject::moveToThread函数用于改变对象的线程依附性,使得对象的槽函数在依附的线程中被调用执行。

    2、QObject线程

    QThread类具有发送信号和定义槽函数的能力。QThread主要信号如下:

    void started();线程开始运行时发送信号

    void finished();线程完成运行时发送信号

    void terminated();线程被异常终止时发送信号

        QThread继承自QObject,发射信号以指示线程执行开始与结束,并提供了许多槽函数。QObjects可以用于多线程,发射信号以在其它线程中调用槽函数,并且向“存活”于其它线程中的对象发送事件。
    QObject的可重入性

        QObject是可重入的,QObject的大多数非GUI子类如 QTimer、QTcpSocket、QUdpSocket、QHttp、QFtp、QProcess也是可重入的,在多个线程中同时使用这些类是可能的。可重入的类被设计成在一个单线程中创建与使用,在一个线程中创建一个对象而在另一个线程中调用该对象的函数,不保证能行得通。有三种约束需要注意:

        A、一个QObject类型的孩子必须总是被创建在它的父亲所被创建的线程中。这意味着,除了别的以外,永远不要把QThread对象(this)作为该线程中创建的一个对象的父亲(因为QThread对象自身被创建在另外一个线程中)。

        B、事件驱动的对象可能只能被用在一个单线程中。特别适用于计时器机制(timer mechanism)和网络模块。例如:不能在不属于这个对象的线程中启动一个定时器或连接一个socket,必须保证在删除QThread之前删除所有创建在这个线程中的对象。在run()函数的实现中,通过在栈中创建这些对象,可以轻松地做到这一点。

        C、虽然QObject是可重入的,但GUI类,尤其是QWidget及其所有子类都不是可重入的,只能被用在GUI线程中。QCoreApplication::exec()必须也从GUI线程被调用。

        在实践中,只能在主线程而非其它线程中使用GUI的类,可以很轻易地被解决:将耗时操作放在一个单独的工作线程中,当工作线程结束后在GUI线程中由屏幕显示结果。

        一般来说,在QApplication前创建QObject是不行的,会导致奇怪的崩溃或退出,取决于平台。因此,不支持QObject的静态实例。一个单线程或多线程的应用程序应该先创建QApplication,并最后销毁QObject。

    3、线程的事件循环

        每个线程都有自己的事件循环。主线程通过QCoreApplication::exec()来启动自己的事件循环,但对话框的GUI应用程序,有些时候用QDialog::exec(),其它线程可以用QThread::exec()来启动事件循环。就像 QCoreApplication,QThread提供一个exit(int)函数和quit()槽函数。

        线程中的事件循环使得线程可以利用一些非GUI的、要求有事件循环存在的Qt类(例如:QTimer、QTcpSocket、和QProcess),使得连接一些线程的信号到一个特定线程的槽函数成为可能。

        

        一个QObject实例被称为存活于它所被创建的线程中。关于这个对象的事件被分发到该线程的事件循环中。可以用QObject::thread()方法获取一个QObject所处的线程。

        QObject::moveToThread()函数改变一个对象和及其子对象的线程所属性。(如果对象有父对象的话,对象不能被移动到其它线程中)。

    从另一个线程(不是QObject对象所属的线程)对该QObject对象调用delete方法是不安全的,除非能保证该对象在那个时刻不处理事件,使用QObejct::deleteLater()更好。一个DeferredDelete类型的事件将被提交(posted),而该对象的线程的 件循环最终会处理这个事件。默认情况下,拥有一个QObject的线程就是创建QObject的线程,而不是 QObject::moveToThread()被调用后的。

        如果没有事件循环运行,事件将不会传递给对象。例如:在一个线程中创建了一个QTimer对象,但从没有调用exec(),那么,QTimer就永远不会发射timeout()信号,即使调用deleteLater()也不行。(这些限制也同样适用于主线程)。

        利用线程安全的方法QCoreApplication::postEvent(),可以在任何时刻给任何线程中的任何对象发送事件,事件将自动被分发到该对象所被创建的线程事件循环中。

        所有的线程都支持事件过滤器,而限制是监控对象必须和被监控对象存在于相同的线程中。QCoreApplication::sendEvent()(不同于postEvent())只能将事件分发到和该函数调用者相同的线程中的对象。

    4、其他线程访问QObject子类

        QObject及其所有子类都不是线程安全的。这包含了整个事件交付系统。重要的是,切记事件循环可能正在向你的QObject子类发送事件,当你从另一个线程访问该对象时。

        如果你正在调用一个QObject子类的函数,而该子类对象并不存活于当前线程中,并且该对象是可以接收事件的,那么你必须用一个mutex保护对该QObject子类的内部数据的所有访问,否则,就有可能发生崩溃和非预期的行为。

        同其它对象一样,QThread对象存活于该对象被创建的线程中 – 而并非是在QThread::run()被调用时所在的线程。一般来说,在QThread子类中提供槽函数是不安全的,除非用一个mutex保护成员变量。

        另一方面,可以在QThread::run()的实现中安全地发射信号,因为信号发射是线程安全的。

    5、跨线程的信号槽

        线程的信号槽机制需要开启线程的事件循环机制,即调用QThread::exec()函数开启线程的事件循环。

    Qt信号-槽连接函数原型如下:

    bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection ) 

    Qt支持5种连接方式

    A、Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用,同步执行)

        当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。

        当信号发射时,槽函数将直接被调用。

        无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。

    B、Qt::QueuedConnection(队列方式)(此时信号被塞到事件队列里,信号与槽函数关系类似于消息通信,异步执行)

        当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。

        当控制权回到接收者所依附线程的事件循环时,槽函数被调用。

        槽函数在接收者所依附线程执行。

    C、Qt::AutoConnection(自动方式)

         Qt的默认连接方式,如果信号的发出和接收信号的对象同属一个线程,那个工作方式与直连方式相同;否则工作方式与队列方式相同。

    如果信号在接收者所依附的线程内发射,则等同于直接连接

    如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接

    D、Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中,否则就产生死锁)

        槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回。

    E、Qt::UniqueConnection

        与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。

        QThread是用来管理线程的,QThread对象所依附的线程和所管理的线程并不是同一个概念。QThread所依附的线程,就是创建QThread对象的线程,QThread 所管理的线程,就是run启动的线程,也就是新建线程。QThread对象依附在主线程中,QThread对象的slot函数会在主线程中执行,而不是次线程。除非QThread对象依附到次线程中(通过movetoThread)。

    工程实践中,为了避免冻结主线程的事件循环(即避免因此而冻结了应用的UI),所有的计算工作是在一个单独的工作线程中完成的,工作线程结束时发射一个信号,通过信号的参数将工作线程的状态发送到GUI线程的槽函数中更新GUI组件状态。

    七、线程的设计

    1、线程的生命周期

    如果线程的正处于执行过程中时,线程对象被销毁时,程序将会出错。

    工程实践中线程对象的生命期必须大于线程的生命期。

    2、同步线程类设计

    线程对象主动等待线程生命期结束后才销毁,线程对象销毁时确保线程执行结束,支持在栈或堆上创建线程对象。

    在线程类的析构函数中先调用wait函数,强制等待线程执行结束。

    使用场合:适用于线程生命期较短的场合

    #ifndef SYNCTHREAD_H

    #define SYNCTHREAD_H

     

    #include <QThread>

     

    class SyncThread : public QThread

    {

      Q_OBJECT

    protected:

      void run()

      {

        

      }

    public:

      explicit SyncThread(QObject* parent = 0):QThread(parent)

      {

        

      }

      ~SyncThread()

      {

        wait();

      }

    };

     

    #endif // SYNCTHREAD_H

    3、异步线程类设计

    线程生命期结束时通知线程对象销毁。

    只能在堆空间创建线程对象,线程对象不能被外界主动销毁。

    在run函数中最后调用deleteLater()函数。

    线程函数主动申请销毁线程对象。

    使用场合:

    线程生命期不可控,需要长时间运行于后台的线程。

    #ifndef ASYNCTHREAD_H

    #define ASYNCTHREAD_H

     

    #include <QThread>

     

    class AsyncThread : public QThread

    {

      Q_OBJECT

    protected:

      void run()

      {

     

        deleteLater();

      }

      explicit AsyncThread(QObject* parent = 0):QThread(parent)

      {

     

      }

      ~AsyncThread()

      {

     

      }

    public:

      static AsyncThread* newThread(QObject* parent = 0)

      {

        return new AsyncThread(parent);

      }

    };

     

    #endif // ASYNCTHREAD_H

    八、线程的使用方式

    1、子类化QThread

    QThread的两种使用方法:

    (1)不使用事件循环

     A、子类化 QThread

        B、重写run函数,run函数内有一个 while 或 for 的死循环

        C、设置一个标记为来控制死循环的退出。

        适用于后台执行长时间的耗时操作,如文件复制、网络数据读取。

    (2)使用事件循环。

        A、子类化 QThread

        B、重写run 使其调用 QThread::exec() ,开启线程的事件循环

    C、为子类定义信号和槽,由于槽函数并不会在新开的 Thread 运行,在构造函数中调用 moveToThread(this)。

    适用于事务性操作,如文件读写、数据库读写。

    2、Worker-Object

        在Qt4.4之前,run 是纯虚函数,必须子类化QThread来实现run函数。
        而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化 QThread,只需要子类化一个 QObject 。

        通过继承的方式实现多线程已经没有任何意义,QThread是操作系统线程的接口或控制点,用于充当线程操作的集合。

        使用Worker-Object通过QObject::moveToThread将它们移动到线程中。

        指定一个线程对象的线程入口函数的方法:

    A、在类中定义一个槽函数void tmain()作为线程入口函数

    B、在类中定义一个QThread成员对象m_thread

    C、改变当前对象的线程依附性到m_thread

    D、连接m_thread的started()信号到tmain槽函数。

     

    #ifndef WORKER_H

    #define WORKER_H

     

    #include <QObject>

    #include <QThread>

    #include <QDebug>

     

    class Worker : public QObject

    {

      Q_OBJECT

      QThread m_thread;

    protected slots:

      void tmain()

      {

        qDebug() << "void tmain()";

      }

    public:

      explicit Worker(QObject* parent = 0):QObject(parent)

      {

        moveToThread(&m_thread);

        connect(&m_thread, SIGNAL(started()), this, SLOT(tmain()));

      }

      void start()

      {

        m_thread.start();

      }

      void terminate()

      {

        m_thread.terminate();

      }

     

      void exit(int c)

      {

        m_thread.exit(c);

      }

      ~Worker()

      {

        m_thread.wait();

      }

    };

     

    #endif // WORKER_H

    九、多线程与GUI组件的通信

    1、多线程与GUI组件通信基础

        GUI系统的设计原则:

        所有界面组件的创建只能在GUI线程(主线程)中完成。子线程与界面组件的通信有两种方式:

        A、信号槽方式

        B、发送自定事件方式

    2、信号槽方式

    使用信号槽解决多线程与界面组件的通信的方案:

    A、在子线程中定义界面组件的更新信号

    B、在主窗口类中定义更新界面组件的槽函数

    C、使用异步方式连接更新信号到槽函数

    子线程通过发送信号的方式更新界面组件,所有的界面组件对象只能依附于GUI线程(主线程)。

    子线程更新界面状态的本质是子线程发送信号通知主线程界面更新请求,主线程根据具体信号以及信号参数对界面组件进行修改。

    使用信号槽在子线程中更新主界面中进度条的进度显示信息。

    工作线程类:

    #ifndef WORKTHREAD_H

    #define WORKTHREAD_H

    #include <QThread>

     

    class WorkThread : public QThread

    {

      Q_OBJECT

    signals:

      void signalProgressValue(int value);

    protected:

      void run()

      {

        work();

        exec();

      }

     

    public:

      WorkThread()

      {

        m_stop = false;

        moveToThread(this);

      }

      void work()

      {

        for(int i = 0; i < 11; i++)

        {

            emit signalProgressValue(i*10);

            sleep(1);

        }

      }

    };

     

    #endif // WORKTHREAD_H

    主界面类:

    #ifndef WIDGET_H

    #define WIDGET_H

     

    #include <QWidget>

    #include <QProgressBar>

    #include "WorkThread.h"

     

    class Widget : public QWidget

    {

      Q_OBJECT

      QProgressBar* m_progress;//进度条

      WorkThread* m_thread;//工作线程

    public:

      Widget(QWidget *parent = 0):QWidget(parent)

      {

        m_progress = new QProgressBar(this);

        m_progress->move(10, 10);

        m_progress->setMinimum(0);

        m_progress->setMaximum(100);

        m_progress->setTextVisible(true);

        m_progress->resize(100, 30);

        m_thread = new WorkThread();

        m_thread->start();

        connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater()));

        //连接工作线程的信号到界面的槽函数

        connect(m_thread, SIGNAL(signalProgressValue(int)), this, SLOT(onProgress(int)));

      }

      ~Widget()

      {

      }

    protected slots:

      void onProgress(int value)

      {

        m_progress->setValue(value);

      }

    };

     

    #endif // WIDGET_H

    Main函数:

    #include "Widget.h"

    #include <QApplication>

     

    int main(int argc, char *argv[])

    {

      QApplication a(argc, argv);

      Widget w;

      w.show();

     

      return a.exec();

    }

    3、发送自定义事件方式

        A、自定义事件用于描述界面更新细节

        B、在主窗口类中重写事件处理函数event

        C、使用postEvent函数(异步方式)发送自定义事件类对象

        子线程指定接收消息的对象为主窗口对象,在event事件处理函数更新界面状态

        事件对象在主线程中被处理,event函数在主线程中调用。

        发送的事件对象必须在堆空间创建

        子线程创建时必须附带目标对象的地址信息

    自定义事件类:

    #ifndef PROGRESSEVENT_H

    #define PROGRESSEVENT_H

    #include <QEvent>

     

    class ProgressEvent : public QEvent

    {

      int m_progress;

    public:

      const static Type TYPE = static_cast<Type>(QEvent::User + 0xFF);

      ProgressEvent(int progress = 0):QEvent(TYPE)

      {

        m_progress = progress;

      }

      int progress()const

      {

        return m_progress;

      }

    };

     

    #endif // PROGRESSEVENT_H

     

    自定义线程类:

    #ifndef WORKTHREAD_H

    #define WORKTHREAD_H

    #include <QThread>

    #include <QApplication>

    #include <ProgressEvent.h>

     

    class WorkThread : public QThread

    {

      Q_OBJECT

    protected:

      volatile bool m_stop;

      void run()

      {

        work();

        exec();

      }

     

    public:

      WorkThread()

      {

        m_stop = false;

      }

      void stop()

      {

        m_stop = true;

      }

      void work()

      {

        for(int i = 0; i < 11; i++)

        {

            QApplication::postEvent(parent(), new ProgressEvent(i*10));

            sleep(1);

        }

      }

    };

     

    #endif // WORKTHREAD_H

    自定义界面类:

    #ifndef WIDGETUI_H

    #define WIDGETUI_H

     

    #include <QWidget>

    #include <QProgressBar>

    #include "WorkThread.h"

    #include "ProgressEvent.h"

     

    class WidgetUI : public QWidget

    {

      Q_OBJECT

      QProgressBar* m_progress;//进度条

      WorkThread* m_thread;//工作线程

    public:

      WidgetUI(QWidget *parent = 0):QWidget(parent)

      {

        m_progress = new QProgressBar(this);

        m_progress->move(10, 10);

        m_progress->setMinimum(0);

        m_progress->setMaximum(100);

        m_progress->setTextVisible(true);

        m_progress->resize(100, 30);

        m_thread = new WorkThread();

        m_thread->setParent(this);

        m_thread->start();

      }

      ~WidgetUI()

      {

        m_thread->quit();

      }

    protected:

      bool event(QEvent *event)

      {

        bool ret = true;

        if(event->type() == ProgressEvent::TYPE)

        {

            ProgressEvent* evt = dynamic_cast<ProgressEvent*>(event);

            if(evt != NULL)

            {

                //设置进度条的进度为事件参数的值

                m_progress->setValue(evt->progress());

            }

        }

        else

        {

            ret = QWidget::event(event);

        }

        return ret;

      }

    };

     

    #endif // WIDGETUI_H

    Main函数:

    #include "WidgetUI.h"

    #include <QApplication>

     

    int main(int argc, char *argv[])

    {

      QApplication a(argc, argv);

      WidgetUI w;

      w.show();

     

      return a.exec();

    }

    展开全文
  • 多线程作业

    千次阅读 2017-12-12 21:25:31
    多线程 一、判断题(T为正确,F为错误),每题1分 1.如果线程死亡,它便不能运行。(T) 2.在Java中,高优先级的可运行线程会抢占低优先级线程。( F) 3.线程可以用yield方法使低优先级的线程运行。(F)//暂停...

    (其实我也不知道正确答案,)
    多线程
    一、判断题(T为正确,F为错误),每题1分
    1.如果线程死亡,它便不能运行。(T)
    2.在Java中,高优先级的可运行线程会抢占低优先级线程。( F)
    3.线程可以用yield方法使低优先级的线程运行。(F)//暂停
    4…程序开发者必须创建一个线程去管理内存的分配。(F)
    5.一个线程在调用它的start方法,之前,该线程将一直处于出生期。( T)
    6.当调用一个正在进行线程的stop( )方法时,该线程便会进入休眠状态。(F)
    7.一个线程可以调用yield方法使其他线程有机会运行。(F)
    8. 多线程没有安全问题(F)
    9. 多线程安全问题的解决方案可以使用Lock提供的具体的锁对象操作(T)
    10. Stop()方法是终止当前线程的一种状态(F)

    二、选择题(不定项选择题),每题2分
    1.Java语言中提供了一个▁线程,自动回收动态分配的内存。D
    A.异步
    B.消费者
    C.守护
    D.垃圾收集
    2.Java语言避免了大多数的▁错误。C
    A.数组下标越界
    B.算术溢出
    C.内存泄露
    D.非法的方法参数
    3.有三种原因可以导致线程不能运行,它们是▁。ABC
    A.等待
    B.阻塞
    C.休眠
    D.挂起及由于I/O操作而阻塞
    4.当▁方法终止时,能使线程进入死亡状态。A
    A.run
    B.setPrority//更改线程优先级
    C.yield//暂停当前线程的执行 执行其他线程
    D.sleep//线程休眠
    5.用▁方法可以改变线程的优先级。B
    A.run
    B.setPrority
    C.yield
    D.sleep
    6.线程通过▁▁方法可以使具有相同优先级线程获得处理器。A
    A.run
    B.setPrority
    C.yield
    D.sleep
    7.线程通过▁▁方法可以休眠一段时间,然后恢复运行。D
    A.run
    B.setPrority
    C.yield
    D.sleep
    8.方法resume( )负责重新开始▁▁线程的执行。D
    A.被stop( )方法停止
    B.被sleep( )方法停止
    C.被wait( )方法停止
    D.被suspend( )方法停止
    9.▁▁方法可以用来暂时停止当前线程的运行。BC
    A.stop( )
    B.sleep( )
    C.wait( )
    D.suspend( )
    10. 请问下列哪些类是定义在java.io包中的抽象类(ABD)
    A. InputStream
    B. OutputStream
    C. PrintStream
    D. Reader
    E. FileInputStream
    F. FileWriter

    三、简述题,每题5分
    1.简述程序、进程和线程之间的关系?什么是多线程程序?
    进程是系统分配资源调用的一个独立单位. 正在运行的程序就是一个进程.
    线程依赖于进程而存在,一个线程相当于进程中的某个任务.
    多线程程序:一个程序中开启了多个任务,具有多线程环境,有共享数据,有多条语句对共享数据进行操作。

    3.什么是线程调度?Java的线程调度采用什么策略?
    为了保证线程按照一定的规则执行而采取的措施叫做线程调度。
    线程调度策略分为两种:
    抢占式调度。
    抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
    过程:一个线程运行一个时间片,再切换到第二个线程运行一个时间片…直到最后一个线程,CPU在各线程之间来回切换,直到最后一个线程执行完毕。
    协同调度。
    协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

    4.如何在Java程序中实现多线程?
    方式一:继承自Thread类实现
    1)自定义一个类继承Thread类
    2)在该类中重写run方法
    3)在main中创建该类的对象,启动线程
    方式二:实现Runnable接口
    1)自定义一个类实现Runnable接口
    2)重写接口中的run方法
    3)在main中创建该类的实例对象
    4)创建Thread类的对象,将该类的实例对象作为参数进行传递。
    5)启动线程
    方式三:通过Callable接口
    1)自定义一个类实现Callable接口
    2)在该类中重写call方法
    3)利用工厂类:Executors的newFixedThreadPool方法创建线程池对象
    4)通过所创建的线程池对象,将自定义的实现Callable接口的类对象作为参数,提交异步任务。
    5)调用Future接口中的get方法返回具体的结果

    1. 试简述Thread类的子类或实现Runnable接口两种方法的异同?、
      6)相同点:都需要重写run方法
      不同点:因为java中为了避免类的多重继承所带来的混淆,所以只允许类的单继承,但是允许接口的多实现。所以继承自Thread类的方式,具有局限性。
      而实现接口的方式使用更普遍,一个类可继承多个接口。
      然后就是两者的需要创建线程时的方式不同了。使用自定义类继承Thread的方式,只需实例化该类对象,然后启动就好了,而采用自定义类实现接口的方式,需要实例化该类的对象再将其作为new Thread()的参数创建一个Thread对象,再启动线程。

    2. 说明缓冲流的优点和原理
      原理:缓冲流在进行读写文件时会提供一个缓冲区数组(内存中),在读文件时可根据需要从包含的输入流填充该内部缓冲区,一次填充多个字节或字符。在写文件时输出缓存流调用write方法将数据写在缓冲区中,等到缓冲区满了之后才将数据发送到目的地(文件或磁盘).所以写入缓冲区的数据太小时,就经常会写不到指定文件中,所以最好在调用write方法后再调用flush方法,强制的将缓冲区中的数据发送到流中,而不必等到缓冲区满.

    优点:相对于基本流字节或者字符流是直接对目的地进行读写的,相当于读一个写一个效率比较低。而使用缓冲流避免了多次访问文件,提高了读写速度。

    8:在Java中wait()和sleep()方法的不同?
    (1)wait方法来自Object针对的是需要同步的对象
    Sleep方法来自Thread类针对的是某个线程
    (2)sleep方法没有释放锁,而调用wait方法释放了锁,这使其它线程可以使用同步代码块或同步方法。
    (3)Wait方法只能在同步代码块或同步方法中使用,而sleep方法可以在任何地方使用。

    (4)sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。
    注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程
    wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。

    9:Java中Runnable和Callable有什么不同?
    (1)Callable规定的方法是call(),Runnable规定的方法是run()。其中Runnable可以提交给Thread来包装下,直接启动一个线程来执行,而Callable则一般都是提交给ExecuteService来执行。
    (2)Callable的任务执行后可返回值,而Runnable的任务不能返回值
    (3)call方法可以抛出受检查的异常,run方法不可以
    (4)运行Callable任务可以拿到一个Future对象,再通过这个对象调用get方法得到计算结果。

    四、程序设计题
    http://note.youdao.com/noteshare?id=e7382f8c99e3a1d634cc707a92d000f6&sub=A0A335CE1C424670AA4704998A26803A
    1.编写一个应用程序,在线程同步的情况下来实现“生产者―消费者”问题。
    2.修改上题,由于各个线程是异步运行的,因此无法预计其相对速度,为了使生产者能够不断地生产,可以使用循环缓冲区,保证有足够多的内存区保存更多的产品。(生产者——仓库——消费者)

    3 :
    1)将若干个Student对象;若干个Teacher对象,写出到d:/0404/a.txt中,
    2)将该文件中所有的Student对象反序列化回来,装入List,所有的Teacher对象反序列化回来装入另一个List

    4:实现字符串和字节数组之间的相互转换,比如:将字符串”西部开源技术中心xbkyjszx”转换为字节数组,并将字节数组再转换回字符串!
    5:用Java编程一个会导致死锁的程序,你将怎么解决?请你设计

    6:递归实现输入任意目录,列出文件以及文件夹

    展开全文
  • JAVA多线程并发

    千次阅读 多人点赞 2019-09-18 12:14:29
    JAVA多线程并发1 JAVA并发知识库2 JAVA 线程实现/创建方式2.1 继承 Thread 类2.2 实现 Runnable 接口2.3 Callable 、Future 、ExecutorService 有返回值线程2.4 基于线程池的方式2.4.1 4种线程池2.4.1.1 ...

    JAVA多线程并发

    1 JAVA并发知识库

    在这里插入图片描述

    2 JAVA 线程实现/创建方式

    2.1 继承 Thread 类

    Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

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

    2.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()
    public void run() {
    	if (target != null) {
    		target.run();
    	}
    }
    

    2.3 Callable 、Future 、ExecutorService 有返回值线程

    有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    //创建一个线程池
    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);
    	list.add(f);
    }
    // 关闭线程池
    pool.shutdown();
    // 获取所有并发任务的运行结果
    for (Future f : list) {
    	// 从 Future 对象上获取任务的返回值,并输出到控制台
    	System.out.println("res:" + f.get().toString());
    }
    

    2.4 基于线程池的方式

    线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {
    	threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
    		@Override
    		public void run() {
    			System.out.println(Thread.currentThread().getName() + " is running ..");
    			try {
    				Thread.sleep(3000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	});
    }
    

    2.4.1 4种线程池

    Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。

    2.4.1.1 newCachedThreadPool

    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

    2.4.1.2 newFixedThreadPool

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

    2.4.1.3 newScheduledThreadPool

    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
    scheduledThreadPool.schedule(new Runnable(){
    	@Override
    	public void run() {
    		System.out.println("延迟三秒");
    	}
    }, 3, TimeUnit.SECONDS);
    scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
    	@Override
    	public void run() {
    		System.out.println("延迟 1 秒后每三秒执行一次");
    	}
    },1,3,TimeUnit.SECONDS);
    

    2.4.1.4 newSingleThreadExecutor

    Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

    3 线程生命周期(状态)

    在这里插入图片描述
    当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

    3.1 新建状态(NEW)

    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。

    3.2 就绪状态(RUNNABLE)

    当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

    3.3 运行状态(RUNNING)

    如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

    3.4 阻塞状态(BLOCKED)

    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。

    阻塞的情况分三种:

    • 等待阻塞( o.wait -> 等待对列 ):运行(running)的线程执行 o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞 ( lock -> 锁池 ) :运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM
      会把该线程放入锁池(lock pool)中。
    • 其他阻塞 ( sleep/join ) :运行(running)的线程执行 Thread.sleep(long ms)或
      t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当
      sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪(runnable)状态。

    3.5 线程死亡(DEAD)

    线程会以下面三种方式结束,结束后就是死亡状态。

    • 正常结束:run()或 call()方法执行完成,线程正常结束。
    • 异常结束:线程抛出一个未捕获的 Exception 或 Error。
    • 调用 stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

    4 终止线程的4种方式

    4.1 正常运行结束

    程序运行结束,线程自动结束。

    4.2 使用退出标志退出线程

    一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出,代码示例:

    public class ThreadSafe extends Thread {
    	public volatile boolean exit = false;
    	public void run() {
    		while (!exit){
    			//do something
    		}
    	}
    }
    

    定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

    4.3 Interrupt 方法结束线程

    使用 interrupt()方法来中断线程有两种情况:

    • 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
    • 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
    class ThreadSafe extends Thread {
    	public void run() {
    		while (!isInterrupted()) { // 非阻塞过程中通过判断中断标志来退出
    			try {
    				Thread.sleep(5 * 1000);// 阻塞过程捕获中断异常来退出
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    				break;// 捕获到异常之后,执行 break 跳出循环
    			}
    		}
    	}
    }
    

    4.4 stop方法终止线程

    程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

    5 相关知识

    5.1 sleep 与 wait 区别

    • 对于sleep()方法,我们首先要知道该方法是属于Thread 类中的。而wait()方法,则是属于Object 类中的。
    • sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复就绪状态。
    • 在调用sleep()方法的过程中,线程不会释放对象锁。
    • 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入就绪状态。

    5.2 start 与 run 区别

    • start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
    • 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
    • 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

    5.3 JAVA后台线程

    • 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
    • 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
    • 设置:通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是调用线程对象的 setDaemon 方法(thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程)。
    • 在 Daemon 线程中产生的新线程也是 Daemon 的。
    • 线程是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。正是因为这个很隐晦的问题,所以很多有经验的开发者不太赞成在Web应用中私自启动线程。
    • example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
    • 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

    6 JAVA锁

    6.1 乐观锁

    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    6.2 悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

    6.3 自旋锁

    • 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
    • 线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。
    • 如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    6.3.1 自旋锁的优缺点

    • 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
    • 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

    6.3.2 自旋锁时间阈值(1.6 引入了适应性自旋锁)

    • 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
    • JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

    6.3.3 自旋锁的开启

    JDK1.6 中 -XX:+UseSpinning 开启;
    -XX:PreBlockSpin=10 为自旋次数;
    JDK1.7 后,去掉此参数,由 jvm 控制;

    6.4 Synchronized同步锁

    synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

    6.4.1 Synchronized 作用范围

    1. 作用于方法时,锁住的是对象的实例(this);
    2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
    3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    6.4.2 Synchronized 核心组件

    1. Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
    2. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    3. Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck;
    5. Owner:当前已经获取到所资源的线程被称为 Owner;
    6. !Owner:当前释放锁的线程。

    6.4.3 Synchronized 实现

    在这里插入图片描述

    1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
    2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
    3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
    4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
    5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
    6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
    7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
    8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
    9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
    10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
    11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

    6.5 ReentrantLock

    ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

    6.5.1 Lock 接口的主要方法

    1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
    2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
    3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
    4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
    5. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。
    6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
    7. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
    8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
    9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
    10. hasQueuedThreads():是否有线程等待此锁
    11. isFair():该锁是否公平锁
    12. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
    13. isLock():此锁是否有任意线程占用
    14. lockInterruptibly():如果当前线程未被中断,获取锁
    15. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
    16. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

    6.5.2 非公平锁

    JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。

    6.5.3 公平锁

    公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。

    6.5.4 ReentrantLock 与 synchronized

    1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
    2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。

    6.5.5 ReentrantLock 实现

    public class MyService {
    	private Lock lock = new ReentrantLock();
    	//Lock lock = new ReentrantLock(true);//公平锁
    	//Lock lock = new ReentrantLock(false);//非公平锁
    	private Condition condition = lock.newCondition();// 创建 Condition
    
    	public void testMethod() {
    		try {
    			lock.lock();// lock 加锁
    			//1:wait 方法等待:
    			//System.out.println("开始 wait");
    			condition.await();
    			//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
    			//:2:signal 方法唤醒
    			condition.signal();// condition 对象的 signal 方法可以唤醒 wait 线程
    			for (int i = 0; i < 5; i++) {
    				System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    }
    

    6.5.6 Condition 类和 Object 类锁方法区别

    1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
    2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
    3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
    4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

    6.5.7 tryLock 和 lock 和 lockInterruptibly 的区别

    1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。
    2. lock 能获得锁就返回 true,不能的话一直等待获得锁。
    3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

    6.6 Semaphore 信号量

    Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。

    6.6.1 实现互斥锁(计数器为 1 )

    我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

    // 创建一个计数阈值为 5 的信号量对象
    // 只能 5 个线程同时访问
    Semaphore semp = new Semaphore(5);
    try { // 申请许可
    	semp.acquire();
    	try {
    		// 业务逻辑
    	} catch (Exception e) {
    	
    	} finally {
    		// 释放许可
    		semp.release();
    	}
    } catch (InterruptedException e) {
    
    }
    

    6.6.2 Semaphore 与 ReentrantLock

    • Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
    • 此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
    • Semaphore的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。

    6.7 AtomicInteger

    • 首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference将一个对象的所有操作转化成原子操作。
    • 我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。 通常我们会使用 synchronized将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger 的性能是 ReentantLock 的好几倍。

    6.8 可重入锁(递归锁)

    这里讲的是广义上的可重入锁,而不是单指JAVA下的 ReentrantLock。可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

    6.9 公平锁与非公平锁

    6.9.1 公平锁( Fair )

    加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

    6.9.2 非公平锁( Nonfair )

    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

    1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
    2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

    6.10 ReadWriteLock 读写锁

    为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
    Java 中读写有个接口 java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock。

    6.10.1 读锁

    如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。

    6.10.2 写锁

    如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    6.11 共享锁和独占锁

    java 并发包提供的加锁模式分为独占锁和共享锁。

    6.11.1 独占锁

    独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

    6.11.2 共享锁

    共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

    1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
    2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

    6.12 锁的状态

    锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

    锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。

    6.12.1 重量级锁( Mutex Lock )

    Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

    6.12.2 轻量级锁

    “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀
    为重量级锁。

    6.12.3 偏向锁

    Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

    6.13 分段锁

    分段锁也并非一种实际的锁,而是一种思想。ConcurrentHashMap 是学习分段锁的最好实践。

    6.14 锁优化

    6.14.1 减少锁持有时间

    只用在有线程安全要求的程序上加锁.

    6.14.2 减小锁粒度

    将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。

    6.14.3 锁分离

    最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发 Java 五]JDK 并发包 1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue 从头部取出,从尾部放数据

    6.14.4 锁粗化

    通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

    6.14.5 锁消除

    锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。

    7 线程基本方法

    线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。
    在这里插入图片描述

    7.1 线程等待( wait )

    调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

    7.2 线程睡眠( sleep )

    sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。

    7.3 线程让步( yield )

    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

    7.4 线程中断( interrupt )

    中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
      程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
    2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
      InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
    3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
    4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

    7.5 Join 等待其他线程终止

    join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

    为什么要用 join() 方法 ?很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

    System.out.println(Thread.currentThread().getName() + "线程运行开始!");
    Thread6 thread1 = new Thread6();
    thread1.setName("线程 B");
    thread1.join();
    System.out.println("这时 thread1 执行完毕之后才能执行主线程");
    

    7.6 线程唤醒( notify )

    Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
    争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

    7.7 其他方法

    1. sleep():强迫一个线程睡眠N毫秒。
    2. isAlive(): 判断一个线程是否存活。
    3. join(): 等待线程终止。
    4. activeCount(): 程序中活跃的线程数。
    5. enumerate(): 枚举程序中的线程。
    6. currentThread(): 得到当前线程。
    7. isDaemon(): 一个线程是否为守护线程。
    8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    9. setName(): 为线程设置一个名称。
    10. wait(): 强迫一个线程等待。
    11. notify(): 通知一个线程继续运行。
    12. setPriority(): 设置一个线程的优先级。
    13. getPriority()::获得一个线程的优先级。

    8 线程上下文切换

    巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
    在这里插入图片描述

    8.1 进程

    (有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。

    8.2 上下文

    是指某一时间点 CPU 寄存器和程序计数器的内容。

    8.3 寄存器

    是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

    8.4 程序计数器

    是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

    8.5 PCB-“切换桢”

    上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。

    8.6 上下文切换的活动

    1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
    2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
    3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。

    8.7 引起线程上下文切换

    1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
    2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
    3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
    4. 用户代码挂起当前任务,让出 CPU 时间;
    5. 硬件中断;

    9 同步锁与死锁

    9.1 同步锁

    当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

    9.2 死锁

    何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

    10 线程池原理

    线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

    10.1 线程复用

    每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

    10.2 线程池的组成

    一般的线程池主要分为以下 4 个组成部分:

    1. 线程池管理器:用于创建并管理线程池
    2. 工作线程:线程池中的线程
    3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
    4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

    Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
    在这里插入图片描述
    ThreadPoolExecutor 的构造方法如下:

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, 
    		TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    		Executors.defaultThreadFactory(), defaultHandler);
    }
    
    1. corePoolSize:指定了线程池中的线程数量。
    2. maximumPoolSize:指定了线程池中的最大线程数量。
    3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。
    4. unit:keepAliveTime 的单位。
    5. workQueue:任务队列,被提交但尚未被执行的任务。
    6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
    7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

    10.3 拒绝策略

    线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

    JDK 内置的拒绝策略如下:

    1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
    2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
    3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
    4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

    以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

    10.4 Java 线程池工作过程

    • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
    • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
      a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
      b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
      c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
      d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
    • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
      在这里插入图片描述

    11 JAVA阻塞队列原理

    阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

    • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
      在这里插入图片描述
    • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
      在这里插入图片描述

    11.1 阻塞队列的主要方法

    在这里插入图片描述

    • 抛出异常:抛出一个异常;
    • 特殊值:返回一个特殊值(null 或 false,视情况而定);
    • 阻塞:在成功操作之前,一直阻塞线程;
    • 超时:放弃前只在最大的时间内阻塞;

    11.1.1 插入操作

    • public abstract boolean add(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。如果该元素是 NULL,则会抛出 NullPointerException 异常。
    • public abstract boolean offer(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。
    • public abstract void put(E paramE) throws InterruptedException: 将指定元素插入此队列中,将等待可用的空间(如果有必要)。
    public void put(E paramE) throws InterruptedException {
    	checkNotNull(paramE);
    	ReentrantLock localReentrantLock = this.lock;
    	localReentrantLock.lockInterruptibly();
    	try {
    		while (this.count == this.items.length)
    			this.notFull.await();//如果队列满了,则线程阻塞等待
    		enqueue(paramE);
    		localReentrantLock.unlock();
    	} finally {
    	localReentrantLock.unlock();
    	}
    }
    
    • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入 BlockingQueue,则返回失败。

    11.1.2 获取数据操作

    • poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null;
    • poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。
    • take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
    • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

    11.2 Java 中的阻塞队列

    • ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
    • DelayQueue:使用优先级队列实现的无界阻塞队列。
    • SynchronousQueue:不存储元素的阻塞队列。
    • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:由链表结构组成的双向阻塞队列
      在这里插入图片描述

    11.2.1 ArrayBlockingQueue(公平、非公平)

    用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
    ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

    11.2.2 LinkedBlockingQueue(两个独立锁提高并发)

    基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)。

    11.2.3 PriorityBlockingQueue(compareTo 排序实现优先)

    是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

    11.2.4 DelayQueue(缓存失效、定时任务 )

    是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将 DelayQueue 运用在以下应用场景:

    • 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
    • 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。

    11.2.5 SynchronousQueue(不存储数据、可用于传递数据)

    是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另 外 一 个 线 程 使 用 , SynchronousQueue 的 吞 吐 量 高 于 LinkedBlockingQueue 和ArrayBlockingQueue。

    11.2.6 LinkedTransferQueue

    是 一 个 由 链 表 结 构 组 成 的 无 界 阻 塞 TransferQueue 队 列 。 相 对 于 其 他 阻 塞 队 列 ,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。

    • transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
    • tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。

    11.2.7 LinkedBlockingDeque

    是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法 add 等同于 addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 Jdk 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在“工作窃取”模式中。

    12 CyclicBarrier 、CountDownLatch 、Semaphore 的用法

    • CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不同;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行;而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;另外,CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。
    • Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

    12.1 CountDownLatch(线程计数器)

    CountDownLatch类位于java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。

    final CountDownLatch latch = new CountDownLatch(2);
    new Thread() {
    	public void run() {
    		System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
    		try {
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
    		latch.countDown();
    	};
    }.start();
    new Thread() {
    	public void run() {
    		System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
    		try {
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
    		latch.countDown();
    	};
    }.start();
    System.out.println("等待 2 个子线程执行完毕...");
    latch.await();
    System.out.println("2 个子线程已经执行完毕");
    System.out.println("继续执行主线程");
    

    12.2 CyclicBarrier(回环栅栏-等待至 barrier 状态再全部同时执行)

    CountDownLatch类位于java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier 可以被重用。我们暂且把这个状态就叫做 barrier,当调用 await()方法之后,线程就处于 barrier 了。

    CyclicBarrier 中最重要的方法就是 await 方法,它有 2 个重载版本:

    1. public int await():用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务;
    2. public int await(long timeout, TimeUnit unit):让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务。

    具体使用如下,另外 CyclicBarrier 是可以重用的。

    public class Test {
    	public static void main(String[] args) {
    		int N = 4;
    		CyclicBarrier barrier = new CyclicBarrier(N);
    		for (int i = 0; i < N; i++)
    			new Writer(barrier).start();
    	}
    
    	static class Writer extends Thread {
    		private CyclicBarrier cyclicBarrier;
    
    		public Writer(CyclicBarrier cyclicBarrier) {
    			this.cyclicBarrier = cyclicBarrier;
    		}
    
    		@Override
    		public void run() {
    			try {
    				Thread.sleep(5000); // 以睡眠来模拟线程需要预定写入数据操作
    				System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕");
    				cyclicBarrier.await();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			} catch (BrokenBarrierException e) {
    				e.printStackTrace();
    			}
    			System.out.println("所有线程写入完毕,继续处理其他任务,比如数据操作");
    		}
    	}
    }
    

    12.3 Semaphore(信号量-控制同时访问的线程个数)

    S emaphore 翻译成字面意思为 信号量,Semaphore 可以控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Semaphore 类中比较重要的几个方法:

    1. public void acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    2. public void acquire(int permits):获取 permits 个许可
    3. public void release() { } :释放许可。注意,在释放许可之前,必须先获获得许可。
    4. public void release(int permits) { }:释放 permits 个许可

    上面 4 个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

    1. public boolean tryAcquire():尝试获取一个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
    2. public boolean tryAcquire(long timeout, TimeUnit unit):尝试获取一个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
    3. public boolean tryAcquire(int permits):尝试获取 permits 个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
    4. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
    5. 还可以通过 availablePermits()方法得到可用的许可数目。例子:若一个工厂有5 台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过 Semaphore 来实现:
    public class Test {
    	public static void main(String[] args) {
    		int N = 8; // 工人数
    		Semaphore semaphore = new Semaphore(5); // 机器数目
    		for (int i = 0; i < N; i++)
    			new Worker(i, semaphore).start();
    	}
    
    	static class Worker extends Thread {
    		private int num;
    		private Semaphore semaphore;
    
    		public Worker(int num, Semaphore semaphore) {
    			this.num = num;
    			this.semaphore = semaphore;
    		}
    
    		@Override
    		public void run() {
    			try {
    				semaphore.acquire();
    				System.out.println("工人" + this.num + "占用一个机器在生产...");
    				Thread.sleep(2000);
    				System.out.println("工人" + this.num + "释放出机器");
    				semaphore.release();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    13 volatile 关键字的作用 (变量可见性、禁止重排序)

    Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

    13.1 变量可见性

    其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

    13.2 禁止重排序

    volatile 禁止了指令重排。

    13.3 比 sychronized 更轻量级的同步锁

    在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
    在这里插入图片描述
    当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。

    13.4 适用场景

    值得说明的是对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
    (1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
    (2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

    14 如何在两个线程之间共享数据

    Java 里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性原子性。Java 内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:

    14.1 将数据抽象成一个类,并将数据的操作作为这个类的方法

    将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“

    class MyData {
    	private int j = 0;
    
    	public synchronized void add() {
    		j++;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public synchronized void dec() {
    		j--;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public int getData() {
    		return j;
    	}
    }
    
    class AddRunnable implements Runnable {
    	MyData data;
    
    	public AddRunnable(MyData data) {
    		this.data = data;
    	}
    
    	public void run() {
    		data.add();
    	}
    }
    
    class DecRunnable implements Runnable {
    	MyData data;
    
    	public DecRunnable(MyData data) {
    		this.data = data;
    	}
    
    	public void run() {
    		data.dec();
    	}
    
    }
    
    public class mwjtest {
    	public static void main(String[] args) {
    		MyData data = new MyData();
    		Runnable add = new AddRunnable(data);
    		Runnable dec = new DecRunnable(data);
    		for (int i = 0; i < 2; i++) {
    			new Thread(add).start();
    			new Thread(dec).start();
    		}
    	}
    }
    

    14.2 Runnable 对象作为一个类的内部类

    将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个 Runnable 对象调用外部类的这些方法。

    class MyData {
    	private int j = 0;
    
    	public synchronized void add() {
    		j++;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public synchronized void dec() {
    		j--;
    		System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
    	}
    
    	public int getData() {
    		return j;
    	}
    }
    
    public class mwjtest {
    	public static void main(String[] args) {
    		final MyData data = new MyData();
    		for (int i = 0; i < 2; i++) {
    			new Thread(new Runnable() {
    				public void run() {
    					data.add();
    				}
    			}).start();
    			new Thread(new Runnable() {
    				public void run() {
    					data.dec();
    				}
    			}).start();
    		}
    	}
    }
    

    15 ThreadLocal 作用( 线程本地存储 )

    ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

    15.1 ThreadLocalMap (线程的一个属性)

    1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
    2. 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
    3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义ThreadLocal.ThreadLocalMap threadLocals = null;
      在这里插入图片描述

    15.2 使用场景

    最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。

    private static final ThreadLocal threadSession = new ThreadLocal();
    public static Session getSession() throws InfrastructureException {
    	Session s = (Session) threadSession.get();
    	try {
    		if (s == null) {
    			s = getSessionFactory().openSession();
    			threadSession.set(s);
    		}
    	} catch (HibernateException ex) {
    		throw new InfrastructureException(ex);
    	}
    	return s;
    }
    

    16 synchronized 和 ReentrantLock 的区别

    16.1 两者的共同点

    1. 都是用来协调多线程对共享对象、变量的访问
    2. 都是可重入锁,同一线程可以多次获得同一个锁
    3. 都保证了可见性和互斥性

    16.2 两者的不同点

    1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
    2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
    3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
    4. ReentrantLock 可以实现公平锁
    5. ReentrantLock 通过 Condition 可以绑定多个条件
    6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
    7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
    8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
    9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
    11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

    17 ConcurrentHashMap 并发

    17.1 减小锁粒度

    减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是 ConcurrentHashMap(高性能的 HashMap)类的实现。对于 HashMap 而言,最重要的两个方法是 get 与 set 方法,如果我们对整个 HashMap 加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment 的大小也被称为 ConcurrentHashMap 的并发度。

    17.2 ConcurrentHashMap 分段锁

    ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

    17.3 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

    ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
    在这里插入图片描述

    18 Java 中用到的线程调度

    18.1 抢占式调度

    抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

    18.2 协同式调度

    协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
    在这里插入图片描述

    18.3 JVM 的线程调度实现(抢占式调度)

    java 使用的线程调使用抢占式调度,Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

    18.4 线程让出 cpu 的情况

    1. 当前运行线程主动放弃 CPU,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权),例如调用 yield()方法。
    2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
    3. 当前运行线程结束,即运行完 run()方法里面的任务。

    19 进程调度算法

    19.1 优先调度算法

    1. 先来先服务调度算法(FCFS)
      当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用 FCFS 算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较
      简单,可以实现基本上的公平。
    2. 短作业(进程)优先调度算法
      短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。该算法未照顾紧迫型作业。

    19.2 高优先权优先调度算法

    为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。

    1. 非抢占式优先权算法
      在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
    2. 抢占式优先权调度算法
      在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
    3. 高响应比优先调度算法
      在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:
      在这里插入图片描述
      (1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
      (2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
      (3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

    19.3 基于时间片的轮转调度算法

    1. 时间片轮转法
      在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。
    2. 多级反馈队列调度算法
      (1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第 i+1 个队列的时间片要比第 i 个队列的时间片长
      一倍。
      (2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按 FCFS 原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。
      (3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第 1~(i-1)队列均空时,才会调度第 i 队列中的进程运行。如果处理机正在第 i 队列中为某进程服务时,又有新进程进入优先权较高的队列(第 1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第 i 队列的末尾,把处理机分配给新到的高优先权进程。在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。

    20 什么是 CAS( 比较并交换-乐观锁机制-锁自旋 )

    20.1 概念及特性

    CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。

    CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

    20.2 原子包 java.util.concurrent.atomic(锁自旋)

    JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。相对于对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。由于一般 CPU 切换时间比 CPU 指令集操作更加长, 所以 J.U.C 在性能上有了很大的提升。如下代码:

    public class AtomicInteger extends Number implements java.io.Serializable {
    	private volatile int value;
    	public final int get() {
    		return value;
    	}
    	public final int getAndIncrement() {
    		for (;;) { //CAS 自旋,一直尝试,直达成功
    			int current = get();
    			int next = current + 1;
    			if (compareAndSet(current, next))
    			return current;
    		}
    	}
    	public final boolean compareAndSet(int expect, int update) {
    		return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    	}
    }
    

    getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。
    在这里插入图片描述

    20.3 ABA 问题

    CAS 会导致“ABA 问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

    21 什么是 AQS( 抽象的队列同步器 )

    AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
    在这里插入图片描述
    它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的访问方式有三种:
    getState()
    setState()
    compareAndSetState()
    AQS 定义两种资源共享方式:

    1. Exclusive 独占资源 -ReentrantLock
      Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
    2. Share 共享资源 -Semaphore/CountDownLatch
      Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。

    AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
    1. isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
    2. tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
    3. tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
    4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回 false。

    21.1 同步器 的实现是 ABS 核心(state 资源状态计数)

    同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。

    21.2 ReentrantReadWriteLock 实现独占和共享两种

    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock。

    注:以上内容来源网络收集和归纳,如有错误请不吝赐教。

    展开全文
  • 《C#多线程编程实战》读书笔记

    千次阅读 多人点赞 2018-01-13 00:26:49
    本文是一篇读书笔记,由《C#多线程编程实战》一书中的内容整理而来,主要梳理了.NET中多线程编程相关的知识脉络,从Thread、ThreadPool、Task、async/await、并发集合、Parallel、PLINQ到Rx及异步I/O等内容,均有所...

    本文是一篇读书笔记,由《C#多线程编程实战》一书中的内容整理而来,主要梳理了.NET中多线程编程相关的知识脉络,从Thread、ThreadPool、Task、async/await、并发集合、Parallel、PLINQ到Rx及异步I/O等内容,均有所覆盖。为了帮助大家理解本文内容,首先给出博主在阅读该书过程中绘制的思维导图,大家可以根据个人需要针对性的查漏补缺。

    《多线程编程实战》思维导图

    线程基础

    • Tips1:暂停线程,即通过Thread.Sleep()方法让线程等待一段时间而不用消耗操作系统资源。当线程处于休眠状态时,它会占用尽可能少的CPU时间。
    • Tips2:线程等待,即通过Join()方法等待另一个线程结束,因为不知道执行所需要花费的时间,此时Thread.Sleep()方法无效,并且第一个线程等待时是处于阻塞状态的。
    • Tips3:终止线程,调用Abort()方法会给线程注入ThreadAbortException异常,该异常会导致程序崩溃,且该方法不一定总是能终止线程,目标线程可以通过处理该异常并调用Thread.ResetAbort()方法来拒绝被终止,因此不推荐使用Abort()方法来终止线程,理想的方式是通过CancellationToken来实现线程终止。
    • Tips4:线程优先级,线程优先级决定了该线程可占用多少CPU时间,通过设置IsBackground属性可以指定一个线程是否为后台线程,默认情况下,显式创建的线程都是前台线程。其主要区别是:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。需要注意的是,如果程序定义了一个不会赞成的前台线程,主程序并不会正常结束。
    • Tips5:向线程传递参数,可以通过ThreadStart或者lambda表达式来向一个线程传递参数,需要注意的是,由lambda表达式带来的闭包问题
    • Tips6:竞争条件是多线程环境中非常常见的导致错误的原因,通过lock关键字锁定一个静态对象(static&readonly)时,需要访问该对象的所有其它线程都会处于阻塞状态,并等待直到该对象解除锁定,这可能会导致严重的性能问题,
    • Tips7:发生死锁的原因是锁定的静态对象永远无法解除锁定,通常Monitor类用以解除死锁,而lock关键字用以创建死锁,Monitor类的TryEnter()方法可以用以检测静态对象是否可以解锁,lock关键字本质上是Monitor类的语法糖。
    bool acquiredLock = false;
    try
    {
      Monitor.Enter(lockObject, ref acquiredLock)
    }
    finally
    {
      if(acquiredLock)
      {
        Monitor.Exit(lockObject)
      }
    }
    • Tips8:不要在线程中抛出异常,而是在线程代码中使用try…catch代码块。

    线程同步

    • Tips9:无须共享对象,则无须进行线程同步,通过重新设计程序来移除共享状态,从而避免复杂的同步构造;使用原子操作,这意味着一个操作只占用一个量子的时间,一次就可以完成,并且只有当前操作完成后,其它线程方可执行其它操作,因此,无须实现其它线程等待当前操作完成,进而避免了使用锁,排除了死锁。
    • Tips10:为了实现线程同步,我们不得不使用不同的方式来协调线程,方式之一是将等待的线程设为阻塞,当线程处于阻塞状态时,会占用尽可能少的CPU时间,然而这意味着会引入至少一次的上下文切换。上下文切换,是指操作系统的线程调度器,该调度器会保存等待的线程状态,并切换到另一个线程,依次恢复等待的线程状态,而这需要消耗更多的资源。
    • Tips11:线程调度模式,当线程挂起很长时间时,需要操作系统内核来阻止线程使用CPU时间,这种模式被称为内核模式;当线程只需要等待一小段时间,而不需要将线程切换到阻塞状态,这种模式被称为用户模式;先尝试按照用户模式进行等待,如线程等待足够长时间,则切换到阻塞状态以节省CPU资源,这种模式被称为混合模式。
    • Tips12:Mutex是一种原始的同步方法,其只对一个线程授予对共享资源的独占访问,Mutex可以在不同的程序中同步线程。
    • Tips13:SemaphoreSlim是Semaphore的轻量级版本,用以限制同时访问同一个资源的线程数量,超过该数量的线程需要等待,直到之前的线程中某一个完成工作,并调用Release()方法发出信号,其使用了混合模式,而Semaphore则使用内核模式,可以在跨程序同步的场景下使用。
    • Tips14:AutoResetEvent类用以从一个线程向另一个线程发送通知,该类可以通知等待的线程有某个事件发生,其实例在默认情况下初始状态为unsignaled,调用WaitOne()方法时将会被阻塞,直到我们调用了Set方法;相反地,如果初始状态为signaled,调用WaitOne()方法时将会被立即处理,需要我们再调用一次Set方法,以便向其它线程发出信号。
    • Tips15:ManualResetEventSlim类是使用混合模式的线程信号量,相比使用内核模式的AutoResetEvent类更好(因为等待时间不能太长),AutoResetEvent像一个旋转门,一次仅允许一个人通过,而ManualResetEventSlim是ManualResetEvent的混合版本,一直保持大门开启直到手动屌用Reset方法。
    • Tips16:EventWaitHandle类是AutoResetEvent和ManualResetEvent的基类,可以通过调用其WaitOne()方法来阻塞线程,直到Set()方法被调用,它有两种状态,即终止态和非终止态,这两种状态可以相互转换,调用Set()方法可将其实例设为终止态,调用Reset()方法可以将其实例设为非终止态。
    • Tips17:CountdownEvent类可以用以等到直到一定数量的操作完成,需要注意的是,如果其实例方法Signal()没有达到指定的次数,则其实例方法Wait()将一直等待。所以,请确保使用CountdownEvent时,所有线程完成后都要调用Signal()方法。
    • Tips18:ReaderWriterLockSlim用以创建一个线程安全的机制,在多线程中对一个集合进行读写操作,ReaderWriterLockSlim代表了一个管理资源访问的锁,允许多个线程同时读取,以及独占写。其中,读锁允许多线程读取数据,写锁在被释放前会阻塞其它线程的所有操作。
    • Tips19:SpinWait类是一个混合同步构造,被设计为使用用户模式等待一段时间,然后切换到内核模式以节省CPU时间。

    使用线程池

    • Tips20:volatile关键字指出一个字段可能会被同时执行的多个线程修改,声明为volatile的字段不会被编译器和处理器优化为只能被单线程访问。
    • Tips21:创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。线程池的用途是执行运行时间短的操作,使用线程池可以减少并行度耗费及节省操作系统资源。在ASP.NET应用程序中使用线程池时要相当小心,ASP.NET基础切实使用自己的线程池,如果在线程池中浪费所有的工作者线程,Web服务器将不能够服务新的请求,在ASP.NET中只推荐使用I/O密集型的异步操作,因为其使用了一个不同的方式,叫做I/O线程。
    • Tips22:APM,即异步编程模型,是指使用BeginXXX/EndXXX和IAsyncResult对象等方式,其通过调用BeginInvoke方法返回IAsyncResult对象,然后通过调用EndInvoke方法返回结果,我们可通过轮询IAsyncResult对象的IsCompleted或者调用IAsyncResult对象的AsyncWaitHandle属性的WaitOne()方法来等待直到操作完成。
    • Tips23:ThreadPool.RegisterWaitForSingleObject()方法允许我们将回调函数放入线程池中的队列中,当提供的等待事件处理器收到信号或发生超时时,该回调函数将被调用,这做鱼我们为线程池中的操作实现超时功能。具体思路是:ManualResetEvent + CancellationToken,当接收到ManualResetEvent对象的信号时处理超时,或者是使用CancellationToken来处理超时。
    • Tips24:CancellationToken是.NET4.0中被引入的实现异步操作的取消操作的事实标准,我们可以使用三种方式来实现取消过程,即轮询IsCancellationRequested属性、抛出OperationCanceledException异常、为CancellationToken注册一个回调函数。
    • Tips25:Timer对象用以在线程池中创建周期性调用的异步操作。
    • Tips26:BackgroundWorker组件,是典型的基于事件的异步模式,即EAP,当通过RunWorkerAsync启动一个异步操作时,DoWork事件所订阅的事件处理器,将会运行在线程池中,如果需要需要取消异步操作,则可以调用CancelAsync()方法。

    使用任务并行库

    • Tips27:TPL即任务并行库,在.NET 4.0中被引入,目的是解决APM和EAP中获取结果和传播异常的问题,TPL在.NET4.5中进行了调整,使其在使用上更简单,它可以理解为线程池之上的又一个抽象层,对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的API。TPL的核心概念是任务,一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或者不使用独立线程运行。TPL相比之前的模式,一个关键优势是其具有用于组合任务的便利的API。
    • Tips28:Task.Run是Task.Factory.StartNew的一个快捷方式,后者有附加的选项,在无特殊需求的情况下,可以直接使用Task.Run,通过TaskScheduler,我们可以控制任务的运行方式。
    • Tips29:使用Task实例的Start方法启动任务并等待结果,该任务会被放置在线程池中并且主线程会等待,直到任务返回前一直处于阻塞状态;使用Task实例的RunSynchronously方法启动任务,该任务是运行在主线程中,这是一个非常好的优化,可以避免使用线程池来执行非常短暂的操作;我们可以通过轮询Task实例的状态信息来判断一个任务是否执行结束。
    • Tips30:通过Task实例的ContinueWith方法可以为任务设置一个后续操作,通过TaskContinuationOptions选项来指定后续任务以什么样的方式执行。
    • Tips31:通过Task实例的FromAsync可以实现APM到Task的转换
    • Tips32:通过TaskCompletionSource可以实现EAP到Task的转换
    • Tips33:TaskScheduler是一个非常重要的抽象,该组件实际上负责如何执行任务,默认的任务调度程序将任务放置在线程池的工作线程中。为了避免死锁,绝对不要通过任务调度程序在UI线程中使用同步操作,请使用ContinueWith或async/await方法。

    使用C# 6.0

    • Tips34:异步函数是C# 5.0引入的语言特性,它是基于TPL之上的更高级别抽象,真正简化了异步编程。要创建一个异步函数,首先需要使用async关键字标注一个方法,其次异步函数必须返回Task或Task类型,可以使用async void的方法,但是更推荐async Task的方法,使用async void的方法的唯一合理的地方就是在程序中使用顶层UI控制器事件处理器的时候,在使用async关键字标注的方法内部,可以使用await操作符,该操作符可与TPL任务一起工作,并获取该任务中异步操作的结果,在async方法外部不能使用await关键字,否则会有编译错误,异步函数代码中至少要拥有一个await关键字。
    • Tips35:在Windows GUI或ASP.NET等环境中不推荐使用Task.Wait和Task.Result,因为非常有可能会造成死锁。
      async可以和lambda表达式联用,在表达式体中应该至少含有一个await关键字标示,因为lambda表达式的类型无法通过自身推断,所以必须显式地向C#编译器指定类型。
    • Tips36:异步并不总是意味着并行执行
    • Tips37:单个异步操作可以使用try…catch来捕获异常,而对于一个以上的异步操作,使用try…catch仅仅可以从底层的AggregateException对象中获得第一个异常,为了获得所有的异常,可以使用AggregateException的Flatten()方法将层级异常放入一个列表,并从中提取出所有的底层异常。
    • Tips38:通过Task实例的ConfigureAwait()方法,可以设置使用await时同步上下文的行为,默认情况下,await操作符会尝试捕捉同步上下文,并在其中执行代码,即调度器会向UI线程投入成千上百个后续操作任务,这会使用它的消息循环来异步地执行这些任务,当我们不需要在UI线程中运行这些代码时,向ConfigureAwait方法传入false将会是一个更高效的方案。
    • Tips39:async void方法会导致异常处理方法,会放置到当前的同步上下文中,因此线程池中未被处理的异常会终结整个进程,使用AppDomain.UnhandledException事件可以拦截未处理的异常,但不能从拦截的地方恢复进程,async void的lambda表达式,同Action类型是兼容的,强烈建议仅仅在UI事件处理器中使用async void方法,在其他情况下,请使用返回Task或者Task的方法。

    使用并行集合

    • Tips40:ConcurrentQueue使用了原子的比较和交换(CAS),以及SpinWait来保证线程安全,它实现了一个先进先出(FIFO)的集合,这意味着元素出队列的顺序与加速队列的顺序是一致的,可以调用Enqueue方法向对接中加入元素,调用TryDequeue方法试图取出队列中第一个元素,调用TryPeek方法试图得到第一个元素但并不从队列中删除该元素。
    • Tips41:ConcurrentStack的实现同样没有使用锁,仅采用了CAS操作,它是一个后进先出(LIFO)的集合,这意味着最后添加的元素会先返回,可以调用Push和PushRange方法添加元素,使用TryPop和TryPopRange方法获取元素,使用TryPeek方法检查元素。
    • Tips42:ConcurrentBag是一个支持重复元素的无序集合,它针对以下情况进行了优化,即多个线程以这样的方式工作:每个线程产生和消费其自身的任务,极少发生线程间的交互(因为要交互就要使用锁)。可以调用Add方法添加元素,调用TryPeek方法检查元素,调用TryTake方法获取元素。
    • Tips43:ConcurrentDictionary是一个线程安全的字典集合的实现,对于读操作无需使用锁,对于写操作则需要使用锁,该并发字典使用多个锁,在字典桶之上实现了一个细粒度的锁模型(使用锁的常规字典称为粗粒度锁),参数concurrentLevel可以在构造函数中定义锁的数量。这意味着预估的线程数量将并发地更新该字典。由于并发字典使用锁,如无必要请避免使用以下操作:Count、IsEmpty、Keys、Values、CopyTo及ToArray,因为需要获取该字典中的所有锁。
    • Tips44:BlockingCollection是一个针对IProducerConsumerCollection泛型接口实现的高级封装,它有很多先进的功能来实现管道场景,即当你有一些步骤需要使用之前步骤运行的结果时。BlockingCollection类支持分块、调整内部集合容量、取消集合操作、从多个块集合中获取元素等。
    • Tips45:对BlockingCollection进行迭代时,需要注意的是,使用GetConsumingEnumerable()进行迭代,因为虽然BlockingCollection实现了IEnumerable接口,但是它默认的行为是表示集合的“快照”,这不是我们期望的行为。

    使用PLINQ

    • Tips46:将程序分割成一组任务并使用不同的线程来运行不同的任务,这种方式被称为任务并行
      将数据分割成较小的数据块,对这些数据进行并行计算,然后聚合这些计算结果,这种编程模型称为数据并行
    • Tips47:结构并行确实更易维护,应该尽可能地使用,但它并不是万能的。通常有很多情况我们是不能简单地使用结构并行,那么以非结构化的方式使用TPL任务并行也是完全可以的。
      Parallel类中的Invoke方法是最简单的实现多任务并行的方法,Invoke方法会阻塞其它线程直到所有线程都完成。
    • Tips48:Parallel类中的For和ForEach方法可以定义并行循环,通过传入一个委托来定义每个循环项的行为,并得到一个结果来说明循环是否成功完成,ParallelOptions类可以为并行循环定义最大并行数,使用CollectionToken取消任务,使用TaskScheduler类调度任务。
    • Tips49:ParallelLoopState可以用于从循环中跳出或者检查循环状态,它有两种方式:Break和Stop,Stop是指循环停止处理任何工作,而Break是指停止其之后的迭代,继续保持其之前的迭代工作。
    • Tips50:同Task类似,当使用AsParallel()方法并行查询时,我们将得到AggregateException,它将包含运行PLINQ期间发生的所有异常,我们可以使用Flatten()方法和Handle()方法来处理这些异常。
    • Tips51:ParallelEnumerable类含有PLINQ的全部逻辑,并且作为IEnumerable集合功能的一组扩展方法,默认情况下结果会被合并单个线程中,我们可以通过ForAll方法来指定处理逻辑,此时它们使用的是同一个线程,将跳过合并结果的过程,除了AsParallel()方法,我们同样可以使用AsSequential()方法,来使得PLINQ查询以顺序方式执行(相对于并行)
    • Tips52:PLINQ中提供了丰富用以PLINQ查询的选项,例如WithCancellation()方法用以取消查询,这将导致引发OperationCanceledException异常,并取消剩余的工作;例如WithDegreeOfParallelism()方法用以指定执行查询时实际并行分割数,可以决定并行执行会占用多少资源及其性能如何;例如WithExecutionMode()可以重载查询执行的模式,即我们可以决定选择以顺序执行还是并行执行的方式去执行查询;例如WithMergeOptions()方法可以用以调整对查询结果的处理,默认PLINQ会将结果合并到单个线程中,因此在查询结果返回前,会缓存一定数量的结果,当发现查询花费大量时间时,更合理的方式是关闭结果缓存从而尽可能快地得到结果;例如AsOrdered()方法,用以告诉PLINQ我们希望按照集合中的顺序进行处理(并行条件下,集合中的项有可能不是按顺序被处理的)

    使用异步I/O

    • Tips53:异步I/O,对服务器而言,可伸缩性是最高优先级,这意味着单个用户消耗的资源越少越好,如果为每个用户创建多个线程,则可伸缩性并不好,在I/O密集型的场景中需要使用异步,因为不需要CPU工作,其瓶颈在磁盘上,这种执行I/O任务的方式成为I/O线程。
      在异步文件读写中,FileOptions.Asynchronous是一个非常重要的选项,无论有无此参数都可以,以异步的方式使用该文件,区别是前者仅仅是在线程池中异步委托调用,而后者可以对FileStream垒使用异步I/O。
    • Tips54:对HttpListener类,我们可以通过GetContextasync()方法来异步地获取上下文。
    • Tips55:对数据库而言,我们可以通过OpenAsync()、ExecuteNonQueryAsync()等方法异步地执行SQL语句。

    好了,以上就是这篇读书笔记的主要内容啦,听说掌握了这55条Tips的人,都敢在简历上写”精通多线程编程“,哈哈,晚安啦,各位!

    展开全文
  • 多线程

    千次阅读 2006-09-13 13:19:00
    可以看到,AfxBeginThread最终还是调用了CreateThread来创建线程。在CreateThread创建的线城中使用CRT函数singnal()的话,会产生一些Memory Leak。_beginthreadex就不会。_beginthreadex()在内部先为线程创建一个线程...
  • Java基础:多线程

    千次阅读 多人点赞 2017-03-05 10:46:30
    1. 多线程概述人们在日常生活中,很多事情都是可以同时进行的。例如,一个人可以一边听音乐,一边打扫房间,可以一边吃饭,一边看电视。在使用计算机时,很多任务也是可以同时进行的。例如,可以一边浏览网页,一边...
  • C#多线程编程

    千次阅读 2011-12-07 09:52:08
    在.NET多线程编程这个系列我们讲一起来探讨多线程编程的各个方面。首先我将在本篇文章的开始向大家介绍多线程的有关概念以及多线程编程的基础知识;在接下来的文章中,我将逐一讲述。NET平台上多线程编程的知识,诸如...
  • 多线程-并发编程

    千次阅读 2020-07-01 13:25:17
    进程和线程的区别 进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O): ...一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • MFC多线程编程注意事项

    万次阅读 热门讨论 2008-05-26 11:17:00
    MFC多线程编程注意事项PeterLee整理 2008-05-261. 表现——错误示例关于启动线程时传输窗口对象(指针?句柄?)的问题: 在选择菜单中的开始线程后: void cmainframe::onmenu_start() { ... afxbeginthread...
  • 一、线程与进程 ① 线程与进程的定义 线程 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行; 进程要想执行任务,必须得有线程,进程至少要有一条线程; 程序启动会默认开启一条线程,这条线程被称为...
  • PyQt5 笔记4 -- 多线程

    千次阅读 2018-08-01 14:24:04
    PyQt5 笔记4 – 多线程 1、多线程 QT 程序的设计应该遵循UI线程与工作线程分离的原则,否则可能会因为工作线程运行时间太久造成程序未响应问题。如果程序有多个功能要同步执行,也必须另开线程执行。 QT...
  • 彻头彻尾理解单例模式与多线程

    万次阅读 多人点赞 2017-03-20 16:26:03
    多线程环境下,我们特别介绍了五种方式来在多线程环境下创建线程安全的单例,使用synchronized方法、synchronized块、静态内部类、双重检查和ThreadLocal实现懒汉式单例,并给出实现效率高且线程安全的单例所需要...
  • Java多线程Socket资料

    千次阅读 2012-03-16 15:40:45
    利用Java,编写安全高效的多线程程序变得简单,而且利用多线程和Java的网络包我们可以方便的实现多线程服务器程序。 ---- Java是伴随Internet的大潮产生的,对网络及多线程具有内在的支持,具有网
  • Windows多线程问题

    千次阅读 2015-08-30 00:57:04
    进程和线程是操作系统里面经常遇到的两个概念,还有一个概念,是应用程序。应用程序包括指令和数据,在开始运行之前,只是分布在磁盘上的指令和数据。正在执行的应用程序称为...每个进程可以运行线程线程也有一
  • Java多线程

    千次阅读 2012-09-23 21:19:22
    Java多线程是核心Java的重要构成部分之一。很多时候,我们需要接受多种任务并及时给予处理,这实际上就是一种并发性行为。而Java多线程就可以很好地实现这种并发性行为。本文就是探讨Java的多线程内容,包括线程是...
  • .NET多线程编程

    千次阅读 2005-02-18 02:16:00
    .NET多线程编程(1):多任务和多线程在.NET多线程编程这个系列我们讲一起来探讨多线程编程的各个方面。首先我将在本篇文章的开始向大家介绍多线程的有关概念以及多线程编程的基础知识;在接下来的文章中,我将逐一讲述...
  • 多进程和多线程的较量

    千次阅读 2020-03-10 22:53:22
    通常任务的实现,我们都是设计 Master-Worker,Master 负责分配任务,Worker 负责执行任务,因此任务环境下,通常是一个 Master 和个 Worker。 如果用进程实现 Master-Worker,主进程就是 Master,其...
  • 多线程程序杂记

    千次阅读 2011-07-15 20:11:17
    最近又完成一个C++服务器项目的开发,使用了多线程,在测试过程中遇到一些问题,记录一下,以供以后查看。 1 内存泄露 由于采用了缓冲的机制,而且操作的是大块数据,因此用的new比较多。在做压力测试的时候经常把...
  • java多线程相关问题汇总

    千次阅读 2020-06-20 11:03:22
    1、线程和进程的关系 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。...包含关系:如果一个进程内有线程,则执行过程不是一条线的,而是条线(线...
  • 多进程与多线程总结

    千次阅读 2013-03-14 21:11:17
    进程与线程总结一句话就是:线程是进程的一部分,进程是程序的一部分。...在基于线程任务的环境中,所有进程有至少一个线程,但他们可以具有个任务。这意味着单个程序可以并发执行两个或者个任
  • Java Web基础篇之Java多线程

    千次阅读 2019-05-08 13:18:06
    1、多线程与进程 1.1、是什么? 线程有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元,进程与线程区别如下:一个进程至少有一个线程.。 1.2、扩展: 进程与线程区别,线程的划分尺度小于...
  • Python多线程

    千次阅读 2013-04-25 18:22:56
    多线程是程序设计中的一个重要方面,尤其是在服务器Deamon程序方面。无论何种系统,线程调度的开销都比传统的进程要快得多。  Python可以方便地支持多线程。可以快速创建线程、互斥锁、信号量等等元素,支持线程...
  • 一、多线程使用情景: 1.用户需要同时得到多个反馈,例如下载过程中进度条改变,读取文件的时候显示结果。 2.提高程序执行性能,提高CPU使用效率,。 多线程的主要是需要处理大量的IO操作或者处理的情况需要花...
  • 多线程测试题(附答案)

    千次阅读 2020-06-11 13:53:17
    **多线程** 一、判断题(T为正确,F为错误),每题1分 1.如果线程死亡,它便不能运行。( T ) 2.在Java中,高优先级的可运行线程会抢占低优先级线程。( T ) 3.线程可以用yield方法使低优先级的线程运行。( F ) ...
  • linux/unix多线程多进程编程总结(二) linux/unix多线程,多进程编程是在实际工作中经常使用到的技能,在C语言或者C++语言面试的时候也经常会被问到此部分内容。 本文对linux/unix系统中的pthread相关的多进程和...
  • 学了C然后C++,然后MFC/Windows,然后是C#,其中数据类型,由基本类型衍生的typedef类型也N。熟知基本数据类型是我们正确表达实际问题中各种数据的前提,因此我分类总结了一下C/C++/Windows /C#基本数据类型,...
  • 大家都知道线程之间共享变量要用volatile关键字。但是,如果不用volatile来标识,会不会导致线程死循环?比如下面的伪代码: static int flag = -1; void thread1(){ while(flag > 0){ //wait or do something ...
  • (一)Java 多线程开发 (二)Android 多线程开发 2.1)基础使用 1、继承Thread类 (1)简介 Thread类是Java中实现多线程的具体类,封装了所需线程操作。在Android开发中用于实现多线程。 注:线程对象&运行...
  • Java之十三 Java多线程

    千次阅读 2016-05-25 19:19:23
    然而,任务处理有两种截然不同的类型:基于进程的和基于线程的。认识两者的不同是十分重要的。对很读者,基于进程的任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。因此,基于进程(process-...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 52,829
精华内容 21,131
关键字:

多线程类型动态转换崩溃