并发基础知识(一)

1 篇文章 0 订阅
1 篇文章 0 订阅

一直对并发编程十分感兴趣,仔细研读了《Java并发编程实战》这本书,以下是读书笔记。

书籍详情信息:https://book.douban.com/subject/10484692/

线程安全带来的问题

  • 竞态条件(race condition)。线程安全性问题是非常复杂的,在没有充足同步的条件下,多线程中的操作执行顺序是不可预测的。
    例如:
public class UnsafeSequence {

    private int mSeq = -1;

    public int getNext() {
        return mSeq++;
    }   

}

主要的线程安全问题在于,如果执行的时机不对,那么在调用getNext时会得到相同的值!因为 mSeq++操作并不是原子性操作,它实际上是三个独立的操作。getNext是否返回唯一的值取决于线程中的操作的交替执行方式。

  • 活跃性问题
    活跃性意味着:某个正确的事情最终一定会发生。当某个操作无法继续执行下去是,就会发生活跃性问题。活跃性问题常见的形式就是无意中造成的无限循环。是循环之后的代码得不到执行的机会。

  • 性能问题
    在线程调度程序中,并发执行的线程数量过多,就会造成频繁的上下文切换,这种操作会带来极大的开销。CPU时间将更多地花在线程调度上而并不是线程执行上。当对在多线程访问共享数据的情况下,必须要采取恰当的同步机制,而同步机制会抑制编译器的优化,(如内存缓存区中的数据无效),造成额外的性能开销。

线程安全性

  构建稳健的并发程序必须要正确使用线程和锁,这些仅仅只是一些机制。
  核心在于要对对象的状态访问操作进行管理,特别是共享的(Shared)和可变的(mutable)状态。其中对象的状态是指对象中存在的状态变量(实例或者是静态域),或者是对于其它对象的依赖。例如HashMap的状态保存于Map.Entry中。
  共享指的是对象在多个线程中可以同时被访问,可变意味着变量的值在其生命周期中会发生变化。一个对象是否需要是线程安全的 ,取决于是否在多个线程中进行访问。要确保线程安全性,要使用正确的同步机制来协同对对象可变状态的访问。
  如果在多线程中访问某个状态变量,其中有一个线程进行的是写入的操作,那么必须要采用同步机制来协同这些线程对状态变量的访问。Java中的主要同步机制是synchronized关键字,还有其他的同步如volatile类型的变量、显示锁以及原子性变量。
  那么什么是线程安全性(thread-safe)?
  线程安全性比较权威的定义:如果一个类在多线程中被访问时,这个类始终能表现出正确的(correctly)可预期(predictly)的行为,那么就是线程安全的。

竞态条件

  当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。常见的竞态条件就是先检查后执行,通过一个可能失效的观测的结果来决定下一步的动作。例如,延迟初始化。需要注意的是:竞态条件并不总是会产生错误,还必须有不恰当的执行时序。
  发生竞态条件的本质原因就是并发条件下,一些复合操作(先检查后执行、 读写-修改-写入等操作称为复合操作)缺乏原子性,因此,避免发生竞态条件的措施就是要保证对于共享可变的状态的操作时原子性的。仅仅在一处使用同步的操作时不够的,应当在所有访问共享变量的位置加以同步,来协调对这个可变状态的访问,当使用锁的机制来实现同步时,要使用同一个锁保证在线程正在修改某个变量的时候,通过某种方式(恰当的同步方式)防止其他线程使用这个变量,让其它线程只能在修改操作完成之前或者之后读取和修改状态。而不是在修改状态过程之中。并非要对所有数据都要使用锁的保护来实现同步操作,只有在多线程访问下,并且数据是可变的才需要锁的同步操作。
  需要注意的是,同步可以避免竞态条件问题,但并不意味着所有方法都应该添加同步的操作,过多的同步方法很容易引起性能上的问题,而且如果操作是复合操作,即使单个独立操作是原子性的,在没有保证整体操作具有原子性的前提下这个复合的操作也会产生竞态条件。
  例如:针对Vector操作

if(!vector.contains(element)) {
     vector.add(element);
}

虽然两个独立的操作都是原子性的,但是整体操作仍然会产生竞态条件(整体操作并不具有原子性)。还是需要额外的加锁机制。

