java并发编程基础()

一、基础概念:
○同步和异步:
同步,异步通常是用来形容一次方法的调用。同步方法一旦开始,调用者必须等到方法调用返回侯,才能继续后续的行为。异步方法更像一个消息传递。

○并发和并行:
严格意义讲:并行的多个任务是真实的同时进行;并发是,这个过程交替进行,对外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间是并行执行的错觉。
○阻塞和非阻塞:
用来形容多线程间的相互影响。

○Java的内存模型(JMM):围绕着多个线程的原子性、可见性、有序性来建立的。

○进程和线程的区别:

○一个线程的生命周期:new runnable、waiting。。。

○线程的基本操作:

○Start()与run()的区别:
Start()方法回新建一个线程并让这个线程执行run()方法;
如果只调用Run()方法,只会在当前线程中,串行执行run()中的代码;

○中止线程stop()与线程中断的区别:
一般来说,线程在执行完毕之后就会接受,无须手工关闭但是对于一些服务端的后台常驻线程,需要提供而外的方法;
Stop()方法回直接马上终止线程,不推荐使用
Interrupt(),通知线程准备中断,不会立马退出。

○Wait()与notify()必须包含在对应的synchronizid中;

○等待线程结束(join)和谦让(yield):
比如一个线程数数1000,如果在主线程中调用这个线程,并且打印这个数,如果没用join,则这个数不一定会是1000,如果主线程中调用了join,则一定会等到1000之后再执行后面的打印代码。
Yield()是一个静态代码,一旦执行,它会使当前线程让出cpu,重新进行cpu资源的争夺。

○Synchronized的三种使用方法:
指定加锁对象:
直接作用的于实例方法:相当于对当前实例加锁,进入同步代码前:需要获得当前的实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁

○可重入锁:ReentrantLock:
可重入的定义是:所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
ReentrantLock可设置公平锁,重入锁的condition条件:await(),signal(),signalall()方法;

Readwritelock读写锁, 读读不互斥,读写互斥,写写互斥;

○倒计时器 :CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数的值就会减1。当计数器值到达0时,它所表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
有一个countDown()方法,就是在其他线程的减一操作。

✦线程池的问题:
* 一、线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。

二、线程池的体系结构:
java.util.concurrent.Executor : 负责线程的使用与调度的根接口
|–ExecutorService 子接口: 线程池的主要接口
|–AbstractExecutoServicer 抽象类
|–ThreadPoolExecutor 线程池的实现类
|–ScheduledExecutorService 子接口:负责线程的调度
|–ScheduledThreadPoolExecutor :继承 ThreadPoolExecutor, 实现 ScheduledExecutorService

三、工具类 : Executors
ExecutorService newFixedThreadPool() : 创建固定大小的线程池
ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
ExecutorService newSingleThreadExecutor() : 创建单个线程池。线程池中只有一个线程

ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务。

工具类Executors,其实内部都使用了TreadPoolExecutor实现,所以需要了解下TreadPoolExecutor:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);

corePoolSize:核心池的大小
maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize
unit:参数keepAliveTime的时间单位,有7种取值。

当有任务提交到线程池之后的一些操作:
a、 若当前线程池中的线程数小于corepoolsize,则每来一个任务就创建一个线程去执行;
b、 若当前线程池中线程数>=corepoolsize,会尝试将任务添加到任务缓冲队列中去,若添加成功,则任务会等待空闲进程将其取出执行,若添加失败,则尝试创建线程去执行这个任务;
c、 补哦当前线程池中线程数>=Maximumpoolize,则采取拒绝策略:
1、 abortpolicy丢弃任务,抛出RejectedExecutionException
2、 discardpolicy拒绝执行,不抛异常
3、 discardoldestpolicy丢弃任务缓冲队列中最老的任务,并且尝试重新提交新的任务
4、 callerrunspolicy有反馈机制,使任务提交的速度变慢。

✦LinkedList是线程不安全的高效读写的队列:ConcurrentLinkedQueue是线程安全的,concurrentLinkedQueue是高并发环境中性能最好的队列;

✦CopyOnWriteArrayList类:读取完全不用加锁,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。所谓CopyOnWrite就是写入操作时,进行一次自我复制,即当这个List需要修改时,我并不修改原有的内容,而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后,再将修改玩的副本替换原来的数据。

✦数据共享通道:BlockingQueue,这个是一个接口;实现类有:ArrayBlockingQueue是基于数组实现的,适合做有界队列,LinkedBlocking适合做无界队列。以arrayBlockingQueue为例,关注put()和take():put()方法也是将元素压入队列末尾,但如果队列满了,它会一直等待,直到队列中有空闲的位置。Take()从队列中取元素,但如果队列空的,它会一直等待。底层实现原理主要使用了Reentrantlock中的 condition的await()和signal()方法

