【学习笔记】Java核心技术——并发 - Go语言中文社区

【学习笔记】Java核心技术——并发


其他知识点的相关链接:

【学习笔记】Java核心技术——集合

相关知识点

1.什么是多任务?

2.什么是线程?

3.多进程与多线程的本质区别?

4.创建线程的方法(2种)

5.为何不要调用Thread类或Runnable对象的run方法?

6.线程中断的原因?

7.有无强制终止线程的方法?interrupt方法的用途?调用interrupt方法时,线程会发生什么变化?

8.调用sleep方法(或其它的可中断方法),isInterrupted检测有无用处?

9.在中断状态被置位时,调用sleep方法会发生什么变化?

10.interrupted和isInterrupted的区别?

11.java.lang.Thread()中包含哪些方法,对应的含义是什么?

12.线程的状态有哪6个?

13.如何获取当前线程的状态?

14.被阻塞和等待的区别是什么?

15.线程状态转换图?

16.线程的属性包括?

17.常见的线程优先级是?线程优先级高度依赖于什么?

18.守护线程的用途?当系统只剩下守护进程时,虚拟机会怎么样?

19.为什么需要未捕获异常?如何使用uncaughtException方法?

20.锁对象及其使用方式?使用锁对象是能否使用带资源的try?

21.当两个线程同时访问同一个对象时,锁提供服务的方式是?

22.条件对象的使用?

23.signalAll() 和 signal()的区别?

24.锁和条件的关键之处?

25.synchronized关键字的作用范围和区别?

26.内部锁和条件存在的局限?

27.在代码中应该使用Lock和Condition对象还是同步方法?

28.notifyAll()方法和notify()方法的区别?

29.监视器的特征?

30.使用同步出现错误的可能原因?

31.同步格言?

32.volatile关键字为实例域的同步访问提供了一种免锁机制。

33.为什么volatile变量不能提供原子性?

34.final变量能否提供安全性访问?

35.AtomicInteger类提供了方法incrementAndGet和decrementAndGet?

36.Atomic类有哪些方法?

37.为什么有大量线程要访问相同的原子值,性能会大幅下降?

38.为什么increment方法不会返回原值?

39.产生死锁的原因?

40.使用线程局部变量的原因?

41.使用tryLock方法的原因?

42.tryLock 工作原理

43.lock方法与tryLock方法的区别?

44.java.util.concurrent.locks包定义了哪两个锁类?

45.使用读/写锁的必要步骤?

46.为什么要弃用stop方法?

47.为什么要启用suspend方法?

48.stop方法和suspend方法的共同点?

49.什么时候会出现阻塞队列?

50.阻塞队列的方法及其含义?

51.阻塞队列方法分类依据?

52.java.util.concurrent包提供的阻塞队列变种有?

53.线程安全的集合?

54.集合返回弱一致性的迭代器意味着什么?

55.什么是并发散列映射提供的批操作?

56.并发散列映射批操作有哪些?

57.并发散列映射批操作的版本有?

58.Callable和Runnable的区别?

59.Future的作用?

60.FutureTask包装器的作用?

61.为什么需要使用线程池?

62.执行者工厂方法及作用?

63.如何将一个Runnable对象或Callable对象提交给ExecutorService?

64.使用连接线程池是应该做的事情有?

65.ScheduledExecutorService接口的目的是什么?

66.控制任务组?

67.Fork-Join框架的作用?

68.fork-join框架如何平衡可用线程的工作负载?

69.可完成Future的作用?

70.可完成Future的动作及其含义?

71.组合多个future的方法有?

72.同步器的类及其含义?

73.关于信号量

74.关于倒计时门栓?

75.关于障栅

76.关于交换器

 

面试常见问题整理:

AQS:

AQS原理概览?

AQS对资源的共享方式?

AQS介绍?

关于synchronized关键字:

说说自己对于synchronized关键字的了解?

说说自己是怎么使用synchronized关键字,在项目中用到了吗?

讲一下synchronized关键字的底层原理?

说说JDK1.6之后的synchronized关键字底层做了哪些优化?可以详细介绍一下这些优化吗?

谈谈synchronized和ReenTrantLock的区别?

线程池:

为什么要使用线程池?

实现Runnable和Callable接口的区别?

执行execute()方法和submit()方法的区别是什么?

如何创建线程池?

Atomic原子类:

