当前位置:   article > 正文

Java并发编程常见面试题_java并发编程面试题

java并发编程面试题

synchronized修饰普通方法和静态方法的区别?什么是可见性?

普通方法对应于对象锁,是作用于对象实例;
静态方法对应于类锁,是作用于一个类的class对象;
类的对象实例可以有多个,但类的class对象只有一个;
不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁;
类锁只是一个概念的东西,真实并不存在,类锁其实锁的是每个类的class对象;

可见性是值:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。根据java内存模型可知,多线程操作中存在可见性问题,可以通过volatile关键字或者加锁解决。

锁的分类

锁的分类
悲观锁和乐观锁
悲观锁:在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
乐观锁:在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

实现方式:
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁。synchronized关键字和Lock的实现类都是悲观锁。

乐观锁的实现方式主要有两种:CAS机制和版本号机制。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列。
非公平锁:是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁。

两者区别:
公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后安装FIFO的规则从队列中取到自己。
非公平锁: 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

可重入锁和非可重入锁
可重入锁:线程可以再次进入它已经拥有的锁的同步代码块。
非可重入锁:线程不可以再次进入它已经拥有的锁的同步代码块,会导致死锁

synchronized和ReentrantLock都是可重入锁。

共享锁和排它锁

排它锁:又称独占锁,独享锁 synchronized就是一个排它锁;
共享锁:又称为读锁,获得共享锁后,可以查看,但无法删除和修改数据,其他线程此时也可以获取到共享锁,也可以查看但是无法修改和删除数据。

共享锁和排它锁典型是ReentranReadWriteLock 其中,读锁是共享锁,写锁是排它锁。

CAS无锁编程的原理

使用当前的处理器基本都支持CAS()的指令

CAS的基本思路:如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
CAS的基本思路
CAS的三大问题

  1. ABA问题,可以使用版本号解决,对应AtomicMarkableReferenceAtomicStampedReference;
  2. 循环时间长开销大;
  3. 只能保证一个共享变量的原子操作;

ReentrantLock的实现原理

线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。底层则是利用了JUC中的AQS来实现的。

AQS原理

是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。

它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。

AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。

在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。

Synchronized的原理以及与ReentrantLock的区别

synchronized
同步代码块:通过monitorentermonitorexit指令实现;
同步方法:通过ACC_SYNCHRONIZED标识符实现;

同步流程:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

ReentrantLock:通过AQS实现

Synchronized做了哪些优化

引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析等技术来减少锁操作的开销。

逃逸分析
如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

锁消除和粗化
锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。
锁粗化:将临近的代码块用同一个锁合并起来。
消除无意义的锁获取和释放,可以提高程序运行性能。

volatile 能否保证线程安全?在DCL上的作用是什么?

不能保证,在DCL的作用是:volatile是会保证被修饰的变量的可见性和 有序性,保证了单例模式下,保证在创建对象的时候的执行顺序一定是:

  1. 分配内存空间
  2. 实例化对象instance
  3. 把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null了的步骤, 从而保证了instance要么为null 要么是已经完全初始化好的对象。

volatile和synchronized有什么区别?

volatile是最轻量的同步机制。
volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

什么是守护线程?你是如何退出一个线程的?

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。

线程的中止:
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。因为会导致程序可能工作在不确定状态下。

安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。

sleep 、wait、yield 的区别,wait 的线程如何唤醒它?

yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。

yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。

wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。wait 的线程使用notify/notifyAll()进行唤醒。

sleep是可中断的么?

sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。

线程生命周期

线程生命周期

ThreadLocal是什么?

ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
在内部实现上,每个线程内部都有一个ThreadLocalMap,用来保存每个线程所拥有的变量副本。

线程池基本原理

线程池的好处:

  1. 避免创建和销毁线程带来的开销;
  2. 提高响应速度;
  3. 提高线程的可管理性;

线程池的工作机制:

  1. 如果当前运行的线程少于corePoolSize,则创建核心线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  2. 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  3. 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
  4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

有三个线程T1,T2,T3,怎么确保它们按顺序执行?

使用join方法实现, T3所在的线程调用T2.join,T2线程的run方法中执行T1.join。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/808838
推荐阅读
相关标签
  

闽ICP备14008679号