Java并发编程第四讲

Java并发编程第四讲,第1张

你知道有哪几种锁?分别有什么特点? 1.锁的7大分类

2.偏向锁/轻量级锁/重量级锁

偏向锁如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

轻量级锁JVM开发者发现在很多情况下,synchronized中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用CAS就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

重量级锁重量级锁是互斥锁,它是利用 *** 作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

3.可重入锁/非可重入锁

区别在于下次获取需不需要再次获取。 

4.共享锁/独占锁

读写锁的读锁是共享锁,写锁是独占所。 

5.公平锁/非公平锁

公平的含义在于如果线程现在拿不到这把锁那么线程就都会进入等待,开始排队在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。

非公平锁会在一定情况下忽略掉已经在排队的线程发生插队现象。

6.悲观锁/乐观锁

在获取资源之前必须先拿到锁,以便达到“独占”的状态当前线程在 *** 作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。

并不要求在获取资源前拿到锁也不会锁住资源相反,乐观锁利用CAS理念在不独占资源的情况下,完成了对资源的修改。

7.自旋锁/非自旋锁

如果线程现在拿不到锁,并不直接陷入阻塞或者释放CPU资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为自旋”,就像是线程在“自我旋转”自旋不会改变自己的状态。

没有自旋的过程如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

8.可中断锁/不可中断锁????

ReentrantLock是一种典型的可中断锁例如使用lockInterruptibly方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情不需要一直傻等到获取到锁才离开。锁在执行时可被中断,也就是在执行时可以接收 interrupt 的通知,从而中断锁执行。

在Java中synchronized关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路,只能等到拿到锁以后才能进行其他的逻辑处理。

悲观锁和乐观锁的本质是什么?         1.悲观锁

        会在每次获取并修改数据前,都把数据锁住让其他线程无法访问该数据,这样就可以确保数据内容万无一失 

        2.乐观锁

认为自己在 *** 作资源的时候不会有其他线程来干扰所以并不会锁住被 *** 作对象,不会不让别的线程来接触它同时为了确保数据正确性在更新前会去对比在我修改数据期间,数据有没有被其他线程修改过:如果没被修改过,就说明真的只有我自己在 *** 作,那我就可以正常的修改数据如果发现数据和我一开始拿到的不一样了,说明其他线程在这段时间内修改过数据,那说明我迟了一步所以我会放弃这次修改,并选择报错、重试等策略。

        3.典型案例

悲观锁:synchronized关键字和Lock接口
Java中悲观锁的实现包括synchronized关键字和Lock相关类等
例如Lock的实现类ReentrantLock,类中的lock)等方法就是执行加锁,而unlock)方法是执行解锁
处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想
·乐观锁:原子类
乐观锁的典型案例就是原子类
例如Atomiclnteger在更新数据时,就使用了乐观锁的思想,多个线程可以同时 *** 作同一个原子变量
大喜大悲:数据库
数据库中同时拥有悲观锁和乐观锁的思想
例如,我们如果在MySQL选择select for update语句,那就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的

        4.“汝之蜜糖,彼之砒霜”

        有一种说法认为悲观锁由于它的 *** 作比较重量级,不能多个线程并行执行,而且还会有上下文切换等动作所以悲观锁的性能不如乐观锁好,!!应该尽量避免用悲观锁!!,这种说法是不正确的因为虽然悲观锁确实会让得不到锁的线程阻塞,但是这种开销是固定的悲观锁的原始开销确实要高于乐观锁,但是特点是一劳永逸,就算一直拿不到锁,也不会对开销造成额外的影响。

        反观乐观锁虽然一开始的开销比悲观锁小但是如果一直拿不到锁,或者并发量大,竞争激烈,导致不停重试,那么消耗的资源也会越来越多,甚至开销会超过悲观锁所以,同样是悲观锁,在不同的场景下,效果可能完全不同,可能在今天的这种场景下是好的选择,在明天的另外的场景下就是坏的选择,这恰恰是“汝之蜜糖,彼之砒霜”。

        5.两种锁各自的使用场景

        悲观锁:适用于并发写入多临界区代码复杂竞争激烈等场景这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。

        乐观锁:适用于大部分是读取少部分是修改的场景也适合虽然读写都很多但是并发并不激烈的场景在这些场景下,乐观锁不加锁的特点能让性能大幅提高。

