聊聊并发

并发问题的根源

并发是为了提高资源的利用率,协调计算机高速设备(如cpu)和低速设备(如网络io,硬盘上的资源等)。从应用的直观层面上,可能降低反应时间,提高系统的吞吐量。(如果并发的线程数量太高,频繁发生线程切换以及锁竞争,那也会降低吞吐量)。出现并发问题的根本原因是可变和共享。如果一个对象是私有的,那么别人根本就访问不到,更加修改不了;如果一个不可变的对象,那么公开也没事,反正也就只能看看也改不了。举个栗子来说

  • 如果家里有个机器人管家,家里的没有饮料了,恰好男主人和女主人同时在上班的时候给机器人发了条指令买两箱棒冰,结果机器人就买了4箱,但冰箱只能放3箱。这就产生了问题。
    • 共享:男主人和女主人都可以使用机器人管家
    • 可变:机器人管家可以改变冰箱里棒冰的数量
  • 如果这里的机器人听从于男女主人中的一个,或者机器人根本就不能去买棒冰都能避免这个问题。

解决方式

既然根本原因是可变和共享,那么控制住这两个因素就可以了。但有些场景必须要有可变和共享(比如上面机器人管家的例子),那就得双方协商了,按照一定的规则来办事(同步机制)。

可变–使用不可变类(Immutable class)

不可变类指构造方法中初始化一个对象后这个对象就不能修改,以任何形式去修改这个对象,都会返回一个新建的对象。java中最常见的就是String,在改变的地方都是一个return new String(xxx);(注意StringStringBuilderStringBuffer的区别)。不可变类一定是线程安全的。因为不可变类的性质决定了就算别的线程有这个对象的引用,也改不了这个对象的属性。

共享

如果一个对象是本线程独有的,那么也不会出现问题。当一个对象是静态变量,或者通过参数传递到别的线程中,那就是可能会存在问题。

ThreadLocal这个类型很有意思,有句话好像是私有的共享,具体为不同的线程之间相互独立,而线程内部哪里都能调用到。这一点ThreadLocal基本上用来实现上下文。如果子类的进程需要复用父进程的上下文用如下的形式。需要做的

  1. 上下文对象需要实现clonable接口,不然使用相同的引用,会相互影响。
  2. 在子线程对象的构造函数中clone父线程的上下文。
  3. 在子线程启动的时候用克隆出来父线程的上下文作为自己上下文的初始值

共享和可变必须存在–同步机制

当共享和可变必须存在(比如同时更新一条数据),就要使用同步机制了,自然想到java中synchronized这个关键字。在使用同步之前需要考虑下能够不共享吗?能够不可变吗?

这样的场景出错都是由于可见性和竞争条件引起的。

  • 可见性问题: A线程修改了变量param1,之后B线程去读param1不一定能看到变量值。
  • 竞争条件: 多个线程都期望自己能独占一个资源,可实际上是多个线程在使用。比如A,B线程同时去修改一个值,就有可能造成其中一个线程写丢失(这种场景就只能靠日志排查问题,所以日志很重要,要明确打印出操作信息)。竞争条件只能特定的规则解决。

java中可用的同步机制

  1. 使用volitile
  2. 使用synchronized的修饰符
  3. 使用java.util.concurrent下的安全容器