介绍一下Atomic原子类?

JUC包中的原子类是哪4类?

讲讲AtomicInteger的使用?

能不能简单介绍一下AtomicInteger类的原理?

知识点解答

1.什么是多任务?

同一刻运行多个程序的能力

2.什么是线程?

一个任务称为一个线程,它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序

3.多进程与多线程的本质区别?

每个进程拥有自己的一整套变量,而线程则共享数据。

4.创建线程的方法(2种)

  1. 实现Runnable接口的run()方法
  2. 继承Thread类,并重写run方法
  3. 均要start()方法创建新线程

单独的线程中执行一个任务的过程如下:

  1. 将任务代码移到实现了Runnable接口的类的run方法中
  2. 由Runnable创建一个Thread对象
  3. 启动线程

5.为何不要调用Thread类或Runnable对象的run方法?

直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。调用Thread.start方法将创建一个执行run方法的新线程。

6.线程中断的原因?

  1. 当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回
  2. 在方法中没有捕获的异常时

7.有无强制终止线程的方法?interrupt方法的用途?调用interrupt方法时,线程会发生什么变化?

没有,interrupt方法可以用来请求终止线程。调用interrupt方法是,线程的中断状态将被置位。

8.调用sleep方法(或其它的可中断方法),isInterrupted检测有无用处?

无用

9.在中断状态被置位时,调用sleep方法会发生什么变化?

不会休眠,会清除这一状态,并抛出InterruptedException

10.interrupted和isInterrupted的区别?

interrupted是一个静态方法,它检测当前的线程是否被中断,调用interrupted方法会清除该线程的中断状态

isInterrupted是一个实例方法,用来检验是否有线程被中断,调用该方法不会改变线程的中断状态。

11.java.lang.Thread()中包含哪些方法,对应的含义是什么?

start:用来启动一个线程,调用start方法后,系统才会开启一个新的线程来执行用户定义的用户

run:通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务,必须重写run方法

sleep:交出CPU,让CPU去执行其他的任务,但不会释放锁,sleep(long millis)  sleep(long millis,int nanoseconds),如果调用了sleep方法,必须不会InterruptedException异常或者将该异常向上层抛出,当线程睡眠时间满后,不一定会立即得到执行,因为此时CPU可能正在执行其他任务,调用sleep方法相当于让线程进入阻塞状态

yield:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

wait:会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限

join:调用无参join方法,等待tjoin的hread执行完毕;调用指定了时间参数的join方法,等待一定的时间

interrupt:单独调用interrupt方法可以是处于阻塞状态的线程抛出一个InterruptedException,可以用来中断一个正处于阻塞状态的线程

              不能中断处于正在运行中的线程

              调用interrupt方法相当于将中断标志位置为true,调用isInterrupted()判断中断标志是否被置位来中断线程的执行

stop:废弃,调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有对象锁的话,会完全释放锁,导致对象状态不一致,所以stop方法基本不会被用到

destory:废弃

getId:得到线程的ID

getName和setName:获取或者设置线程名称

getPriority 和 setPriority:获取或设置线程优先级

setDaemon和isDaemon:设置线程是否成为守护线程和判断线程是否是守护线程,守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

currenThread():静态方法,获取当前线程

 

 

12.线程的状态有哪6个?

新建(new)

可运行(runnable)——可运行的线程可能正在运行也可能没有运行

被阻塞(blocked)

等待(waiting)

倒计时等待(timed waiting)

被终止(terminated)

13.如何获取当前线程的状态?

getState方法

14.被阻塞、等待、超时参数?

阻塞:当线程试图获取一定内部的对象锁,而该锁被其它线程持有。

等待:当线程等待另一个线程通知调度器一个条件时

超时参数:调用它们导致线程进入计时等待,带有超时参数的方法有Thread.sleep、Object.wait、Thread.join、Lock.tryLock以及Condition.await的计时版

15.线程状态转换图?

16.线程的属性包括?

线程优先级、守护线程、线程组、处理未捕获异常的处理器

17.常见的线程优先级是?线程优先级高度依赖于什么?

MAX_PRIORITY(10)

MIN_PRIORITY(1)

NORM_PRIORITY(5)

线程优先级高度依赖于系统

18.守护线程的用途?当系统只剩下守护进程时,虚拟机会怎么样?

守护线程能够为其它线程提供服务

