多线程并发编程分享


-     基础概念    -
javascript

1. 进程与线程java

  • 如今的操做系统都是多任务操做系统,容许多个进程在同一个CPU上运行。

  • 每一个进程都有独立的代码和数据空间,称为进程上下文

  • CPU从一个进程切换到另外一个进程所作的动做被成为上下文切换,经过频繁的上下文切换来让这些进程看起来像是在同时运行同样

  • 进程的运行须要较多的资源,操做系统可以同时运行的进城数量有限,而且进程间的切换和通讯也存在较大开销。

  • 为了能并并行的执行更多的任务,提高系统效率,才引入了线程概念。线程是CPU调度的最小单位,是进程的一部分,只能由进程建立,共享进程的资源和代码
    c++

  • 以Java进程为例,它至少有一个主线程(main方法所在的线程),经过主线程能够建立更多的用户线程或者守护线程,线程能够有本身独享的数据空间,同时线程间也共享进程的数据空间
    程序员

2.并发与并行web

  • 并行的概念:若是一个CPU有多个核心,并容许多个线程在不一样的核心上同时执行,称为“多核并行”,这里强调的是同时执行。算法

  • 并发的概念:好比在单个CPU上,经过必定的“调度算法”,把CPU运行时间划分红若干个时间片,再将时间片分配给各个线程执行,在一个时间片的线程代码运行时,其它线程处于挂起等待的状态,只不过CPU在作这些事情的时候很是地快速,所以让多个任务看起来“像是”同时在执行,本质上同一时刻,CPU只能执行一个任务。typescript


-     线程状态&状态间转换    -数据库

1.线程状态编程

  • 新建NEW:线程被新建立时的状态,在堆区中被分配了内存缓存

  • 就绪RUNNABLE&READY:线程调用了它的start()方法,该线程进入就绪状态,虚拟机会为其建立方法调用栈和程序计数器,等待得到CPU的使用权

  • 运行RUNNING:线程获取了CPU的使用权,执行程序代码,只有就绪状态才有机会转到运行状态

  • 阻塞BLOCKED:位于对象锁池的状态,线程为了等待某个对象的锁,而暂时放弃CPU的使用权,且不参与CPU使用权的竞争。直到得到锁,该线程才从新回到就绪状态,从新参与CPU竞争,这涉及到“线程同步”

  • 等待WAITING:位于对象等待池的状态,线程放弃CPU也放弃了锁,这涉及到“线程通讯”

  • 计时等待TIME_WAITING:超时等待的状态,它会放弃CPU可是不会放弃对象锁

  • 终止TERMINATED&DEAD:代码执行完毕、执行过程当中出现异常、受到外界干预而中断执行,这些状况均可以使线程终止

2.线程状态间转换图

  • Thread3持有对象锁,Thread1,2,4进入等待获取锁时的状态是BLOCKED

  • Dopey线程调用sleepy.join()后,dopey线程处于WAITING状态,会等待sleepy线程结束,sleepy线程因为调用了sleep()方法,处于TIMED_WAITING状态

  • 若是dopey线程调用sleepy.join(…)方法,dopey会进入TIMED_WAITING状态,它会在超时时间内等待sleepy线程结束,若是超时了sleepy线程还未结束,dopey不会继续等待,它会继续运行

  • 调用了wait(…)方法以后会进入TIMED_WAITING状态,超时等待


-     java对于线程的编程支持间转换    -

1.Thread类经常使用方法

  • t.start() 启动线程t,线程状态有NEW变为RUNNABLE,开始参与CPU竞争

  • t.checkAccess() 检查当前线程是否有权限访问线程t

  • t.isInterrupted() 检查线程t是否要求被中断

  • t.setPriority() 设置线程优先级:1-10,值越大,获得执行的机会越高,通常比较少用

  • t.setDaemon(true) 设置线程为后台线程,代码演示1

  • t.isAlive() 判断线程t是否存活

  • t.join()/t.join(1000L) 当前线程挂起,等待t线程结束或者超时,代码演示2

  • Thread.yield() 让出CPU,若是有锁,不会让出锁。转为RUNNABLE状态,从新参与CPU的竞争

  • Thread.sleep(1000L) 让出CPU,不让锁,睡眠1秒钟以后转为RUNNABLE状态,从新参与CPU竞争

  • Thread.currentThread() 获取当前线程实例

  • Thread.interrupt() 给当前线程发送中断信号

2.wait和sleep的差别和共同点,代码演示3
  • wait方法是Object类的方法,是线程间通讯的重要手段之一,它必须在synchronized同步块中使用;sleep方法是Thread类的静态方法,能够随时使用

  • wait方法会释放synchronized锁,而sleep方法则不会

  • 由wait方法造成的阻塞,能够经过针对同一个synchronized锁做用域调用notify/notifyAll来唤醒,而sleep方法没法被唤醒,只能定时醒来或被interrupt方法中断

  • 共同点1:二者均可以让程序阻塞指定的毫秒数

  • 共同点2:均可以经过interrupt方法打断

