zoukankan      html  css  js  c++  java
  • Java并发编程实战 第2章 线程安全性

    编写线程安全的 代码,核心在与对共享的和可变的对象的状态的访问。

    如果多个线程访问一个可变的对象时没有使用同步,那么就会出现错误。在这种情况下,有3中方式可以修复这个问题:

    • 不在线程之间共享该状态变量
    • 将状态变量修改为不可变的变量
    • 在访问状态变量时使用同步

    线程安全性的定义:

    在多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么我们就说这个类是线程安全的。

    无状态对象:

    不包含任何域,也不包含对其他类中域的引用。

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

    比如我们平时写的Servlet。

    PS:Servlet中可能包含一个Dao域,但是在和Dao本身也是无状态的,我们本身是可以将Dao的代码直接分解到Servlet中,不需要Dao。但是为了实现代码解耦的效果,加入了Dao。

    竞态条件

    竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

    常见的竞态条件如:先检查后执行。

    原子操作:

    一些简单的竞态条件,可以通过原子操作,解决。

    如通过AtomicInteger的incrementAndGet()解决计数器的竞态条件

    加锁机制:

    复杂的竞态条件需要使用锁来解决。

    每个java对象都有一个内置的锁,这个锁被称为内置锁或者监视锁。在这个对象内部的方法上(非static方法)的使用synchronized就是默认使用的这个锁。

    内置锁是互斥的,可重入的。

    用锁来保护状态

    对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,这种状态下,我们成状态变量时由这个锁保护的。

    活跃性和性能

    一些概念:

    安全性是指"永远不会发生糟糕的事情",我们使用锁和同步就是为了保证安全性。

    活跃性是指"某件正确的事情最终会发生",当某个操作不能执行下去,就会出现活跃性问题,如死循环。

    性能问题:响应时间,吞吐率,资源消耗,伸缩性等。

    我们可以通过锁来解决安全问题,但是也要兼顾活跃性和性能。

    重要的是不要粗暴的直接在一个复杂的方法上加synchronize,而是分解。我们看一个例子:

    1. package com.zjf;
    2.  
    3. import java.math.BigInteger;
    4. import java.util.concurrent.ExecutorService;
    5. import java.util.concurrent.Executors;
    6. import java.util.concurrent.TimeUnit;
    7.  
    8. /**
    9.  * 一个获取用于计算整数的平方的类
    10.  * 实现了缓存
    11.  * @author hadoop
    12.  *
    13.  */
    14. public class PowerCache {
    15.  
    16.    //使用全局变量 作为竞争资源
    17.    public static final PowerCache pc = new PowerCache();
    18.    //记录上一个传入的数值 为多个线程共享
    19.    private BigInteger lastNumber;
    20.    //记录上一个计算的平方结果 为多个线程共享
    21.    private BigInteger lastPower;
    22.    //记录power方法调用了多少次 为多个线程共享
    23.    private long hits;
    24.    //记录命中缓存的次数 为多个线程共享
    25.    private long cacheHits;
    26.    //访问了共享数据 注意标注方法为synchronized
    27.    public synchronized long getHits()
    28.    {
    29.       return hits;
    30.    }
    31.    //访问了共享数据 注意标注方法为synchronized
    32.    public synchronized long getCacheHit()
    33.    {
    34.       return cacheHits;
    35.    }
    36.    //命中缓存的概率 访问了共享数据 注意标注方法为synchronized
    37.    public synchronized double getCacheHitRatio()
    38.    {
    39.       return (double)cacheHits / (double) hits;
    40.    }
    41.  
    42.  
    43.    public BigInteger power(BigInteger i)
    44.    {
    45.       BigInteger power = null;
    46.       //分割对比和命中缓存部分为一个synchronized
    47.       synchronized(this) {
    48.          hits++;
    49.          if(i.equals(lastNumber))
    50.          {
    51.             power = lastPower;
    52.             cacheHits++;
    53.          }
    54.       }
    55.       if(power == null)
    56.       {
    57.          //将计算这种耗时逻辑放在synchronized外部
    58.          power = i.pow(2);
    59.          //分割的第二个synchronized块 用于没有命中的结果 需要记录进缓存
    60.          synchronized (this) {
    61.             lastNumber = i;
    62.             lastPower = new BigInteger(power.toString());//复制一份 这一步骤有问题 其实不用复制 这是多此一举 因为BigInteger是不可变对象
    63.          }
    64.       }
    65.       return power;
    66.    }
    67.  
    68.    //测试逻辑
    69.    public static void main(String[] args) throws InterruptedException {
    70.       PowerCache pc = new PowerCache();
    71.       ExecutorService es = Executors.newCachedThreadPool();
    72.       for(int i = 0; i < 100; i++)
    73.       {
    74.          es.execute(new Runnable() {
    75.             public void run() {
    76.                BigInteger bi = BigInteger.valueOf((long)(Math.random() * 10));
    77.                System.out.println(bi + ":" + PowerCache.pc.power(bi));
    78.             }
    79.          });
    80.       }
    81.       es.shutdown();
    82.       TimeUnit.MILLISECONDS.sleep(1000);
    83.       System.out.println(PowerCache.pc.getHits());
    84.       System.out.println(PowerCache.pc.getCacheHit());
    85.       System.out.println(PowerCache.pc.getCacheHitRatio());
    86.    }
    87. }

    要判断同步代码块的合理大小,需要在各种设计之间进行权衡,包括安全性,简单性,和性能。

    当执行时间较长的计算或者无法快速完成的操作时,如网络IO或者控制台IO。一定不要持有锁。

  • 相关阅读:
    poj 2584 T-Shirt Gumbo (二分匹配)
    hdu 1757 A Simple Math Problem (乘法矩阵)
    矩阵之矩阵乘法(转载)
    poj 2239 Selecting Courses (二分匹配)
    hdu 3661 Assignments (贪心)
    hdu 1348 Wall (凸包)
    poj 2060 Taxi Cab Scheme (二分匹配)
    hdu 2202 最大三角形 (凸包)
    hdu 1577 WisKey的眼神 (数学几何)
    poj 1719 Shooting Contest (二分匹配)
  • 原文地址:https://www.cnblogs.com/xiaolang8762400/p/7054186.html
Copyright © 2011-2022 走看看