一、java基础
1、集合原理
【1】HashMap
hashmap是一种key value类型的数据结构,在jdk1.8以前采用的是数组+链表的方式实现的,在1.8之后采用的是数组+链表+红黑树的方式实现的,在hashmap创建的时候会实例化一个初始容量为16的entry数组,当往map里面put的时候首先会根据key的hashcode计算出index即key在entry数组中的位置存放在哪个entry链表上,如果entry没有存放任何节点则直接插入,否则遍历整个链表,如果发现这个key已经存在则直接覆盖掉原来的值,否则插入到链表的末尾。
当map中元素的个数超过阈值=16(初始容量)*0.75(负载因子)=12的时候会进行扩容,扩容到原来的2倍,并且重新计算元素在map中的位置。
如果不同的key计算出来的index(对16取模)相同就会发生哈希碰撞,存放在同一个hash桶上形成链表,当链表的长度超过8的时候链表会进化成一颗红黑树,当长度小于6时又会重新退化成链表。
1、为什么用红黑树不用avl树,为什么链表要进化成树?
链表查询的时间复杂度为o(n),红黑树查询的时间复杂度为o(logn), 红黑树效率更高,有时候只需要变色就能达到重新平衡,不需要频繁的左旋和右旋。
2、为甚么扩容扩容成原来的2倍?
扩容的倍数太大会造成资源的浪费。
3、entry的数据结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
}
4、 put方法的过程,如果key相同会发生什么,key的hashcode一样会发生什么?
jdk7
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
hashcode 一样只是说明他们放在同一个hash 桶中,但是如果值不一样是不会覆盖的
以string作为key看下
string的equals方法如下,它是比较两个字符串的各个字符是否都一样
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
string的hashcode方法
public int hashCode() {
int h = hash;// hash初始为0
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
普通的对象都是继承Object的,Object的hashcode是native方法,equals方法比较的是两个对象的内存地址是否一样,即使hashcode一样,也不一定两个对象相等。
5、头插法1.7中扩容时存在的死循环问题?
有两个线程A,B都进行put操作,线程A先扩容,执行到代码Entry<K,V> next = e.next;执行完这段代码,线程A挂起;然后线程B开始执行transfer函数中的while循环,会把原来的table变成一个table(线程B自己的栈中),再写入到内存中。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// 这个里面出现死循环
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
// 这里形成死循环
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
【2】ConcurrentHashMap
ConcurrentHashMap 主要用于解决HashMap多线程环境下的安全问题而诞生的,它基于分段锁Segment实现,Segment继承ReentrantLock,Segment相当于一个小的HashMap,只是要操作Segment之前先要获取Segment的锁,得到锁之后才能进行写操作。
ConcurrentHashMap的segment段数默认是16,如果没有特别通过concurrentLevel参数指定的话;创建ConcurrentHashMap的时候如果指定的了mapsize,那么这个mapsize是所有segment中entry个数的总和。同时也是每个segment中hashEntry数组的长度。
与此同时map扩容也是针对不同的segment中的hashEntry数组进行扩容,操作和hashmap一样。segment是不能扩容的
和HashMap不一样的地方体现在如下
1、多线程环境下segment 创建的时候安全问题?
多线程环境下,要往ConcurrentHashMap中put元素的时候,首先需要计算key应该分配到哪个segment中,如果该segment不存在那么需要创建一个新的segment对象,为了避免segment创建的时候出现多线程同时创建的问题,采用cas+自旋的方式来获取当前操作应该选取的segment。
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
// 先创建segment但是最后使用的不一定是创建的这个
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
// cas更新
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
2、segment生成完后往segment的数组中插入时的安全问题,segment获取和释放锁的过程?
tryLock方法不会阻塞当前线程,如果获取到锁则直接获取entry节点并插入,如果获取不到锁,则执行scanAndLockForPut方法,这个方法中会先完成一线put操作前的准备工作,比如判断entry节点是否存在,当前key是否存在,如果entry不存在则先创建好entry节点。如果准备工作做完后重试了若干次后还没有获取到锁则直接调用lock方法阻塞当前线程等待锁。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 非阻塞tryLock
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut 方法
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 尝试获取锁,没有获取到锁则执行准备操作
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
// 重试若干次后还是没有拿到锁直接通过lock方法阻塞并放回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
【3】HashSet
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。 整个Set家族都是通过相应的Map实现,LinkedHashSet通过LinkedHashMap实现,TreeSet通过TreeMap实现。
key是对应的值,value是一个虚拟的空对象。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
// 底层使用HashMap来保存HashSet中所有元素。
private transient HashMap<E,Object> map;
// 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。
private static final Object PRESENT = new Object();
/**
* 默认的无参构造器,构造一个空的HashSet。
*
* 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
*/
public HashSet() {
map = new HashMap<E,Object>();
}
/**
* 构造一个包含指定collection中的元素的新set。
*
* 实际底层使用默认的加载因子0.75和足以包含指定
* collection中所有元素的初始容量来创建一个HashMap。
* @param c 其中的元素将存放在此set中的collection。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
* 以指定的initialCapacity和loadFactor构造一个空的HashSet。
*
* 实际底层以相应的参数构造一个空的HashMap。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}
/**
* 以指定的initialCapacity构造一个空的HashSet。
*
* 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。
* @param initialCapacity 初始容量。
*/
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}
/**
* 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
* 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
*
* 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
* @param initialCapacity 初始容量。
* @param loadFactor 加载因子。
* @param dummy 标记。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
/**
* 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。
*
* 底层实际调用底层HashMap的keySet来返回所有的key。
* 可见HashSet中的元素,只是存放在了底层HashMap的key上,
* value使用一个static final的Object对象标识。
* @return 对此set中元素进行迭代的Iterator。
*/
public Iterator<E> iterator() {
return map.keySet().iterator();
}
/**
* 返回此set中的元素的数量(set的容量)。
*
* 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。
* @return 此set中的元素的数量(set的容量)。
*/
public int size() {
return map.size();
}
/**
* 如果此set不包含任何元素,则返回true。
*
* 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。
* @return 如果此set不包含任何元素,则返回true。
*/
public boolean isEmpty() {
return map.isEmpty();
}
/**
* 如果此set包含指定元素,则返回true。
* 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e))
* 的e元素时,返回true。
*
* 底层实际调用HashMap的containsKey判断是否包含指定key。
* @param o 在此set中的存在已得到测试的元素。
* @return 如果此set包含指定元素,则返回true。
*/
public boolean contains(Object o) {
return map.containsKey(o);
}
/**
* 如果此set中尚未包含指定元素,则添加指定元素。
* 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2))
* 的元素e2,则向此set 添加指定的元素e。
* 如果此set已包含该元素,则该调用不更改set并返回false。
*
* 底层实际将将该元素作为key放入HashMap。
* 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key
* 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),
* 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变,
* 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,
* 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
* @param e 将添加到此set中的元素。
* @return 如果此set尚未包含指定元素,则返回true。
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* 如果指定元素存在于此set中,则将其移除。
* 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e,
* 则将其移除。如果此set已包含该元素,则返回true
* (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。
*
* 底层实际调用HashMap的remove方法删除指定Entry。
* @param o 如果存在于此set中则需要将其移除的对象。
* @return 如果set包含指定元素,则返回true。
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* 从此set中移除所有元素。此调用返回后,该set将为空。
*
* 底层实际调用HashMap的clear方法清空Entry中所有元素。
*/
public void clear() {
map.clear();
}
/**
* 返回此HashSet实例的浅表副本:并没有复制这些元素本身。
*
* 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。
*/
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
}
【4】TreeSet
TreeSet实现了SortedSet接口,它是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。
//无参构造方法,初始化一个TreeMap对象
public TreeSet() {
this(new TreeMap<E,Object>());
}
【5】ArrayList
ArrayList 通过数组实现,一旦我们实例化 ArrayList 无参数构造函数默认为数组初始化长度为 10
add 方法底层实现如果增加的元素个数超过了 10 个,那么 ArrayList 底层会新生成一个数组,长度为原数组的 1.5 倍,然后将原数组的内容复制到新数组当中,并且后续增加的内容都会放到新数组当中。当新数组无法容纳增加的元素时,重复该过程。是一旦数组超出长度,就开始扩容数组。
扩容数组调用的方法
Arrays.copyOf(objArr, objArr.length + 1);
【6】LinkedList
LinkedList 底层的数据结构是基于双向循环链表的,且头结点中不存放数据
2、异常分类处理
【1】checked异常
受检异常是指编译器要求必须处置的异常,即程序在运行时由于外界因素造成的一般性异常,具体如下:
● java.lang.ClassNotFoundExeption:没有找到具有指定名称的类异常。
● java.lang.FileNotFoundException:访问不存在的文件异常。
● java.lang.IO Exception:操作文件时发生的异常。
● java.sql.SQL Exception:操作数据库时发生的异常。
Java编译器要求Java程序必须捕获或声明所有受检异常。如FileNotFoundException、IO Exception等。因为,对于这类异常来说,如果程序不进行处理,可能会带来意想不到的结果。而非受检异常可以不做处理,因为这类异常很普遍,若全部处理可能会对程序的可读性和运行效率产生影响。
【2】unchecked异常
非受检异常是指编译器不要求强制处置的异常。一般是指因设计或实现方式不当而导致的问题。也可以说,是程序员的原因导致的,是本来可以避免发生的情况。
java.lang.RuntimeException类及其子类都是非受检异常。具体如下:
● java.lang.ClassCastException:错误的类型转换异常。
● java.lang.ArrayIndexOutOfBoundsException:组下标越界异常。
● java.lang.NullPointException:空指针访问异常。
● java.lang.ArithmeticException:除零溢出异常。
【3】异常捕获或者抛出
throws
如果一个方法可以引发异常,而它本身并不对该异常进行处理,那么该方法必须将这个异常抛给调用者可以使程序能够继续执行下去,这时候就要用到throws语句。
throw
throw语句通常用在方法体中,并且抛出一个异常对象。程序在执行到throw语句时立即停止,它后面的语句都不执行。
try catch
捕获异常,越小的异常越在前面捕获
3、反射机制
【1】反射的概念
反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行的时候才动态加载类,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,他允许运行中的Java程序获取类的信息,并且可以操作类或对象内部属性。程序中对象的类型一般都是在编译期就确定下来的,而当我们的程序在运行时,可能需要动态的加载一些类,这些类因为之前用不到,所以没有加载到jvm,这时,使用Java反射机制可以在运行期动态的创建对象并调用其属性,它是在运行时根据需要才加载。
编译时类型:申明对象时采用的类型
运行时类型:对象赋值时采用的类型。
例如:Person p = new Student();
Person是p的编译时类型,Student是p的运行时类型;运行时需要获取p的私有属性和方法就需要用反射机制获取。
【2】反射的优缺点
优点: 使用反射,我们就可以在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
缺点:打破封装的特性,造成不安全;会消耗一定的性能
4、注解
【1】定义
用来描述类、属性、方法等元素的元数据信息。
作用在其他注解的注解(或者说 元注解)是:
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation1 {
}
【2】注解的作用
1、编译时检查,如Override注解可以检查是否有实现某个方法,Deprecated注解用于检查方法过期
2、和反射机制结合,获取元素上的注解信息,如流控时通过自定义注解,配置相关的信息,然后通过反射获取到这些信息后做相应的处理
3、 通过给 Annotation 注解加上 @Documented 标签,能使该 Annotation 标签出现在 javadoc 中。
5、泛型
泛型的本质是参数化类型,传统的方法中我们如果需要创建一个方法需要指定方法参数和返回的类型,如果仅仅使用Object作为参数或者返回的化需要做类型转化不安全,泛型能够支持编译时检查类型,提高了代码的安全性和重用新。
二、java多线程
1、线程创建
【1】继承thread
单继承,通过start方法启动一个线程,通过run方法执行线程方法
【2】实现Runnable或者Callable
多实现,灵活性更高,callable是带回调函数Runnable,通过Future接口接受返回,Future的get方法会阻塞当前线程继续执行知道获得返回。
【3】线程生命周期
【4】线程基本方法
方法名 | 解释 |
---|---|
join | 加入到当前线程中,只有当线程执行完当前线程才能继续往下执行 |
yield | 让出cpu,使当前线程由执行状态,变成为就绪状态,让出cpu时间,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。 |
sleep | 暂停,主要是为了暂停当前线程,把cpu片段让出给其他线程,减缓当前线程的执行。 |
start | 启动一个线程 |
run | 执行方法 |
interrupt | 中断线程;对于大部分阻塞线程的方法,使用Thread.interrupt(),可以立刻退出等待,抛出InterruptedException. 这些方法包括Object.wait(), Thread.join(),Thread.sleep(),以及各种AQS衍生类:Lock.lockInterruptibly()等任何显示声明throws InterruptedException的方法。 |
private volatile Thread thread;
public void stop() {
thread.interrupt();
}
public void run() {
thread = Thread.currentThread();
while (flag) {
try {
Thread.sleep(interval);
} catch (InterruptedException e){
//current thread was interrupted
return;
}
}
}
2、线程池
【1】线程池核心参数
corePoolSize:核心线程数,当前没有任务也不会被回收
maxPoolSize:最大线程数,当阻塞队列满的时候新的任务会通过创建新的线程来执行
timeUnit:时间单位
keepAliveTime:非核心线程空闲时存活时间
rejectHandler:拒绝处理器
threadFactory: 新建线程的工厂
blockingQueue:阻塞队列,LinkedBlockingQueue、ArrayListBlockingQueue
ExecutorService 创建的几种类型
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
阻塞队列介绍
名称 | 解释 |
---|---|
ArrayBlockingQueue | 动态数组实现 |
LinkedBlockingQueue | 链表实现 |
SynchronousQueue | 同步队列器, 生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。 |
拒绝策略介绍
名称 | 解释 |
---|---|
AbortPolicy | 默认的实现,直接抛出一个RejectedExecutionException异常,让调用者自己处理 |
DiscardPolicy | 丢弃任务,什么也不做 |
DiscardOldestPolicy | 丢弃旧的任务 |
CallerRunsPolicy | 通过当前线程执行任务,即在主线程运行当前任务 |
【2】线程池创建线程的过程
【3】线程池怎么保证核心线程不被回收
核心worker线程会在while循环中不断从任务队列中获取任务,如果获取不到任务则阻塞,通过take()方法获取任务,take方法是阻塞的。
【4】线程池生命周期
RUNNING:可接收新任务,可执行等待队列里的任务
SHUTDOWN:不可接收新任务,可执行等待队列里的任务
STOP:不可接收新任务,不可执行等待队列里的任务,并且尝试终止所有在运行任务
TIDYING:所有任务已经终止,执行terminated()
TERMINATED:terminated()执行完成
三、线程安全
1、Synchronized
【1】实现原理
1、代码层级:synchronized(o)
2、字节码层级:monitorenter monitorexit
3、cpu汇编:lock comxchg 指令
4、执行过程中自动进行锁升级:无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁
【2】对象内存布局
markword、class指针、实际数据,如果是对象指针则为数据指针的大小,对齐
【3】锁升级过程
设置和撤销偏向锁
轻量锁膨胀过程
-
Object o = new Object() 锁 = 0 01 无锁态
-
o.hashCode() 001 + hashcode
00000001 10101101 00110100 0011011001011001 00000000 00000000 00000000 little endian big endian 00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000
-
默认synchronized(o) 00 -> 轻量级锁 默认情况 偏向锁有个时延,默认是4秒 why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。 -XX:BiasedLockingStartupDelay=0
-
如果设定上述参数 new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock 打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101
-
如果有线程上锁 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
-
如果有线程竞争 撤销偏向锁,升级轻量级锁 线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
-
如果竞争加剧 竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制 升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。
(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)
偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数
加锁,指的是锁定对象
锁升级的过程
JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低
现代版本进行了优化
无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
【4】问题
如果Synchronized加锁的线程一直不释放锁,那么等待锁的线程怎么办?ReentrantLock可以打断,Synchronized怎么处理?
synchronized不可以被打断
public class LockTest {
private static Integer lock = new Integer(1);
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
System.out.println("t1 get lock");
try {
// 让t2一直拿不到锁
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 release lock");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 让t1线程先拿到锁
Thread.sleep(1);
synchronized (lock) {
System.out.println("t2 get lock");
}
System.out.println("t2 release lock");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
try {
// 主线程睡1s,然后打断t2
Thread.sleep(1000);
t2.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2、ReentrantLock & AQS
【1】产生背景
不论是Synchronized还是reentrantlock都是为了防止多线程环境下不同线程同时操作相同的对象造成的安全问题,最早的Synchronized设计是想操作系统内核申请一把重量级锁来给需要操作的资源进行加锁,由于申请重量级锁需要切换到内核态去操作因此性能不高,后来dogele为了提高性能,开发了ReentrantLock类来提高锁的性能。
Java的内置锁一直都是备受争议的,在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),但是它却缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。
参考博客: https://blog.csdn.net/java_lyvee/article/details/98966684
ReentrantLock的lock方法实际是调用AQS(AbstractQueuedSynchronizer)的lock方法实现,AQS的lock方法实际是通过修改其内部的state变量实现。
private volatile int state; 然后通过cas修改state的状态实现加锁的功能。
【2】如何自己实现一个同步锁
1、cas + 自旋
volatile int status=0;//标识---是否有线程在同步块-----是否有线程上锁成功
void lock(){
while(!compareAndSet(0,1)){
}
//lock
}
void unlock(){
status=0;
}
boolean compareAndSet(int except,int newValue){
//cas操作,修改status成功则返回true
}
缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会白白的花费Ns的cpu资源
解决思路:让得不到锁的线程让出CPU
2、cas + 自旋 + yield
volatile int status=0;
void lock(){
while(!compareAndSet(0,1)){
yield();//自己实现
}
//lock
}
void unlock(){
status=0;
}
要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁的时候能把cpu资源给让出来,yield()方法就能让出cpu资源,当线程竞争锁失败时,会调用yield方法让出cpu。
自旋+yield的方式并没有完全解决问题,当系统只有两个线程竞争锁时,yield是有效的。需要注意的是该方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,想想会有什么问题?
3、cas + 自旋 + sleep
volatile int status=0;
void lock(){
while(!compareAndSet(0,1)){
sleep(10);
}
//lock
}
void unlock(){
status=0;
}
缺点:sleep的时间为什么是10?怎么控制呢?很多时候就算你是调用者本身其实你也不知道这个时间是多少
4、cas + 自旋 + park + queue (主要用来实现公平锁)
volatile int status=0;
Queue parkQueue;//集合 数组 list
void lock(){
while(!compareAndSet(0,1)){
//
park();
}
//lock 10分钟
。。。。。。
unlock()
}
void unlock(){
lock_notify();
}
void park(){
//将当期线程加入到等待队列
parkQueue.add(currentThread);
//将当期线程释放cpu 阻塞
releaseCpu();
}
void lock_notify(){
//得到要唤醒的线程头部线程
Thread t=parkQueue.header();
//唤醒等待线程
unpark(t);
}
这种方法就比较完美,当然我写的都伪代码,我看看大师是如何利用这种机制来实现同步的;JDK的JUC包下面ReentrantLock类的原理就是利用了这种机制;
【3】适用的场景
reentrantLock适合于线程交替执行操作对象的场景;此场景下不需要向操作系统申请锁操作。
如果是竞争执行的场景需要向操作系统申请锁操作。
【4】加锁执行过程
锁对象:其实就是ReentrantLock的实例对象,下文应用代码第一行中的lock对象就是所谓的锁
自由状态:自由状态表示锁对象没有被别的线程持有,计数器为0
计数器:再lock对象中有一个字段state用来记录上锁次数,比如lock对象是自由状态则state为0,如果大于零则表示被线程持有了,当然也有重入那么state则>1
waitStatus:仅仅是一个状态而已;ws是一个过渡状态,在不同方法里面判断ws的状态做不同的处理,所以ws=0有其存在的必要性
tail:队列的队尾 head:队列的对首 ts:第二个给lock加锁的线程 tf:第一个给lock加锁的线程 tc:当前给线程加锁的线程
tl:最后一个加锁的线程 tn:随便某个线程
当然这些线程有可能重复,比如第一次加锁的时候tf=tc=tl=tn
节点:就是上面的Node类的对象,里面封装了线程,所以某种意义上node就等于一个线程
公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改计数器看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队;永远排队记住这点 。
acquire方法方法源码分析
public final void acquire(int arg) {
//tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
//acquireQueued方法下文会有解释
//加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了;被打断下次分析
//为什么需要执行这个方法?下文解释
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法源码分析
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
int c = getState();
if (c == 0) {//没人占用锁--->我要去上锁----1、锁是自由状态
//hasQueuedPredecessors,判断自己是否需要排队这个方法比较复杂,
//下面我会单独介绍,如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
//继而返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程为拥有锁的线程,方面后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入)
setExclusiveOwnerThread(current);
return true;
}
}
//如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
//如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
//那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
hasQueuedPredecessors判断是否需要排队的源码分析
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/**
* 下面提到的所有不需要排队,并不是字面意义,我实在想不出什么词语来描述这个“不需要排队”;不需要排队有两种情况
* 一:队列没有初始化,不需要排队,不需要排队,不需要排队;直接去加锁,但是可能会失败;为什么会失败呢?
* 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;有一个必然失败
* 比如t1先拿到锁,那么另外一个t2则会CAS失败,这个时候t2就会去初始化队列,并排队
*
* 二:队列被初始化了,但是tc过来加锁,发觉队列当中第一个排队的就是自己;比如重入;
* 那么什么叫做第一个排队的呢?下面解释了,很重要往下看;
* 这个时候他也不需要排队,不需要排队,不需要排队;为什么不需要排对?
* 因为队列当中第一个排队的线程他会去尝试获取一下锁,因为有可能这个时候持有锁锁的那个线程可能释放了锁;
* 如果释放了就直接获取锁执行。但是如果没有释放他就会去排队,
* 所以这里的不需要排队,不是真的不需要排队
*
* h != t 判断首不等于尾这里要分三种情况
* 1、队列没有初始化,也就是第一个线程tf来加锁的时候那么这个时候队列没有初始化,
* h和t都是null,那么这个时候判断不等于则不成立(false)那么由于是&&运算后面的就不会走了,
* 直接返回false表示不需要排队,而前面又是取反(if (!hasQueuedPredecessors()),所以会直接去cas加锁。
* ----------第一种情况总结:队列没有初始化没人排队,那么我直接不排队,直接上锁;合情合理、有理有据令人信服;
* 好比你去火车站买票,服务员都闲的蛋疼,整个队列都没有形成;没人排队,你直接过去交钱拿票
*
* 2、队列被初始化了,后面会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;(不绝对,还有第3中情况)
* h != t 返回true;但是由于是&&运算,故而代码还需要进行后续的判断
* (有人可能会疑问,比如队列初始化了;里面只有一个数据,那么头和尾都是同一个怎么会成立呢?
* 其实这是第3种情况--对头等于对尾;但是这里先不考虑,我们假设现在队列里面有大于1个数据)
* 大于1个数据则成立;继续判断把h.next赋值给s;s有是对头的下一个Node,
* 这个时候s则表示他是队列当中参与排队的线程而且是排在最前面的;
* 为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;下文有详细解释
* 因为h也就是对头对应的Node对象或者线程他是持有锁的,但是不参与排队;
* 这个很好理解,比如你去买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
* 队列里面的h是不参与排队的这点一定要明白;参考下面关于队列初始化的解释;
* 因为h要么是虚拟出来的节点,要么是持有锁的节点;什么时候是虚拟的呢?什么时候是持有锁的节点呢?下文分析
* 然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;
* 假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个Node的时候h.next肯定不为空;
* 由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况
* 2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;
* 那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true 最后true)
* 结果: true && true 方法最终放回true,所以需要去排队
* 其实这样符合情理,试想一下买火车票,队列不为空,有人在排队;
* 而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队
* 2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程
* 这个时候整体结果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最后false)
* 结果:true && false 方法最终放回false,所以不需要去排队
* 不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;
* 这里又分为两种情况(日了狗了这一行代码;有同学课后反应说子路老师老师老是说这个AQS难,
* 你现在仔细看看这一行代码的意义,真的不简单的)
* 2.2.1 第一种情况加锁成功?有人会问为什么会成功啊,如这个时候h也就是持有锁的那个线程执行完了
* 释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 自己看代码
* 2.2.2 第二种情况加锁失败?有人会问为什么会失败啊。假如这个时候h也就是持有锁的那个线程没执行完
* 没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if(else if是相对于 if (c == 0)的)
* 那么如果失败怎么办呢?后面分析;
*
*----------第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是有个插曲;
* ----------他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放
*----------也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;
*----------如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);
* --------- 你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的
* 疑问:比如如果在在排队,那么他是park状态,如果是park状态,自己怎么还可能重入啊。
* 希望有同学可以想出来为什么和我讨论一下,作为一个菜逼,希望有人教教我
*
*
* 3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?ts加锁的时候里面就只有一个数据?
* 其实不是,因为队列初始化的时候会虚拟一个h作为头结点,tc=ts作为第一个排队的节点;tf为持有锁的节点
* 为什么这么做呢?因为AQS认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,
* 而ts其实需要排队的,因为这个时候tf可能没有执行完,还持有着锁,ts得不到锁,故而他需要排队;
* 那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,
* 故而ts无法排在tf之后,只能虚拟一个thread=null的节点出来(Node对象当中的thread为null);
* 那么问题来了;究竟什么时候会出现队列当中只有一个数据呢?假设原队列里面有5个人在排队,当前面4个都执行完了
* 轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个
* 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了;
* 所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点
* 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析
* 记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁
* h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队
*
*
*-------------第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,
*-------------这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas;尝试加锁
*-------------这是AQS的设计原理,他会判断你入队之前,队列里面有没有人排队;
*-------------有没有人排队分两种情况;队列没有初始化,不需要排队
*--------------队列初始化了,按时只有一个节点,也是没人排队,自己先也不排队
*--------------只要认定自己不需要排队,则先尝试加锁;加锁失败之后再排队;
*--------------再一次解释了不需要排队这个词的歧义性
*-------------如果加锁失败了,在去park,下文有详细解释这样设计源码和原因
*-------------如果持有锁的线程释放了锁,那么我能成功上锁
*
**/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
acquireQueued(addWaiter(Node.exclusive),arg))方法解析
如果代码能执行到这里说tc需要排队
需要排队有两种情况—换言之代码能够执行到这里有两种情况:
1、tf持有了锁,并没有释放,所以tc来加锁的时候需要排队,但这个时候—队列并没有初始化
2、tn(无所谓哪个线程,反正就是一个线程)持有了锁,那么由于加锁tn!=tf (tf是属于第一种情况,我们现在不考虑tf了),所以队列是一定被初始化了的,tc来加锁,那么队列当中有人在排队,故而他也去排队
addWaiter(Node.EXCLUSIVE)源码分析
private Node addWaiter(Node mode) {
//由于AQS队列当中的元素类型为Node,故而需要把当前线程tc封装成为一个Node对象,下文我们叫做nc
Node node = new Node(Thread.currentThread(), mode);
//tail为对尾,赋值给pred
Node pred = tail;
//判断pred是否为空,其实就是判断对尾是否有节点,其实只要队列被初始化了对尾肯定不为空,
//假设队列里面只有一个元素,那么对尾和对首都是这个元素
//换言之就是判断队列有没有初始化
//上面我们说过代码执行到这里有两种情况,1、队列没有初始化和2、队列已经初始化了
//pred不等于空表示第二种情况,队列被初始化了,如果是第二种情况那比较简单
//直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
//继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了
if (pred != null) {
//直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾,对应 10行的注释
node.prev = pred;
//这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
if (compareAndSetTail(pred, node)) {
//继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了 对应第11行注释
pred.next = node;
//然后把nc返回出去,方法结束
return node;
}
}
//如果上面的if不成了就会执行到这里,表示第一种情况队列并没有初始化---下面解析这个方法
enq(node);
//返回nc
return node;
}
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
//死循环
for (;;) {
//对尾复制给t,上面已经说过队列没有初始化,
//故而第一次循环t==null(因为是死循环,因此强调第一次,后面可能还有第二次、第三次,每次t的情况肯定不同)
Node t = tail;
//第一次循环成了成立
if (t == null) { // Must initialize
//new Node就是实例化一个Node对象下文我们称为nn,
//调用无参构造方法实例化出来的Node里面三个属性都为null,可以关联Node类的结构,
//compareAndSetHead入队操作;把这个nn设置成为队列当中的头部,cas防止多线程、确保原子操作;
//记住这个时候队列当中只有一个,即nn
if (compareAndSetHead(new Node()))
//这个时候AQS队列当中只有一个元素,即头部=nn,所以为了确保队列的完整,设置头部等于尾部,即nn即是头也是尾
//然后第一次循环结束;接着执行第二次循环,第二次循环代码我写在了下面,接着往下看就行
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//为了方便 第二次循环我再贴一次代码来对第二遍循环解释
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
//死循环
for (;;) {
//对尾复制给t,由于第二次循环,故而tail==nn,即new出来的那个node
Node t = tail;
//第二次循环不成立
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//不成立故而进入else
//首先把nc,当前线程所代表的的node的上一个节点改变为nn,因为这个时候nc需要入队,入队的时候需要把关系维护好
//所谓的维护关系就是形成链表,nc的上一个节点只能为nn,这个很好理解
node.prev = t;
//入队操作--把nc设置为对尾,对首是nn,
if (compareAndSetTail(t, node)) {
//上面我们说了为了维护关系把nc的上一个节点设置为nn
//这里同样为了维护关系,把nn的下一个节点设置为nc
t.next = node;
//然后返回t,即nn,死循环结束,enq(node);方法返回
//这个返回其实就是为了终止循环,返回出去的t,没有意义
return t;
}
}
}
}
//这个方法已经解释完成了
enq(node);
//返回nc,不管哪种情况都会返回nc;到此addWaiter方法解释完成
return node;
//再次贴出node的结构方便大家查看
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
-------------------总结:addWaiter方法就是让nc入队-并且维护队列的链表关系,但是由于情况复杂做了不同处理
-------------------主要针对队列是否有初始化,没有初始化则new一个新的Node nn作为对首,nn里面的线程为null
-------------------接下来分析acquireQueued方法
acquireQueued(addWaiter(Node.exclusive),arg))经过上面的解析之后可以理解成为acquireQueued(nc,arg))
acquireQueued方法的源码分析
final boolean acquireQueued(final Node node, int arg) {//这里的node 就是当前线程封装的那个node 下文叫做nc
//记住标志很重要
boolean failed = true;
try {
//同样是一个标志
boolean interrupted = false;
//死循环
for (;;) {
//获取nc的上一个节点,有两种情况;1、上一个节点为头部;2上一个节点不为头部
final Node p = node.predecessor();
//如果nc的上一个节点为头部,则表示nc为队列当中的第二个元素,为队列当中的第一个排队的Node;
//这里的第一和第二不冲突;我上文有解释;
//如果nc为队列当中的第二个元素,第一个排队的则调用tryAcquire去尝试加锁---关于tryAcquire看上面的分析
//只有nc为第二个元素;第一个排队的情况下才会尝试加锁,其他情况直接去park了,
//因为第一个排队的执行到这里的时候需要看看持有有锁的线程有没有释放锁,释放了就轮到我了,就不park了
//有人会疑惑说开始调用tryAcquire加锁失败了(需要排队),这里为什么还要进行tryAcquire不是重复了吗?
//其实不然,因为第一次tryAcquire判断是否需要排队,如果需要排队,那么我就入队;
//当我入队之后我发觉前面那个人就是第一个,持有锁的那个,那么我不死心,再次问问前面那个人搞完没有
//如果他搞完了,我就不park,接着他搞我自己的事;如果他没有搞完,那么我则在队列当中去park,等待别人叫我
//但是如果我去排队,发觉前面那个人在睡觉,前面那个人都在睡觉,那么我也睡觉把---------------好好理解一下
if (p == head && tryAcquire(arg)) {
//能够执行到这里表示我来加锁的时候,锁被持有了,我去排队,进到队列当中的时候发觉我前面那个人没有park,
//前面那个人就是当前持有锁的那个人,那么我问问他搞完没有
//能够进到这个里面就表示前面那个人搞完了;所以这里能执行到的几率比较小;但是在高并发的世界中这种情况真的需要考虑
//如果我前面那个人搞完了,我nc得到锁了,那么前面那个人直接出队列,我自己则是对首;这行代码就是设置自己为对首
setHead(node);
//这里的P代表的就是刚刚搞完事的那个人,由于他的事情搞完了,要出队;怎么出队?把链表关系删除
p.next = null; // help GC
//设置表示---记住记加锁成功的时候为false
failed = false;
//返回false;为什么返回false?下次博客解释---比较复杂和加锁无关
return interrupted;
}
//进到这里分为两种情况
//1、nc的上一个节点不是头部,说白了,就是我去排队了,但是我上一个人不是队列第一个
//2、第二种情况,我去排队了,发觉上一个节点是第一个,但是他还在搞事没有释放锁
//不管哪种情况这个时候我都需要park,park之前我需要把上一个节点的状态改成park状态
//这里比较难以理解为什么我需要去改变上一个节点的park状态呢?每个node都有一个状态,默认为0,表示无状态
//-1表示在park;当时不能自己把自己改成-1状态?为什么呢?因为你得确定你自己park了才是能改为-1;
//不然你自己改成自己为-1;但是改完之后你没有park那不就骗人?
//你对外宣布自己是单身状态,但是实际和刘宏斌私下约会;这有点坑人
//所以只能先park;在改状态;但是问题你自己都park了;完全释放CPU资源了,故而没有办法执行任何代码了,
//所以只能别人来改;故而可以看到每次都是自己的后一个节点把自己改成-1状态
//关于shouldParkAfterFailedAcquire这个方法的源码下次博客继续讲吧
if (shouldParkAfterFailedAcquire(p, node) &&
//改上一个节点的状态成功之后;自己park;到此加锁过程说完了
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
【5】AQS队列结构
【6】加锁过程总结
如果是第一个线程tf,那么和队列无关,线程直接持有锁。并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中T2来lock,那么这个时候就会初始化AQS,初始化AQS的时候会在队列的头部虚拟一个Thread为NULL的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park,当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null
【7】加锁的几种方法
lock :获取锁,获取不到锁一直阻塞
tryLock :尝试获取锁,如果能拿到锁则加锁并返回true,拿不到锁则返回false
lockInterruptibly :获取可中断锁,如果在获取锁的过程中,如果拿不到锁会一直阻塞,为了防止长时间阻塞可以打断当前线程等待锁。
【8】读写锁
【9】Condition条件锁
3、CAS
Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁
因为经常配合循环操作,直到完成为止,所以泛指一类操作
cas(v, a, b) ,变量v,期待值a, 修改值b
ABA问题,你的女朋友在离开你的这段儿时间经历了别的人,自旋就是你空转等待,一直等到她接纳你为止
解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号
5、volatile
【1】性质
保证线程之间的可见性
public class VolatileTest {
public static /*volatile*/ boolean running;
public static void main(String args[]) throws InterruptedException {
running = true;
for (int i = 0; i<10000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
if (running) {
System.out.println("yes");
} else {
System.out.println("no");
}
}
});
thread.start();
}
running = false;
}
}
禁止指令重排序
乱序执行(指令重排序):指令寄存器里a,b两条指令没有顺序关系时,可能先执行b乱序执行实例
public class DisOrder {
private static int x,y,a, b;
public static void main(Strin·g args[]) throws InterruptedException {
int i = 0;
for (;;) {
i++;
x = 0; a = 0;
y = 0; b = 0;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1; // 指令1
x = b; // 指令2
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1; // 指令3
y = a; // 指令4
}
});
thread1.start();thread2.start();
thread1.join(); thread2.join();
// 按执行逻辑不会出现这种情况,只有在指令1,指令2执行顺序反过来,或者指令3指令四反过来是才会出现
if (x ==0 && y == 0) {
System.out.println("第" + i + "次执行, x = 0, y=0");
}
}
}
}
禁止指令重排序方法
volatile 写操作前加StoreStore屏障,写操作后加写读屏障
volatile读操作前加读读屏障,读操作后加读写屏障
hotspot虚拟机实现:锁总线
【2】 缓存行对齐
多级缓存结构
缓存行失效和对齐
MESI协议(缓存一致性协议)在MESI协议中,每个Cache line有4个状态,可用2个bit表示,分别是:M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中;
E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中;
S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中;
I(Invalid):这行数据无效。
如下图所示,core 1 & core2 都加载内存地址为 00000 – 00040 X=3 Y=4 的一个缓存行,状态均为S,当Core 2修改Y=5,因为该缓存行存在于多个Cache,故Core 2中状态=Modified,Core1中状态=Invalid,此时Core1若读取X,也需要从内存中重新加载该缓存行。可以看出,该缓存行中任意一个变量的修改,都会导致该缓存行失效,造成重新从内存中读取。这里引入“伪共享”:在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能。
在多线程情景下,线程1修改X,线程2修改Y,根据MESI,执行顺序为:
Thread1 update X = 10,修改Status=M,通知其他Cache 修改Status=I;
Thread2 更新 Y时,发现Status = I,告之Thread1把脏数据X=10写入内存中,Status = I;
Thread2读取Cache Line,并置Status=E,修改Y=5;
修改完毕后,Status=M;
多个线程操作在同一Cache Line上的不同数据,相互竞争同一Cache Line,导致线程彼此牵制影响,变成了串行程序,降低了并发性。采用缓存行对齐的方式来提高效率。缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高。
缓存行对齐实例
public class T02_CacheLinePadding {
private static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7; // 对比加此行和不加此行的执行效率
}
private static class T extends Padding {
public volatile long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[0].x = i;
}
});
Thread t2 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start)/100_0000);
}
}
总结:volatile作用域堆内存中的共享变量上,它通过MESI协议保证了变量的修改对其他线程的可见性,使其他线程在操作共享变量前重新从内存中读取到cpu缓存中。
6、ThreadLocal
实现原理:在当前线程Thread对象中维护一个ThreadLocalMap(由一个Entry数组实现), key 为指向ThreadLocal对象的引用,Value为具体的对象,ThreadLocalMap中的Entry对象继承的WeekRefence,使软引用key指向threadlocal对象。使用完后需要调用remove方法释放value对象,否则会产生垃圾内存碎片造成内存泄露。Entry数组初始大小为16,发生哈希碰撞时数组扩大一倍重新hash。