java并发编程之线程

java 并发编程

1.线程

1.1 创建线程的方式
  • 实现Runnable接口
  • 继承Thread类
  • 线程池创建线程
  • Callable创建线程
public class CreateThreadTest {
    
    private static ExecutorService pool = Executors.newFixedThreadPool(10);

    class MyThread extends Thread{

        @Override
        public void run() {
            System.out.println("MyThread extends Thread is running");
        }
    }

    class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("MyRunnable implements Runnable is running");

        }
    }

    public static void main(String[] args) {
        CreateThreadTest a = new CreateThreadTest();
        MyThread thread1 = a.new MyThread();
        Thread thread2  = new Thread(a.new MyRunnable());
        thread1.start();
        thread2.start();
        pool.execute(() ->{
            System.out.println("pool created thread is runing");
        });
        pool.submit(() ->{
            System.out.println("callable thread is running");
            return "callable";
        });

    }

总结:

  • 继承Thread 类只是重写了run方法,线程执行则会执行重写的run方法
  • 实现runnable接口,创建线程需要作为target传给 Thread的构造器,run方法执行时,如果target不为空,则执行target的run方法,即新实现的接口run方法
  • 以上俩种,实际上都需要通过Thread的构造器来创建线程,只是一个时重写run方法,一个是动态的替换了执行类target;
  • 线程池创建线程也是基于以上俩字形式;
  • 使用实现接口的方式,更加灵活,职责更加鲜明
1.2 线程的几种状态
  • NEW
  • runnable
  • waiting
  • Time waiting
  • blocked
  • terminated
    在这里插入图片描述
wait/notify 和 sleep 方法的异同
  • 相同点

    • 实现线程阻塞
    • 都可以响应线程interrupt
  • 不同点

    • wait方法必须在synchronized保护的代码中使用,sleep则不需要
    • 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁
    • wait可以不定义超时时间
    • wait/notify属于Object类,sleep时Thread的方法
1.3 如何优雅的停止线程
  • 最正确的停止线程的方式为interrupt
    • catch 中 throw InterruptedException
    • Thread.currentThread().interrupt();
    • 不进行try catch ,method throws InterruptedException