具体分析:

  • volitile只能保证可见性,即满足happens-before原则,不能够解决竞争条件。volitile的变量每次读取都会从堆中读取,更新也会马上写会堆。常常用于单写多读的场景,比如系统中某块的开关。像a++ 这样的操作都不是线程安全的(a++包含3条指令)。
  • synchronized不仅能保证可见性,也能保证竞争条件。基于监视器锁(线程独占),线程进入同步区之前必须获得到监视器锁(本身就拥有也可以,可冲入),一旦进入了同步区就把获取同步器锁(此时别的线程无法进入同步区代码),当同步区的代码执行完后,释放同步区锁(别的线程可以进入)。这个是jvm提供的能力。
    • 在使用Object的wait,notify,notifyall的时候必须拥有该对象的监视器锁,否则会IllegalMonitorStateException,jdk中的api文档。也犯过这样的错误。。
    • 不要使用嵌套的同步器锁,否则可能会出现死锁。在内层的同步代码中调用内层监视器锁的wait会让出当前线程,且释放内层的监视器锁,但是不会释放外层的监视器锁,所以别的线程还是无法进入外层的同步区代码,当然也进不了内层的同步区代码。
    • 境界区的代码必须在一个同步区代码内,否则就是a++的效果
    • 不同临界区的代码可以使用不同的监视器锁(分段锁,比如ConcurrentHashMap,ConcurrentHashMap的读是弱一致性的,不能获取到当前时刻的size。采用了计算3次size,若相同则返回,若不同锁住全部的段重新计算,这样的得到的size已经不是那个时刻的size)
  • java.util.concurrent这个包中提供的实现是基于AbstractQueuedSynchronizor。这个类内部维护了线程队列,使用sun.misc.Unsafe来实现线程调度。对子类只开放了tryAcquire(int)
    ,tryRelease(int),tryAcquireShared(int),tryReleaseShared(int)
    按那种方式来实现锁。常用的几个类

    1. ReentranceLock 可重入锁,内部有公平锁/非公平锁
    2. CountdownLacth 使多个线程同时开始,构造方法中需要有一个初始值个数n。调用await堵塞当前线程,当调用了n次await后,堵塞的线程全部被唤醒。
    3. CyclicBarrier 可复用的CountdownLatch
    4. Semaphore 信号量,用来控制资源并发的类
    5. Condition await和signal

这些都是用到了AQS,比如ReentranceLock中就有两个类NonfairSyncFairSync来提供锁的能力。ReentranceLock的功能全部代理给内部类。

避免死锁的几种方式

  1. 按照固定的顺序获取资源(获取锁),这样不会出现环形依赖。
  2. 给锁设置一定超时时间,这样能增加一定的抵抗能力,回归到初态。但如果没有按照固定顺序获取锁,还是会死锁。

实例代码

用来补充上面例子

子线程复制父线程的上下文

 /**
 * 使用HashMap来作为单个线程上下文的承载容器,实现Cloneable是为子线程clone父线程的上下文
 * 
 * @author carey Mail:carey_yu@126.com
 * @version $Id: ContextMap.java, v 0.1 2016年7月2日 下午11:28:45 Exp $
 * @param <K>
 * @param <V>
 */
public class ContextMap<K, V> extends HashMap<K, V>implements Cloneable {

}


/**
 * 全局上下使用的上下文容器
 * 
 * @author carey Mail:carey_yu@126.com
 * @version $Id: Context.java, v 0.1 2016年7月2日 下午11:15:01 Exp $
 */
public abstract class Context {
    private static ThreadLocal<ContextMap<String, String>> context = new ThreadLocal<>();

    public static ContextMap<String, String> get() {
        return context.get();
    }

    public static void set(ContextMap<String, String> contextMap) {
        context.set(contextMap);
    }

    public static void add(String key, String value) {
        ContextMap<String, String> cmap = get();
        if (null == cmap) {
            cmap = new ContextMap<String, String>();
            set(cmap);
        }
        cmap.put(key, value);
    }

    public static void remove(String key) {
        ContextMap<String, String> cmap = get();
        if (null == cmap) {
            return;
        }
        cmap.remove(key);
    }

}

/**
 * 
 * @author carey Mail:carey_yu@126.com
 * @version $Id: ContextCopy.java, v 0.1 2016年7月2日 下午11:11:10 Exp $
 */
public class ContextCopyTHread implements Runnable {

    private ContextMap<String, String> parentContext;

    /**
    * 
    */
    public ContextCopyTHread(ContextMap<String, String> parentContext) {
        this.parentContext = (ContextMap<String, String>) parentContext.clone();
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        // 必须在这里设置线程上下文,构造方法执行还是在父类的线程中执行的
        Context.set(parentContext);
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            // logger.error("", e);
        }
        System.out.println("child context o is: " + Context.get());
    }

    public static void main(String[] args) {
        Context.add("111", "222");
        ContextCopyTHread childThread = new ContextCopyTHread(Context.get());
        new Thread(childThread).start();
        Context.add("222", "ffff");
        System.out.println(Context.get());
    }

}    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值