✦锁的优化:
减小锁的持有时间;
减小锁粒度:比如ConcurrentHashMap。
锁分离:比如arrayblockingQueue把take()和put()分开了,因为是对队列的前端和尾端作用,并不冲突。
锁粗化:当在循环内请求锁的时候,可以把锁放到循环之外。
HashMap 的实现原理
并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。

Entry 是一个 static class,其中包含了 key 和 value,也就是键值对,另外还包含了一个 next 的 Entry 指针。我们可以总结出:Entry 就是数组中的元素,每个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。(entry其实是一个单向链表)

当我们 put 的时候,如果 key 存在了,那么新的 value 会代替旧的 value,并且如果 key 存在的情况下,该方法返回的是旧的 value,如果 key 不存在,那么返回 null。

当我们往 HashMap 中 put 元素的时候,先根据 key 的 hashCode 重新计算 hash 值,根据 hash 值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

为什么hashmap的大小是2的N次方?
参考:https://www.cnblogs.com/chenssy/p/3521565.html
因为hashmap计算插入位置的时候hash使用的的是h&(length-1),位与运算比%运算在计算机中快,而用length-1,如果length为15,则length-1为14,即为1110,与运算的时候,不管最后以为是1还是0,与之后结果都是0,这样就增大了,hash值碰撞的概率,如果是length为16,则length-1为15,二进制为1111,则与运算就是被与的数字的本身,不会增加碰撞的概率。
当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

Haspmap的扩容:那么 HashMap 什么时候进行扩容呢?当 HashMap 中的元素个数超过数组大小 *loadFactor时,就会进行数组扩容,loadFactor的默认值为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16*0.75=12 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作

Fail-fast机制:
我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。

当某一个线程 A 通过 iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对 HashMap 内容(当然不仅仅是 HashMap 才会有,其他例如 ArrayList 也会)的修改都将增加这个值(大家可以再回头看一下其源码,在很多操作中都有 modCount++ 这句),那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:
注意到 modCount 声明为 volatile,保证线程之间修改的可见性。
fail-fast 机制,是一种错误检测机制。它只能被用来检测错误
hashset的底层实现是用hashmap实现的
HashSet中add方法调用的是底层HashMap中的put()方法,而如果是在HashMap中调用put,首先会判断key是否存在,如果key存在则修改value值,如果key不存在这插入这个key-value。而在set中,因为value值没有用,也就不存在修改value值的说法,因此往HashSet中添加元素,首先判断元素(也就是key)是否存在,如果不存在这插入,如果存在着不插入,这样HashSet中就不存在重复值。

Hashtable与hashmap的简单区别:
1. HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。
2. Hashtable 方法是同步,而HashMap则不是。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。
使用put()方法将元素放入map中,使用add()方法将元素放入set中
3.
LinkedHashMap
其实 LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。
ConcurrentHashMap
ConcurrentHashMap 概述
虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。

所以通俗的讲,ConcurrentHashMap 数据结构为一个 Segment 数组,Segment 的数据结构为 HashEntry 的数组,而 HashEntry 存的是我们的键值对,可以构成链表。

ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作

其实是双hash过程,第一个hash,找到对应的segment,每个segment中包含一个table数组,这个table数组的元素是hashEntry,这个hashentry是个四元组,里面包括key,value,…;
table是一个典型的链表数组,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的。

HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点。
由于value域被volatile修饰,所以其可以确保被读线程读到最新的值,这是ConcurrentHashmap读操作并不需要加锁的另一个重要原因。HashEntry代表hash链中的一个节点。
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
  该构造函数意在构造一个具有指定容量、指定负载因子和指定段数目/并发级别(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap
ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。此外,我们还可以看到,实际上我们对ConcurrentHashMap的put操作被ConcurrentHashMap委托给特定的段来实现。也就是说,当我们向ConcurrentHashMap中put一个Key/Value对时,首先会获得Key的哈希值并对其再次哈希,然后根据最终的hash值定位到这条记录所应该插入的段,
ConcurrentHashMap对Segment的put操作是加锁完成的。在第二节我们已经知道,Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。
上面叙述到,在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作。特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。

总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:
• 用HashEntery对象的不变性来降低读操作对加锁的需求;
• 用Volatile变量协调读写线程间的内存可见性;
• 若读时发生指令重排序现象,则加锁重读;
在ConcurrentHashMap中,有些操作需要涉及到多个段,比如说size操作size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上,在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
  那么,ConcurrentHashMap是如何判断在统计的时候容器的段发生了结构性更新了呢?我们在前文中已经知道,Segment包含一个modCount成员变量,在会引起段发生结构性改变的所有操作(put操作、 remove操作和clean操作)里,都会将变量modCount进行加1,因此,JDK只需要在统计size前后比较modCount是否发生变化就可以得知容器的大小是否发生变化。

猜你喜欢

转载自blog.csdn.net/happy_bigqiang/article/details/80002786