美文网首页
java并发(一)

java并发(一)

作者: 蛮大人我们走 | 来源:发表于2017-11-29 11:33 被阅读11次

    1. 什么是线程安全?

    多个线程在访问同一个对象的时候不需要其他额外的同步手段或措施就能保证该对象被正确的访问并产生正确的执行结果。那么这个对象就是线程安全的。
    线程安全的代码必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步),使用该代码的开发人员无需关心多线程的问题也不用自己采用任何措施来保证多线程的正确调用。
    线程不安全的代码在多个线程中使用时必须作同步处理,否则可能产生不可预期的后果。

    java中的线程安全

    可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
    ①不可变
    如果共享数据是一个基本数据类型,只需要在定义的时候声明为final即可;如果是共享数据是一个对象,则需要保证对象的行为不会对其状态产生任何影响才行(最简单的做法就是把对象中带有状态的变量都声明为final)。
    ②绝对线程安全
    不管运行环境如何,调用者都不需要任何额外的同步措施的类可以称作是绝对线程安全的,但是这通常是需要付出相对较大的代价的。
    ③相对线程安全
    对这个对象单独的操作是线程安全,在调用单个操作的时候不需要做其他额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。总之相对线程安全就是多个对象对这个对象单独操作的时候是线程安全的,但是如果多个线程操作这个对象的不同行为时就需要调用端使用同步的手段来保证调用的正确顺序了。
    在Java语言中,大部分的线程安全类都是属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()等。
    ④线程兼容
    线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。如Vector和HashTable相对应的集合类ArrayList和HashMap等。
    ⑤线程对立
    线程对立是指物理调用端是否采取了同步措施,都无法再多线程环境中并发使用的代码。

    2. 并发编程中的三个概念(原子,可见,有序)

    Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性三个特征建立的。
    ①原子性:一个操作要么全部执行完毕,要么根本就不执行。Java内存模型直接保证的原子性变量操作有read、load、assign、use、store和write。
    ②可见性:多个线程访问同一个变量时一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java语言中的volatile、synchronized和final三个关键字都可保证操作时变量的可见性。
    ③有序性:即程序执行的顺序按照代码的先后顺序执行。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
    总之,要想让并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    3. Volatile,轻量级锁

    Volatile主要使用的场合是在多个线程之间感知实例变量的修改,并且可以获得最新值使用,当线程想要访问volatile修饰的变量时强制从公共堆栈中进行读取。
    Volatile可以保证每次线程从主内存中刷新到最新的变量值,但是不能保证变量值被加载到线程内存之后对该变量做的修改操作是原子性的。Volatile变量在线程内存中被修改之后要立即同步回主内存中,以保证其他线程使用该volatile关键字修饰的变量时获取到的是最新的变量值。
    也就是说,volatile关键字保证的是变量在不同线程之间的可见性,但是无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
    (1) Volatile变量有两个特性
    ①保证可见性:此变量对于所有线程的可见性,指的是当一个线程修改了这个变量的值,新值对于其他线程来说是立即可见的。
    ②禁止指令重排序优化(内存屏障,也会牺牲掉一些性能)
    (2) 某个变量定义为Volatile的应用场景
    ①运算结果不依赖于变量的当前值或者能够确保只有一个线程修改这个变量的值;
    ②变量不需要与其他的状态变量共同参与不变约束。
    (3) Java内存模型对volatile变量定义的特殊规则
    ①在工作内存中每次使用变量前都需要从主内存中刷新最新值;
    ②每次修改变量的值之后都必须立刻同步到主内存中;
    ③要求volatile修饰的变量不会被指令重新排序。
    (4) 不支持原子性,非线程安全
    对一个volatile修饰的变量的读写操作不是原子性的。因为如果在第一个线程加载某个volatile修饰的变量值到工作内存之后有其他线程修改了这个变量值,那么第一个线程是感知不到这个值的变化的。这个时候就会出现线程安全的问题,所以为了保证线程安全问题还是需要synchronized关键字。
    PS:指令重排优化:在保证可以得到程序正确执行结果的前提下,CPU允许将多条指令不按程序规定的顺序分开发送到各个相应电路单元处理。
    (5) Volatile变量的使用
    并不建议过多地依赖volatile变量。如果在代码中过多地依赖volatile变量来控制状态可见性,通常比使用锁的代码更脆弱,也更难以理解。
    仅当volatile变量能简化代码的实现以及对同步策略的验证,才应使用volatile变量。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。Volatile变量的正确使用方式包括:①确保它们自身状态的可见性;②确保它们所引用的对象的状态的可见性;③标识一些重要的程序生命周期事件的发生(比如初始化或关闭)。

    4. static和volatile的区别

    唯一性不等于可见性。

    Java内存中,变量放在主内存中,使用该变量的每个线程,都将从主存区拷贝一份到自己的工作区上进行操作
    Volatile,声明该字段易变,即可能被多个线程使用,java内存模型负责各个线程的工作区与该主内存区的该字段值保持一致。一致性。
    Static,声明该变量是静态的,可以被多个实例共享,是唯一的。
    Static只是声明变量在主存上的唯一性,不能保证工作区和主存区变量值的一致性,除非变量的值是不可变的。即,static声明的变量,不是线程安全的。

    5. Synchronized关键字

    Tables Are Cool
    方法 static方法 锁的是class对象,该类的所有对象访问这个static方法都被阻塞。
    非static方法 锁的是对象,必须持有该对象锁才能访问对象内的同步方法。
    代码块 this对象 与锁非static方法一样,都是给对象上锁。
    非this对象 x给对象x上锁,想访问这个代码段必须持有这个对象x的锁才可以。

    Synchronized关键字保证在同一时刻,只有一个线程可以执行某个对象内某一个方法或某一段代码块。包含两个特征:互斥性和可见性。Synchronized可以解决一个线程看到对象处于不一致的状态,可以保证进入同步方法或者同步代码块的每个线程都可以看到由同一个锁保护之前所有的修改效果。

    Tables Are Cool
    方法 static方法 锁的是class对象,该类的所有对象访问这个static方法都被阻塞。
    非static方法 锁的是对象,必须持有该对象锁才能访问对象内的同步方法。
    代码块 this对象 与锁非static方法一样,都是给对象上锁。
    非this对象 x给对象x上锁,想访问这个代码段必须持有这个对象x的锁才可以。

    class对象 与给static方法加同步关键字一样,锁的都是class对象。

    注意:class锁和对象锁不是同一种锁。持有class锁则可以访问同步的static方法和锁定class对象的代码段;持有对象锁则可以访问同步的非static方法和锁定this对象的代码段。只有多个线程持有同一个对象的锁时,访问该对象内的同步方法才会被阻塞,如果持有的不是同一个对象的锁则异步执行。

    Synchronized关键字可以用于同步方法和同步代码块。同步方法又可分为同步static方法和非static方法:

    • 如果是给static方法加上synchronized关键字,则说明同步的是当前类的.class类,那么后面所有对这个static方法的访问都会被阻塞,但是此时可以访问其他没有加Synchronized关键字的方法或者是加了Synchronized关键字的非static方法;
    • 如果给非static方法加上Synchronized关键字,则同步的是当前对象,这样的话其他想访问同一个对象下的Synchronized同步方法就会被阻塞,但是不影响访问Synchronized同步的static方法,因为Synchronized非static方法是某个对象实例加锁,而Synchronized static方法是给.class对象加锁,但是Class锁是对类的所有对象都有效,也就是说如果现在有个static方法加上了Synchronized关键字,则这个类的所有对象都会对这个方法进行同步操作。

    Synchronized同步代码块的时候分为Synchronized(this 对象)、Synchronized(非this 对象)、Synchronized(class对象)。

    1. Synchronized(this 对象)同步的也是当前对象,而Synchronized(非this 对象)则是对某个非this对象进行同步即锁定。
    2. Synchronized(非this 对象)同步代码块的方法在进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用了。
    3. Synchronized(class对象)与给static方法加上Synchronized关键字是一样的。

    另外使用Synchronized需要注意的地方:

    • Synchronized锁可重入:持有某个对象的锁可继续访问需要持有该对象锁才可访问的方法和代码段;
    • 可重入锁也支持在父子类继承的环境中:调用子类中同步的方法时也可以访问父类中需要对象锁的方法和代码段;
    • 同步方法内出现异常时将会自动释放持有的对象锁;
    • 同步不能继承,所以还需要在子类中需要同步的方法上加同步关键字。

    6. 对象的监视器锁以及为什么该锁是重量锁?

    监视器锁:利用synchronized来修饰代码块,其本质就是对对象进行加锁,达到同步的目的。

    任何对象都有一个monitor与之相关联。被synchronized修饰的代码块在被编译成字节码的时候,在该代码块开始和结束的位置插入monitorenter和monitorexit的指令,虚拟机在执行该指令时,会检查对象的锁状态是否为空或者当前线程是否拥有该对象锁,如果是,对象锁的计数器加去,直接进入到同步代码块执行;如果不是,则该线程被阻塞,直到等到锁释放。

    重量级锁:java的线程是映射到操作系统的原生线程之上的,如果阻塞或者唤醒一个线程就需要操作系统的帮助,即需要从用户态切换到和心态。该切换状态消耗很多处理器时间,甚至多于用户代码运行的时间,因此,synchronized是java中一个重量级的锁。

    7. Fail-fast机制(具体看源码)

    产生原因:ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:

    有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

    解决方案:
    方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
    方案二:使用CopyOnWriteArrayList来替换ArrayList。
    只有容器会出现concurrentModifictionException吗?

    还有一种隐藏迭代器,也会产生这个异常。

    • 编译器将字符串连接的操作转换为调用StringBuilder.append(Object),而这个方法又会调用容器的toString方法,标准的toString方法会迭代容器,并在每个元素上调用toString来生成容器的格式化表示。
    • 容器的HashCode和equals方法也会产生concurrentModifictionException,以及将作为另一个容器的元素或键值的时候。containsAll,removeAll,retainAll等。

    8. ConcurrentHashMap源码,以及JDK1.7和JDK1.8的区别

    • 在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样
    • JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
      详细图解见以下链接:

    深入并发包 ConcurrentHashMap
    http://www.importnew.com/26049.html

    谈谈ConcurrentHashMap1.7和1.8的不同实现
    http://www.jianshu.com/p/e694f1e868ec

    相关文章

      网友评论

          本文标题:java并发(一)

          本文链接:https://www.haomeiwen.com/subject/qulubxtx.html