并发-Java并发编程基础

时间:2021-03-20 18:01:43

Java并发编程基础

并发

在计算机科学中,并发是指将一个程序,算法划分为若干个逻辑组成部分,这些部分可以以任何顺序进行执行,但与最终顺序执行的结果一致。并发可以在多核操作系统上显著的提高程序运行速度。

并发与并行联系与区别

这里参考ErLang之父的解释,ErLang之父谈到了如何向一个5岁小孩解释并发与并行。
[](并发-Java并发编程基础

直观来讲,并发是两个等待队列中的人同时去竞争一台咖啡机,两队列中的排队者也可能约定交替使用咖啡机,也可能是大家同时竞争咖啡机,谁先竞争到咖啡机谁使用,不过后一种的方法可能引发冲突,因为两个队列里面排在队列首位的人可能同时使用咖啡机),每个等待者在使用咖啡机之前不仅需要知道排在他前面那个人是否已经使用完了咖啡机,还需知道另一个队列中排在首位的人是否也正准备使用咖啡机;而并行是每个队列拥有自己的咖啡机,两个队列之间并没有竞争的关系,队列中的某个排队者只需等待队列前面的人使用完咖啡机,然后再轮到自己使用咖啡机。

因此,并发意味着多个执行实体(比方说上面例子中的人)可能需要竞争资源(咖啡机),因此就不可避免带来竞争和同步的问题;而并行则是不同的执行实体拥有各自的资源,相互之间可能互不干扰。

为什么我们非常热衷于并发编程呢,其实有一个很简单的原因,那就是我们希望自己的程序更快!然后,并发编程却是一个复杂的主题,如果不能对并发有一个深刻的理解,很多时候会适得其反,程序不但没有加快,很可能跑的更慢(比如单核操作系统上跑多个线程,多个线程对某个资源有激烈的竞争等等),更有甚者,还会带来很多并发产生问题。

线程无处不在

在我们现今的大规模分布式系统下,处处充满了多线程,如何良好的应用多线程技术成为了现在越来越大的挑战.

多线程的优势

  1. 发挥处理器的强大能力(尤其在CPU核数高的处理器上).
  2. 建模的简单性(每个线程处理各自的任务).
  3. 异步事件的简化处理.
  4. 响应更灵敏的用户界面

多线程带来的风险

  1. 线程安全性问题.
    安全性:永远不会发生糟糕的事情.多线程访问共享变量在未良好同步的情况下,产生共享变量的线程安全问题.

  2. 活跃性问题.
    活跃性:某件正确的事情最终会发生。
    活跃性的问题:死锁,饥饿,活锁.

  3. 性能问题.
    性能问题:服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,可伸缩性低.
    例:线程上下文切换带来的CPU资源调度问题.

线程安全

定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。(在线程安全类中封装了必要的同步操作,因此客户端不需要进一步采取同步措施)

重入:当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然后由于内置锁是可重入的,因此如果某个线程试图获取一个已经由自己持有的锁的时候,那么这个请求就会成功,重入意味着获取锁的粒度是线程,而不是调用。

同步主要作用:
1.线程之前可以互斥的访问临界资源。
2.保证线程的顺序性。
3.保证共享变量的可见性。

竞态条件

竞态条件:由于多线程不恰当的执行时序而出现不正确的结果.最常见的竞态条件类型就是"先检查后执行"操作,即通过一个可能失效的观察结果来决定下一步的动作。与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序。

对象共享

编写正确并发程序的关键在于:在访问共享的可变状态时需要进行正确的管理。

可见性

可见性是一种复杂的属性,在单线程环境下,向某个共享变量写入值后然后再去读,会得到刚刚写入的值。但如果是是在多线程环境下,就不一定,线程写入共享变量之后,另一个线程再去读,并不一定会读到第一个线程写入的值。为了保证可见性问题,我们要正确的使用同步。

java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用同步机制将变量保护起来。

可见性与锁
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所用线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。

可见性与volatile
当把变量声明为volatile类型之后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者其他处理器不可见的地方,因此在读取volatile类型的变量时候总会返回最新写入的值。

volatile 相对synchronized 来说,是一种更轻量级的同步机制。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量的值,在B读取了volatile变量之后,对B也是可见的。因此,从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。

不变性

如果某个对象在被创建之后其状态就不能被修改,那么这个对象就被称为不可变对象。线程安全性是不可变对象的固有属性之一,他们的不变形条件是由构造函数创建的,只要他们的状态不改变,那么这些不变性条件就能得以维持。

当满足以下条件时,对象才是不可变的。

  1. 对象创建以后其状态就不能修改。
  2. 对象的所有域都是final类型的。
  3. 对象是正确创建的(this引用没有逸出).

编写多线程程序时,一些实用的策略

  1. 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  2. 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改。
  3. 线程安全共享。线程安全的对象在其内部实现同步(封装线程安全性),因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。

关于线程上下文的切换

线程切换上下文需要消耗1-100微妙

参考

  1. java并发编程实战
  2. https://en.wikipedia.org/wiki/Concurrency_(computer_science)