并发基础知识

807 阅读17分钟

并发

Synchronized

1.synchronized关键字原理是什么?

synchronized是JVM实现同步互斥的一种方式,被synchronized修饰的代码块在代码编译后在同步代码块开始时插入monitorenter 字节码指令 ,在同步代码块结束和异常处会插入monitorexit指令。JVM会保证每个monitorenter 都有monitorexit于之匹配,任何一个对象都有一个monitor对象和其关联,当线程执行到monitorenter 指令的时候会尝试获取对象对应monitor的所有权,也就是对象的锁,在代码块结束的或者异常的时候会释放对象对应monitor的所有权。同步方法使用的是acc_synchronized指令实现的同步。

2.synchronized是重入的吗?

synchronized是重入的,当一个线程获取执行到monitorenter 指令的时候会获取对象对应的monitor的所有权,即获得锁,锁的计数器会+1。当这个线程在次获取锁的时候锁的计数器会再+1。当同步代码块结束的时候会将锁的计数器-1,直到锁的计数器为0,则释放锁的持有权。JVM以这种方式来实现synchronized的可重入性。

3.synchronized是如何确定锁的对象的?

  • 同步代码块使用的是括号内的对象

  • 同步方法使用的是当前的实例对象

  • 静态方法使用的当前类的class对象(全局锁)

4.synchronized有哪些优化?

在 Java 6 之前,Monitor 的实现完全依赖底层操作系统的互斥锁来实现 ,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代 JDK 中做了大量的优化。

  • 偏向锁

    当一个线程访问同步块并获得锁的时候会在对象头的Mark Word和栈帧中记录线程的ID,以后这个线程再获取锁的时候只需要比较下对象头中存储的偏向锁的ID,不需要进行CAS操作加锁和解锁即可获得锁的使用权。

    如果测试失败则需要检查下对像头Mark Word中的锁标识是否是偏向锁,如果是则尝试使用CAS将对象头中的偏向锁指向当前线程,否则使用CAS竞争锁。

  • 轻量级锁

    偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁,轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord更新为指向LockRecord的指针 。

  • 重量级锁

    虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

5.synchronized是悲观锁吗?什么是CAS操作?

synchronized是悲观锁,它的同步策略是无论是否会有线程竞争都会加锁。CAS操作是一种乐观锁的核心算法,在执行CAS操作的时候不会挂起线程,他的核心思想是内存值,旧的预期值,新值。在提交的时候比较内存值和旧的预期值是否相等,不相等通过一个while操作新计算想要修改的新值 (自旋)。

6.CAS操作和synchronized相比有什么优点和缺点?

  • synchronized涉及到了操作系统用户模式和内核模式的切换,性能比CAS操作低。
  • CAS操作只能保证一个变量的原子更新,无法保证多个变量的原子更新。
  • CAS长时间自选会导致CPU开销增大。
  • CAS存在ABA问题,解决思路是增加版本号。

7.synchronized的内存语义是什么?

获取锁会使该线程对应的本地内存置为失效,线程直接从主内存获取共享变量。 锁的释放会使该线程将本地内存中的共享变量写入到住内存。

8.什么是锁消除和锁粗化?

  • 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除主要判定依据来源于逃逸分析的数据支持

  • 锁粗化,如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了

ReentrantLock

1.ReentrantLock的实现原理是什么?

sychronized是JVM原生的锁,是通过对象头设置标记实现的,ReentrantLock是基于Lock接口的实现类,通过AQS框架实现的一个可重入锁。

  • AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器的框架,各种 Lock 包中的锁(常用的有 ReentrantLock、ReadWriteLock),以及其他如 Semaphore、CountDownLatch等都是通过AQS实现的。
  • 队列同步器维护了一个基于双向链表实现的同步队列,当线程获取同步状态失败的时候,会将当前线程维护为一个节点存入到同步队列的队尾,并且阻塞当前的线程。而首节点是获取到同步状态的节点,当首节点释放同步状态的时候会唤醒后继节点,后继节点获取同步状态成功后会将自己设置为首节点。
  • AQS获取同步状态的方式是在内部定义了一个volatile int state的变量用来表示同步状态。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1;如果 state=1,则说明有线程目前正在使用共享变量,线程必须加入同步队列进行等待。
  • AQS 通过内部类 ConditionObject 构建等待队列,等待队列可能会有多个,当 Condition 调用 wait() 方法后,线程将会加入等待队列中,而当 Condition 调用 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争,AQS 和 Condition 各自维护了不同的队列,在使用 Lock 和 Condition 的时候,其实就是两个队列的互相移动

2.ReentrantLock和sychronized使用上有什么区别?

  • ReentrantLock在获取锁的时候可以优先响应中断。
  • ReentrantLock支持超时获取锁,如果超过超时时间则返回。
  • ReentrantLock可以尝试获取锁,如果锁被其他线程持有,则返回
  • ReentrantLock可以实现公平锁。

3.常用的同步器有哪些?

  • CountDownLancth
CountDownLatch(int count) #构造一个以给定计数CountDownLatch
await() #等待当前的计数器清零
await(long timeout, TimeUnit unit) #等待当前的计数器清零或到达超时时间
countDown() #减少锁存器的计数,如果计数达到零,释放所有等待的线程。

CountDownLantch使用案例:并发测试工具

  • CyclicBarrier
