并发编程-学习总结(上)

news2024/11/25 12:29:48

目录

1、线程基础

1.1、线程实现方法

1.2、如何正确停止线程

1.3、Java线程的六种状态

1.4、wait/notify/notifyAll注意事项

1.4.1、为什么 wait 、notify、notifyAll必须在 synchronized 保护的同步代码中使用?

1.4.2、为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

1.4.3、wait/notify 和 sleep 方法的异同?

1.5、实现生产者-消费者模式的方法

1.5.1、BlockingQueue 

1.5.2、Condition 

1.5.3、 wait/notify

2、线程安全

2.1、3种线程安全问题

2.1.1、运行结果问题

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

2.1.3、活跃性问题

2.2、哪些场景需要注意线程安全问题        

2.3、为什么多线程会带来性能问题

3、线程池

3.1、线程池的好处

3.2、创建线程池各参数的含义

3.3、线程池执行流程

3.4、默认四种拒绝策略

3.4.1、拒绝时机

3.4.2、拒绝策略

3.4、常见线程池

3.5、ForkJoinPool

3.5.1、功能

3.5.2、父类

3.5.3、自定义Task类

3.5.4、创建线程池

3.5.5、任务运行原理

3.6、线程池常用的阻塞队列

3.6、为什么不要通过Executors创建线程池

3.7、合适的线程数量

3.7.1、CPU密集型任务:

3.7.2、耗时IO型任务: 

3.8、如何正确关闭线程池

3.9、如何设计线程池

4、锁

4.1、锁分类

4.1.1、偏向锁/轻量级锁/重量级锁

4.1.2、可重入锁/非可重复锁

4.1.3、共享锁/独占锁

4.1.4、公平锁/非公平锁

4.1.5、悲观锁/乐观锁

4.1.6、自旋锁/非自旋锁

4.1.7、可中断锁/不可中断锁

4.2、synchronized的monitor锁

4.2.1、同步代码块

4.2.2、同步方法

4.3、如何选择synchronized和Lock

4.3.1、相同点

4.3.2、不同点

4.3.3、如何选择

4.4、Lock的常用方法(ReentrantLock)

4.5、公平锁和非公平锁

4.5.1、为什么有非公平锁

4.6、读写锁ReadWriteLock

4.7、读写锁升降级

4.7.1、非公平锁下的读写操作

4.7.2、读写锁的降级

4.7.3、不支持读写锁的升级:读锁->写锁。

4.7.4、锁降级中读锁的获取是否必要呢?

4.8、自旋锁

4.9、JVM对synchronized的优化

4.9.1、自旋

4.9.2、锁消除

4.9.3、锁粗化

4.9.4、锁升级策略

4.10、锁升级策略

4.10.1、无锁

4.10.2、无锁->偏向锁

4.10.3、偏向锁升级为轻量级锁

4.10.4、轻量级锁->重量级锁

4.10.5、适用场景

4.10.6、锁的优缺点比较

5、并发容器

5.1、HashMap为什么线程不安全

5.2、ConcurrentHashMap

5.3、为什么Map桶中超过8个才转为红黑树

5.4、ConcurrentHashMap 和 Hashtable 的区别

5.5、CopyOnWriteArrayList 

5.5.1、适用场景

5.5.2、特点

5.5.3、原理

5.5.4、缺陷

6、阻塞队列(BlockingQueue )

6.1、阻塞队列

6.2、常用方法

6.3、常见的阻塞队列

6.4、并发安全原理

6.5、如何选择阻塞队列

6.5.1、线程池对应的阻塞队列

6.5.2、如何选择

7、原子类

7.1、原子类如何利用CAS保证线程安全

7.1.1、原理

7.1.2、原子类

7.2、如何解决AtomicInteger在高并发下的性能问题

7.2.1、原因

7.2.2、解决方案 

7.2.3、适用场景

7.3、原子类和volatile

7.4、AtomicInteger 和 synchronized 的异同点?

7.5、Java 8 中 Adder 和 Accumulator 有什么区别?

8、ThreadLocal

8.1、适用场景

8.2、ThreadLocal是用来解决共享资源的多线程访问问题吗?

8.3、多个ThreadLocal在Thread的threadlocals里是如何存储的?

8.4、内存泄露

8.4.1、key的内存泄露

8.4.2、value的内存泄露

8.4.3、避免内存泄露


1、线程基础

1.1、线程实现方法

  • 实现Runnable接口(无返回值)
  • 继承Thread类
  • 线程池创建线程
  • 实现Callable接口(有返回值)

1.2、如何正确停止线程

  • interrupt()方法:请求中断,设置中断标志(推荐)

        若线程处于正常状态,则会将该线程的中断标志设置为true,之后线程将继续正常运行,不受影响。
        若线程处于被阻塞状态,或由正常状态变为阻塞状态(sleep()、wait()),线程将立刻退出被阻塞状态,并抛出一个InterruptedException异常。可以通过捕获异常然后return。

  • stop():停止线程(不推荐)

        直接把线程停止,影响数据完整性

  • suspend():暂停线程(不推荐)

        暂停线程,但不会释放锁,可能会导致死锁

  • resume():恢复因suspend挂起的线程(不推荐)
  • volatile变量(推荐)

        在存在循环的线程run方法中进行判断是否退出线程
        但在某些特殊的情况下,比如线程被长时间阻塞的情况,就无法及时感受中断,所以 volatile 是不够全面的停止线程的方法。