当系统只剩下守护进程是,虚拟机会退出,由于只剩下守护线程,就没必要继续运行程序了

19.为什么需要未捕获异常?如何使用uncaughtException方法?

线程的run方法不能抛出任何受查异常,但是,非受查异常会导致线程终止

  1. 可以使用setUncaughtExceptionHandler方法为任何线程安装一个处理器
  2. 可以用Thread类的静态方法setDefaultUncaughtException为所有线程安装一个默认的处理器

UncaughtException的做法如下:

  1. 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用
  2. 如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用这个处理器
  3. 如果Throwable是ThreadDeath的一个实例,什么都不做
  4. 线程的名字以及Throwable的栈轨迹被输出到System.err上

20.锁对象及其使用方式?使用锁对象是能否使用带资源的try?

在进入临界区之前进行lock,在finally处必须要进程unlock操作,不能使用带资源的try

21.当两个线程同时访问同一个对象时,锁提供服务的方式是?

每一个Bank对象有自己的ReentrantLock对象,同时访问同一个对象时,锁以串行的方式提供服务

22.条件对象的使用?

可以用newCondition方法获得一个条件对象,通过await()方法阻塞线程,通过signalAll()/signal()唤醒线程

23.signalAll() 和 signal()的区别?

signalAll()方法不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问

signal()方法会随机解除等待集中某个线程的阻塞状态

24.锁和条件的关键之处?

  • 锁可用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
  • 锁可用管理试图进入被保护代码段的线程
  • 锁可拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

25.synchronized关键字的作用范围和区别?

修饰实例方法 —— 作用于当前实例加锁,进入同步方法代码前要获得当前实例的锁

修饰静态方法 —— 作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;

                             访问静态synchronized方法占用的锁是当前类的class对象,而访问非静态synchronized方法占用的锁是当前实例对象锁

修饰代码块 —— 指定加锁对象,对给定对象加锁,进入同步代码块钱要获得给定对象的锁

                         将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象

26.内部锁和条件存在的局限?

  • 不能中断一个正在试图获得锁的线程
  • 试图获得锁时不能设置超时
  • 每个锁仅有单一的条件,可能是不够的

27.在代码中应该使用Lock和Condition对象还是同步方法?

  • 最好既不使用Lock/Condition也不使用synchronized关键字,可以使用java.util.concurrent包中的一种机制,它会处理所有的加锁
  • 如果synchronized关键字适合程序,则尽量使用它
  • 如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition

28.notifyAll()方法和notify()方法的区别?

notifyAll():解除那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内使用

notify():随机选择一个在该对象调用wait方法的线程的阻塞状态,如果该线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException

29.监视器的特征?

  • 监视器是只包含私有域的类
  • 每个监视器类的对象有一个相关的锁
  • 使用该锁对所有的方法进行加锁。
  • 该锁可以有任意多个相关文件

30.使用同步出现错误的可能原因?

  • 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值,导致运行在不同处理器上的线程可能在同一个内存位置取到不同的值
  • 编译器可以改变指令执行的顺序以是吞吐量最大化,这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有明显的修改指令时才改变,然而内存的值可以被另一个线程改变

31.同步格言?

如果向一个变量写入值,而这个变量接下来可能被另一个线程读取。或者。从一个变量读值,而这个变量可能是个之前被另一个线程写入的,此时必须使用同步。

32.volatile关键字为实例域的同步访问提供了一种免锁机制。

33.为什么volatile变量不能提供原子性?

不能确保翻转域中的值。不能保证读取、翻转和写入不被中断

34.final变量能否提供安全性访问?

final定义的变量其值一旦定义后是不能进行修改的,故能提供安全性访问

35.AtomicInteger类提供了方法incrementAndGet和decrementAndGet?

获得值、增1并设置然后生成新值的操作时不会中断的

自增1并后去增长后的值

36.Atomic类有哪些方法?

基本类型:AtomicInteger、AtomicLong、AtomicBoolean

数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference

对象的属性修改类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

37.为什么有大量线程要访问相同的原子值,性能会大幅下降?

因为乐观更新需要太多次重试

38.为什么increment方法不会返回原值?

 

39.产生死锁的原因?

死锁的前提:互斥、持有、非抢占、循环等待

40.使用线程局部变量的原因?

为了避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例

ThreadLocal辅助类为各个线程提供一个单独的生成器

