Java多线程&&并发面试题01

进程与线程

前置知识

1.进程

1.1 进程概念

  进程就是正在运行的程序,它会占用对应的内存区域,由CPU进行执行与计算。

1.2 进程的特点
  • 独立性
    进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都有自己私有的地址空间,在没有经过进程本身运行的情况,一个用户进程不能直接访问其他进程的地址空间
  • 动态性
    进程和程序 的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
  • 并发性
    多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.

2.线程

2.1线程概念

线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
我们看到的进程的切换,切换的也是不同进程的主线程
多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。

1. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?

  线程和进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
  程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
  进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

在这里插入图片描述
 每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
 所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成.

线程的状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态

在这里插入图片描述

RUNNABKLE= Ready(就绪态)+Running(真正持有CPU态)
在这里插入图片描述
调用start()后等待cpu分配时间片,分配到了就是Running ,没分配到就是Ready
yield线程:线程礼让,让出cpu,不知道礼让成功与否
join线程:线程插队 ,先执行插队的线程所有任务(谁插队调用谁)

2.什么是守护线程(Daemon)

守护线程(即 Daemon thread),是个服务线程,准确地来说就是服务其他的线程
只要首线程停止,尽管子线程未停止也要停止

线程同步机制

  在多线程编程中,一些敏感数据不允许被多线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。 或者说,当一个线程在对内存操作,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
Synchronized 通过对象锁保证了临界区内代码的原子性(不可分割)

方法1Synchronized 

synchronized(对象){//得到对象的锁 ,才能操作同步代码 this
	//需要被同步代码
}
2.synchronized还能放在方法声明中,表示整个方法-为同步方法
public synchronized void m(String name){
	//需要被同步代码
}

当关键字synchronized 来与对象的互斥锁联系,表面该对象在任意时刻只能有一个线程访问
局限性:执行效率低
如果同步方法是加在非静态的 锁是this 也可以是其他对象但是要求同一个对象
如果同步方法是加在静态的锁是当前类本身 静态方法上面那个类

3.Java 实现同步的几种方式(六种)

一、synchronized同步的方法:

1、synchronized同步方法
  即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2、synchronized同步代码块
  即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

二、通过Object wait与notify:
实现多个线程互斥访问临界区资源, Object类这几个方法必须配合synchronized来进行使用。
wait(): 使一个线程处于等待状态,并且释放所持有的对象的lock
sleep(): 使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉interruptedException异常
notify():唤醒一个处于WaitSet中等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待线程,而是由JVM确定唤醒哪个线程,而且不是按照优先级。
notifyaAll(): 唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

三、使用特殊域变量(volatile)实现线程同步
  关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
volatile 为啥能同步的原因:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。
    保证他的内存可见性 。
    内存屏障 保证他的有序性
      ​ Java中的内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。

  • 读屏障(Load Memory Barrier) :在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。

  • 写屏障(Store Memory Barrier) :在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。

  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。

四、使用ReentrantLock/Condition重入锁实现线程同步
  ReentrantLock类是可重入、互斥、实现了Lock接口的锁。
  ReentrantLock锁的实现原理虽然和synchronized不用,但是它和synchronized一样都是通过保证线程间的互斥访问临界区,来保证线程安全,实现线程间的通信。相比于synchronized使用Object类的三个方法(wait、notify、notifyAll)来实现线程的阻塞和运行两个状态的切换,ReentrantLock使用Condition阻塞队列的await()、signal()、signalAll()三个方法来实现线程阻塞和运行两个状态的切换,进而实现线程间的通信,而且相比前者使用起来更清晰也更简单。前者是java底层级别的,后者是语言级别的,后者可控制性和扩展性更好。

五、使用阻塞队列实现线程同步
1.通过一个阻塞队列(二个线程一个存取一个取出 ,当一个取完会阻塞,等另一个线程存完)
  前面5种同步方式都是在底层实现的线程同步,实现起来代码比较繁琐,但是我们在实际开发当中,应当尽量远离底层结构。

ArrayBlockingQueue queue = new ArrayBlockingQueue<Integer>(64);
  queue.put(i);					 //put(E e) : 在队尾添加一个元素,如果队列满则阻塞
  int num = (int) queue.take();  //take() : 移除并返回队头元素,如果队列空则阻塞

2.通过两个阻塞队列(两个队列分别一个线程一个读取一个取出)
  使用一个阻塞队列能够实现线程同步的功能,两个阻塞队列也可以实现线程同步。原理是ArrayBlockingQueue他是具有容量的,如果把他的容量定位1则意味着他只能放进去一个元素,第二个方进行就会就会被阻塞。按照这个原理进行来实现。

 			//数据的存放
            ArrayBlockingQueue queue1 = new ArrayBlockingQueue<Integer>(1);
            //用于控制程序的执行
            ArrayBlockingQueue queue2 = new ArrayBlockingQueue<Integer>(1);
容量都是一也就是当一个线程存一个元素后就得等下面那个线程的阻塞队列2 取出,才能继续存储。