1.3、Java线程的六种状态

  • New(新建):已创建线程,但还未执行start()
  • Runnable(就绪):对应操作系统的Running和Ready状态(CPU分配时间片)
  • Blocked(被阻塞):进入 synchronized 保护的代码时没有抢到 monitor 锁
  • Waiting(等待):执行没有设置timeout参数的Object.wait()、Thread.join()方法
  • Timed Waiting(计时等待):执行设置了timeout参数的Object.wait()、Thread.join()、Thread.sleep()方法
  • Terminated(终止):run()方法执行完毕;或者出现未捕获的异常,导致意外终止

注意:waiting和Timed Waiting两种状态的线程,在被其他线程使用notify/notifyAll唤醒时,会先进入到Blocked状态。因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

1.4、wait/notify/notifyAll注意事项

1.4.1、为什么 wait 、notify、notifyAll必须在 synchronized 保护的同步代码中使用?

        预防饥饿线程:在以下消费者-生产者模式代码中,生产者执行give方法,消费者执行take方法。如果执行顺序如下所示,且没有其他生产者,那么可能导致消费者线程长久处于wait()状态。

1.4.2、为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

        每个对象都有一个monitor锁,锁是对象级别的,而非线程级别的操作。wait、notify、notifyAll都是锁级别的操作,所以将这三个方法定义在Object类中(Object是所有对象的父类)

        而sleep的作用是休眠线程,是线程级别的,不会操作对象锁,所以直接放在Thread类中。

1.4.3、wait/notify 和 sleep 方法的异同?

相同点:

        都会阻塞线程、都可以响应interrupt中断--->抛出异常
不同点:

  1. wait/notify必须放在synchronized中,sleep不用;
  2. wait阻塞线程会释放锁,sleep不会释放锁;
  3. wait的线程必须等待notify唤醒,而sleep可设置休眠时间恢复自动唤醒线程;
  4. wait/notify是Object类的方法,sleep是Thread类方法。

1.5、实现生产者-消费者模式的方法

1.5.1、BlockingQueue 

public static void main(String[] args) {
  BlockingQueue<Object> queue = new ArrayBlockingQueue<>(10);
 Runnable producer = () -> {
    while (true) {
          queue.put(new Object());
  }
 };
new Thread(producer).start();
new Thread(producer).start();

Runnable consumer = () -> {
      while (true) {
           queue.take();
	}
};
new Thread(consumer).start();
new Thread(consumer).start();
}

1.5.2、Condition 

public class MyBlockingQueueForCondition {
   private Queue queue;
   private int max = 16;
   private ReentrantLock lock = new ReentrantLock();
   private Condition notEmpty = lock.newCondition();
   private Condition notFull = lock.newCondition();

   public MyBlockingQueueForCondition(int size) {
       this.max = size;
       queue = new LinkedList();
   }
   public void put(Object o) throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == max) {
               notFull.await();
           }
           queue.add(o);
           notEmpty.signalAll();
       } finally {
           lock.unlock();
       }
   }

   public Object take() throws InterruptedException {
       lock.lock();
       try {
           while (queue.size() == 0) {
               notEmpty.await();
           }
           Object item = queue.remove();
           notFull.signalAll();
           return item;
       } finally {
           lock.unlock();
       }
   }
}

1.5.3、 wait/notify

class MyBlockingQueue {
   private int maxSize;
   private LinkedList<Object> storage;

   public MyBlockingQueue(int size) {
       this.maxSize = size;
       storage = new LinkedList<>();
   }

   public synchronized void put() throws InterruptedException {
       while (storage.size() == maxSize) {
           wait();
       }
       storage.add(new Object());
       notifyAll();
   }

   public synchronized void take() throws InterruptedException {
       while (storage.size() == 0) {
           wait();
       }
       System.out.println(storage.remove());
       notifyAll();
   }
}

2、线程安全

2.1、3种线程安全问题

2.1.1、运行结果问题

        多线程情况下,多次执行同一段代码,运行结果不同。

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

        新建一个线程用于对象初始化,在初始化操作完成之前,另一个线程使用该对象,对象的属性可能就不是预期的初始化后的值。

2.1.3、活跃性问题

  • 死锁:两个线程互相持有对方需要的资源(锁),但又互不相让。
  • 活锁:线程未阻塞,但线程运行异常,报错时无限重试。导致线程处于活跃状态却永远得不到结果。
  • 饥饿:一个线程长时间在等待资源:比如1个低优先级的线程,和1000000个高优先级线程(不断生产),此时这个低优先级线程就可能没机会运行,导致饥饿。

2.2、哪些场景需要注意线程安全问题        

  • 访问共享变量或资源
  • 多线程下依赖时序的操作
if (map.containsKey(key)) {
    map.remove(obj)
}
  • 对方没有声明自己是线程安全的(比如ArrayList)

2.3、为什么多线程会带来性能问题

  • 上下文切换:上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行
  • 缓存失效:线程切换后,CPU高速缓存会失效
  • 多线程间协作开销大

3、线程池

3.1、线程池的好处

  • 池中有固定线程,复用线程,避免频繁创建销毁线程,减少线程生命周期的开销,提高性能
  • 根据配置和任务数量控制线程数量,避免线程过多导致资源浪费
  • 统一管理池中的线程

3.2、创建线程池各参数的含义

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

->

new ThreadPoolExecutor(核心线程数,最大线程数,非核心线程存活时间,时间单位,缓冲队列,创建线程使用的工厂,任务拒绝策略)。

3.3、线程池执行流程

new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

核心线程数->缓冲队列->最大线程数->拒绝策略。