4.3如何看到synchronized背后的“monitor锁”?         1.获取和释放monitor锁的时机

         每个Java对象都可以用作一个实现同步的锁这个锁也被称为内置锁或monitor锁获得monitor锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法线程在进入被synchronized保护的代码块之前,会自动获取锁并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。

        2.用javap命令查看反汇编的结果         同步代码块
public class SynTest {
    public void synBlock() {
        synchronized (this) {
            System.out.println("lagou");
        }
    }
}

首先用 cd 命令切换到 SynTest.java 类所在的路径,然后执行 javac SynTest.java,于是就会产生一个名为 SynTest.class 的字节码文件,

然后我们执行 javap -verbose SynTest.class,就可以看到对应的反汇编内容。

 public void synBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                      // String lagou
         9: invokevirtual #4               // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return

 monitorenter 与monitorexit 就是获取锁与释放锁的动作,14:的monitorexit是同步代码块正常执行完毕。19:的monitorexit是执行同步代码块反生异常进行相关的动作。

  • monitorenter

执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

a. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。

b. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。

c. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

  • monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

        同步方法

这个方法会有一个叫作 ACC_SYNCHRONIZED 的 flag 修饰符,来表明它是同步方法。

public synchronized void synMethod() {
 
}
  public synchronized void synMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 16: 0
synchronized和Lock孰优孰劣,如何选择?         1.相同点

synchronized 和 Lock 都是用来保护资源线程安全

synchronized 和 ReentrantLock 都拥有可重入的特点

都可以保证可见性

        2.不同点
  • 用法区别

synchronized 关键字可以加在方法上,不需要指定锁对象(此时的锁对象为 this),也可以新建一个同步代码块并且自定义 monitor 锁对象;而 Lock 接口必须显示用 Lock 锁对象开始加锁 lock() 和解锁 unlock(),并且一般会在 finally 块中确保用 unlock() 来解锁,以防发生死锁。

与 Lock 显式的加锁和解锁不同的是 synchronized 的加解锁是隐式的,尤其是抛异常的时候也能保证释放锁,但是 Java 代码中并没有相关的体现。

  • 加解锁顺序不同
lock1.lock();
lock2.lock();
...
lock1.unlock();
lock2.unlock();
synchronized(obj1){
    synchronized(obj2){
        ...
    }
}

synchronized 的嵌套顺序加解锁,不能自行控制 

  • synchronized 锁不够灵活

一旦 synchronized 锁已经被某个线程获得了,此时其他线程如果还想获得,那它只能被阻塞,直到持有锁的线程运行完毕或者发生异常从而释放这个锁。

相比之下,Lock 类在等锁的过程中,如果使用的是 lockInterruptibly 方法,那么如果觉得等待的时间太长了不想再继续等待,可以中断退出,也可以用 tryLock() 等方法尝试获取锁,如果获取不到锁也可以做别的事,更加灵活。

  • synchronized 锁只能同时被一个线程拥有

例如在读写锁中的读锁,是可以同时被多个线程持有的。

  • 原理区别 

synchronized 是内置锁,由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁。 

Lock 根据实现不同,有不同的原理,例如 ReentrantLock 内部是通过 AQS 来获取和释放锁的。

  • 是否可以设置公平/非公平
  • 性能区别

在 Java 5 以及之前,synchronized 的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。

        3.如何选择
  1. 如果能不用最好既不使用 Lock 也不使用 synchronized。因为在许多情况下你可以使用 java.util.concurrent 包中的机制,它会为你处理所有的加锁和解锁 *** 作,也就是推荐优先使用工具类来加解锁。
  2. 如果 synchronized 关键字适合你的程序, 那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全。
  3. 如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。
Lock有哪几个常用方法?分别有什么用?

3.Iock()方法

lock 的加锁和释放锁都必须以代码的形式写出来,所以使用 lock() 时必须由我们自己主动去释放锁,因此最佳实践是执行 lock() 后,首先在 try{} 中 *** 作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中释放锁,以保证发生异常时锁一定被释放,示例代码如下所示。

