【J2SE】java并发编程实战 读书笔记( 1、2、三章)

线程的优缺点

线程是系统调度的基本单位。线程若是使用得当,能够有效地下降程序的开发和维护等成本,同时提高复杂应用程序的性能。多线程程序能够经过提升处理器资源的利用率来提高系统的吞吐率。与此同时,在线程的使用开发过程当中,也存在着诸多须要考虑的风险。java

  1. 安全性:有合理的同步下,多线程的并发随机执行使线程安全性变得复杂,如++i
  2. 活跃性:在多线程中,常由于缺乏资源而处于阻塞状态,当某个操做不幸形成无限循环,没法继续执行下去的时候,就会发生活跃性问题。
  3. 性能:线程总会带来程序的运行时开销,多线程中,当频繁地出现上下文切换操做时,将会带来极大的开销。

线程安全性

线程安全的问题着重于解决如何对状态访问操做进行管理,特别是对共享和可变的状态。共享意味着可多个线程同时访问;可变即在变量在其生命周期内能够被改变;状态就是由某个类中的成员变量(Field)。缓存

一个无状态的对象必定是线程安全的。由于它没有可被改变的东西。
public class LoginServlet implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        System.out.println("无状态Servlet,安全的类,没有字段可操做");
    }
}

原子性

正如咱们熟知的 ++i操做,它包含了三个独立的“读取-修改-写入”操做序列,显然是一个复合操做。为此java提供了原子变量来解决 ++i这类问题。当状态只是一个的时候,彻底能够胜任全部的状况,但当一个对象拥有两个及以上的状态时,仍然存在着须要思考的复合操做,尽管状态都使用原子变量。以下:安全

public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = 
        new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors = 
        new AtomicReference<BigInteger[]>();

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get())) {
            encodeIntoResponse(resp, lastFactors.get());
        } else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
} // lastNumber lastFactors 虽然都是原子的,可是 if-else 是复合操做,属“先验条件”

既然是复合操做,最直接,简单的方式就是使用synchronized将这个方法同步起来。这种方式能到达预期效果,但效率十分低下。多线程

既然提到synchronized加锁同步,那么就必须知道 锁的特色:并发

  1. 锁是能够重入的。即子类的同步方法能够调用本类或父类的同步方法。
  2. 同一时刻,只有一个线程可以访问对象中的同步方法。
  3. 静态方法的锁是 类;普通方法的锁是 对象自己。

回顾上面的代码,一个方法体中,只要涉及了多个状态的时候,就必定须要同步整个方法吗?答案是否认的,同步只是为了让多步操做为原子性,即对复合操做同步便可,所以须要明确的即是哪些操做是复合操做。以下:函数

public class CachedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    
    public synchronized long getHits() {
        return hits;
    }
    
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }
    
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        
        if (factors == null) {
            factors = factor(i);
            synchronized (this) {
                lastNumber = 1;
                lastFactors = factors.clone();
            }
        }
        
        encodeIntoResponse(reqsp, factors);
    }
}// 两个synchronized分别同步独立的复合操做。

对象共享

重排序:当一个线程修改对象状态后,其余线程没有看见修改后的状态,这种现象称为“重排序”。性能

java内存模型容许编译器对操做顺序进行重排序,并将数据缓存在寄存器中。当缺少同步的状况下,每个线程在独立的缓存中使用缓存的数据,并不知道主存中的数据已被更改。这就涉及到内存可见性的问题。this

可见性

内存可见性:同步的另外一个重要的方面。咱们不只但愿防止多个线程同时操做对象状态,并且还但愿确保某一个线程修改了状态后,能被其余线程看见变化。线程

volatile:使用 synchronized能够实现内存可见,但java提供了一种稍弱的更轻量级得同步机制volatile变量。在访问volatile变量时不会执行加锁操做,所以不会产生线程阻塞。即使如此仍是不能过分使用volatile,当且仅当能简化代码的实现以及对同步策略的验证时,才考虑使用它。code

发布与逸出

发布指:使对象可以在当前做用于以外的代码中使用。即对象引用能被其余对象持有。发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。

逸出指:当某个不该该发布的对象被发布时,这种状况被称为逸出。

// 正确发布:对象引用放置公有静态域中,全部类和线程均可见
class CarFactory {
    public static Set<Car> cars;
    
    private CarFactory() {
        cars = new HashSet<Car>();
    }    // 私有,外部没法获取 CarFactory的引用
    