流程:

  1. 核心线程数未满时,每次来任务就会创建新的核心线程来执行任务;
  2. 核心线程满了,队列未满,则将任务放到队列中;
  3. 核心线程满了,队列满了,线程未达到最大线程数,创建非核心线程数执行线程;
  4. 核心线程满了,队列满了,线程已达到最大线程数,此时线程池工作饱和,执行线程池的拒绝策略。 

3.4、默认四种拒绝策略

3.4.1、拒绝时机

  1. 调用shutdown等方法关闭线程池后,正在执行线程池内剩余的线程,此时再向线程池提交任务,就会遭到拒绝。
  2. 线程池已经没有能力处理新的任务:队列已满、线程池已满。

3.4.2、拒绝策略

  • AbortPolicy:直接抛出异常
  • DiscardPolicy:直接丢弃新来的任务,用户无法感知
  • DiscardOldestPolicy:丢弃队列中存活时间最长的任务,用户无法感知
  • CallerRunsPolicy:把新来的任务直接交个调用者线程运行

3.4、常见线程池

FixedThreadPool:固定线程池:核心线程数 = 最大线程数
CachedThreadPool:可缓存线程池:线程数可以无限增加,并可对闲置线程进行回收
ScheduledThreadPool:定时/周期性线程池:支持定时或周期性执行任务(多数情况下可用Timer类代替)
SingleThreadExecutor:单线程线程池

3.5、ForkJoinPool

3.5.1、功能

        将一个大任务拆分成多个小任务后,使用fork可以将小任务分发给其他线程同时处理,使用join可以将多个线程处理的结果进行汇总

3.5.2、父类

        RecursiveAction:用于没有返回结果的任务(类似Runnable)
        RecursiveTask:用于有返回结果的任务(类似Callable)

3.5.3、自定义Task类

class CountTask extends RecursiveTask<Integer> { 
    private static final int THREADHOLD = 2;
    private int start;
    private int end;
    /**
     * @Description: 最小的一个任务单元,以及它要处理的范围
     * @param:
     * @return:
     */
    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }
 
    @Override
    protected Integer compute() {
        int sum = 0;
        boolean canCompute = (end - start) <= THREADHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            //继续将任务细分
            int middle = (start + end) / 2;
            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);
            //执行子任务
            leftTask.fork();
            int rightValue = rightTask.compute();
            //合并结果
            sum = leftTask.join() + rightValue;
        }
        return sum;
    }

3.5.4、创建线程池

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool(2);
        CountTask sumTask1 = new CountTask(0, 4);
 
        ForkJoinTask task1 = forkJoinPool.submit(sumTask1);
        System.out.println(task1.get());
    }

3.5.5、任务运行原理

3.6、线程池常用的阻塞队列

LinkedBlockingQueue:无界队列
    核心线程数固定的线程池,需要一个没有容量限制的阻塞队列。
        FixedThreadPool、SingleThreadExector
SynchronousQueue
    容量为0,一旦有任务进入队列,就立刻将任务提交给线程池,所以需要线程池有足够的线程数
        CachedThreadPool
DelayedWorkQueue
    该队列把任务按照延时时间长短进行排序,方便任务进行
        ScheduledThreadPool 

3.6、为什么不要通过Executors创建线程池

  • 内存溢出:newFixedThreadPool、newSingleThreadExecutor
  • OOM:newCachedThreadPool
  • 通过使用new ThreadPoolExecutor()手动创建线程池,根据实际情况选择合适的队列、拒绝策略、线程数等,避免资源耗尽的风险。

3.7、合适的线程数量

目的:合适的线程数量可以更好地使用CPU和内存等资源,从而最大限度地提升程序的性能。

3.7.1、CPU密集型任务:

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

计算任务重,单个任务耗时长,如果线程数过多,会造成不必要的上下文切换,降低系统性能。

3.7.2、耗时IO型任务: 

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

        IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

3.8、如何正确关闭线程池

  • shutdown()-推荐

        拒绝新任务的提交,执行完当前线程池中正在执行的任务及队列中的任务后,关闭线程池。

  • isShutdown()

        判断线程池是否已经开始了关闭工作;
        当调用shutdown()或shutdownNow()方法后返回为true;
        为true仅表示关闭流程开始,不一定已经关闭。

  • isTerminated()

        线程池已关闭,且池中和队列中所有线程已执行完毕时,返回true

  • awaitTermination()

        线程等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,那么方法就返回 true,否则超时返回 fasle。

  • shutdownNow()-推荐

        给所有线程发送中断信号,然后会将队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作。

3.9、如何设计线程池

  • 考虑线程池中的个数是固定的还是动态变化的。
  • 考虑队列大小:无界队列(可能导致内存耗尽);有界队列(队列满了拒绝策略)
  • 每次提交新任务,是直接放入队列,还是开启新线程
  • 没有任务时,线程是睡眠一段时间还是进入阻塞

4、锁

4.1、锁分类

4.1.1、偏向锁/轻量级锁/重量级锁

        特指synchronized锁的状态。在对象头中的mark word来表明锁的状态。

        偏向锁:不加锁,加标记。

        轻量级锁:不阻塞,通过自旋和CAS获取锁。

        重量级锁:阻塞,互斥锁。

        锁升级:无锁→偏向锁→轻量级锁→重量级锁

4.1.2、可重入锁/非可重复锁

        已经持有锁,是否能在不释放当前锁的情况下,再次获取当前锁。

4.1.3、共享锁/独占锁

        是否可被多个线程同时获取锁。典型为读写锁。

4.1.4、公平锁/非公平锁

        线程是否按照先来先到的原则获取锁。