41.使用tryLock方法的原因?

线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞

42.tryLock 工作原理

tryLock方法视图申请一个锁,在成功获得锁后返回true,否则,立即返回false

43.lock方法与tryLock方法的区别?

lock方法不能被中断,如果一个线程在等待获得一个锁是被中断,中断线程在获得锁之前一直处于阻塞状态,如果出现死锁,lock方法将无法被终止

tryLock可以调用超时参数,如果线程线程在等待期间被中断,将抛出InterruptedException异常

44.java.util.concurrent.locks包定义了哪两个锁类?

ReentrantLock

ReentrantReadWriteLock

45.使用读/写锁的必要步骤?

  1. 构造ReentrantReadWriteLock对象
  2. 抽取读锁和写锁
  3. 对所有的获取方法加读锁
  4. 对所有的修改方法加写锁

46.为什么要弃用stop方法?

stop方法终止所有未结束的方法,当线程被终止,立即释放被它锁住的所有对象的锁,会导致对象处于不一致的状态。当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。

47.为什么要启用suspend方法?

suspend方法经常导致死锁,如果用suspend挂起一个持有锁的线程,该锁在恢复之前是不可用的,如果调用supend方法的线程视图获得同一个锁,那么程序死锁

48.stop方法和suspend方法的共同点和不同点?

共同点:都试图控制一个给定线程的行为;

不同点:stop会破坏对象,而suspend不会破坏对象                                                                                                                                                                                   

49.什么时候会出现阻塞队列?

当视图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞

50.阻塞队列的方法及其含义?

51.阻塞队列方法分类依据?

  • 如果队列当作线程管理工具来使用,将要用到put和take方法
  • 试图向满的队列中添加或从空的队列中移出元素时,add、remove和element操作抛出异常
  • 试图向满的队列中添加或从空的队列中移出元素时,add、remove和element操作抛出异常

注意:

  • poll和peek方法返回空来指示失败。因此,向这些队列中插入null值是非法的,还有带有超时的offer方法和poll方法的变体
  • 在不带超时参数时,offer和poll方法等效

52.java.util.concurrent包提供的阻塞队列变种有?

ArrayBlockingQueue

  • 构造时需要指定容量
  • 有一个可选的参数来指定是否需要公平性
  • 若设置公平参数,那么等待了最长时间的线程会优先得到处理
  • 公平性会降低性能,只有在确实非常需要时才使用它

PriorityBlockingQueue

  • 带优先级的队列,而不是先进先出队列
  • 元素按照它们的优先级顺序被移出
  • 没有容量上限
  • 如果队列是空的,取元素的操作会阻塞

getDelay

  • 返回对象的残留延迟
  • 负值表示延迟已经结束
  • 元素只有在延迟用完的情况下才能从DelayQueue移出
  • 必须实现compareTo方法

TransferQueue

  • 允许生产者线程等待,直到消费者准备就绪就可以接收一个元素

53.线程安全的集合?

如果多线程要并发地修改一个数据结构,可能会破坏这个数据结构

54.集合返回弱一致性的迭代器意味着什么?

意味着迭代器不一定能反映出它们被构造之后的所有的修改,但它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常

55.什么是并发散列映射提供的批操作?

即使有其他线程在处理映射,这些操作也能安全地执行、批操作会变量映射,处理变量过程中中熬到的元素,无需冻结当前映射的快照

56.并发散列映射批操作有哪些?

搜索:为每个键或值提供一个函数,直到函数生成一个非null的结果。然后搜索终止,返回这个函数的结果

归约:组合所有键或值,这里要使用所提供的一个累加函数

foeEach:为每个键或值提供一个函数

57.并发散列映射批操作的版本有?

operationKeys(处理键)

operationValues(处理值)

operation(处理键和值)

operationEntries(处理Map.Entry对象)

58.Callable和Runnable的区别?

Runnable封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法

Callable有返回值,是一个参数化的类型,只有一个call方法

59.Future的作用?

Future保存异步计算的结果

60.FutureTask包装器的作用?

能将Callable转换成Future和Runnable

61.为什么需要使用线程池?

  1. 构建新的线程有一定的代价,因为涉及与操作系统交互
  2. 可以用来减少并发线程的数目。创建大量线程会大大降低性能甚至是虚拟机奔溃

62.执行者工厂方法及作用?

