多线程高并发

多线程高并发

线程锁

Lock synchronized

  • synchronized 是 java 内置的语言,是 java 的关键字

  • synchronized 不需要手动去释放锁,当 synchronized 方法或者 synchronized 代码块执行完毕。系统会自动释放对该锁的占用。

    而 lock 必须手动的释放锁,如果没有主动的释放锁,则可能造成死锁的问题

  • ReentrantLock 可重入锁 两个构造函数;公平锁和非公平锁

  • ReentrantReadWriteLock 读写锁

    • 当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,但却可以读取数据 ( 可并发读)
    ReadWriteLock lock = w new ReentrantReadWriteLock();
    lock.writeLock().unlock();
    //在 unlock()和 lock 这段时间,可能其他线程会获取锁,从而中断读取操作,甚至导致读取的数据是脏数据
    lock.readLock().lock();
    
    ReentrantReadWriteLock 有一个锁降级和锁升级的概念:
    锁降级:写锁在没得到释放的情况下可以降级到读锁
    readWriteLock.writeLock().lock();
    readWriteLock.readLock().lock();
    readWriteLock.writeLock().unlock();
    锁升级:读锁在没有得到释放的情况下是可以升级到写锁的
    ReentrantReadWriteLock 只支持锁降级
    

线程池

Executors

  • SingleThreadExecutor 只有一个线程的线程池
  • newCachedThreadPool 可根据需要创建新线程的线程池
    • 线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过 60 秒内没执行, 那么将被终止并从池中删除
    • 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。但是当无可回收的线程时,就会创建新的线程。如果并发量特别搞,就会造成 OOM
  • newFixedThreadPool 固定的线程池
  • Scheduled Thread l Pool 可调度的线程池 创建一个定长线程池,支持定时及周期性任务执行
    • scheduleAtFixedRate
      • scheduledThreadPool.schedule(new Runnable() {}, 3,TimeUnit.SECONDS);
      • scheduledThreadPool.scheduleAtFixedRate(new Runnable(),1,3,TimeUnit.SECONDS)
      • 1 延迟执行 3 执行周期
      • 如果run方法内运行超时, 就按超时时间运行
    • scheduleWithFixedDelay
      • scheduledThreadPool.scheduleWithFixedDelay(new Runnable(),1,3,TimeUnit.SECONDS)
      • 如果run方法内运行超时, 依然按照规定时间运行
  • Single Thread Scheduled Pool 可调度的单线程的线程池

并发包消息队列

在 Java 并发包中提供了两种类型的队列,非阻塞队列与阻塞队列,当然它们都是线程安全的,无需担心在多线程并发环境所带来的不可预知的问题。为什么会有非阻塞和阻塞之分呢?

这里的非阻塞与阻塞在于有界与否,也就是在初始化时有没有给它一个默认的容量大小,对于阻塞有界队列来讲,如果队列满了的话,则任何线程都会阻塞不能进行入队操作,反之队列为空的话,则任何线程都不能进行出队操作。而对于非阻塞无界队列来讲则不会出现队列满或者队列空的情况。它们俩都保证线程的安全性,即不能有一个以上的线程同时对队列进行入队或者出队操作

非阻塞队列:ConcurrentLinkedQueue
阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、……

市面很多消息队列的原型都是借鉴 JDK1.5 以后的并发包,最核心的是 BlockingQueue;
BlockingQueue 是 java.util.concurrent 下的主要用来控制线程同步的工具

插入:
1)add(anObject):把 anObject 加到 BlockingQueue 里,即如果 BlockingQueue 可以容纳,则返回 true,否则抛出异常
2)offer(anObject):表示如果可能的话,将anObject加到 BlockingQueue里,即如果 BlockingQueue可以容纳,则返回 true,否则返回 false.
3)put(anObject):把 anObject 加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻塞直到BlockingQueue 里

读取:
4)poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null
5)take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 Blocking 有新的对象被加入为止
  • ArrayBlockingQueue
    • 基于数组的阻塞队列,ArrayBlockingQueue 里面维护了一个定长数组,用来缓存对象
    • ArrayBlockingQueue 的生产者和消费者使用的都是同一把锁
    • 生产者把数据插入的队列的末尾,消费者总是从队列的头部拿数据,当队列满了之后,再往队列中插入数据会造成队列的阻塞,同样,从空的队列中取数据,也是阻塞的
    • 如果希望,队列中的数据总是按照生产者生产数据的顺序,取队列中的数据,那么我们可以在构造函数中把 fair 参数设置为 true就可以了, 但是这种操作会降低吞吐量
  • LinkedBlockingQueue
    • 基于链表的阻塞队列,其实内部也是要维护一个数据的缓冲队列,只不过这个队列是由链表组成
    • 在并发上更加高效,因为 LinkedBlockingqueue 对生产端和消费端设计的是不同的锁(即读写锁是相互独立),这样在高并发场景下,linkedBlockingQueue 优于 ArrayBlockingQueue

    ArrayBlockingQueue 是有界的,必须指定队列的大小;
    LinkedBlockingQueue 是无界的,可以不指定队列的大小,但是默认是 Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的。

    ArrayBlockingQueue 中的锁是没有分离的,即生产和消费用的是同一个锁;
    LinkedBlockingQueue 中的锁是分离的,即生产用的是 putLock,消费是 takeLock

  • DelayQueue
    • DelayQueue 是一个无界阻塞队列,只有在延迟期满时才能从中提取元素
    • 客户端长时间占用连接的问题,超过这个空闲时间了,可以移除的
      处理长时间不用的缓存;如果队列里面的对象长时间不用,超过了空闲时间,就移除
      任务超时处理
    • DelayQueue 是一个使用优先队列(PriorityQueue)实现的 BlockingQueue,优先队列的比较基准值是时间
      所谓的“比较基准是时间” 是用到了对象继承 Delayed 后会实现 compareTo 和 getDelay 方法;
      通过实现这两个函数来实现比较
  • PriorityBlockingQueue

    • 基于优先级的阻塞队列(优先级的判断通过构造函数传入的对象,这个对象里面实现的 compareTo 方法决定的)和 ArrayBlockingQueue 非常相似,生产和消费使用的是通一把锁,不同的是,不同的是,不管有无指定长度,都会实现可以实现自动扩容
  • SynchronousQueue
  • concurrentLinkedQueue