4.1.5、悲观锁/乐观锁

        悲观锁:获取资源前,必须先获得锁,锁住资源,synchronized。
        乐观锁:获取资源前,不用获得锁,不会锁住资源,CAS。

        适用场景:
        悲观锁:并发写入多、临界区代码复杂、竞争激烈等场景。
        乐观锁:大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。

4.1.6、自旋锁/非自旋锁

        是否利用循环,不停地尝试获取锁。

4.1.7、可中断锁/不可中断锁

        可中断锁:一旦申请锁,就只能拿到锁后才能做其他操作(synchronized)。
        不可中断锁:线程申请锁后,可中断获取锁的流程(ReentrantLock)。

4.2、synchronized的monitor锁

javac xxx.java:产生xxx.class
javap -verbose xxx.class:生成class文件的反汇编内容

4.2.1、同步代码块

        monitorenter(加锁)、monitorexit(释放锁)指令:每个对象头有一个记录着锁次数的计数器

4.2.2、同步方法

ACC_SYNCHRONIZED标识符:如果方法有该标识符,那么在调用方法时会先判断获取对象的monitor锁,方法执行之后释放monitor锁。

4.3、如何选择synchronized和Lock

4.3.1、相同点

  • 保证线程安全;
  • 保证可见性(前一个加锁操作结果对后一个加锁操作可见);
  • 可重入锁。

4.3.2、不同点

synchronized

  1. 自动加锁和释放锁。
  2. 加解锁顺序:加锁Lock1->加锁Lock2->解锁Lock2->解锁Lock1。
  3. 申请锁后,只有获得锁后才能继续其他操作,否则会永远等待。
  4. 只能被一个线程拥有。
  5. 不可设置公平锁/非公平锁。
  6. Java5之前,性能低;Java6之后,增加自旋锁、轻量级锁、偏向锁,性能提高

Lock

  1. 必须显示lock()和unlock()
  2. 加解锁顺序:可自由控制加解锁顺序
  3. 申请锁后,可中断退出。
  4. 可被多个线程拥有。
  5. 可设置公平锁/非公平锁。

4.3.3、如何选择

  1. 最好的情况是两者都不用,优先使用并发工具包来加解锁;
  2. 尽量使用synchronized由于lock,因为synchronized可自动加解锁;
  3. 需要使用到尝试获取锁、锁中断、超时功能时,才使用Lock。

4.4、Lock的常用方法(ReentrantLock)

  • lock()

        获取锁,不能被中断,死锁->永久等待。必须在finally中unlock解锁。

  • tryLock()

        尝试获取锁,获取成功,返回true;否则返回false。可根据是否获得锁决定后续程序的行为。

  • tryLock(long time, TimeUnit unit)

        与tryLock类似,区别在于获取锁失败后会等待一段时间,一段时间后还获取不到锁,再返回false。

  • lockInterruptibly()

        获取锁,能获取则直接返回,不能获取则一直尝试获取锁,可响应中断。

  • unlock()

        释放锁,必须写在finally块中。

4.5、公平锁和非公平锁

  • 公平锁:按照线程请求顺序分配锁。慢,吞吐量小,唤醒阻塞线程开销大。
  • 非公平锁:在一定情况下,允许插队,但不是随意插队。快,吞吐量大,有可能产生饥饿线程。
  • 通过new ReentrantLock(false) 来设置公平锁/非公平锁。

4.5.1、为什么有非公平锁

示例:A,B,C三个线程,A持有锁,B阻塞等待获取锁
A释放锁时,刚好C来获取锁,非公平锁就可把锁给C。
(B处于阻塞状态,唤醒B需要很大开销)
新线程都会先获取锁,获取不到,再讲线程放到队列尾,之后按顺序唤醒线程获取锁。

4.6、读写锁ReadWriteLock

  • 读读共享,读写互斥、写写互斥。读锁可以被多个线程获取,而写锁不会。
  • 读写锁适用于读多写少的场景。

4.7、读写锁升降级

4.7.1、非公平锁下的读写操作

  • 写锁允许插队:在没有其他线程持有读锁、写锁的时候,能插队成功;插队失败进行等待队列。
  • 读锁不允许插队(防止饥饿线程)

4.7.2、读写锁的降级

        在不释放写锁的情况下,直接获取读锁,然后再释放写锁。(区别于直接释放写锁之后再获取读锁---中间有时间差,其他线程可能获得锁)
        长时间持有写锁,浪费资源,此时会禁止其他线程的读取。

4.7.3、不支持读写锁的升级:读锁->写锁。

场景:升级时,需要等待所有读锁都释放。
示例:A、B两个线程持有读锁,且都想升级为写锁,且A、B都在等待对方释放读锁,此时就会发生死锁现象。

4.7.4、锁降级中读锁的获取是否必要呢?

        答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

4.8、自旋锁

在java1.5及以上的并发包中,AtomicXXX等原子类基本都是自旋锁的实现。

定义

  • 自旋锁:不会放弃CPU时间片,不停地尝试获取锁,直到获取成功为止。
  • 非自旋锁:获取锁失败后,线程休眠、释放CPU资源。

好处
        阻塞和唤醒线程需要高昂开销,自旋锁可避免上下文切换,节省线程状态切换带来的开销。
缺点
        如果锁一直被其他线程持有,那么就会一直自旋,白白浪费处理器资源。

4.9、JVM对synchronized的优化

自旋、锁消除、锁粗化、偏向锁、轻量级锁

4.9.1、自旋

        自适应的自旋锁:会根据最近自旋尝试的成功率、失败率等因素来决定自旋时间,以解决自旋锁长时间进行无用自旋的问题。