3.sleep与yield,代码示例4

  • 线程调用sleep方法后,会进入TIMED_WAITING状态,在醒来以后会进入RUNNABLE状态,而调用yield方法后,则是直接进入RUNNABLE状态再次竞争CPU

  • 线程调用sleep方法后,其余线程不管优先级高低,都有机会运行;而执行yield方法后,只会给那些相同或者更高优先级的线程运行的机会

  • sleep方法须要声明InterruptedException,yield方法没有声明任何异常。


-    线程池    -

线程的建立和销毁会消耗资源,在大量并发的状况下,频繁地建立和销毁线程会严重下降系统的性能。所以,一般须要预先建立多个线程,并集中管理起来,造成一个线程池,用的时候拿来用,用完放回去。
  • 经常使用线程池:FixedThreadPool,CachedThreadPool,ScheduledThreadPool,代码演示5

  • 主要关注的功能:shutDown方法;shutDownNow方法;execute(Runnable)向线程池提交一个任务,不须要返回结果;submit(task)向线程池提交一个任务,且须要返回结果,这里涉及到Future编程模型,代码演示6


-    线程安全    -

  • 怎么理解线程安全?线程安全,本质上是指“共享资源”在多线程环境下的安全,不会由于多个线程并发的修改而出现数据破坏,丢失更新,死锁等问题。

  • 为何会出现线程不安全?我的的一些思考,读操做是线程安全的,它不会改变值;写操做也是线程安全的,这里的写操做是指对于内存或者硬盘上的值进行更改的那个动做,这个动做自己是具备原子性的。有不少人说,共享资源不安全是由于“并发的写”,这里我想说“写”这个动做自己不会破坏资源的安全性。这里要结合操做系统的工做特色来讲明一下这个问题。

  • 各个线程从主内存中读取数据到工做内存中,而后在工做内存中根据代码指令对数据进行运算加工,最后写回主内存中。

  • 引伸出线程安全要解决的三个问题

    • 原子性,某个线程对共享资源的一系列操做,不可被其余线程中断和干扰。

    • 可见性,当多个线程并发的读写某个共享资源时,每一个线程老是能读取到该共享资源的最新数据。

    举例://线程1执行的代码int i = 0;i = 10;//线程2执行的代码j = i;
      • 倘若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,而后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有当即写入到主存当中。

      • 此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值仍是0,那么就会使得j的值为0,而不是10.这就是可见性问题,线程1对变量i修改了以后,线程2没有当即看到线程1修改的值。

    • 有序性,单个线程内的操做必须是有序的。

      解释一下什么是指令重排序,通常来讲,处理器为了提升程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行前后顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

    int a = 10; //语句1int r = 2//语句2a = a + 3; //语句3r = a*a; //语句4
      • 好比上面的代码中,语句1和语句2谁先执行对最终的程序结果并无影响,那么就有可能在执行过程当中,语句2先执行而语句1后执行。

      • 可是执行顺序不多是 语句2—语句1—语句4—语句3,由于这样会改变最终结果。

    虽然重排序不会影响单个线程内程序执行的结果,可是多线程呢?

    //线程1:context = loadContext(); //语句1inited = true; //语句2//线程2:while(!inited ){sleep()}doSomethingwithconfig(context);

    上面这段代码在单线程看来,语句1和语句2没有必然联系,那若是这时发生了指令重排序,语句2先执行,那这时线程2会认为初始化已经完成,直接跳出循环,但其实线程1的初始化不必定完成了,这样就会产生程序错误。



-    线程同步    -

线程同步指的是线程之间的协调和配合,是多线程环境下解决线程安全和效率的关键。主要包括四种经常使用方式来实现
  • 临界区,表示同一时刻只容许一个线程执行的“代码块”被称为临界区,要想进入临界区则必须持有锁

  • 互斥量,即咱们理解的锁,只有拥有锁的线程才被容许访问共享资源

  • 自旋锁:与互斥量相似,它不是经过休眠使进程阻塞,而是在获取锁以前一直处于忙等(自旋)阻塞状态。用在如下状况:锁持有的时间短,并且线程并不但愿在从新调度上花太多的成本,"原地打转"。

  • 信号量,容许有限数量的线程在同一时刻访问统一资源,当访问线程达到上限时,其余试图访问的线程将被阻塞

  • 事件,经过发送“通知”的方式来实现线程的同步


