面试

面试题汇总(线程安全)

2019-11-03  本文已影响0人  王勇1024

1.请问下面的方法是线程安全的吗?

public int getNext(){
    return value++;
}

解答:该方法不是线程安全的。虽然递增运算someValue++看上去是单个操作,但实际上它包含三个独立操作:读取value,将value加1,并将计算结果写入value

2.解决线程安全问题的方法有哪些?

A.不在线程之间共享该状态变量。(如Servlet)
B.将状态变量修改为不可变的变量。(如String)
C.在访问状态变量时使用同步。(synchronized、Lock)

3.什么是线程安全?

当多个线程访问某一个类时,这个类始终能够表现出正确的行为,那么就称这个类是线程安全的。

4.Java内存模型

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

5.并发编程中的三个概念

6.对double和long的操作是否是原子性的?

在32位平台下,对64位数据的读取和赋值是可能需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

7. volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。

7.1 volatile如何保证可见性?

第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

7.2 volatile如何保证原子性?

volatile无法保证原子性。

7.3 volatile如何保证有序性?

volatile通过禁止指令重排在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

8. synchronized

参考:
深入理解Java并发之synchronized实现原理
synchronized特性与原理

8.1 synchronized的使用方式

synchronized关键字最主要有以下3种应用方式,下面分别介绍

8.2 synchronized的特性

9. TheadLocal

10.CAS & Atomic

10.1 CAS

参考:Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作

由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。

CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

11. 多线程优化

下面的代码是线程安全的吗?问题在哪儿?
答:不是线程安全的。它的核心问题有两点:一个是锁有可能会变化,另一个是 Integer 和 String 类型的对象不适合做锁。如果锁发生变化,就意味着失去了互斥功能。 Integer 和 String 类型的对象在 JVM 里面是可能被重用的,除此之外,JVM 里可能被重用的对象还有 Boolean,那重用意味着什么呢?意味着你的锁可能被其他代码使用,如果其他代码 synchronized(你的锁),而且不释放,那你的程序就永远拿不到锁,这是隐藏的风险。

class Account {
  // 账户余额  
  private Integer balance;
  // 账户密码
  private String password;
  // 取款
  void withdraw(Integer amt) {
    synchronized(balance) {
      if (this.balance > amt){
        this.balance -= amt;
      }
    }
  } 
  // 更改密码
  void updatePassword(String pw){
    synchronized(password) {
      this.password = pw;
    }
  } 
}

下面的代码是线程安全的吗?问题在哪儿?
答:不是线程安全的。contains() 和 add() 方法虽然都是线程安全的,但是组合在一起却不是线程安全的。所以你的程序里如果存在类似的组合操作,一定要小心。

void addIfNotExist(Vector v, 
    Object o){
  if(!v.contains(o)) {
    v.add(o);
  }
}

InterruptedException 异常处理需小心
下面的代码存在什么问题?

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
  // 省略业务代码无数
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}

这段代码在执行的时候,大部分时间都是阻塞在 sleep(100) 上,当其他线程通过调用th.interrupt().来中断 th 线程时,大概率地会触发 InterruptedException 异常,在触发 InterruptedException 异常的同时,JVM 会同时把线程的中断标志位清除,所以这个时候th.isInterrupted()返回的是 false。

正确的处理方式应该是捕获异常之后重新设置中断标志位,也就是下面这样:

try {
  Thread.sleep(100);
}catch(InterruptedException e){
  // 重新设置中断标志位
  th.interrupt();
}

ReentrantLock

你已经知道 tryLock() 支持非阻塞方式获取锁,下面这段关于转账的程序就使用到了 tryLock(),你来看看,它是否存在死锁问题呢?

class Account {
  private int balance;
  private final Lock lock
          = new ReentrantLock();
  // 转账
  void transfer(Account tar, int amt){
    while (true) {
      if(this.lock.tryLock()) {
        try {
          if (tar.lock.tryLock()) {
            try {
              this.balance -= amt;
              tar.balance += amt;
            } finally {
              tar.lock.unlock();
            }
          }//if
        } finally {
          this.lock.unlock();
        }
      }//if
    }//while
  }//transfer
}

while(true) 没有 break 条件,从而导致了死循环。除此之外,这个实现虽然不存在死锁问题,但还是存在活锁问题的,解决活锁问题很简单,只需要随机等待一小段时间就可以了。

上一篇 下一篇

猜你喜欢

热点阅读