63.如何将一个Runnable对象或Callable对象提交给ExecutorService?

Future<?> submit(Runnable task)

  • 可以使用这样一个对象来调用isDone、cancel或isCancelled
  • get方法在完成的时候只是简单地返回null

Future<?> submit(Runnable task,T result) 

  • get方法在完成的时候返回指定的result对象

Future<?> submit(Callable<T> task)

  • 提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它

64.使用连接线程池是应该做的事情有?

  1. 调用Executors类中静态的方法newCachedThreadPool 或 newFixedThreadPool
  2. 调用submit提交Runnable或Callable对象
  3. 如果想要取消一个任务,或如果提交Callable对象,那就要保存返回的Future对象
  4. 当不再提交任何任务时,调用shutdown

65.ScheduledExecutorService接口的目的是什么?

为预定执行或重复执行任务而设计的方法

  • 允许使用线程池机制java.util.Timer的泛化
  • 可以预定Runnable或Callable在初始的延迟之后只运行一次
  • 可以预定一个Runnable对象周期性地运行

66.如何控制任务组?

invokeAll方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果

缺点:

  • 如果第一个任务恰巧花去很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义
  • 可以用ExecutorCompletionService来进行排列

常规方法

ExecutorCompletionService<T> service = new ExecutorCompletionService<>(executor);
for(Callable<T> task:tasks) service.submit(task);
for( int i=0;i<tasks.size();i++)
    processFurther(service.take().get());

67.Fork-Join框架的作用?

每个处理器内核分别使用一个线程,来完成计算密集型任务

68.fork-join框架如何平衡可用线程的工作负载?

工作密取,每个工作现场都有一个双端队列来完成任务,一个工作线程将子任务压入其双端队列的队投。(只有一个线程可以访问队头,所以不需要加锁)。一个工作线程空闲是,它会从另一个双端队列的队尾”密取”一个任务。由于大的子任务都在队尾,这种密取很少出现

69.可完成Future的作用?

可以指定你希望做什么,以及希望以什么顺序执行这些工作

70.可完成Future的动作及其含义?

71.组合多个future的方法有?

72.同步器的类及其含义?

73.关于信号量

  • 通过信号量,线程通过调用acquire请求许可。
  • 信号量仅维护一个技术
  • 许可的数目是固定的,由此线程了通过的线程数量
  • 可以调用release释放许可
  • 许可不是必须由获取它的线程释放,任何线程都可以释放任意数目的许可

74.关于倒计时门栓?

  • 让一个线程集等待直到计数变为0
  • 是一次性的,一旦计数为0,就不能再重用

75.关于障栅

  • CyclicBarrier类,实现了一个集结点
  • 如果任何一个在障栅上等待的线程离开了障栅,障栅就被破坏了(线程可能离开是因为他调用await时设置了超时,或者因为它被中断了)
  • 其他线程await方法抛出BrokenBarrierException异常
  • 那些已经在等待的线程立即终止await的调用
  • 障栅被称为是循环的,因为可以在所有等待线程释放后被重用

76.关于交换器

当两个线程在同一数据缓冲区的两个实例上工作的时候

面试常见问题整理:

AQS:

AQS原理概览?

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结 点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁 的分配。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该 同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过procted类型的getState,setState,compareAndSetState进行操作

//返回同步状态的当前值
protected final int getState() {
        return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) {
}
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

AQS对资源的共享方式?

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

         公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
         非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、 CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某 一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方 式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

AQS介绍

AQS全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。

AQS是一个用来构建所和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符号我们自己需求的同步器。

关于synchronized关键字:

说说自己对于synchronized关键字的了解?

同问题25.

说说自己是怎么使用synchronized关键字,在项目中用到了吗?

  • 修饰实例方法,作用于当前对象实例加锁进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作 用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态 资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实 例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允 许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方 法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态 方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!

单例模式了解一下

public class Singleton{
    private volatile static Singleton uniqueInstance;
    
    private Singleton(){}