Java中对实现线程安全与线程同步提供哪些主要的能力

  • Volatile,被volatile修饰以后就具有了两层语义:

    • 保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

    • 禁止进行指令重排序,即对一个变量的写操做先行发生于后面对这个变量的读操做

看下面一段代码://线程1boolean stop = false;while(!stop){doSomething();}//线程2stop = true;

这段代码是一种典型的多线程写法,线程1根据布尔值stop的值来决定是否跳出循环;而线程2则会决定是否将布尔值stop置为true。若是线程2改变了stop的值,可是却迟迟没有写入到主存中,那线程1其实还觉得stop=false,会一直循环下去。可是用volatile修饰以后就变得不同了:

  • 使用volatile关键字会强制将修改的值当即写入主存;

  • 使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  • 因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。

基于上面的描述,咱们可能会问volatile这样的能力是否是能保证原子性了呢?答案是否认的,代码示例7

具体缘由我的理解以下:

java语言的指令集是一门基于栈的指令集架构。也就是说它的数值计算是基于栈的。好比计算inc++,翻译成字节码就会变成:

0: iconst_11: istore_12: iinc 1, 10:的做用是把1放到栈顶1:的做用是把刚才放到栈顶的1存入栈帧的局部变量表2:的做用是对指令后面的1 ,1相加

由第0步能够看到,当指令序列将操做数存入栈顶以后就再也不会从缓存中取数据了,那么缓存行无效也就没有什么影响了。

  • Synchronized,用于标记一个方法或方法块,经过给对象上“锁”的方式,将本身的做用域变成一个临界区,只有得到锁的线程才能够进入临界区。每一个java对象在内存中都有一个对应的监视器monitor,它用来存储“锁”标记,记录哪个线程拥有这个对象的“锁”,又有哪些线程在竞争这个“锁”。锁,本质上是并发转串行,所以它自然就能解决原子性,可见性,有序性问题。代码示例8

  • CAS与atomic包

    Synchronized是一种独占锁,悲观锁,等待锁的线程处于BLOCKED状态,影响性能;锁的竞争会致使频繁的上下文切换和调度延时,开销较大;存在死锁的风险等等。
    基于这些问题,咱们还有另一个方案,那就是CAS(Compare And Swap),其原理与咱们经常使用的数据库乐观锁相似,即变量更新前检查当前值是否符合预期,若是符合则用新值替换当前值,不然就循环重试,直到成功。当下主流CPU直接在指令层面上支持了CAS指令,好比atomic底层调用的compareAndSwapInt方法就是这样一个native方法。所以,CAS的执行效率仍是比较高的。 CAS在使用上还须要注意几点:
    • 经过版本号的方式,避免ABA问题

    • 循环开销,冲突严重时过多地线程处于循环重试的状态,将增长CPU的负担

    • 只能保证一个共享变量的原子性操做,若是想要多个变量同时保证原子性操做,能够考虑将这些变量放在一个对象中,而后使用AtomicReference类,这个类提供针对对象引用的原子性,从而保证对多个变量操做的原子性。代码示例9

  • Lock自旋锁

    • Java提供了Lock接口以及其实现类ReentLock;ReadWriteLock接口以及其实现类ReentrantReadWriteLock

    • 与synchronized锁不一样的是,线程在获取Lock锁的过程当中不会被阻塞,而是经过循环不断的重试,直到当前持有该Lock锁的线程释放该锁

    • Synchronized是关键字,由编译器负责生成加锁和解锁操做,而ReentrantLock则是一个类,这个加锁和解锁的操做彻底在程序员手中,所以在写代码时,调用了lock方法以后必定要记得调用unlock来解锁,最好放在finally块中

    • 参见代码示例10

  • Condition条件变量

    • Synchronized的同步机制要求全部线程等待同一对象的监视器“锁”标记。而且在经过wait/notify/notifyAll方法进行线程间通讯时,只能随机或者所有且无序的唤醒这些线程,并无办法“有选择”地决定要唤醒哪些线程,也没法避免“非公平锁”的问题

    • ReentrantLock容许开发者根据实际状况,建立多个条件变量,全部取得lock的线程能够根据不一样的逻辑在对应的condition里面waiting,每一个Condition对象拥有一个队列,用于存放处于waiting状态的线程

    • 这样的一种设计,一样可让开发者根据实际状况,决定唤醒哪些condition内部waiting的线程,同时还可以实现公平锁。

    • 参见代码示例11


-     做者介绍    -

chris
架构师一枚,早期就任于知名通讯公司,致力于通信软件解决方案。以后就任于五百强咨询公司,致力于为大型车企提供数字化转型方案。现就任于平安银行信用卡中心,帮助平安银行落地核心系统的去IOE化改造。追求技术本质,目前主要方向是复杂系统的分布式架构设计。

本文分享自微信公众号 - 川聊架构(gh_44ec4115d261)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索