Java并发中常见概念

本文主要记录自己阅读《Java并发编程实战》后,对并发编码的浅薄认识,为原创内容,如有文中有书写或其他问题,请留言指导修正,互相交流,共同进步,本人QQ:417213902。

常见的并发概念

  • 原子性

    符合原子操作的那么就说具有原子性,那么原子操作指不会被线程调度
    机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有
    任何上下文切换。
    

    比如我们常见的++a,它的操作是原子的,因为它并不会作为一个不可
    分割的操作来执行,这是一个“读取-修改-写入”的操作序列,并且其结果
    状态依赖于之前的状态。
    我们可以通过采用java.util.concurrent.atomic包中包含的原子变量类,用于实现在数值和对象引用上的原子状态转换。原子变量类采用了CAS(compare and swap)无锁算法,属于乐观锁。CAS的原理是有3个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当A和V相等时,将V改为B,否则什么也不做。
    还有一种方式可以采用加锁机制,即内置锁及重入机制,用关键字synchronized同步代码块能达到原子操作。

  • 竞态条件

    在并发编程中,当某个计算的正确性取决于多个线程的交替执行时序
    时,那么就会发生竞态条件。
    

    比如说“先检查后执行”操作,即可能通过一个可能失效的观测结果来决定
    下一步的动作。常见的有单例模式。那么此时我们采用可以原子操作解决此问题,即保证在单例模式中初始化变量的方法增加锁synchronized,或者定义原子性变量。

  • 指令重排序

     在没有充分同步的程序中,如果调度器采用不恰当的方式来交替执行
     不同线程的操作,那么将导致不正确的结果,更糟的是,JVM还使得不
     同线程看到的操作执行顺序是不同,从而导致在缺乏同步的情况下,要
     推断操作的执行顺序将变得更加复杂,此些都可以归为重排序。
    

    在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会把变量保存在寄存器而不是内存中;处理器可以采用乱序或并行等方式执行指令;缓存可能会改变将写入变量提交到主内存的次序;而且,保存在处理器本地缓存中值,对于其他处理器是不可见的。当然对于乱序,我觉得应该是没有依赖或者关联的指令可以乱序。

  • 可见性

    当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其 
    他线程能够立即看得到修改的值。
    

    首先我们得要理解下,在实际内存中,每个线程内都会有个工作内存,仅每个线程自己可见,且还有共享内存,工作内存中的值由共享内存复制过来;当把变量声明为volatile类型后,线程每次操作都是修改主内存中的变量,读取volatile类型变量时也总是返回最新写入的值。这就说明volatile能解决 线程对变量的可见性,注意volatile不可以对非原子操作的变量具有可见性。加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。synchronized同样可以保证可见性。

  • 不可变

    如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。
    

    主要关键字为final。

  • 内置锁及重入性
    java本身提供了同步代码块,由关键字synchronized实现,每个java对象都可以用做一个实现同步的锁,这些锁我们称为内置锁。java的内置锁相当于一个互斥体,这意味着最多只有一个线程能持有这种锁,当线程A尝试获取由线程B持有的锁时,线程A必须等待或阻塞。
    在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。重入意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程,当计数值为0时,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出 同步代码块时,计数器会相应地递减,当计数值为0时,这个锁将被释放。

  • 并发中常见关键字synchronized、volatile、final
    synchronized :
    修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    synchronized(this)同步代码块时,代码块为原子操作,多线程将被阻塞。
    synchronized(某个对象)同步代码块时,这个对象将只能被拥有锁的线程修改
    volatile:
    volatile是Java虚拟机提供的轻量级的同步机制。关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总是立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作(如a++)在多线程环境并不保证安全性,这是因为a++操作是非原子操作,可以将该方法用synchronized修饰。

  /**
 * Created by zejian on 2017/6/11.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class DoubleCheckLock {
    private static DoubleCheckLock instance;
    private DoubleCheckLock(){}
    public static DoubleCheckLock getInstance(){
        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤1和步骤2间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
final:
final修饰的对象可以在定义时或构造器中初始化
static final修饰的对象表示常量,只有在定义时赋值

除非需要某个域是可变的,否则应将其声明为final域,也是一个良好的编程习惯。摘自《Java并发编程实战》

2018-05-08 23:31:00

你可能感兴趣的:(Java并发中常见概念)