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。一定不要持有锁。

  • 相关阅读:
    QT visual stuido 集成插件不能打开ui文件的原因
    KLSudoku 1.2 数独游戏软件发布
    QT for linux 的错误 undefined reference to 'FcFreeTypeQueryFace' 的解决方法
    tp3.2源码解析——入口文件
    关于php调用.net的web service 踩过的坑
    CentOS7 LNMP+phpmyadmin环境搭建(二、LNMP环境搭建)
    CentOS7 LNMP+phpmyadmin环境搭建(一、虚拟机及centos7安装)
    php无限分类
    php的文件引用
    CentOS7 LNMP+phpmyadmin环境搭建(三、安装phpmyadmin)
  • 原文地址:https://www.cnblogs.com/xiaolang8762400/p/7054186.html
Copyright © 2011-2022 走看看