4.9.2、锁消除

        如果某些对象不可能被其他对象访问到,就可以把他们当成栈上数据,只能被当前线程访问,绝对线程安全。此时,编译器就会把synchronized给消除,省去加锁和解锁的开销。

4.9.3、锁粗化

举例说明:
        有三个连续的同步代码块,每执行一个同步代码块时,都要进行加锁和解锁操作。因为连续,此时可以把三个同步代码块,合并为一个同步代码块,增大了同步区域,即为锁粗化。
        减少了中间无意义的加锁和解锁过程。
        如果循环场景使用锁粗化功能,会导致其他线程长时间无法获得锁,所以锁粗化功能仅适用于非循环的场景。

4.9.4、锁升级策略

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

4.10、锁升级策略

  • 锁升级:无锁->偏向锁->轻量级锁->重量级锁
  • Java对象头 = Mark Word + 指向类的指针 + 数组长度(只有数组对象才有)
  • Mark Word记录对象和锁有关的信息,根据锁标志位来确定当前对象是什么锁状态。

4.10.1、无锁

        锁标志位 = 01,是否偏向锁 = 0,存储对象的hashcode值。

        当对象没有被当成锁时,Mark Word记录的是对象的hashcode。

4.10.2、无锁->偏向锁

        锁标志位 = 01,是否偏向锁 = 1,存储占有偏向锁线程的线程id。

        当对象被当成同步锁并且线程A通过CAS抢到锁时,进入偏向锁状态,将线程A的线程id写入Mark Word。

        当线程A再次试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值等于线程A的id,就可以直接执行同步锁的代码块;

        当线程B来试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值不等于线程B的id,线程B就会通过CAS去尝试获取锁:如果获取成功,则将Mark Word中的偏向锁的线程id修改为线程B的id;如果获取失败,则将同步锁升级为轻量级锁;

4.10.3、偏向锁升级为轻量级锁

        出现锁竞争(CAS获取锁失败)时,偏向锁升级为轻量级锁。

        锁标志位 = 00,存储指向栈中锁记录的指针。

        JVM在当前线程的栈帧中开辟一块独立空间,用以保存指向对象锁Mark Word的指针,同时Mark Word保存指向栈帧中独立空间的指针。

        如果保存成功,则当前线程获取该对象的轻量级锁,执行同步代码;

        如果保存失败,则当前线程自旋,不断尝试获取锁(尝试N次,由JVM决定),如果抢锁成功,执行同步代码;如果抢锁失败,则将轻量级锁升级为重量级锁。

4.10.4、轻量级锁->重量级锁

        轻量级锁自旋超过JVM指定次数时,升级为重量级锁。

        锁标志位 = 10,存储指向重量级锁的指针。

        未抢占到锁的线程会被阻塞,抢到锁的线程在释放锁的同时会唤醒那些阻塞的线程。

重量级锁下,线程之间的切换需要从用户态到内核态,成本较高。

4.10.5、适用场景

  • 偏向锁:只有一个线程在临界区执行,无需操作系统介入;
  • 轻量级锁:多个线程交替进入临界区,竞争不激烈,就算有冲突,线程自旋几次就能获取锁;
  • 重量级锁:多个线程出现激烈竞争。

4.10.6、锁的优缺点比较

5、并发容器

5.1、HashMap为什么线程不安全

  • 扩容期间取出的值不准确

        扩容时会创建一个新的Entry数组,然后遍历原Entry数组,把所有的Entry重新Hash到新数组。
        在这个过程中另一个线程从HashMap取数据,就有可能取出null。

  • 循环链表(死循环)

        原因:并发+头插
        Jdk1.7在高并发下,HashMap产生死循环,造成CPU100%负载:HashMap在存储时,若size超过当前最大容量*负载因子时,会增加桶的数目,进行HashMap数组扩容(resize()),在resize过程中,会调用transfer()方法将链表转换成新链表,在多线程情况下可能导致链表回路,从而导致死循环。
        扩容时,会调用transfer方法将旧的元素重新hash后放到新的table中。

        Jdk1.8中HashMap的put元素操作由1.7的头插改为尾插,避免了死循环问题。

5.2、ConcurrentHashMap

  • JDK1.7

        Segment锁分段技术
        segment数组(存储锁)+hashEntry数组
        采用锁分段技术来保证线程安全,将数据分为一段一段的,给每段数据各分配一把锁,不同数据段之间的读写互不影响,效率高
        最大并发个数就是 Segment 的个数,默认是 16

  • JDK1.8

        Node数组+链表/红黑树
        并发控制使用 synchronized 和 CAS 来操作
        synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍

5.3、为什么Map桶中超过8个才转为红黑树

  •         链表长度>=8,且容量>=MIN_TREEIFY_CAPACITY(默认为64)时,链表转为红黑树
  •         链表长度<=6时,红黑树转为链表

单个TreeNode需要占用的空间大约是普通Node的两倍。所以使用红黑树,就是用空间换时间。

泊松分布:
在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率

5.4、ConcurrentHashMap 和 Hashtable 的区别

  • Hashtable

        synchronized -锁整个Map
        不允许在迭代期间修改内容

  • ConcurrentHashMap

        CAS + synchronized + Node-锁一个Node
        允许在迭代期间修改内容

5.5、CopyOnWriteArrayList 

5.5.1、适用场景

  • 对读操作性能有要求,对写操作性能没要求;
  • 读多写少。

5.5.2、特点

        写入不会阻塞读取操作,可以在写入的同时读取。打破了读写锁读写互斥的限制

5.5.3、原理

  1. 在写入操作时,先复制一份容器的副本;
  2. 在副本中进行写操作,并行的读操作依然在原容器中进行;
  3. 修改完成后,将原容器的引用指向副本容器。

