并发常见问题

Q1:Synchroized原理?

Synchroized是由jvm实现的一种实现互斥同步的方式。查看编译后的字节码文件,发现其实是使用了monitorenter和monitoorexit指令。指令内部实现:

  • 当指令运行到monitorenter时,获取锁,把锁的计数器+1。
  • 当指令运行到monitorexit时,锁计数器-1。
  • 当锁计数器为0时,锁被释放。
  • 如果获取锁失败,则当前线程阻塞等待,直到获取锁。

Q2:Synchronized锁的使用?

Synchronized的用法有两种,分别是Synchronized方法和Synchronized块。如:

方法锁

方法所又分为锁实例方法和锁静态方法。

使用示例方法锁时,当一个类有多个实例时,调用该方法时不一定能拿到锁,只有当调用的是同一个实例的该方法才能使用同一个锁,达到加锁的效果。

  public synchronized static void test1() {
        //todo
  }

使用静态方法锁时,锁住的是类对象,是唯一的,所以在调用静态方法的锁时,使用的是同一把锁,可以满足加锁效果。

  public synchronized void test2() {
      //todo
  }

代码块

代码块中可以传入实例对象或类对象或实例对象的object。

锁实例对象时,所得是该类的实例对象,要使锁起效,需要确保加锁的是同一个实例。

  public void test3() {
        synchronized (this) {
            //todo
        }
  }

锁类的实例对象时,由于类是唯一的,所以在调用时满足加锁效果。

  public void test3() {
        synchronized (Foo.class) {
            //todo
        }
  }

锁实例对象object时,需要根据传入的对象来加锁,常见是传入String对象作为锁。

  public void test3() {
        String lock = “test”;
        synchronized (lock) {
            //todo
        }
  }

锁的选择

方法锁的锁颗粒度较大,在并发编程中会造成效率低下。因此在场景允许的情况下,尽量的使用颗粒度更小的锁。对需要操作的部分代码进行代码块级别的加锁,从而达到提升效率。

Q3:什么是可重入性,为什么Synchronized是可重入锁?

当前线程拿到锁之后,同一个类中一个同步方法调用另一个同步方法也仍可以获取锁,而不会导致死锁,就实现了可重入。

Synchronized在内部维护了锁拥有者和计数器。重入时计数器加1,同步方法结束时计数器减1,当计数器为0时释放锁。其他线程发现锁拥有者不是自己时,就会进行等待,直到可获取锁。

Q4:Synchronized优化?(todo)

使用CAS操作使线程不需进行阻塞的操作,减少线程状态切换。 锁实现效率从低到高 偏向锁 -> 轻量级锁 -> 重量级锁。jvm会根据锁竞争状态进行升级。 todo据说升级后不能降级?

Q5:为什么说Synchronized是非公平锁?

非公平性主要体现在获取锁的行为上,并不是按照申请锁的先后顺序分配锁。而是在锁释放之后,每一个等待锁的线程都有可能获取到锁。

  • 优点:提高了执行效率。
  • 缺点:产生线程饥饿现象。

Q6:什么是锁消除?什么是锁粗化?

锁消除

根据逃逸分析,判断对象的使用是否会被外部的方法作为一个全局对象使用。如果会则可能发生线程安全问题,此时就发生了锁逃逸。如果不会发生锁逃逸,虚拟机进行锁消除操作,忽略同步而直接运行。

锁粗化

锁的作用粒度是越小越好,但是如果一系列的操作要反复的进行加锁和释放锁操作,会导致不必要的性能损耗,此时可以适当的增加锁的作用域,即锁粗化。

Q7:为什么说Synchronized是悲观锁?

Synchronized的加锁策略是:不管会不会产生锁竞争,都先进行加锁,先尝试获取锁,如果没有竞争则执行同步代码块,如果发生了竞争则等锁释放再获取锁进行加锁。 乐观锁的核心算法是CAS,策略是比较执行,先执行,如果没有竞争则修改为新值,如果发生竞争则舍弃本次操作。

Q8:乐观锁一定好吗?

cas避免了独占锁的现象,能提高并发性能。 缺点如下:

  1. 乐观锁只能保证一个共享变量的原子操作。
  2. 长时间的自旋会导致cpu的开销大。
  3. ABA问题,第一次对比时值是A,之后被另一线程改为B,又被另一线程改为A,第二次读取时发现是A。cas认为值没有被改变。可以引入版本号来解决aba问题。

Q9:可重入锁ReentrantLock与Synchronized实现原理不同点?

Synchronized是JVM原生的锁的实现方式。ReentrantLock是基于Lock接口的一个实现类,本质是一个AQS框架(AbstractQueuedSynchronizer)来实现。

Q10:Synchronized和ReentrantLock的异同?

Synchronized是一个java的关键字,ReentrantLock是java的Lock接口下的一个实现类。以下简称Synchronized为内置锁,ReentrantLock为重入锁。

相同

  • 加锁方式同步
  • 同步都是阻塞的
  • 都是可重入的锁