Lock lock = ...;
lock.lock();
try{
    //获取到了被本锁保护的资源,处理任务
    //捕获异常
}finally{
    lock.unlock();   //释放锁
}

4.tryLock()

因为该方法会立即返回,即便在拿不到锁时也不会一直等待,所以通常情况下,我们用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑,典型使用方法如下

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则做其他事情
}

5.tryLock(long time,TimeUnit unit)

有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。

lockInterruptibly()

顾名思义,lockInterruptibly() 是可以响应中断的。相比于不能响应中断的 synchronized 锁,lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时,保持对中断的响应。我们可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过 lockInterruptibly() 永远不会超时。该方法会抛出异常然后抛出InterruptedException并清除当前线程的中断状态

7.unlock()

对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数。

Lock有哪几个常用方法?分别有什么用?         1.什么是公平和非公平
/**
 * 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自Java并发编程实战手册2.7。
 */
public class FairAndUnfair {
 

    public static void main(String args[]) {
        PrintQueue printQueue = new PrintQueue();

        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue), "Thread " + i);
        }

        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class Job implements Runnable {

    private PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());
        printQueue.printJob(new Object());
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }

}

class PrintQueue {

    private final Lock queueLock = new ReentrantLock(false);

    public void printJob(Object document) {
        queueLock.lock();

        try {
            Long duration = (long) (Math.random() * 10000);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try {
            Long duration = (long) (Math.random() * 10000);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
                    Thread.currentThread().getName(), (duration / 1000));
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
            }
    }
}

 查看ReentrantLock类的构造发现 传入的布尔值可控制是否为公平锁,下面有  

FairSync 与 NonfairSync 

 FairSync  中比 NonfairSync 多了一行判断 hasQueuedPredecessors()

 HashMap为什么是线程不安全的?         1.源码分析
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 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++ 是一个复合 *** 作
    modCount++;
 
    addEntry(hash, key, value, i);
    return null;
}

 i++

  • 第一个步骤是读取;
  • 第二个步骤是增加;
  • 第三个步骤是保存。
        2.试验:扩容期间取出的值不准确
public class HashMapNotSafe {
 
    public static void main(String[] args) {
        final Map map = new HashMap<>();
 
        final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
        final String targetValue = "v";
        map.put(targetKey, targetValue);
 
        new Thread(() -> {
            IntStream.range(0, targetKey).forEach(key -> 
            // hashMap的扩容机制
            // https://blog.csdn.net/zs18753479279/article/details/115390563
            map.put(key, "someValue"));
        }).start();
 
        while (true) {
            if (null == map.get(targetKey)) {
                throw new RuntimeException("HashMap is not thread safe.");
            }
        }
    }
}
        3.同时put碰撞导致数据丢失

比如,有多个线程同时使用 put 来添加元素,而且恰好两个 put 的 key 是一样的,它们发生了碰撞,也就是根据 hash 值计算出来的 bucket 位置一样,并且两个线程又同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。

        4.可见性问题无法保证

我们再从可见性的角度去考虑一下。可见性也是线程安全的一部分,如果某一个数据结构声称自己是线程安全的,那么它同样需要保证可见性,也就是说,当一个线程 *** 作这个容器的时候,该 *** 作需要对另外的线程都可见,也就是其他线程都能感知到本次 *** 作。可是 HashMap 对此是做不到的,如果线程 1 给某个 key 放入了一个新值,那么线程 2 在获取对应的 key 的值的时候,它的可见性是无法保证的,也就是说线程 2 可能可以看到这一次的更改,但也有可能看不到。所以从可见性的角度出发,HashMap 同样是线程非安全的

        5.死循环造成CPU100%

下面我们再举一个死循环造成 CPU 100% 的例子。HashMap 有可能会发生死循环并且造成  CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中如果需要使用 Map,应该尽量避免使用线程不安全的 HashMap。同时,虽然 Collections.synchronizedMap(new HashMap()) 是线程安全的,但是效率低下,因为内部用了很多的 synchronized,多个线程不能同时 *** 作。推荐使用线程安全同时性能比较好的 ConcurrentHashMap。关于 ConcurrentHashMap 我们会在下一个课时中介绍。

