java并发编程的艺术

1. JAVA并发机制的底层实现原理

1. 锁的状态
  1. since jdk1.6, 级别从低到高有:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
  2. 锁的状态只能升级,不能降级。目的在于为了提高获得锁和释放锁的效率。
2. 原子操作的实现原理
  1. 使用总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将请求阻塞住,那么该处理器可以独享内存。
  2. 通过缓存锁定来保证原子性。总线锁是锁定CPU和内存之间的通信,在这期间,其他处理器不能操作其他内存地址的数据,因此,总线锁的开销较大。 而缓存锁则是保证在同一时刻,只需要保证对某个内存地址操作是原子性即可。

2. java内存模型

1. 基础
  1. 在java中,所有的实例域、静态域和数据元素都是存储在堆内存中,堆内存在线程之间共享;局部变量、方法定义参数和异常处理参数不会在线程之间共享,不存在线程可见性问题。
  2. 重排序:
    1. 编译器优化的重排序: 编译器在不改变单线程程序语义的前提下,可以重排序。
    2. 指令级并行的重排序:若不存在数据之间的依赖性,处理器可以改变语句对应的机器指令的执行顺序。
    3. 内存系统的重排序:由于处理器使用缓存和读写缓存区,使得加载和存储操作看起来是在乱序执行。
    4. 从java源码到实际执行的指令序列:按照1->2->3进行重排序。
  3. happens-before原则:
    1. 定义:在JMM中若一个执行操作的结果对于另一个操作可见,两个操作之间比逊存在happens-before原则。两个操作可以在同一个线程,也可以在不同的线程中。
    2. 原则:
      1. 监视器锁规则:对于一个锁的解锁,必须位于对于这个锁加锁之后。
      2. volatile规则:对于一个volatile域的写,然后才能对这个volatile域的读取。
      3. 传递性:A 优先于 B, B 优先于C, 那么A必须优先于C;
2. 重排序
  1. 重排序是为了优化程序性能而使用的一种手段。
  2. 编译器和处理器在进行重排序时,会遵守数据依赖性,不会对于存在数据依赖关系的两个操作进行重排序。但是,只是针对单线程中,多线程或是不同处理器之间的数据依赖性,则不遵循。
  3. as-if-serial语义:不管如何进行重排序,不能改变最终结果。

4. JAVA并发编程基础

1. Synchronized底层指令
  1. 对于同步块而言moniorentermonitorexit指令;而对于方法而言使用的是ACC_SYNCHRONIZED来完成。但是不管使用的是什么指令,目的都是为了获取到对象的监视器,这个获取过程是排他的;没有获取到锁的所有线程都处于Blocked状态;而不是Wating状态。调用了wait方法,才会使线程处于Waiting状态。
2. Thread.join
  1. join若当前线程存活则一直wait;当线程结束时,会调用自身的notifyAll,会通知所有等待在该线程对象上的线程。
  2. 通过join,把所有的线程,变为一个队列。
private static void threadJoin() throws InterruptedException {
    Thread pre = Thread.currentThread();

    for (int i = 0; i < 10; i++) {        /**
         * {@link Thread#join()}: 当join的thread停止后, 程序便会接着执行.
         * 此段代码: 
         *  1. 通过主线程的join, 不断阻塞后线程的执行; 因为设置的join线程都还在运行.
         *  2. 主线程join第一个线程, 第一个线程join第二个线程, 依次类推, 可以想象所有的线程变成了一个队列, 只能依次出队.
         *  2. 然后, 主线程的代码运行完毕, 主线程停止, 第一个被主线程join的线程接着运行. 依次类推.
         */
        Thread current = new Thread(new Domino(pre, i));
        current.start();
        pre = current;
    }
    
    TimeUnit.SECONDS.sleep(5);
    System.out.println(Thread.currentThread().getName() + " is terminate!");
}

/**
  * 使用final去修饰属性, 使得只能通过构造函数传入参数; 保证了数据的安全性; 因此, 此类也是一个线程安全的类。
  */
static class Domino implements Runnable {

    private final Thread thread;

    private final Integer no;

    public Domino(Thread thread, Integer no) {        
    	this.thread = thread;
        this.no = no;
    }
    
    @Override
    public void run() {        
    	try {
            thread.join();
        } catch (InterruptedException e) {            
        	e.printStackTrace();
        }
        
        System.out.println(Thread.currentThread().getName() + " is terminate!");
    }
 }

5. JAVA中的锁