    public static Singleton getUniqueInstance(){
        //先判断对象是否已经实例过了,没有实例化过才进入加锁代码
        if(uniqueInstance == null){
            //类对象加锁
            synchronized (Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分

为三步执行:

  1. 为 uniqueInstance 分配内存空间

  2. 初始化 uniqueInstance

  3. 将 uniqueInstance 指向分配的内存地址

        由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在 多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

       使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

讲一下synchronized关键字的底层原理?

  1. Synchronization基于进入和退出管程(Monitor)对象实现,无论现实同步还是隐式同步都是如此
  2. Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor底下
  3. 每个对象有一个监视器锁。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令尝试获取monitor的所有权
  • 如果monitor的进入数为0,则该线程进入monitor,然后进入数设置为1,该线程即为monitor的所有者
  • 如果线程已经占有该monitor,只是重新进入,则进入Monitor的进入数加1
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,在重新尝试获取monitor的所有权    

     4.执行monitorexit的线程必须是objectref所对应的monitor的所有者

  • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者,其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权

说说JDK1.6之后的synchronized关键字底层做了哪些优化?可以详细介绍一下这些优化吗?

偏向锁:引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。

轻量级锁:轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。

自旋锁和自适应自旋:

  • 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
  • 互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
  • 一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。

锁消除:虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间

锁粗化:编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

谈谈synchronized和ReenTrantLock的区别?

功能区别

相同点:

都是加锁方式同步,而且都是阻塞式同步

不同点:

  • Synchronized是JAVA语言的关键字,是原生语法层面的互斥,需要JVM实现
  • ReenTrantLock是API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

性能区别

synchronized:

  • Synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令
  • 在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1
  • 在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了
  • 如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

ReentrantLock:

  • 是java.util.concurrent包下提供的一套互斥锁
  • 提供的高级功能 —— 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。锁绑定多条件:一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

线程池:

为什么要使用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。

实现Runnable和Callable接口的区别?

同问题58

执行execute()方法和submit()方法的区别是什么?

  1. 接收参数不一样
  2. submit 有返回值,而execute没有
  3. submit方便Exception处理,如果在task里会抛出checked或者unchecked exception,又希望外面的调用者能够感知这些exception并做出及时的处理,那么久需要用到submit,通过捕获Future.get()抛出的异常

如何创建线程池?

不允许使用Executors去创建,而是通过ThreadPoolExecutor的方法,原因如下:

Executors返回线程池对象的弊端:

  • FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM
  • CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

方式一:通过构造方法实现

方式二:通过Executor框架的工具类Executors来实现

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交是,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
  • SingleThreadPool:方法返回一个只有一个线程的线程池。若多于一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
  • CachedThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但,若有空闲的线程可以服用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程吃力任务。所有线程在当前执行完毕后,将返回线程池进行复用。

Atomic原子类:

介绍一下Atomic原子类?

CAS原理

参数:V 要更新的变量;E 预期值 ;N 新值

参数约束:

  • 仅当V值等于E值是,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前显示什么都不做,最后,CAS返回当前V的真实值。
  • 当多个线程同时使用CAS操作一个变量是,只有一个会胜出,并成功更新,其余均会失败。失败会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作

CPU指令:CAS整个操作过程是一个原子操作,它是由一条CPU指令完成的,从指令层保证操作可靠,不会被多线程干扰

无锁与volatile:

  • 无锁可以通过cas来保证原子性与线程安全
  • 当给变量加了volatile关键字,表示该变量对所有线程可见,但不保证原子性

JUC包中的原子类是哪4类?

同问题36

讲讲AtomicInteger的使用?

 AtomicInteger类常用方法

public final int get();//获取当前的值
public final int getAndSet(int newValue);//获取当前的值,并设置新的值
public final int getAndIncrement();//获取当前的值,并自增
public final int getAndDecrement();//获取当前的值,并自减
public final int getAndAdd(int delta);//获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update);//如果输入的数值等于预期的值,则以原子方式将该值设置为输入值(true)
public final void lazySet(int newValue);//最终设置为newValue,使用lazySet设置之后可能导致其他线程在之后一小段时间内还是可以读到旧的值

AtomicInteger类的使用示例

class AtomicIntegerTest{
    private AtomicInteger count = new AtomicInteger();
    public void increment(){
        count.incrementAndGet();
    }
    public int getCount(){
        return count.get();
    }
}

能不能简单介绍一下AtomicInteger类的原理?

AtomicInteger类的部分源码:

//setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static{
    try{
        valueOffset =     unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch(Exception ex){ throw new Error(ex);} 
}
private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS的原理是拿期望的值原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset()方法 是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_38855195/article/details/88234744
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 13:50:33
  • 阅读 ( 1266 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