5.5.4、缺陷

  1. 占用内存,在写操作时,会同时存在两个对象占用内存;
  2. 容器元素较多时,复制开销大;
  3. 写入操作会先写入副本,对其他线程来说,数据不能保证可见性。

6、阻塞队列(BlockingQueue )

6.1、阻塞队列

特点:线程安全。
        阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。(take()、put())

适用场景:常用于生产者-消费者模式

常用实现类
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue

6.2、常用方法

抛出异常
        add:往队列里添加一个元素,如果队列满了,抛出异常;
        remove:删除元素,如果队列为空,抛出异常;
        element:返回队列的头部节点,但不删除,如果队列为空,抛出异常。
返回结果但不抛出异常
        offer:插入一个元素,插入成功返回true,插入失败(队列已满)返回false;
        poll:移除并返回队列头节点,队列为空,则返回null;
        peek:返回队列的头节点但不删除,队列为空,则返回null。
阻塞
        put:插入一个元素,如果队列满了,则阻塞;
        take:获取并移除队列头节点,如果队列为空,则阻塞。

6.3、常见的阻塞队列

  • ArrayBlockingQueue

        底层为数组,有界队列,利用ReentrantLock实现线程安全。可设置容量和是否公平。

  • LinkedBlockingQueue

        底层为链表,无界队列。

  • SynchronousQueue

        该队列容量为0,内部不存放任何数据吗,数据直接传递;
        每次放数据都会阻塞,直到有消费者来取数据;
        每次取数据都会阻塞,直到有生产者来存放数据。

  • PriorityBlockingQueue

        自定义排序(元素必须支持比较大小),支持优先级的无界阻塞队列。
        自定义类实现compareTo()方法指定元素排序规则;
        初始化算构造器参数comparator指定排序规则。

  • DelayQueue

        具有延迟功能的无界队列;
        根据延迟时间的长度,决定元素在队列中的位置;
        越靠近队列头的代表越早过期。

6.4、并发安全原理

  • 阻塞队列(ArrayBoockingQueue为例)
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;

        读操作和写操作都需要获得lock独占锁才能进行下一步操作;
        读操作时如果队列为空,线程就会进入到读线程专属的 notEmpty 的 Condition 的队列中去排队,等待写线程写入新的元素;
        写操作时如果队列已满,此时写操作的线程会进入到写线程专属的 notFull 队列中去排队,等待读线程将队列元素移除并腾出空间。

  • 非阻塞队列(ConcurrentLinkedQueue为例)

        死循环 + 乐观锁

6.5、如何选择阻塞队列

6.5.1、线程池对应的阻塞队列

根据线程池的容量和队列的容量来选择

6.5.2、如何选择

  1. 功能:排序、延迟等;
  2. 容量:容量固定、容量无限、容量为0;
  3. 能否扩容:考虑是否需要动态扩容;
  4. 内存结构:数组、链表;
  5. 性能:SynchronousQueue(直接传递,无需保存,性能高);LinkedBlockingQueue(两把锁)、ArrayBlockingQueue(一把锁),并发程度高时,LinkedBlockingQueue性能更好。

7、原子类

7.1、原子类如何利用CAS保证线程安全

7.1.1、原理

循环 + CAS;        Unsafe 类是 CAS 的核心类。

CAS操作包括了3个操作数(多个操作,原子性由硬件层面保证):
    需要读写的内存位置(V)           
    进行比较的预期值(A)
    拟写入的新值(B)
CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

Unsafe类中的getAndAddInt方法:
public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
   do {
       var5 = this.getIntVolatile(var1, var2);
   } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
   return var5;
}

7.1.2、原子类

  • Atomic* 基本类型原子类

        AtomicInteger、AtomicLong、AtomicBoolean

以AtomicInteger为例:
public final int get() //获取当前的值
public final int getAndIncrement() //获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)
  • Atomic*Array 数组类型原子

        AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

        数组里的元素,都可以保证原子性

  • Atomic*Reference 引用类型原子

        AtomicReference、AtomicStampedReference、AtomicMarkableReference

        让一个对象保证原子性

  • Atomic*FieldUpdater 升级类型原子

        AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

        把一个普通变量升级成原子类。
        场景:
        1、历史代码变量,已经被声明为普通变量并广泛应用,修改成本高,可升级成原子类保证线程安全;
        2、每天大部分时间,该变量不需要保持原子性,只有在少数一两次操作时,需要保证原子性。(原子类比普通变量更耗费资源)

  • Adder 累加器

        LongAdder、DoubleAdder

  • Accumulator 积累器    

        LongAccumulator、DoubleAccumulator

7.2、如何解决AtomicInteger在高并发下的性能问题

7.2.1、原因

高并发下,本地内存和共享内存间的数据同步会耗费大量资源。

7.2.2、解决方案 

LongAdder:分段累加
内部有两个参数参与计数:第一个叫作 base,它是一个变量,第二个是 Cell[] ,是一个数组。
 

低并发时:base变量
可直接将累加结果修改到base变量上;

高并发时:Cell数组
        LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中,就大大减少了刚才的 flush 和 refresh,以及降低了冲突的概率。
        最后会执行 LongAdder.sum() ,把各个线程里的 Cell 累计求和,并加上 base,形成最终的总和。

7.2.3、适用场景

  • LongAdder:场景仅仅是需要用到加和减操作的话。
  • AtomicLong:需要利用 CAS 比如 compareAndSet 等复杂操作。