六、使用 Semaphore(信号量)

  • new Semaphore(1):互斥的信号量。
  • Semaphore#acquire():获取一个许可,如果没有就等待。
  • Semaphore#release():释放一个许可。

4. 创建线程的几种方式?

  1. 继承 Thread 类创建线程;
  2. 实现 Runnable 接口创建线程;
  3. 通过 Callable 和 Future 创建线程;
  4. 通过线程池创建线程。

5.Java 中实现多线程有几种方法?

  继承Thread类;
  实现Runnable接口;
  实现Callable接口通过FutureTask包装器来创建Thread线程;
  使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)。

6. sleep() 和 wait() 的区别?

1.使用地方不同 ,sleep()在仍地方都可以使用, 而wait()方法只能再同步方法和同步代码块中使用
2.调用二者方法后 sleep() 会保持对象锁 ,仍然占有该锁 ,仅仅让出cpu
wait()睡眠后会释放锁。
注意:notify 的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说 notify 只是让之前调用 wait 的线程有权利重新参与线程的调度);

7. 线程的 run() 和 start() 有什么区别?

start()方法
用start方法启动线程是真正实现了多线程 ,通过调用Thread类的start()方法启动一个线程,这时此线程处于就绪状态并没有运行,在获得cpu时间片 就开始执行方法,而且并不需要等run方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。
run()方法
run()方法就是类的一个普通方法而已,如果调用Run方法,程序中依然只有主线程这一个线程。
区别:
1.二者根本就是一个是只是类中的普通方法 ,另一个是在调用的时候会创建一个新线程 去调用run()方法。
start() 方法能够异步调用run()方法,但是直接调用run()方法却是同步的,因此也就无法达到多线程的目的。

8.在 Java 程序中怎么保证多线程的运行安全?

线程安全在三方面体现:

  • 原子性:提供互斥访问,同一时候只能有一个线程对数据进行操作(atomic ,synchronized ,互斥锁(LOCK), CAS)
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、volatile);
  • 有序性问题: Happens-Before规则
    原子性 - 保证指令不会受到线程上下文切换的影响
    可见性 - 保证指令不会受 cpu 缓存的影响
    有序性 - 保证指令不会受 cpu 指令并行优化的影响

9. Runnable 和 Callable 有什么区别?

1)Runnable提供run方法,无法通过throws抛出异常,所有CheckedException必须在run方法内部处理。Callable提供call方法,直接抛出Exception异常。

2)Runnable的run方法无返回值,Callable的call方法提供返回值用来表示任务运行的结果

3)Runnable可以作为Thread构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行。而Callable只能通过线程池执行。

10.Thread.interrupt() 方法的工作原理是什么?

  在 Java 中,线程的中断 interrupt 只是改变了线程的中断状态,至于这个中断状态改变后带来的结果,那是无法确定的,有时它更是让停止中的线程继续执行的唯一手段。不但不是让线程停止运行,反而是继续执行线程的手段。
  在一个线程对象上调用 interrupt() 方法,真正有影响的是 wait、join、sleep 方法,当然这 3 个方法包括它们的重载方法。请注意:上面这三个方法都会抛出 InterruptedException。
  1、对于 wait 中的等待 notify、notifyAll 唤醒的线程,其实这个线程已经“暂停”执行,因为它正在某一对象的休息室中,这时如果它的中断状态被改变,那么它就会抛出异常。这个 InterruptedException 异常不是线程抛出的,而是 wait 方法,也就是对象的 wait 方法内部会不断检查在此对象上休息的线程的状态,如果发现哪个线程的状态被置为已中断,则会抛出 InterruptedException,意思就是这个线程不能再等待了,其意义就等同于唤醒它了,然后执行 catch 中的代码。

  2、 对于 sleep 中的线程,如果你调用了 Thread.sleep(一年);现在你后悔了,想让它早些醒过来,调用 interrupt() 方法就是唯一手段,只有改变它的中断状态,让它从 sleep 中将控制权转到处理异常的 catch 语句中,然后再由 catch 中的处理转换到正常的逻辑。同样,对于 join 中的线程你也可以这样处理。

11.谈谈对 ThreadLocal 的理解?

   通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量,就需要用到ThreadLocal类。
  1、Java 的 Web 项目大部分都是基于 Tomcat。每次访问都是一个新的线程,每一个线程都独享一个 ThreadLocal,我们可以在接收请求的时候 set 特定内容,在需要的时候 get 这个值。
  2、 ThreadLocal 提供 get 和 set 方法,为每一个使用这个变量的线程都保存有一份独立的副本。

public T get() {...}
public void set(T value) {...}
public void remove() {...}
protected T initialValue() {...}

1、get() 方法是用来获取 ThreadLocal 在当前线程中保存的变量副本;
2、set() 用来设置当前线程中变量的副本;
3、 remove() 用来移除当前线程中变量的副本;
4、 initialValue() 是一个 protected 方法,一般是用来在使用时进行重写的,如果在没有 set 的时候就调用 get,会调用 initialValue 方法初始化内容。

set 和get方法底层是调用 ThreadLocalMap方法 像集合中的Map方法一样