区别

  • 构成方式不同,内置锁是java提供的关键字,重入锁是JDK1.5之后的API层面的互斥锁。
  • 使用方式不同,内置锁无须显式的获取和释放锁,重入锁需要显式的获取和释放锁。
  • 灵活性不同,内置锁不可中断,除非抛出异常,重入锁可以。
    • 内置锁中断方式:
      1. 代码执行完毕,正常释放锁。
      2. 抛出异常,jvm退出等待。
    • 重入锁中断方式:
      1. tryLock((long timeout, TimeUnit unit),超时退出。
      2. lockInterruptibly(),调用interrupt()方法可中断。
  • 是否允许公平锁,内置锁是非公平锁,重入锁默认(无参构造器)为不公平锁,可以选择非公平锁(传入true为公平锁,false为不公平锁)。
  • 是否提供非阻塞获取锁方式,内置锁不支持,重入锁可用tryLock(),返回的是一个boolean类型。根据boolean执行不同的方法。也可以tryLock(long timeout, TimeUnit unit),等待一段时间获取锁,在时间内获取锁则返回true,否则false。
  • 锁是否可以绑定条件,内置锁不可以,重入锁可以同时绑定多个Condition实例。
  • 性能,java6之前内置锁性能明显低于重入锁,之后再低竞争情况下,内置锁优于重入锁,高竞争内置锁性能下降,而重入锁基本维持常态。

Q11:ReentrantLock 是如何实现可重入性的?

ReentrantLock内部的同步器Sync实现了CAS算法,把线程对象放在了一个双向链表结构中,每次获取锁都会进行比较维护的线程ID和当前线程ID是否一致,一致即可重入。

Q12:除了ReetrantLock,你还用过JUC中哪些并发工具(todo)?

  • ConcurrentHashMap等线程安全的容器
  • ArrayBlockingQueue、PriorityBlockingQueue等并发队列
  • Executor框架的线程池
  • AtomicInteger等原子操作类
  • TimeUnit

Q13:ReadWriteLock和StampedLock?

ReadWriteLock是java1.5提供的读写锁。并发情况下,读读不锁,读写,写写会锁,用于进行读写分离。在读操作远远大于写操作的情况下,性能提高明显。 但是在实际应用中,ReadWriteLock的性能不好,因此提供了新的StampedLock,基于CLH,获取锁的方式是一个long类型的票据(stamp),是保证不会产生饥饿且FIFO。StampedLock是一个不可重入锁。SteampedLock的乐观读机制在大量读操作,少量写操作时,能提供极高的性能。

Q14:jUC同步器?(todo)https://blog.csdn.net/FAw67J7/article/details/79885944

Q15:java线程池是如何实现的?

java中的threadpool中有两个重要的成员属性。workqueue和works,workqueue中存放所有需要执行的任务,works中进行处理。

Q16:线程池的核心构造参数?

  • corePoolSize:线程池的核心线程数
  • maximumPoolSize:线程池允许的最大线程数
  • keepAliveTime:超过核心线程数时闲置线程的存活时间
  • workQueue:线程等待队列

Q17:线程池中的线程是怎么被创建的?是一开始就随着线程池的创建而创建的吗?

线程池默认初始化后不启动Worker,等待有请求才会启动。每当调用execute()方法时,线程池会做如下判断:

  • 如果正在运行的线程数量小于corePoolSize,那么立马创建线程运行这个任务。
  • 如果正在运行的线程数量大于或等于corePoolSize,那么这个任务放入队列。
  • 如果队列满了,且正在运行线程数量小于maximumPoolSize,那么创建非核心线程立即运行任务。
  • 如果队列满了,且正在运行的线程数量大于或等于maximumPoolSize,则线程池会抛出异常RejectExecutionException
  • 当一个任务完成时,将会从队列中取下一个任务来执行。
  • 当线程空闲时间超过keepAliveTime时,如果当前运行线程数大于corePoolSize,则线程就会被销毁。直到线程数为corePoolSize大小。

Q18:java中默认实现的线程池?并比较异同。

SingleThreadExecutor 单线程线程池

线程池中只有一个核心线程,最大线程数也是1。如果这个线程因为异常结束,则会有新的线程来替代。该线程池可以保证任务按提交顺序执行。

  • corePoolSize:1
  • maximumPoolSize:1
  • keepAliveTime:0L
  • workQueuenew LinkedBlockQueue<Runnable>()

FixedThreadpool 固定大小线程池

线程池中的线程大小固定不变,每次excute()就产生一个线程,知道上线。如果线程异常结束,则生成一个线程补充。

  • corePoolSize:n
  • maximumPoolSize:n
  • keepAliveTime:0L
  • workQueuenew LinkedBlockQueue<Runnable>()

CachedThreadPool 缓存线程池

最大线程数不做限制,只要有新的任务就创建一个线程。线程空闲60后被销毁。SynchronousQueue是一个队列大小为1的阻塞队列。

  • corePoolSize:0
  • maximumPoolSizeInteger.MAX_VALUE
  • keepAliveTime:60L
  • workQueuenew SynchronousQueue<Runnable>()

ScheduledThreadPool 调度线程池

核心线程数固定,最大线程数无界。支持定时已经周期性的执行任务的线程池。

  • corePoolSize:n
  • maximumPoolSizeInteger.MAX_VALUE
  • keepAliveTime:0
  • workQueuenew DelayedWorkQueue<Runnable>()

Q19:java线程池中怎么提交任务

execute()

ExecutorService.execute()接收一个Runnable实例。

submit()

ExecutorService.submit()返回一个Future实例。

Q20:volatile是怎么保证变量对所有线程的可见性的?

当共享变量的值被修改后,会立即刷新到主内存中。当其他线程需要读取共享变量时,会去主内存中获取新值。

被volatile关键字修饰的变量,具有两层语义:

  • 保证变量的可见性
  • 禁止进行指令重排

Q21:是否基于volatile变量的运算就能保证并发安全?

不是,volatile不能保证原子性。所以在并发操作下也不安全。

Q22:对比下 volatile 对比 Synchronized 的异同?

volatile只保证了可见性,不保证原子性;而Synchronized既保证了可见性,也保证了原子性。

Q23:现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

使用join,运行的时候按照

//other....
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
//other....

Q24:java中wait和sleep方法的不同?

wait是Object类中的方法,在等待时会释放锁。sleep是Thread类中的方法,会一直持有锁。wait常用于线程之间的交互,而sleep用于线程的暂停。