zoukankan      html  css  js  c++  java
  • Java并发编程(三)概念介绍

    在构建稳健的并发程序时,必须正确使用线程和锁。但是这终归只是一些机制。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)可变的(Mutable)状态的访问。

    对象的状态是指存储在状态变量(例如实例或静态域)中的数据。

    对象的状态可能包括其他依赖对象的域。比如某个HashMap的状态不仅是HashMap对象本身,还存储在许多Map.Entry对象中。

    "共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以变化。

    image

    线程安全性在于如何防止数据上发生不可控的并发访问

    一个对象是否需要是线程安全的,取决于它是否被多个线程访问。

    要使对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能导致数据破坏以及其他不该出现的结果。

    协同多个线程对变量访问的同步机制主要有:

    1. 关键字synchronized

    2. 关键字volatile

    3. 显式锁(Explicit Lock)

    4. 原子变量

    协同多线程访问一个可变的状态变量的方法有:

    1. 不在线程之间共享该状态变量

    2. 将状态变量修改为不可变的变量

    3. 在访问状态变量时使用同步

    什么是线程安全?

    一个类在多线程环境下被访问,这个类始终能表现出正确的行为,那么就称这个类是线程安全的。

    在线程安全类中往往都封装了必要的同步机制,因此客户端无须进一步采取措施。

    无状态的对象一定是线程安全的。

    无状态可以极大降低类在实现线程安全性时的复杂性。只有当类在保存一些信息的时候,线程安全才会成为一个问题。

    原子性

    当一个操作被CPU无可分割地执行时就是原子操作。

    典型的非原子性操作就是a++,但是实际上这是一个"读取—修改—写入"的操作序列,并且结果状态依赖于之前的状态。

    在并发编程中,由于不恰当的执行顺序而出现不正确的结果是一种非常重要的情况,就是竞态条件(Race Condition)。

    竞态条件

    当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。最常见的竞态条件就是"先检查后执行(Check-Then-Act)"操作,即通过一个可能失效的观测结果来决定下一步的动作。比如延迟初始化的单例模式

    复合操作

    要避免竞态条件的问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成或者之后读取和修改状态,而不是在修改状态的过程中。为了确保安全性,"先检查后执行"(例如延迟初始化)和"读取—修改—写入"(如递增运算)等必须是原子的。我们把"先检查后执行"(例如延迟初始化)和"读取—修改—写入"等操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。

    加锁机制

    那么如何确保复合操作以原子的方式执行呢?这就要用到Java提供的加锁机制了。

    Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。(加锁机制以及其他的同步机制的另一个重要方面是:可见性)

    同步代码块包括两部分:

    1. 一个作为锁的对象引用

    2. 一个作为这个锁保护的代码块

    以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法一Class对象作为锁。

    synchronized (lock) {
        // 访问或修改由锁保护的共享状态
    }

    每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

    Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放锁,那么A也将永远等待下去。

    由于每次只能有一个线程执行内置锁保护的代码,因此,由这个锁保护的同步代码块会以原子的方式执行,多个线程在执行该代码块时也不会互相干扰。并发环境中的原子性与事务应用程序中的的原子性有着相同的含义——一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正执行由同一个锁保护的同步代码块。

    重入

    当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。"重入"意味着获取锁的操作的粒度是"线程",而不是"调用"。(POSIX中pthread互斥体的获取操作是以"调用"为粒度的)重用的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。计数值为0时,这个锁将会被释放。

    重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。子类改写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将会产生死锁。

    用锁来保护状态

    由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。如果在符合操作的执行过程中持有一个锁,那么会使复合操作成为原子操作。然而,仅仅将复合操作封装到一个同步代码块中是不够的。如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都要使用同一个锁。

    一种常见的错误是认为,只有在写入共享变量时才需要使用同步,然而事实并非如此。

    对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了避免显式地创建锁对象。你需要自行构造加锁协议或者同步策略实现对共享状态的安全访问,并且在程序中自始至终地使用它们。

    每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

    一种常见的加锁约定是,将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。在许多线程安全的类中都使用了这种模式,例如Vector和其他的同步集合类。在这种情况下,对象状态中的所有变量都由对象的内置锁保护起来。

    然而,这种模式并没有特殊之处,编译器或运行时都不会强制实施这种(或其他的)模式。如果再添加新的方法或者代码路径时忘了使用同步,那么这种加锁协议很容易被破坏。

    并非所有的数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。当某个变量由锁来保护时,意味着每次访问这个变量都要首先获得锁,这样就确保在同一时刻只有一个线程可以访问这个变量。

    当类的不变性条件涉及多个状态变量时,那么还有另外一个需求:在不变性条件中的每个变量都必须由同一个锁来保护。

    虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还需要额外的加锁机制。此外,每个方法都作为同步方法还可能导致活跃性问题(Liveness)或性能问题(Performance)。

  • 相关阅读:
    sqlserver中判断表或临时表是否存在
    Delphi 简单方法搜索定位TreeView项
    hdu 2010 水仙花数
    hdu 1061 Rightmost Digit
    hdu 2041 超级楼梯
    hdu 2012 素数判定
    hdu 1425 sort
    hdu 1071 The area
    hdu 1005 Number Sequence
    hdu 1021 Fibonacci Again
  • 原文地址:https://www.cnblogs.com/tuhooo/p/7909653.html
Copyright © 2011-2022 走看看