12.在哪些场景下会使用到 ThreadLocal?

  在调用 API 接口的时候传递了一些公共参数,这些公共参数携带了一些设备信息(是安卓还是 ios),服务端接口根据不同的信息组装不同的格式数据返回给客户端。假定服务器端需要通过设备类型(device)来下发下载地址,当然接口也有同样的其他逻辑,我们只要在返回数据的时候判断好是什么类型的客户端就好了。上面这种场景就可以将传进来的参数 device 设置到 ThreadLocal 中。用的时候取出来就行。避免了参数的层层传递。

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

  synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
  另外,在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。==如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,==这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。JDK6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
  synchronized 关键字底层原理属于 JVM 层面。

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l
在这里插入图片描述
  从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

14.如何在项目中使用 synchronized 的?

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

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

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

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

  JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
  锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,它们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

轻量级锁

  轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化.
  轻量级锁对使用者是透明的,即语法仍然是 synchronized
  假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块 A
 method2();
 }
}
public static void method2() {
 synchronized( obj ) {
 // 同步块 B
 }
}

何为 Mark Word 也就是对象头

enum { locked_value             = 0,  // 轻量级锁
       unlocked_value           = 1,  // 无锁,普通对象
       monitor_value            = 2,  // 重量级锁
       marked_value             = 3,  // GC标记
       biased_lock_pattern      = 5   // 偏向锁

在这里插入图片描述
在这里插入图片描述
对象头源码地址

  创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
在这里插入图片描述
   让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
在这里插入图片描述
  如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
在这里插入图片描述

  • 如果 cas 失败,有两种情况
     如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
     如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
    在这里插入图片描述

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    在这里插入图片描述

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

  如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    在这里插入图片描述
  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED
      在这里插入图片描述
  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

偏向锁

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

自旋锁和自适应自旋

  重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况
在这里插入图片描述
自旋重试失败的情况
在这里插入图片描述

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

锁消除

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

锁粗化

  原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小。只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
  大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗
  对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。

16.谈谈 synchronized 和 ReenTrantLock 的区别?

  1. synchronized 是和 for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)、性能已不是选择标准。
  2. synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API。synchronized 是依赖于 JVM 实现的,JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
  3. ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

17.synchronized 和 volatile 的区别是什么?

  1. volatile 本质是在告诉 JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

18. 谈一下你对 volatile 关键字的理解?

  volatile 关键字是用来保证有序性和可见性的。这跟 Java 内存模型有关。我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排序,CPU 也会做重排序的,这样做是为了减少流水线阻塞,提高 CPU 的执行效率。这就需要有一定的顺序和规则来保证,不然程序员自己写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作、有序性实现的是通过插入内存屏障来保证的。
被 volatile 修饰的共享变量,就具有了以下两点特性:

  1. 保证了不同线程对该变量操作的内存可见性;
  2. 禁止指令重排序。

19.说下对 ReentrantReadWriteLock 的理解?

  ReentrantReadWriteLock 允许多个读线程同时访问,但是不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁:一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。

  ReentrantReadWriteLock 基于 AQS 实现,它的自定义同步器(继承 AQS)需要在同步状态 state 上维护多个读线程和一个写线程,该状态的设计成为实现读写锁的关键。
  ReentrantReadWriteLock 很好的利用了高低位。来实现一个整型控制两种状态的功能,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。

  • ReentrantReadWriteLock 的特点:
    1. 写锁可以降级为读锁,但是读锁不能升级为写锁;
    2. 不管是 ReadLock 还是 WriteLock 都支持 Interrupt,语义与 ReentrantLock 一致;
    3. WriteLock 支持 Condition 并且与 ReentrantLock 语义一致,而 ReadLock 则不能使用 Condition,否则抛出 UnsupportedOperationException 异常;
  1. 默认构造方法为非公平模式 ,开发者也可以通过指定 fair 为 true 设置为公平模式 。
  • 升降级
    1. 读锁里面加写锁,会导致死锁;
    2. 写锁里面是可以加读锁的,这就是锁的降级。

20.说下对悲观锁和乐观锁的理解?

  • 悲观锁
      总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。。传统的关系型数据库里边就用到了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
  • 乐观锁
      总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,==可以使用版本号机制和 CAS 算法实现.==乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的
  • 两种锁的使用场景
      从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

21.乐观锁常见的两种实现方式是什么?

乐观锁一般会使用版本号机制或者 CAS 算法实现。

  • 版本号机制
      一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
  • CAS 算法
      即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数:
    1. 需要读写的内存值 V
    2. 进行比较的值 A
    3. 拟写入的新值 B
        当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
        结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

23. 乐观锁的缺点有哪些?

    1. ABA 问题
        如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA” 问题。
        JDK 1.5 以后的AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
    1. 循环时间长开销大
        自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一:它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二:它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。
    1. 只能保证一个共享变量的原子操作
         CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。 但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 AtomicReference 类把多个共享变量合并成一个共享变量来操作。

24.CAS 和 synchronized 的使用场景?

  简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)。

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Red-P

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值