美文网首页
java 多线程T(重写) --- 4---2021-10-07

java 多线程T(重写) --- 4---2021-10-07

作者: 鄙人_阿K | 来源:发表于2021-10-08 23:51 被阅读0次

《面试题对标大纲》

题目列表集

1、说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
2、单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!
3、说一下 synchronized 底层实现原理?
4、synchronized可重入的原理
5、什么是自旋
6、多线程中 synchronized 锁升级的原理是什么?
7、线程 B 怎么知道线程 A 修改了变量
8、当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
9、synchronized、volatile、CAS 比较
10、synchronized 和 Lock 有什么区别?
11、synchronized 和 ReentrantLock 区别是什么?
12、volatile 关键字的作用
13、Java 中能创建 volatile 数组吗?
14、volatile 变量和 atomic 变量有什么不同?
15、volatile 能使得一个非原子操作变成原子操作吗?
16、synchronized 和 volatile 的区别是什么?
17、final不可变对象,它对写并发应用有什么帮助?
18、Lock 接口和synchronized 对比同步它有什么优势?
19、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
20、什么是 CAS

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

synchronized关键字最主要的三种使用方式:

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

  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个
    实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一
    份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个
    实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态
    synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实
    例对象锁。

  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

2、单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!

双重校验锁实现对象单例(线程安全):

  • 说明:双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多
    线程并行进来不会出现重复new对象,而且也实现了懒加载
    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 的指令重排,保证在多线程环境下也能正常运行。

3、说一下 synchronized 底层实现原理?

  • Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成

  • 每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就
    会处于锁定状态并且尝试获取monitor的所有权 ,过程:
    (1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为
    monitor的所有者。
    (2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,(次数)
    (3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再
    重新尝试获取monitor的所有权。

须知:synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件

4、synchronized可重入的原理

  • 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线
    程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0
    时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

5、什么是自旋

  • 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁
    可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然
    synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized
    的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更
    好的策略。

  • 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了
    CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓
    存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避
    免重建缓存和减少等待重建的时间就可以使用它了。(阿K:有点像是连接池)

6、多线程中 synchronized 锁升级的原理是什么?

  • synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候
    threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断
    threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为
    轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的
    对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

  • 偏向锁:顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访
    问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比
    如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇
    到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标
    准的轻量级锁。

  • 轻量锁:竞争锁的线程首先需要拷贝对象头中的Mark Word到帧栈的锁记录中。拷贝成功后使用CAS操作尝试将对象的Mark Word更新为指向当前线程的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。如果更新失败,那么意味着有多个线程在竞争。
    当竞争线程尝试占用轻量级锁失败多次之后(使用自旋)轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。

  • 重量级锁:是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻
    塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

  • 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

很有趣的案例(便于理解):https://www.jianshu.com/p/afa5296a4832

image.png

7、线程 B 怎么知道线程 A 修改了变量

(1)volatile 修饰变量
(2)synchronized 修饰修改变量的方法
(3)wait/notify
(4)while 轮询

8、当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的
synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取
走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

9、synchronized、volatile、CAS 比较

(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)

10、synchronized 和 Lock 有什么区别?

(1)首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

(2)synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;
而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

(3)通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

11、synchronized 和 ReentrantLock 区别是什么?

  • synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质
    区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继
    承、可以有方法、可以有各种各样的类变量

  • synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在
    Java 6 中对 synchronized 进行了非常多的改进。

  • 相同点:两者都是可重入锁
    可重入锁 概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,
    此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,
    就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

  • 主要区别如下:
    (1)ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
    (2)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
    (3)ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
    (4)二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,
    synchronized 操作的应该是对象头中 mark word

  • Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
    (1)普通同步方法,锁是当前实例对象
    (2)静态同步方法,锁是当前类的class对象
    (3)同步方法块,锁是括号里面的对象

12、volatile 关键字的作用

  • 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,
    确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,
    它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。

  • 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见
    java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

  • volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

13、Java 中能创建 volatile 数组吗?

  • 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思
    是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元
    素,volatile 标示符就不能起到之前的保护作用了。

14、volatile 变量和 atomic 变量有什么不同?

  • volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
    例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。

  • 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会
    原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

15、volatile 能使得一个非原子操作变成原子操作吗?

  • 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同
    一个实例变量需要加锁进行同步。

  • 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子
    性。

所以从Oracle Java Spec里面可以看到:

(1)对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时
候,可以分成两步,每次对32位操作。
(2)如果使用volatile修饰long和double,那么其读写都是原子操作对于64位的引用地址的读写,都是原子操作
(3)在实现JVM时,可以自由选择是否把读写long和double作为原子操作,推荐JVM实现为原子操作

16、synchronized 和 volatile 的区别是什么?

作用:

(1)synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
(2)volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的
可见性;禁止指令重排序。

区别:

(1)volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
(2)volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改
可见性和原子性。
(3)volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
(4)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
(5)volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是
volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键
字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻
量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场
景还是更多一些。

17、final不可变对象,它对写并发应用有什么帮助?

  • 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就
    不能改变,反之即为可变对象(Mutable Objects)。

  • 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如
    String、基本类型的包装类、BigInteger 和 BigDecimal 等。

  • 只有满足如下状态,一个对象才是不可变的;
    (1)它的状态不能在创建后再被修改;
    (2)所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。

  • 不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

18、Lock 接口和synchronized 对比同步它有什么优势?

它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定
时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操
作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当
然,在大部分情况下,非公平锁是高效的选择。

19、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候
    都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多
    这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的
    同步原语 synchronized 关键字的实现也是悲观锁。

  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是
    在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适
    用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实
    都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观
    锁的一种实现方式 CAS 实现的。

20、什么是 CAS

  • CAS 是 compare and swap 的缩写,即我们所说的比较交换。

  • cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁
    住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态
    度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很
    大的提高。

  • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的
    值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果
    在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才
    有可能机会执行。

相关文章

网友评论

      本文标题:java 多线程T(重写) --- 4---2021-10-07

      本文链接:https://www.haomeiwen.com/subject/wgoznltx.html