1. AbstractQueuedSynchronized(AQS, 同步器)
  1. 定义:用来构建锁或者其它同步组件的基础框架,它使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源的获取线程的排队工作。
  2. 使用:主要通过继承来实现AQS的抽象方法,保证同步状态;修改状态主要使用的是AQS的三个方法getState()setState(int newState)compareAndSet(int expect, int update)来修改状态,因为这三个操作时线程安全的;
  3. 锁和AQS的区别:锁是面向开发者,隐藏了实现细节;AQS是锁的实现者,屏蔽了同步状态的管理、线程的排队、等待与唤醒等底层操作。
  4. AQS提供的模板方法分为:独占式获取和释放同步状态(ReentrantLock)、共享式获取和释放(ReadWriteLock)、查询同步队列中的等待线程情况。
2. LockSupport
  1. 定义了一组以park开头的方法用来阻塞当前线程,以及unpark方法唤醒一个被阻塞的线程。
3. Condition
  1. 类似Object的waitnotify/notifyAll,必须放在synchronized中;使用Lock对象去获取关联的Condition对象;Condition使用是awaitsignal/signalAll,必须在Lock中;
  2. 实现分析:
    1. 等待队列:是一个FIFO的队列;里面存放着等待的线程引用。线程调用await,则会释放锁,然后,加入到队列中,等待被唤醒。Condition更新队列是没有使用CAS,是因为,调用await方法的线程,必然是获取了锁的线程,因此,直接使用锁来保证队列数据的安全性。
    2. 通知:当某个线程调用signal的时候,会把等待时间最久的线程唤醒,因为等待队列使用的就是FIFO的队列结构。

6. JAVA并发容器和框架

1. ConcurrentHashMap
  1. ConcurrentHashMap出现的原因:
    1. 在并发中使用HashMap会导致死循环: 在并发执行put操作时,会引起死循环。因为多线程会导致HashMap的Entry链表形成环形数据结构,导致Entry的next节点永远不为空,就会产生死循环获取Entry。
    2. 使用线程安全的HashTable则性能低下:所有的方法都使用synchronized加锁,不管读取数据还是修改数据,都会加锁。
    3. ConcurrentHashMap的锁分段技术有效的提升并发访问率;因为数据使用了多个锁去保证数据的安全性,因此,减小了锁竞争。
  2. ConcurrentHashMap结构
    1. 由Segment数据结构和HashEntry数组组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap中是作为锁的角色;HashEntry则用于存储键值对数据。一个Segment里包含着一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数据的元素,当修改HashEntry的数据时,必须获取与它对应的Segment锁
    2. Segment数组的长度:为了能够通过按位与的散列算法来定位Segment数组的索引,必须保证Segment数组的长度是2的N次方,所以,必须计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为Segment的数组长度。
    3. 定位Segment:ConcurrentHashMap不管是在获取或是存储数据的时候,都会对元素的HashCode进行再次散列(Wang/Jenkins hash);之所以再散列,就是为了减少散列冲突。
  3. ConcurrentHashMap操作
    1. get:整个get操作是不加锁的,除非读到的值是空才会加锁重读。最根本的原因是将要使用的共享变量都定义成volatile的类型,因此,能够保证读取到的数据,总是最新被修改之后的数据;而且,只是读取操作,不需要对原数据进行操作。
    2. put:对共享数据加锁;put首先定位到Segment,然后在Segment里进行插入操作。可能会扩容。
      1. 判断是否需要扩容:插入之前,判断当前Segment中的HashEntry数组是否超过容量,若超过了阀值,则扩容。
      2. 扩容:
        1. 创建一个的数组,把原数组的元素进行再散列,插入到新的数组。
        2. 只对当前的这个Segment进行扩容,而不是整个容器进行扩容。
    3. size:统计ConcurrentHashMap的所有元素大小,就必须对所有Segment元素大小进行求和。为了保证多线程下获取到的size是安全的,把put、remove和clean方法锁住。这样会造成性能的低效。因此,ConcurrentHashMap采用的是:先尝试2次不锁住Segment统计大小,若是两次的大小不一致,则锁住Segment得到size。而ConcurrentHashMap使用modCount来判断容器是否发生改变,每进行一次put/remove/cleanmodCount就会加1。
2. ConcurrentLinkedQueue
  1. 实现线程安全的队列,可以采用阻塞方式的一个锁(出队和入队都采用一个锁)或两个锁(出队和入队采用不同的锁);或者使用不阻塞的CAS算法。而ConcurrentLinkedQueue就是不阻塞的安全队列。
  2. 基础知识
    1. 是基于链接节点的无界线程安全队列,FIFO对节点排序。
    2. 入队操作:1.定位出尾节点。2.使用CAS算法将入队节点设置成尾节点的next节点,若不成功则重试。
  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值