基本概念

  • 重入
    如果一个线程请求一个由其他线程所持有的锁时,那么这个线程会被阻塞,然而由于内置锁时可重入的,如果某个线程.试图获得一个已经由它自己持有的锁,那么这个请求就会成功。也就是说重入意味着获取锁的操作的粒度是“线程”而非“调用”.重入避免了死锁的发生.

  • synchronized关键字:一种常见的错误就是,syn只能用来保证原子性和实现“临界区的语义”,它还有一个重要的方面:内存可见性(memory visibility)即当一个线程更改了一个对象的状态后,其它线程可以观察到更改后的变化。如果没有恰当的同步的话,那么这种情况就无法实现。

  • 可见性:在单线程的环境下,如果向某个变量写入值,然后在没有其它写入操作的情况下读取这个变量,那么总能得到相同的值。如果读写操作在不同的线程中,那么情况就有所不同了,因为无法确保执行读操作的线程能够观察到其他线程所写入的值带来的变化。因此,为了确保多个线程之间对内存操作的可见性,必须采用同步机制!可见性能够导致一个问题:一个线程对一个变量进行写操作,另一个线程针对同一个变量进行读操作,在缺乏同步的条件下,读线程很有可能看到一个失效的值(并没有观察到写操作所作出的更改,仍然读取到了未更改之前的值既失效的值)。如代码所示:
public class NoVisibility {
    private boolean mReady;
    private int mNumber;

    private class Reader extends Thread {
        @Override
        public void run() {
            while (!mReady) {// not be ready
                Thread.yield();
            }
            System.out.println(mNumber);
        }
    }

    private class Writer extends Thread {
        @Override
        public void run() {
            mNumber = 42;
            mReady = true;
        }
    }

    public void start() {
        new Reader().start();
        new Writer().start();
    }
}

由于重排序的影响,Reader值可能打印出0而并非42!而且更糟糕的是Reader可能持续循环下去,因为看不到写线程作出的任何改变,缺乏了可见性。
 我认为,缺乏可见性的本质是内存不一致,各线程都维护着自己的一块缓存数据,读取和操作都转移到了本地缓存中,避免发生内存一致性错误的关键就是要正确理解happens-before关系。这个关系简洁的说明了一个特定的语句对于另一个语句的可见性规定。

  • 加锁与可见性:锁可以确保某一线程以一种可预测的方式来查看另一个线程的执行结果。防止某个执行读操作的线程看到失效的数据。
  • volatile关键字:用来保证变量的更新操作会通知其他线程,使针对于这个变量的操作不会与其它内存操作一起重排序,变量不会缓存在寄存器或者其它对处理器看不见的地方,volatile通常用于做某个操作完成中断或者状态的标志。它的使用场景:对变量的写入操作并不依赖变量的当前值,或者能确保只有单个线程更新变量的值。
      注意:加锁机制既可以确保可见性又可以确保原子性,而Volatile变量只能确保可见性(在JVM1.5之后,JSR 133加强了volatile、final、synchronized 的语意。链接地址:
    https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile
    针对同一个volatile变量进行读写操作的时候与锁的获取与释放具有相同的内存语意,会遵循happens-before原则)。

减少锁的竞争程度

减少锁的竞争程度可以提高性能和可伸缩性因此提高并发的程度。
有两种因素能够影响在锁上发生的竞争:锁的请求频率以及每次持有锁的时间。
如果二者的乘积较小,那么大多数获取锁的操作都不会发生竞争,因此在该锁上的竞争不会对性能造成严重影响。然而在极端情况下,锁的请求量很高并且需要获取锁的线程将被阻塞并等待,处理器还是会被闲置。
有三种方式来降低锁的竞争程度:减少锁的持有时间、降低锁的请求频率、放弃使用独占锁,采用其它协调机制。

  • 减少锁的持有时间
    将锁的范围减小(“快进快出”)将一些与锁无关的代码移出同步代码块,特别是耗时的以及可能被阻塞的操作例如:I/O操作。
  • 减小锁的粒度(降低锁的请求频率)
    这种方法按照降低锁请求频率的不同程度可分为锁分解和锁分段。
    但是共同的思想是:避免使用一个全局的锁来保护多个相互独立的变量,这样可能造成全局串行化的操作。将全局锁分解成多个相互独立的锁,来保护相互独立的变量,每个独立的锁保护属于自己的变量。锁分解主要是使用这个思路。
    锁分段是在锁分解的基础上进一步对锁进行分解,它是将锁分解扩展为对一组独立对象上进行锁分解,例如ConcurrentHashMap使用的就是这种技术。
  • 采用其它协调机制
    使用读-写锁(ReadWriteLock)、并发容器、不可变对象以及原子变量来代替独占锁的方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值