美文网首页
【多线程问题的解决】

【多线程问题的解决】

作者: hello高world | 来源:发表于2017-01-21 23:28 被阅读0次

    需要回顾之前博文《多线程的问题》

    一、 CPU对于两个冒险的解决办法

    • 结构冒险(CPU对某一个存储器读取资源为例):
      使用同步,依赖硬件提供同步指令。
    • 数据冒险:

    二、多线程对问题的解决办法

    • <b>2.1 安全问题的代码</b>
     package com.tinygao.thread.safe;
    public class UnSafe {
        private int value;
    
        public int getNext() {
            return value++;
        }
    }
    
    • <b>2.2 为什么不安全</b>
    A/B代表两个不同线程,不安全执行错误情况

    <b>!!因为它具备4个特性。永远记住这四点,绝大部分只要这4点!!</b>

    • <b>有可变的状态(以下三个地方代表类是有状态的特征)</b>
      1、有类变量
      2 、有实例变量(本例子中的value属于这类)
      3、有其他对象的引用(比如map.entry对象)
      </br>
    • <b>复合操作(value++包含三个原子操作)</b>
      1、读取value
      2、value+1计算
      3、将value写入主存
      </br>

    • <b>顺序没有控制</b>
      B线程没有等待A操作完,就读取了value。导致执行两次加法,但最终结果都是10
      </br>

    • <b>状态不可见</b>
      A线程中value值的改变,对B来说不可见。线程栈内存数据是互相隔离的,看不到!

    • 为什么不安全之提问

    1、没有状态的类,是线程安全的 (√)
    2、只要使用线程安全的类写出来的代码块一定是线程安全的(×)
    ==>多个安全类在一起成了复合操作了,加上没有控制顺序的手段,可能会出现不可预测的结果。
    3、不可变对象一定是线程安全的(√)
    ==>什么是不可变对象?
    1、对象创建之后其状态就不能修改。
    2、对象的所有状态都是final类型
    3、对象是正确创建的(this引用没有逸出)

    ↓对于第二问看concurrentMap是线程安全的,用了他的代码块可不是线程安全的,来举个栗子吧↓

    package com.tinygao.thread.safe;
    
    import com.google.common.collect.Maps;
    import com.google.common.util.concurrent.ThreadFactoryBuilder;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Created by gsd on 2017/1/26.
     */
    @Slf4j
    public class UnSafe {
        private int num;
        private Map<String, String> map = Maps.newConcurrentMap();
    
        public int getNumAdd() {
            return num++;
        }
    
        public String getMapValue() {
            if(!map.containsKey("tinygao")) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put("tinygao", Thread.currentThread().getName());
            }
            return map.get("tinygao");
        }
    
        public static void main(String[] args) {
            UnSafe unSafe = new UnSafe();
            ExecutorService es = Executors.newFixedThreadPool(
                    2,
                   new ThreadFactoryBuilder().setNameFormat("map-%d").build());
    
            es.submit(()->{
                log.info("map {}",unSafe.getMapValue());
            });
            es.submit(()->{
                log.info("map {}",unSafe.getMapValue());
            });
        }
    }
    
    
    • 我们的本意:当第一个线程判断不存在mapkey的时候去填充这个key的值,之后的其他线程只要从map get出来这个key就可以了。
    • 事实:两个线程都判断了mapkey不存在,都各自put了一把到map上。导致mapkey的值被覆盖了。打印结果(你的可能跟我不一样):↓
      [map-1] INFO com.tinygao.thread.safe.UnSafe - map map-1
      [map-0] INFO com.tinygao.thread.safe.UnSafe - map map-0

    • <b>2.3 解决安全问题</b>

    对应上面的四个特性取反:
    <b>1、去可变状态</b>
    <b>2、复合操作改成原子操作(记住两个常见的复合操作)</b>
    -- if-then操作(像上面map的例子)
    -- 取-读-写(像上面value++的例子)
    <b>3、控制执行顺序,即保证有序性</b>
    <b>4、若有状态,则让状态在线程间互相可见</b>
    </br>

    <b>2.3.1、怎么去可变状态</b>

    • 不再是成员变量、实例变量了。比如:把他移入到方法内部,这样就在自己的线程栈中了
     public int getNumAdd() {
            int num = 0;
            return num++;
        }
    
    • 不在线程之间共享该状态(让状态封闭)
    private ThreadLocal<Integer> num = new ThreadLocal<>();
       public int getNumAdd() {
           num.set(num.get()+1);
           return num.get();
       }
    
    • 让状态做到不可变
      final inti num = 1
      再看一个不安全的
    ```
    

    package com.tinygao.thread.safe;
    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    //这个是不安全的
    @Slf4j
    public class Unfinal {
    private Integer lastNumber;
    private Integer currentNumber;

    public void setLastNumber(Integer i) {
        this.lastNumber = i;
    }
    
    public Integer getCurrentNumber(Integer i) {
        if(lastNumber == null || !lastNumber.equals(i)) {
            return null;
        }
        else {
            return 1;
        }
    }
    
    public static void main(String[] args) {
        Unfinal unfinal = new Unfinal();
    
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(()->{
            unfinal.setLastNumber(1);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //预期打印是1,看看实际打印多少?
            log.info("get first {}", unfinal.getCurrentNumber(1));
        });
    
        es.submit(()->{
            unfinal.setLastNumber(2);
            //预期打印是null,看看实际打印多少?
            log.info("get seconde {}", unfinal.getCurrentNumber(1));
        });
        es.shutdown();
    }
    

    }

    看一个final安全的,保证在构造函数中初始化一次状态后不可变了
    
    

    package com.tinygao.thread.safe;
    import lombok.extern.slf4j.Slf4j;

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;

    @Slf4j
    public class FinalClass {
    private final Integer lastNumber;
    private final Integer currentNumber;

    public FinalClass(Integer lastNumber, Integer currentNumber) {
        this.lastNumber = lastNumber;
        this.currentNumber = currentNumber;
    }
    
    public Integer getCurrentNumber(Integer i) {
        if(lastNumber == null || !lastNumber.equals(i)) {
            return null;
        }
        else {
            return 1;
        }
    }
    
    public static void main(String[] args) {
        FinalClass finalclass = new FinalClass(1, 1);
    
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(()->{
            log.info("get first {}", finalclass.getCurrentNumber(1));
        });
    
        es.submit(()->{
            log.info("get seconde {}", finalclass.getCurrentNumber(1));
        });
        es.shutdown();
    }
    

    }

    
    <b> 2.3.2、怎么将复合操作变成原子操作</b>
      - "读-操作-写" 使用Atomic类
    

    private AtomicInteger num2 = new AtomicInteger(0);
    public int getNumAdd2() {
    return num2.incrementAndGet();
    }

      - "if-then" 使用java自带的原子操作 
    

    private Map<String, String> map = Maps.newConcurrentMap();
    public void safeMap() {
    map.putIfAbsent("tinygao", Thread.currentThread().getName());
    }

    
    <b> 2.3.3、怎么控制执行顺序</b>
       - 同步
        1、synchronized
        2、volatile类型的变量(但复合操作变量就有问题了)
        3、显式锁
        4、原子变量(long和double可能不是原子变量,看处理器架构)
    
    <b> 2.3.4、怎么让状态在线程间可见</b>
       - 同步
        1、synchronized
        2、volatile类型的变量(但复合操作变量就有问题了)
        3、显式锁
        4、原子变量(long和double可能不是原子变量,看处理器架构)
    
    
    
    
    采用互斥。对于共享资源访问的<b>代码片段</b>叫做临界区
    有多种方法,比如锁变量(0/1)、严格轮换法
    
    概念:管程、信号量、互斥量、同步、阻塞、协作、互斥。
    https://zh.wikipedia.org/wiki/%E7%9B%A3%E8%A6%96%E5%99%A8_(%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96)
    198.35.26.96 zh.wikipedia.org
    待续~

    相关文章

      网友评论

          本文标题:【多线程问题的解决】

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