ConcurrentHashMap在Java7和8有什么不同?

        1.Java7版本的ConcurrentHashMap

 

在 ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。

每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发 *** 作( *** 作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

        2.Java8版本的ConcurrentHashMap

 

种情况的链表长度大于某一个阈值(默认为 8),且同时满足一定的容量要求的时候,ConcurrentHashMap 便会把这个链表从链表的形式转化为红黑树的形式,目的是进一步提高它的查找性能。

        3.分析Java8版本的ConcurrentHashMap的重要源码

Node 节点

我们先来看看最基础的内部存储结构 Node,这就是一个一个的节点,如这段代码所示:

static class Node implements Map.Entry {
    final int hash;
    final K key;
    volatile V val;
    volatile Node next;
    // ...
}

 可以看出,每个 Node 里面是 key-value 的形式,并且把 value 用 volatile 修饰,以便保证可见性,同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构。

put 方法源码分析

put 方法的核心是 putVal 方法,为了方便阅读,我把重要步骤的解读用注释的形式补充在下面的源码中。我们逐步分析这个最重要的方法,这个方法相对有些长,我们一步一步把它看清楚。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) {
        throw new NullPointerException();
    }
    //计算 hash 值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node[] tab = table; ; ) {
        Node f;
        int n, i, fh;
        //如果数组是空的,就进行初始化
        if (tab == null || (n = tab.length) == 0) {
            tab = initTable();
        }
        // 找该 hash 值对应的数组下标
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //如果该位置是空的,就用 CAS 的方式放入新值
            if (casTabAt(tab, i, null,
                    new Node(hash, key, value, null))) {
                break;
            }
        }
        //hash值等于 MOVED 代表在扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
        }
        //槽点上是有值的情况
        else {
            V oldVal = null;
            //用 synchronized 锁住当前槽点,保证并发安全
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //如果是链表的形式
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表
                        for (Node e = f; ; ++binCount) {
                            K ek;
                            //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                            if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent) {
                                    e.val = value;
                                }
                                break;
                            }
                            Node pred = e;
                            //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    //如果是红黑树的形式
                    else if (f instanceof TreeBin) {
                        Node p;
                        binCount = 2;
                        //调用 putTreeVal 方法往红黑树里增加数据
                        if ((p = ((TreeBin) f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) {
                                p.val = value;
                            }
                        }
                    }
                }
            }
            if (binCount != 0) {
                //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
                if (binCount >= TREEIFY_THRESHOLD) {
                    treeifyBin(tab, i);
                }
                //putVal 的返回是添加前的旧值,所以返回 oldVal
                if (oldVal != null) {
                    return oldVal;
                }
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

槽点是未初始化、空、扩容、链表、红黑树等不同情况做出不同的处理。

public V get(Object key) {
    Node[] tab; Node e, p; int n, eh; K ek;
    //计算 hash 值
    int h = spread(key.hashCode());
    //如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        //判断头结点是否就是我们需要的节点,如果是则直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //如果头结点 hash 值小于 0,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //遍历链表来查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
总

总结一下 get 的过程:

  1. 计算 Hash 值,并由此值找到对应的槽点;
  2. 如果数组是空的或者该位置为 null,那么直接返回 null 就可以了;
  3. 如果该位置处的节点刚好就是我们需要的,直接返回该节点的值;
  4. 如果该位置节点是红黑树或者正在扩容,就用 find 方法继续查找;
  5. 否则那就是链表,就进行遍历链表查找。
ReentrantReadWriteLock

 同时存在 写锁与读锁竞争的时候 读锁会被禁用。

 

写锁,1.其他线程不占用该锁ReentrantReadWriteLock 时候可获取

2.在不释放锁的时候空重新获取锁 

        代码演示 
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     演示读写锁用法
 */
public class ReadWriteLockDemo {
    // 非公平策略创建
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock
            .readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock
            .writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> read()).start();
        new Thread(() -> read()).start();
        new Thread(() -> write()).start();
        new Thread(() -> write()).start();
    }
}

 

ReentrantReadWriteLock的公平锁与非公平的的差异。

  公平锁的读写都需要排

apparentlyFirstQueuedIsExclusive()显然,先排队是唯一的 先执行排队队列的写锁

 

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/923667.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存