7.3、原子类和volatile

  • volatile:保证变量可见性,变量每次修改会直接写入共享内存。
  • 原子类:保证i++等复合操作的原子性。
  • synchronized:既能保证原子性,也能保证可见性。

7.4、AtomicInteger 和 synchronized 的异同点?

  • AtomicInteger:

        CAS保证线程安全;
        只能修饰一个对象;
        性能:乐观锁,开销随着时间增加,逐步上涨。

  • synchronized:

        monitor 锁保证线程安全;
        可以锁方法或代码块;
        性能:悲观锁,开销固定。JDK6之后的锁升级会提升性能。

7.5、Java 8 中 Adder 和 Accumulator 有什么区别?

  • Adder:

        分段锁(降低冲突)。base(低并发),Cell[]数组(高并发)。
        在高并发下 LongAdder 比 AtomicLong 效率更高

  • Accumulator:

        Accumulator 就是一个更通用版本的 Adder。
        LongAdder 的 API 只有对数值的加减,而 LongAccumulator 提供了自定义的函数操作。

8、ThreadLocal

8.1、适用场景

用作保存每个线程独享的对象,为每个线程都创建一个副本。

每个线程需要独立保存信息,以便当前线程内其他方法使用。类似于线程内的全局变量。
省去一步步传参的步骤。

示例:simpleDateFormat对象;
1000个线程,共享一个simpleDateFormat对象------不安全。
1000个线程,每个线程创建一个simpleDateFormat 对象------占用内存大,性能低;
线程池,容量为16,设置simpleDateFormat为每个线程的ThreadLocal变量,一共就16个simpleDateFormat对象。

8.2、ThreadLocal是用来解决共享资源的多线程访问问题吗?

不是。ThreadLocal存储的对象是线程独享的,不是共享资源。
注意:当放入ThreadLocal中的对象是static修饰的,那么ThreadLocal并不能解决线程安全问题。

ThreadLocal 是通过让每个线程独享自己的副本,避免了资源的竞争。
synchronized 主要用于临界资源的分配,在同一时刻限制最多只有一个线程能访问该资源。

8.3、多个ThreadLocal在Thread的threadlocals里是如何存储的?

Thread>ThreadLocalMap>ThreadLocal

        一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。

        如果在一个想在Thread中保存多个ThreadLocal对象,那么直接new多个ThreadLocal存储对象即可。

ThreadLocal threadLocal1 = new ThreadLocal();        
threadLocal1.set("string1");
ThreadLocal threadLocal2 = new ThreadLocal();
threadLocal2.set("string2");

8.4、内存泄露

8.4.1、key的内存泄露:

        ThreadLocalMap里的Entry中的key是弱引用。

        如果在Entry中强引用了ThreadLocal,尽管执行 ThreadLocal instance = null,
        根据可达性分析算法,该ThreadLocal对象仍然是可达的,就不会被回收。

8.4.2、value的内存泄露:

        正常情况下,当线程终止,key 所对应的 value 是可以被正常垃圾回收的,因为没有任何强引用存在了。但是有时线程的生命周期是很长的,如果线程迟迟不会终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了。在这种情况下,我们应该保证它们都能够被正常的回收。

8.4.3、避免内存泄露:

        调用 ThreadLocal 的 remove 方法。调用这个方法就可以删除对应的 value 对象,可以避免内存泄漏。

以上内容为个人学习理解,如有问题,欢迎在评论区指出。

部分内容截取自网络,如有侵权,联系作者删除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈QWebChannel、QWebChannelAbstractTransport、QWebSocketServer、QWebSocket用法及之间关系

1.前言在现实业务中&#xff0c;经常遇到这样的需求&#xff1a;一端采用web形式开发的&#xff0c;如&#xff1a;客户端采用html、javascript、nodejs开发&#xff1b;而另一端采用C开发&#xff0c;如&#xff1a;Qt开发的服务端。web页面端需和Qt开发的服务端进行通信、数据…

深入理解java虚拟机精华总结:如何判断对象是否可回收、引用、finalize、方法区回收、垃圾收集算法、垃圾收集器、内存分配与回收策略

深入理解java虚拟机精华总结&#xff1a;如何判断对象是否可回收、引用、finalize、方法区回收、垃圾收集算法、垃圾收集器、内存分配与回收策略如何判断对象是否可回收引用计数可达性分析法引用finalize方法区回收垃圾收集算法标记-清除算法标记-复制算法标记-整理算法垃圾收集…

141周期acwing(kmp)

一个字符串的前缀是从第一个字符开始的连续若干个字符&#xff0c;例如 abaab 共有 5 个前缀&#xff0c;分别是 a&#xff0c;ab&#xff0c;aba&#xff0c;abaa&#xff0c;abaab。 我们希望知道一个 N 位字符串 S 的前缀是否具有循环节。 换言之&#xff0c;对于每一个从…

_vue-1

谈谈你对MVVM的理解 为什么要有这些模式&#xff0c;目的&#xff1a;职责划分、分层&#xff08;将Model层、View层进行分类&#xff09;借鉴后端思想&#xff0c;对于前端而已&#xff0c;就是如何将数据同步到页面上 MVC模式 代表&#xff1a;Backbone underscore jquer…

Docker buildx 的跨平台编译

docker buildx 默认的 docker build 命令无法完成跨平台构建任务&#xff0c;我们需要为 docker 命令行安装 buildx 插件扩展其功能。buildx 能够使用由 Moby BuildKit 提供的构建镜像额外特性&#xff0c;它能够创建多个 builder 实例&#xff0c;在多个节点并行地执行构建任…

YOLOv5模型学习记录