并发类容器

  • java.util.concurrent 提供了多种并发容器

    • 队列 Queue 类型的 BlockingQueue 和 ConcurrentLinkedQueue
    • Map 类型的 ConcurrentMap
    • Set 类型的 ConcurrentSkipListSet 和 CopyOnWriteArraySet
    • List 类型的 CopyOnWriteArrayList
  • CopyOnWrite容器

    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
    CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
    一般情况下,CopyOnWriteArrayList 的使用场景是:在并发过程中,读为主,并且不想发生并发过程中线程冲突时候可以采取的
    CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器
    
  • ConcurrentMap容器

    • ConcurreentHashMap
    • ConcurrentSkipListMap
    HashMap线程不安全
    因为多线程环境下,使用 HashMap 进行 put 操作会引起死循环,导致 CPU 利用率接近 100%,所以在并发情况下不能使用 HashMap
    HashTable线程安全
    HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下。
    
    ConcurrentHashMap有16个segment段
    ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类HashTable的结构,Segment 内部维护了一个链表数组
    写操作时只锁一个segment 其他segment不受影响
    
    concurrentSkipListMap 是 JDK1.6 出现的一种数据结构,叫做跳表;可以做到快速的检索数据;通常一些缓存框架中是能够看到 concurrentSkipListMap 的身影,用来存储缓存数据,保证缓存数据能够被快速的定位
    concurrentSkipListMap 与 TreeMap 一样,实现了对有序队列的快速查找,但同时,它还是多线程安全的
    

CPU cache

随着 CPU 的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥 CPU 的计算资源,提高CPU 整体吞吐量,在 CPU 与内存之间引入了一级 Cache。随着热点数据体积越来越大,一级 Cache L1 已经不满足发展的要求,引入了二级 Cache L2,三级 Cache L3

级别越小越接近 CPU,所以速度也更快,同时也代表着容量越小。

缓存行
  • 缓存,是由缓存行组成的。一般一行缓存行有 64 字节。所以使用缓存时,并不是一个一个字节使用,而是一行缓存行、一行缓存行这样使用;换句话说,CPU 存取缓存都是按照一行,为最小单位操作的。

    • 64 位系统,Java 数组对象头固定占 16 字节,而 long 类型占 8 个字节。所以 16+8*6=64 字节,刚好等于一条缓存行的长度(一个缓存行可以装填 6 个 long 类型的数据 )
  • 伪共享

    • 伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
    • 在 Java 类中,最优化的设计是考虑清楚哪些变量是不变的,哪些是经常变化的,哪些变化是完全相互独立的,哪些属性一起变 一起变的在声明时放到同一缓存行
    public class DataPadding{
    	long a1,a2,a3,a4,a5,a6,a7,a8; //防止与前一个对象产生伪共享
    	int value;
    	long modifyTime;
    	long b1,b2,b3,b4,b5,b6,b7,b8; //防止不相关变量伪共享;
    	boolean flag;
    	long c1,c2,c3,c4,c5,c6,c7,c8; //
    	long createTime;
    	char key;
    	long d1,d2,d3,d4,d5,d6,d7,d8; //防止与下一个对象产生伪共享
    }
    
    // 类前加上代表整个类的每个变量都会在单独的 cache line 中
    @sun.misc.Contended
    @SuppressWarnings("restriction")
    	public class ContendedData {
    	int value;
    	long modifyTime;
    	boolean flag;
    	long createTime;
    	char key;
    }
    //或者这种:
    // 属性前加上时需要加上组标签
    @SuppressWarnings("restriction")
    public class ContendedGroupData {
    	@sun.misc.Contended("group1")
    	int value;
    	@sun.misc.Contended("group1")
    	long modifyTime;
    	@sun.misc.Contended("group2")
    	boolean flag;
    	@sun.misc.Contended("group3")
    	long createTime;
    	@sun.misc.Contended("group3")
    	char key;
    }
    

你可能感兴趣的:(Java)