CyclicBarrier(int parties) #构造一个新的CyclicBarrier,拦截线程的数量是parties
CyclicBarrier(int parties, Runnable barrierAction) #构造一个新的CyclicBarrier,拦截线程的数量是parties,到达屏障时优先执行barrierAction
await() #等待
await(long timeout, TimeUnit unit) #带超时时间等待
reset() #将屏障重置为初始状态。

  • Semaphore
Semaphore(int permits) #创建一个Semaphore与给定数量的许可证和非公平公平设置
Semaphore(int permits, boolean fair) #创建一个 Semaphore与给定数量的许可证和给定的公平设置。
acquire() #获取许可
release() #释放许可
  • Exchanger 线程间交换数据
exchange(V x) 

CountDownLatch 是不可以重置的,所以无法重用,CyclicBarrier 没有这种限制,可以重用。CountDownLatch 一般用于一个线程等待N个线程执行完之后,再执行某种操作。CyclicBarrier 用于N个线程互相等待都达到某个状态,再执行。

4.ReadWriteLock 和 StampedLock有什么区别?

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,策略是悲观读。然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。StampedLock控制锁有三种模式(写,读,乐观读 )一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问,在读锁上分为悲观锁和乐观锁。若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定 。

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看乐观读锁案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
     double currentX = x, currentY = y; //将两个字段读入本地局部变量
     if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
        stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
        try {
          currentX = x; // 将两个字段读入本地局部变量
          currentY = y; // 将两个字段读入本地局部变量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
//下面是悲观读锁案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
         if (ws != 0L) { //这是确认转为写锁是否成功
           stamp = ws; //如果成功 替换票据
           x = newX; //进行状态改变
           y = newY; //进行状态改变
           break;
         }
         else { //如果不能成功转换为写锁
           sl.unlockRead(stamp); //我们显式释放读锁
           stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
         }
       }
     } finally {
       sl.unlock(stamp); //释放读锁或写锁
     }
   }
 }

ReentrantReadWriteLock使用时要注意锁的升级和降级问题: 读写锁使用

JAVA 内存模型

1.通信和同步

通信 是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递。 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态隐式进行通信。 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息显式进行通信。 同步 是指程序用于控制不同线程之间操作发生相对顺序的机制。 Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

2.JAVA 内存模型的抽象(JMM)

在 Java 中,所有实例域、静态域 和 数组元素存储在堆内存中,堆内存在线程之间共享,   局部变量、方法定义参数 和异常处理器参数 不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

JMM 定义了线程与主内存之间的抽象关系: 共享变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。

3.volatile关键字的内存语义

对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。对一个 volatile 变量的读会将本地内存的值置为失效从主内存获取。对一个 volatile 变量的写会将本地内存的值写入主内存。被volatile关键字修饰变量会静止指令重排序优化。被volatile关键字修饰的变量只能保证单个变量的原子性类似于 volatile++ 这种复合操作,这些操作整体上不具有原子性。

4.final关键字内存语义

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。

5.happens-before原则

在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器的解锁,happens-before 于随后对这个监视器的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。 ###6.AS-IF-SERIAL原则 as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

线程池

1.为什么要使用线程池?

  • 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率 。

  • 线程并发数量过多,抢占系统资源从而导致阻塞。

  • 对线程进行一些简单的管理 。

2.线程池的核心参数

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。默认情况下keepAliveTime对大于corePoolSize小于maximumPoolSize的线程有效,如果设置allowCoreThreadTimeout=true(默认false)时,核心线程才会超时关闭。
  • workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
  • ThreadFactory :用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

​ 1)AbortPolicy:直接抛出异常。

​ 2) CallerRunsPolicy:只用调用者所在线程来运行任务。

​ 3) DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

​ 4) DiscardPolicy:不处理,丢弃掉

3.线程池如何工作?

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)

  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

4.线程池如何提交线程?

  • execute():ExecutorService.execute 方法接收一个 Runable 实例,它用来执行一个任务。

  • submit():ExecutorService.submit() 方法返回的是 Future 对象。可以用 isDone() 来查询 Future 是否已经完成,当任务完成时,它具有一个结果,可以调用 get() 来获取结果。也可以不用 isDone() 进行检查就直接调用 get(),在这种情况下,get() 将阻塞,直至结果准备就绪。

###5.JAVA中默认的线程池有哪些?

  • SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

SingleThreadExecutor线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  • FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

FixedThreadPool 是固定大小的线程池,只有核心线程。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  • CachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

CachedThreadPool 是无界线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务,适合执行任务时间短的异步任务。

  • ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,用于处理定时任务。

scheduleAtFixedRate:该方法在initialDelay时长后第一次执行任务,以后每隔period时长,再次执行任务。注意,period是从任务开始执行算起的。开始执行任务后,定时器每隔period时长检查该任务是否完成,如果完成则再次启动任务,否则等该任务结束后才再次启动任务。

img

scheduleWithFixDelay:该方法在initialDelay时长后第一次执行任务,以后每当任务执行完成后,等待delay时长,再次执行任务。

这里写图片描述

##并发容器 ###1.ConcurrentHashMap 在多线程环境下,使用HashMap进行put操作时会导致链表成环。

2.ThreadLocal

3. DelayQueue

4.PriorityQueue

其他

  • 使用TimeUnit代替Thread.sleep()