新年伊始&#xff0c;YOLOv8横空出世&#xff0c;这个还未开源时便引发界内广泛热议的目标检测算法&#xff0c;一经问世便再次引发热潮&#xff0c;而作为与其师出同源的YOLOv5&#xff0c;自然要拿来与其比较一番。接下来我们便来学习一下吧。 模型结构 首先便是模型结构了…

Lambda原理及应用

Lambda原理及应用 Lambda介绍 Lambda 是 JDK8 以后版本推出的一个新特性&#xff0c;也是一个重要的版本更新&#xff0c;利用 Lambda 可以简化内部类&#xff0c;可以更方便的进行集合的运算&#xff0c;让你的代码看起来更加简洁,也能提升代码的运行效率。 Lambda语法 非…

优先级队列(堆)  堆排序

前面介绍过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然不合适&#xff0c;比如&a…

【数据库】MySQL

时间戳 可以在创建表的时候&#xff0c;创建时间戳 mysql数据库怎么加入时间戳_帅气的黑桃J的博客-CSDN博客_mysql 插入时间戳 数据库表的命名规范 数据库表字段命名规范 - 腾讯云开发者社区-腾讯云 (tencent.com) MySql 的大小写问题 以下是MySQL详细的大小写区分规则&am…

《爆肝整理》保姆级系列教程python接口自动化(二十三)--unittest断言——上(详解)

简介 在测试用例中&#xff0c;执行完测试用例后&#xff0c;最后一步是判断测试结果是 pass 还是 fail&#xff0c;自动化测试脚本里面一般把这种生成测试结果的方法称为断言&#xff08;assert&#xff09;。用 unittest 组件测试用例的时候&#xff0c;断言的方法还是很多的…

Spring系列-9 Async注解使用与原理

背景&#xff1a; 本文作为Spring系列的第九篇&#xff0c;介绍Async注解的使用、注意事项和实现原理&#xff0c;原理部分会结合Spring框架代码进行。 本文可以和Spring系列-8 AOP原理进行比较阅读 1.使用方式 Async一般注解在方法上&#xff0c;用于实现方法的异步&#xf…

无源晶振匹配电容—计算方法

以前有写过一篇文章“晶振”简单介绍了晶振的一些简单参数&#xff0c;今天我们来说下无源晶振的匹配电容计算方法&#xff1a; 如上图&#xff0c;是常见的的无源晶振常见接法&#xff0c;而今天来说到就是这种常见电路的电容计算方法&#xff0c;有两种&#xff1a; A&#…

CUDA 内存系统

CUDA 内存系统 本文主要是针对<cuda c编程权威指南>的总结,由于原书出版的时候cuda刚刚出到cuda6,之后的cuda版本可能有更新,可能需要我翻一翻文档,待更新. 内存系统架构图 常见的内存作用域与生存期 新特性 早期的 Kepler 架构中一个颇为好用的特性就是 CUDA 程序员可…

没有公网ip怎么外网访问nas?快解析内网端口映射到公网

对于NAS用户而言&#xff0c;外网访问是永远绕不开的话题。拥有NAS后的第一个问题&#xff0c;就是搞定NAS的外网访问。不过众所周知&#xff0c;并不是所有的小伙伴都能得到公网IP&#xff0c;由于IPV4资源的枯竭&#xff0c;一般不会被分配到公网IP。公网IP在很大程度上除了让…

文件的打开关闭和顺序读写

目录 一、文件的打开与关闭 &#xff08;一&#xff09;文件指针 &#xff08;二&#xff09; 文件的打开和关闭 二、文件的顺序读写 &#xff08;一&#xff09;fputc 1. 介绍 2. 举例 &#xff08;二&#xff09;fgetc 1. 介绍 2. 举例1 3. 举例2 &#xff08;三&…

长尾关键词使用方法,通过什么方式挖掘长尾关键词?

当你在搜索引擎的搜索栏中输入有关如何使用长尾关键词的查询时&#xff0c;你可能希望有简单快捷的方式出现在搜索结果中&#xff0c;可以帮助你更好地应用seo。 不过&#xff0c;这里要记住一件事&#xff1a;SEO 策略只会为你的网站带来流量&#xff1b;在你的产品良好之前&a…

VS编译系统 实用调试技巧

目录什么是bug?调试是什么&#xff1f;有多重要&#xff1f;debug和release的介绍windows环境调试介绍、一些调试实例如何写出&#xff08;易于调试&#xff09;的代码编程常见的错误什么是bug?其实bug在英文翻译中有表示臭虫的含义&#xff0c;因为第一次被发现的导致计算机…

【Linux驱动开发100问】什么是模块?如何编写和使用模块?

&#x1f947;今日学习目标&#xff1a;什么是Linux内核&#xff1f; &#x1f935;‍♂️ 创作者&#xff1a;JamesBin ⏰预计时间&#xff1a;10分钟 &#x1f389;个人主页&#xff1a;嵌入式悦翔园个人主页 &#x1f341;专栏介绍&#xff1a;Linux驱动开发100问 什么是模块…

堆的基本存储

一、概念及其介绍堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。堆满足下列性质&#xff1a;堆中某个节点的值总是不大于或不小于其父节点的值。堆总是一棵完全二叉树。二、适用说明堆是利用完全二叉树的结构来维护一组数…

css 画图之质感盒子

前言 css 众所周知可以做很多的事情&#xff0c;比如&#xff1a;界面效果、特效、独特的样式等。今天给各位朋友带来的是以box-shadow来画一个很有质感效果的一个盒子。 之前在网上冲浪的时候&#xff0c;发现了这样的一个效果&#xff0c;所以来记录一下。 下面是实现后的…