    public static Car void newInstance() {    
        Car car = new Car("碰碰车");
        cars.put(car);
        return car;
    }    // 使用方法来获取 car
}
// 逸出
class Person {
    private String[] foods = new String[] {"土豆"};
    
    public Person(Event event) {
        person.registListener {
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            }
        }
    }// 隐式逸出了this,外界获得了Person的引用 而且 EventListener也获取了Person的引用。
    
    public String[] getFoods() {
        return foods;
    }// 对发布的私有 foods,外界仍是能够修改foods内部值
}

线程封闭

将可变的数据仅放置在单线程中操做的技术,称之为发线程封闭。

栈封闭:只能经过局部变量才能访问对象。局部变量的固有属性之一就是封装在执行线程中,它们位于执行线程的栈中,其余线程没法访问这个栈,即只在一个方法内建立和使用对象。

public int test(Person p) {
    int num = 0;
    PersonHolder holder = new PersonHolder();
    
    Person newPerson = deepCopy(p);
    Person woman = holder.getLove(newPerson);
    newPerson.setWomen(person);
    num++;
    
    return num; // 基本类型没有引用,对象建立和修改都没有逸出本方法
}

ThreadLocal类:ThreadLocal可以使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了 getset等访问接口的方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因get老是返回由当前执行线程在调用set时设置的最新值。

private static ThreadLocal<Connection> connectionHolder = 
    new ThreadLocal<Connection>() {
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };

public static Connection getConnection() {
    return connectionHolder.get();
}
当某个频繁执行的操做须要一个临时对象,例如一个缓冲区,而同时又但愿避免在每次执行时都从新分配该临时对象,就可使用ThreadLocal。

不变性

线程安全性是不可变对象的固有属性之一。不可变对象必定是线程安全的,它们的不变性条件是由构造函数建立的,只要它们的状态不可变。

//    在可变对象基础上构建不可变类
public final class ThreadStooges {
    private final Set<String> stooges = new HashSet<String>();
    
    public ThreadStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
    }
    
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}// 没有提供可修改状态的方式,尽管使用了Set可变集合,但被private final修饰着

对象不可变的条件

  1. 对象建立之后其状态就不能修改。
  2. 对象的全部域都是final类型。
  3. 对象是正确建立的(在对象的建立期间,this引用没有逸出)

安全发布

任何线程均可以在不须要额外同步的状况下安全地访问不可变对象,即便在发布这些对象时没有使用同步。
// 安全的 Holder类
class Holder {
    private int n;
    public Holder(int n) {
        this.n = n;
    }
}

public class SessionHolder {
    // 错误的发布,致使 Holder不安全
    public Holder holder;
    
    public void init() {
        holder = new Holder(10);
    }
}// 当初始化 holder的时候,holder.n会被先默认初始化为 0,而后构造函数才初始化为 10;在并发状况下,可能会有线程在默认初始化 与 构造初始化中,获取到 n 值为 0, 而不是 10;

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其余线程可见。一个正确构造的对象能够经过如下方式安全发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到 volatitle 类型的域或者 AtomicReferance 对象中。
  • 将对象的引用保存到某个正确构造对象的 final 类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

在线程并发容器中的安全发布:

  • 经过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,能够安全地将它发布给任何从这些容器中访问它的线程(不管是直接访问仍是经过迭代器访问)。
  • 经过将某个元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,能够将元素安全地发布到任何从这些容器中访问该元素的线程。
  • 经过将某个元素放入 BlockingQueue或者ConcurrentLinkedQueue中,能够将该元素安全地发布到任何从这些队列中访问该元素的线程。

一般,要发布一个静态构造的对象,最简单、安全的方式就是使用静态的初始化器。如public static Holder holder = new Holder(10)。若是对象在发布后状态不会被修改(则称为事实不可变对象),那么在没有额外的同步状况下,任何线程均可以安全地使用被安全发布的不可变对象。

对象的发布需求取决于它的可变性:

  • 不可变对象能够经过任意机制来发布。
  • 事实不可变对象必须经过安全方式来发布。
  • 可变对象必须经过安全方式来发布,而且必须是线程安全的或者有某个锁保护起来。

在并发程序中使用和共享对象时可采用的策略:

  • 线程封闭。将对象封闭在线程中,如在方法中建立和修改局部对象。
  • 只读共享。
  • 线程安全共享。对象内部实现同步,使用公有接口来访问。
  • 保护对象。使用特定的锁来保护对象。
相关文章
相关标签/搜索