//不优雅的停止线程
 public static void main(String[] args) {
        Thread thread1 = new Thread(() ->{
            try {
                Thread.sleep(10000L);
                //当该线程的上游想知道thread1是否正常执行结束时,应该抛出InterruptedException
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("不优雅的停止线程");
                //1.throw e
                //2.Thread.currentThread().interrupt();
                //3. method throws InterruptedException
            }
        });
        thread1.start();
    }

  • 使用volatile标记位来实现线程的停止会有什么问题?

    以下代码不会正常的输入canceled;

    当线程的执行存在某种阻塞的操作时,即使volatile标记会成功标记取消,但线程阻塞时不会重新进行判断,标记位不会生效的;

    如何处理下面的程序?

    package com.lww.thread;
    
    public class StopThreadTest {
    
        class MyProducerRunable implements Runnable{
    
            private volatile boolean canceled = false;
    
            public void setCanceled(boolean canceled) {
                this.canceled = canceled;
            }
    
            @Override
            public void run() {
                try {
                    while (!canceled){
                        //模拟一个阻塞的操作
                        Thread.sleep(Integer.MAX_VALUE);
    
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }finally {
                    System.out.println("Thread is canceled");
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            StopThreadTest a = new StopThreadTest();
            MyProducerRunable runable = a.new MyProducerRunable();
            Thread producer_thread = new Thread(runable);
            producer_thread.start();
            Thread.sleep(1000L);
            runable.setCanceled(true);
    
        }
    }
    

    interrupt 能正确的终止线程,producer_thread.interrupt(); 后 finally能正确的执行canceled

    public static void main(String[] args) throws InterruptedException {
        StopThreadTest a = new StopThreadTest();
        MyProducerRunable runable = a.new MyProducerRunable();
        Thread producer_thread = new Thread(runable);
        producer_thread.start();
        Thread.sleep(1000L);
        //runable.setCanceled(true);
            producer_thread.interrupt();
    }
    
1.4 线程安全

什么是线程安全?

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。

访问共享变量、依赖时序操作、使用不是线程安全类时,存在的线程问题:

  • 运行结果错误

    i++操作等等;

  • 发布和初始化导致线程安全问题

    子线程中初始化值,主线程在未初始化结束使用值等等

  • 活跃性问题

    • 死锁
    • 活锁
      • 线程一直在处理无法处理的同一条数据
    • 饥饿
      • 线程的优先级不同,导致低优先级的线程可能一直获取不到CPU时间分片

2.线程池

2.1 线程池的作用
  • 减少线程生命周期的开销
  • 控制内存和CPU的使用
  • 统一管理
2.2 创建线程池的参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

默认的线程工厂如下:

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}
2.3 线程池的拒绝策略
  • AbortPolicy

    会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException

  • DiscardPolicy

    当新任务被提交后直接被丢弃掉,也不会给你任何的通知

  • DiscardOldestPolicy

    会丢弃任务队列中的头结点,通常是存活时间最长的任务

  • CallerRunsPolicy

    如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务

2.4 常见的线程池
  • FixedThreadPool
    • 无界队列LinkedBlockingQueue
  • CachedThreadPool
    • 线程数无界SynchronousQueue
  • ScheduledThreadPool
    • 延迟队列DelayedWorkQueue,基于时间执行任务
  • SingleThreadExecutor
    • 无界队列LinkedBlockingQueue ,线程数1
  • SingleThreadScheduledExecutor
    • 延迟队列DelayedWorkQueue,基于时间执行任务
  • ForkJoinPool(java7)
2.5 线程池问题
  1. FixedThreadPool和SingleThreadExecutor 由于无界队列,容易OOM
  2. CachedThreadPool 不控制线程数,导致内存不足
  3. ScheduledThreadPool和SingleThreadScheduledExecutor ,延时队列也是无界的,容易OOM
2.6 选择合适的线程数

*线程数 = CPU 核心数 (1+平均等待时间/平均工作时间)

  • CPU密集型任务

    加密、解密、压缩、计算等占用CPU的任务

  • 耗时IO型任务

    数据库、文件的读写,网络通信等任务

2.7 关闭线程池
  • shutdown -> 执行完正在执行的任务和队列中等待的任务后才彻底关闭
  • shutdownNow -> 给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回
  • isShutdown ->是否执行了 shutdown 或者 shutdownNow 方法
  • isTerminating -> 执行了shutdown 或者 shutdownNow 方法,但未完全terminated 返回true
  • isTerminated -> 检测线程池已关闭及其线程池中的所有任务都已经都执行完毕
  • awaitTermination -> 判断线程池状态
/**
 * Initiates an orderly shutdown in which previously submitted
 * tasks are executed, but no new tasks will be accepted.
 * Invocation has no additional effect if already shut down.
 *
 * <p>This method does not wait for previously submitted tasks to
 * complete execution.  Use {@link #awaitTermination awaitTermination}
 * to do that.
 *
 * @throws SecurityException {@inheritDoc}
 */
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

/**
 * Attempts to stop all actively executing tasks, halts the
 * processing of waiting tasks, and returns a list of the tasks
 * that were awaiting execution. These tasks are drained (removed)
 * from the task queue upon return from this method.
 *
 * <p>This method does not wait for actively executing tasks to
 * terminate.  Use {@link #awaitTermination awaitTermination} to
 * do that.
 *
 * <p>There are no guarantees beyond best-effort attempts to stop
 * processing actively executing tasks.  This implementation
 * cancels tasks via {@link Thread#interrupt}, so any task that
 * fails to respond to interrupts may never terminate.
 *
 * @throws SecurityException {@inheritDoc}
 */
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

public boolean isShutdown() {
    return ! isRunning(ctl.get());
}

/**
 * Returns true if this executor is in the process of terminating
 * after {@link #shutdown} or {@link #shutdownNow} but has not
 * completely terminated.  This method may be useful for
 * debugging. A return of {@code true} reported a sufficient
 * period after shutdown may indicate that submitted tasks have
 * ignored or suppressed interruption, causing this executor not
 * to properly terminate.
 *
 * @return {@code true} if terminating but not yet terminated
 */
public boolean isTerminating() {
    int c = ctl.get();
    return ! isRunning(c) && runStateLessThan(c, TERMINATED);
}

public boolean isTerminated() {
    return runStateAtLeast(ctl.get(), TERMINATED);
}

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            if (nanos <= 0)
                return false;
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

通过线程池执行方法查看线程池运行原理:

  1. 运行线程数小于corePoolSize,则直接新建线程执行任务
  2. 线程池在运行状态时,任务队列未满(添加任务成功)下,再次检查是否需要创建线程(距离上次检测可能存在线程结束),如果可工作线程为0则新建线程
  3. 线程池停止或者新建线程失败,则拒绝策略
/**
 * Executes the given task sometime in the future.  The task
 * may execute in a new thread or in an existing pooled thread.
 *
 * If the task cannot be submitted for execution, either because this
 * executor has been shutdown or because its capacity has been reached,
 * the task is handled by the current {@code RejectedExecutionHandler}.
 *
 * @param command the task to execute
 * @throws RejectedExecutionException at discretion of
 *         {@code RejectedExecutionHandler}, if the task
 *         cannot be accepted for execution
 * @throws NullPointerException if {@code command} is null
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

3.锁

3.1 锁的分类
  • 偏向锁、轻量级锁、重量级锁

    这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

    无锁 --> 偏向锁 —> 轻量级锁 -----> 重量级锁

    • 偏向锁:如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
    • 轻量级锁:轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
    • 重量级锁:重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
  • 悲观锁、乐观锁

    • 悲观锁 :是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁。
    • 乐观锁 :并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
  • 自旋锁、非自旋锁

    • 自旋锁:如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。
    • 非自旋锁:没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞。
  • 公平锁、非公平锁

    • 公平锁:FIFO的思想
    • 非公平锁:插队思想
  • 可重入锁、不可重入锁

    • 可重入锁:ReentrantLock,线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。
    • 不可重入锁:线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
  • 共享锁、独占锁(排他锁)或者称作读锁和写锁

    • 共享锁:共享锁指的是我们同一把锁可以被多个线程同时获得
    • 独占锁:这把锁只能同时被一个线程获得
  • 可中断锁、不可中断锁

3.2 synchronized 锁实现
  • synchronized 在方法体上,字节码中是通过ACC_SYSCHRONINZED来标记的,代表使用该标记的方法在执行前需要获取执行对象this的monitor锁,执行结束后会自动释放monitor锁
  • synchronized在代码块中,字节码中时通过monitorenter和monitorexit的出现来加锁和释放锁的
3.3 synchrinized和lock的异同
  • 同:保证可见性、资源线程安全、可重入锁
    • 用法不同、synchrinized加解锁是隐性的,不需要手动释放,lock需要手动unlock
    • lock可以不用加锁顺序反序解锁,synchrinized不行;
    • lock比较灵活,例如lockInterruptibly、tryLock、可实现公平锁和
    • synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制
    • JDK6 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。
3.4 ReadWriteLock
  • 规则:读读共享、读写互斥、写读互斥、写写互斥
  • 适用情况:读多写少的共享资源,可提高并发效率

ReentrantReadWriteLock

  • 公平锁 :R锁和W锁都不能插队
  • 非公平锁:W锁可以插队,减少W锁线程饥饿发生,W锁插队成功的条件为:在当前没有任何其他线程持有读锁和写锁的时候,才能插队成功。
  • 锁的升降级:只能从写锁降级为读锁,不能从读锁升级为写锁。什么时候需要降级?当写操作结束后,需要释放W锁,但是又需要读取共享数据进行后续处理时,我们可以将W锁降级R锁后释放W锁,然后读取共享数据,最后释放R锁。
3.5 自旋锁

自旋锁的特点:它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。

好处:自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

缺点:临界区时间变长,则获取到锁的时间就变长,效率可能还不如线程切换的开销。

适用场景:自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。

4.并发容器

jvm常见的指令,可用于分析可见性和代码的执行

  • lock(锁定):在某一个线程在读取主内存的时候需要把变量锁定。
  • unlock(解锁):某一个线程读取玩变量值之后会释放锁定,别的线程就可以进入操作
  • read(读取):从主内存中读取变量的值并放入工作内存中
  • load(加载):从read操作得到的值放入工作内存变量副本中
  • use(使用):把工作内存中的一个变量值传递给执行引擎
  • assign(赋值):它把一个从执行引擎接收到的值赋值给工作内存的变量
  • store(存储):把工作内存中的一个变量的值传送到主内存中
  • write(写入):把store操作从工作内存中一个变量的值传送到主内存的变量中。

这里我再引入一张别的地方被我搜来的图供大家一起理解:

这里写图片描述

4.1.线程不安全的HashMap
  • HashMap的底层结构
  • 如何添加元素
  • 如何删除元素
  • 扩容
  • 为什么扩容时是2的倍数,有什么好处?
HashMap的几个参数
属性说明JDK8JDK7
DEFAULT_INITIAL_CAPACITY默认初始容量 1 << 4 = 16
MAXIMUM_CAPACITY最大容量 1 << 30
DEFAULT_LOAD_FACTOR默认加载因子 0.75f
TREEIFY_THRESHOLD链表转换红黑树的临界值 8
UNTREEIFY_THRESHOLD红黑树转换链表的临界值 6
MIN_TREEIFY_CAPACITY链表转换红黑树的容量临界值 64
modCount数据改变次数的统计
threshold扩容临界值
loadFactor加载因子,默认为0.75,和扩容有关
HashMap PUT实现
  1. 判断数hash组是否为空,为空则resize() 数组
  2. 通过hash定位元素在数组中的索引,判断数组中该节点是否为NULL
  3. 为NULL则新建Node进行赋值
  4. 不为NULL,则有:
    1. hash值是否相等&&key值是否相等,相等则值用新值覆盖
    2. 节点是否为红黑树,是则插入红黑树
    3. 否,则为链表,遍历链表,
      1. key如果在链表中存在,则用新值覆盖,
      2. 如果不存在,则新节点插入链表尾部
      3. 判断链表长度是否超过TREEIFY_THRESHOLD:8
      4. 超过则调用转换方法
      5. 转换方法中,节点数超过 MIN_TREEIFY_CAPACITY 则链表转红黑树
      6. 小于则扩容
  5. ++modCount和++size
  6. size 大于threshold(扩容临界值),则进行扩容
/**
 * 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) {
    return putVal(hash(key), key, value, false, true);
}
/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
HashMap GET实现
  1. 计算key的hash
  2. hash计算数组的index
  3. 找出index的节点
    1. 第一个节点hash和key相等,直接返回first node
    2. 不相等
      1. 是否为红黑树,是则在树中查找
      2. 链表,则遍历链表,找出节点
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
HashMap的扩容
  1. 为什么数组扩容,每次*2呢?
    • 首先node对应数组的索引为hash & (length-1)
    • 扩容后,node对应的数组的索引为hash & (2*length -1)
    • 例如hash值为21 (10101),则有21 & 15 = 5,扩容后,21 & 31 = 21 = (5+16)
    • 例如hash值为13 (01101),则与13 & 15 = 13 ,扩容后,13 & 31 = 13 = (13)
    • 规律为 hash值对应的数组length的首位位数如果为1,则扩容后的数组位置为原index+length
    • 首位位数如果为0,则扩容后的数组位置不变,WOW,是不是很巧妙,扩容过后,既可以减少节点数组位置的转换,同时也可将部门节点按照原数组位置进行扩容(+length)
/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
4.2.ConcurrentHashMap和HashTable
  • ConcurrentHashMap

    在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现;

    JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作;

    分析 JAVA8 中的PUT源码:

    1. 如果table为空,则初始化数组,重新循环
    2. 如果数组位置节点为空,则CAS创建节点
    3. 如果当前正在扩容,则获取扩容后的数组,重新循环
    4. 使用synchronized 对节点Node 进行加锁
    5. 如果node节点为链表,则在链表尾部插入节点
    6. 如果为红黑树,则插入红黑树中
    7. 最后判断是否需要在转换成功红黑树
    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @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 {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    
    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
    

get 的源码为:可以看到并没有进行锁操作

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    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;
        }
        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;
}
  • HashTable

    HashTable的get、put等方法都使用synchronized关键字来实现的,是一种悲观锁的实现

    public synchronized V put(K key, V value) 
    public synchronized V get(Object key)
    
4.3.CopyOnWriteArrayList

适用场景:

  • 读操作可以尽可能的快,而写即使慢一些也没关系
  • 读多写少
  • 对读写锁规则的升级:读取是完全不用加锁的,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取;
  • 含义:当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。
  • 迭代期间允许修改集合内容。

缺点:

  • 写时内存占用问题 (Copy副本)
  • 在元素较多或者复杂的情况下,复制的开销很大
  • 数据一致性问题

实现:

写时通过ReentrantLock锁实现加锁过程(Copy副本并添加元素)

5.队列

在这里插入图片描述

5.1 阻塞队列
  • ArrayBlockingQueue:有界队列,内部使用数组实现,使用ReentrantLock实现线程安全
  • LinkedBlockingQueue:无界队列,最大容量Integer.MAX_VALUE;
  • SynchronousQueue: 容量为0,没有一个地方来暂存元素,每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
  • PriorityBlockingQueue:支持优先级的无界阻塞队列,自定义compareTo()实现队列中元素的先后;
  • DelayQueue:无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,拥有了比较和排序的能力
5.2 阻塞队列是如何实现的?

ArrayBlockingQueue 的源码如下:

put方法:使用ReentrantLock实现加锁,notFull 队列未满Condition实现阻塞等待

offer: 使用ReentrantLock实现加锁,队列已满则返回false,不阻塞等待

take方法: 使用ReentrantLock实现加锁,notEmpty 队列不为空 Condition实现阻塞等待

poll: 使用ReentrantLock实现加锁,队列为空返回Null,不阻塞等待

/**
 * Inserts the specified element at the tail of this queue if it is
 * possible to do so immediately without exceeding the queue's capacity,
 * returning {@code true} upon success and throwing an
 * {@code IllegalStateException} if this queue is full.
 *
 * @param e the element to add
 * @return {@code true} (as specified by {@link Collection#add})
 * @throws IllegalStateException if this queue is full
 * @throws NullPointerException if the specified element is null
 */
public boolean add(E e) {
     if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
}
public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
}

/**
 * Inserts the specified element at the tail of this queue if it is
 * possible to do so immediately without exceeding the queue's capacity,
 * returning {@code true} upon success and {@code false} if this queue
 * is full.  This method is generally preferable to method {@link #add},
 * which can fail to insert an element only by throwing an exception.
 *
 * @throws NullPointerException if the specified element is null
 */
public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

/**
 * Inserts the specified element at the tail of this queue, waiting
 * for space to become available if the queue is full.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

/**
 * Inserts the specified element at the tail of this queue, waiting
 * up to the specified wait time for space to become available if
 * the queue is full.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    checkNotNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            if (nanos <= 0)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        lock.unlock();
    }
}

ConcurrentLinkedQueue:

  • 使用乐观锁的机制实现线程安全
  • 循环+CAS来进行入队列
/**
 * Inserts the specified element at the tail of this queue.
 * As the queue is unbounded, this method will never return {@code false}.
 *
 * @return {@code true} (as specified by {@link Queue#offer})
 * @throws NullPointerException if the specified element is null
 */
public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        if (q == null) {
            // p is last node
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}
5.3 选择合适的队列
  • 需要排序或者延迟则选择PriorityBlockingQueue、DelayQueue
  • 是否需要存储,根据存储的大小选择对应的有界队列或无界队列,无需存储则SynchronousQueue
  • 如果不确定容量则更加是否能够扩容选择队列
  • 根据对应的内存结构和加锁粒度及其锁的分类选择队列,对存储空间有限制的可以选择ArrayBlockingQueue,对并发情况一般可以选择无阻塞队列ConcurrentLinkedQueue,

6.CAS

在这里插入图片描述

7.ThreadLocal

7.1 使用场景
  • ThreadLocal用来保存每个线程独享的对象,可以解决线程安全的问题

  • ThreadLocal用来保存每个线程独有的信息,提供其他方法方便获取,例如用户登录信息等等

  • 内存泄漏问题:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例

    • 当threadLocal已经不被使用,但是一直存活不终止,即使将threadLocal的引用置为NULL,但是当threadLocal中的Value一直不回收,则引用链就一致存在,无法回收
    • 即使threadLocal虽然为WeakReference,value不回收则不会回收
    • 使用threadLocal中的remove方法解决此问题

8.AQS (AbstractQueuedSynchronizer)

AQS的作用:AQS 是一个用于构建锁、同步器等线程协作工具类的模板框架

8.1 AQS的实现有哪些?
  • Semaphore
  • CountDownLatch
  • ReentrantLock
    • CyclicBarrier
  • ReentrantReadWriteLock
  • ThreadPoolExcutor
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值