java面试题之多线程

1谈谈 volatile 理解

volatile: 低配的同步锁,保障有序性(禁止指令重排,内存屏障),可见性(打小报告)
有序性是如何保证的:通过插入内存屏障,来禁止 屏障 之前与屏障之后的指令交换位置
可见性:

2谈谈CAS

含义,底层原理,Unsafe 类理解
CAS 缺点:

多线程环境下,对共享变量的操作,要么加锁,要么CAS

神马是CAS?
CAS 底层实现?
CAS 优缺点?
CAS ABA 问题?
如何解决?
原子引用?

CAS 含义以及的层代码实现(AtomicInteger 类)

加锁 :保证只能同时有一个线程去操作 数据
CAS:比较交换,偏量值 ,主要思想是通过读取主内存的值 和预期旧值比较,如果相同,则将新值=预期旧值+偏量值 写入主内存(如果写入失败,重新读取旧值)
经典的使用例子便是jdk 中的atomic 包(查看源码可以看见有乐观锁的思想)
以AtomicInteger 的 getAndIncrement() 方法为例

AtomicInteger atomicInteger=new AtomicInteger();
        atomicInteger.getAndIncrement();
        --/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    

Unsafe 类:


       /**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object o
     * at the given offset.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
     // O 是指当前对象,或者说是首地址
     //offset 是指内存偏移量// delta 是指变化量
     
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
        // 读取当前对象+offset 内存地址上的值
            v = getIntVolatile(o, offset);// 获取主内存值
        } while (!compareAndSwapInt(o, offset, v, v + delta));// 如果没有交换成功,重新读取主内存值,进行下一次交换,直到成功为止
        return v;
    }

compareAndSwapInt 的底层就是 c语言的实现了,位于unsafe.cpp 中

UNSAFE_ENTRY(jboolean,uNSAFE_cOMPAREaNDsWAPiNT(jnieNV *env,jobject unsafe,jlong offset,iint e,jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p=JNIHandles::resolve(obj);// 对象首地址
jint * addr=(jint*) index_oop_from_field_offset_long(p,offset);// 对象的属性字段的地址
return (jint)(Atomic::cmpxchg(x,addr,e))==e;// 注意atomic 则保证此操作是原子性的不可中断的
UNSAFE_END

CAS=Compare and Swap CPU 并发原语(原子性)

CAS 优缺点

  1. 如果CAS 失败,会一直尝试CAS,空转,CPU开销
  2. 只能保证单个变量
  3. ABA 问题

ABA 问题

主内存:初始值 1
线程 A 和线程B 读取主内存 值 为自己工作空间(拷贝一份)
线程 在CAS 过程中 将数据从主内存获取(得到1)
线程A 将数据改为 改为2 并且完成刷回主内存
线程A 将数据改为1 并且完成刷回主内存
线程B 执行 CompareAndSwap 操作 将数据 更改为3(CAS 个会成功)
因为线程B 读取到的仍是1(虽然不是最初的那个1,中途被改变,线程B无法感知)

忽略了中间过程的变化,只注意了首尾状态

原子引用更新

Atomic 只是提供了 基本数据类型的原子操作
如果现在有User/Account 这些类型也需要原子操作呢?

class User{
    String name;
    int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
       User user1=new User("zhangsan",10);
       User user2=new User("lisi",20);
        AtomicReference<User> atomicReference=new AtomicReference<>();
        atomicReference.set(user1);
        System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
        System.out.println(atomicReference.compareAndSet(user1,user2)+"\t"+atomicReference.get());
    }
}

输出结果:
java面试题之多线程_第1张图片

3 原子类AtomicInteger ABA 问题?(待续)

如何解决ABA 问题
原子类的缺点:只能保证单个变量,而不能保证一个复杂的引用对象

4 原子更新引用(待续)

AtomicReference

解决ABA可以使用AtomicStampReference(相比AtomicReference 多了一个版本号stamp)

5 ArrayList 是线程不安全的,编写一个不安全的代码(待续)

给出解决方案(比较方案的底层实现)

  1. 同步容器 Vector
  2. Collections.synchronizedList (包装一层,需要先获取(mutux 互斥锁) 才能操作数组
  3. CopyOnWriteList 读写分离(写时复制,读 在 原数组读,写时,将拷贝原数组,然后在副本中写入,然后用副本替换原数组(使用了volatile 修饰数组,保证了可见性)

类似线程不安全的 还有hashMap,hashSet(底层是HashMap) 实现

6 公平锁、非公平锁、可重入锁、自旋锁(待续)

锁的功能:让临界区互斥执行
锁的内存语义:

  1. 线程释放锁时,JMM会将该线程的本地内存中的共享变量刷新到主内存中(和volatile 写有相同的内存语义,刷回主内存)

  2. 线程获取锁时,JMM会把该线程的本地内存中的的共享变量置为无效(和volatile 读有相同的内存语义,去主内存读取)。

  3. 公平vs 不公平
    公平:得到锁的顺序和申请的顺序一致(FIFO)
    非公平:不保证顺序,新线程申请锁是先尝试获取锁,获取失败,再排队(锁的实现默认是非公平的,性能更好)

  4. 可重入(递归锁)
    持有锁的线程,可以进入 该锁 控制的所有代码
    场景:单个线程重复多次访问同一个对象

  5. 自旋锁
    获取锁失败,会循环尝试获取锁,而不是阻塞,
    优点:不阻塞
    缺点:不释放cpu ,尝试减获取失败,会影响cpu 利用率

手写一个自旋锁(可以使用AtomicReference 去实现)


public class SpinLock {
     AtomicReference<Thread> lock=new AtomicReference<>();
    public void lock(){
         Thread t=Thread.currentThread();
         System.out.println(t.getName()+" 进入 lock");
         while (!lock.compareAndSet(null,t)){
             // 自旋直到获取到锁
         }

     }
    public void unLock(){
        Thread t=Thread.currentThread();
       lock.compareAndSet(t,null);// 只有持有锁的线程才能解锁
        System.out.println(t.getName()+" unlock");
    }

    public static void main(String[] args) {
        SpinLock spinLock=new SpinLock();
        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                spinLock.unLock();
            }


        },"thread1").start();
        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                spinLock.unLock();
            }
        },"thread2").start();
    }
}

效果

thread1 进入 lock
thread2 进入 lock
thread1 unlock
thread2 unlock

  1. synchronized vs lock
  2. java面试题之多线程_第2张图片

7 CountDownLatch/CycliBarrier/Semaphore(待续)

CountDownLatch: 倒计时,计数为0,回到主线程

8 阻塞队列知道吗(待续)

特点:

阻塞队列,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致是:线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。
当阻塞队列是空时,从队列中获取元素的操作将被阻塞。 当阻塞队列是满时,往队列里添加元素的操作将被阻塞。

优点:

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦满足条件,被挂起的线程又会自动被唤醒。

为什么需要BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你包办了。

在concurrent包发布之前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给程序带来不小的复杂度。

核心方法:

方法 无异常 带异常 阻塞操作 限时阻塞
添加 offer(不抛异常) add(满了会抛异常) put offer(E e, long timeout, TimeUnit unit)
移除 poll remove(空了会抛异常) take offer(E e, long timeout, TimeUnit unit)
第一个元素 peek element(空会抛异常)

种类,使用场景

9 线程池用过吗?ThreadPoolExecutor 的理解(待续)

线程池的优点:线程复用;并发量可控;资源可控
如何使用
重要参数:
核心线程数:内部员工,
阻塞队列:任务仓库;
非核心线程:外包人员,
无空闲线程,放仓库,仓库堆满了,任务办不完,让外包人员做,如果外包人员达到最大数量,也做不完,就交给拒绝策略
底层工作原理

10 线程池,如何设置合理参数(待续)

  1. 线程池的拒绝策略
  2. 你在工作中使用哪一个创建线程池的方法?哪一个用的多?
    答案是:一个都不用,生产中使用ThreadPoolExecutor 创建
  3. 既然jdk 提供了Executors 为什么不使用?
    因为自带的会默认是 请求队列最大创建Integer.MAX_VALUE ,可能会导致OOM,大量请求积压
  4. 在工作中如何使用线程池,是否自定义过线程池使用
    ThreadPoolExecutor 去创建

拒绝策略:(内置)均实现 RejectedExecutionHandler

ThreadPoolExecutor.AbortPolicy;// 抛异常
ThreadPoolExecutor.CallerRunsPolicy;// 调用者执行
ThreadPoolExecutor.DiscardOldestPolicy;// 移除最久未执行的任务
ThreadPoolExecutor.DiscardPolicy;// 静默处理,直接丢掉新任务

  1. 合理配置线程池你是如何考虑的
    cpu 密集型:(系统需要大量运算),尽量少的上下文切换,没有阻塞,cpu 一直全速运行
    一般公式:cpu核数+1个
    io 密集型:任务线程并不是一直执行任务,则应配置尽可能多的线程,如cpu 核数*2

IO密集型,即该任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。

所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。

11 死锁编程及定位分析(待续)

什么是死锁
死锁代码
解决

12 java 里面锁谈谈你的理解,能说多少说多少

13 lock vs synchronized

lock:可中断,可非公平,可公平,多个condition(可以精确唤醒),手动释放
synchronized: 非公平,不可中断
例题:A线程打印10次,然后B打印10次,然后C 打印10次
重复11次上述流程

**
 * A,B,C 三个线程,A 输出5次,B 输出10次,C输出15次  重复11轮(A->B->C)
 */

public class ReadLock {

    private int state = 0;//a 0,b1,c2
    private ReentrantLock lock = new ReentrantLock();
    private Condition[] conditions=new AbstractQueuedSynchronizer.ConditionObject[3];// 每个线程对应一个condition

    {
        for (int i = 0; i < 3; i++) {
            conditions[i] = lock.newCondition();
        }
    }

    private void print(String content, int times, int threadNo) {
        try {
            lock.lock();
            while (state != threadNo) {
                conditions[threadNo].await();
            }
            for (int i = 0; i < times; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + content);
            }

        } catch (Exception e) {

        } finally {
            state+=1;
            state%=3;
            conditions[state].signal();// 唤醒下一个线程
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadLock readLock=new ReadLock();
        new Thread(()->{
            for(int i=0;i<11;i++)
            readLock.print("a",5,0);
        },"A").start();
        new Thread(()->{
            for(int i=0;i<11;i++)
            readLock.print("b",10,1);
        },"B").start();
        new Thread(()->{
            for(int i=0;i<11;i++)
            readLock.print("c",15,2);
        },"C").start();
    }

}

final 引用不能从构造函数溢出?

class A{
final obejct b;
A(){
b=bew Object()'}
}

run

写final 域的重排序规则可以确保:将引用变量被任意线程可见之前,该引用变量指向对象的final域已经在构造函数正确初始化过 。
也就是如下操作:

A a=new A();
过程细分为:
1. 进入A 构造函数
2. 初始化final 域(也就是b 属性)// 写final域的重排序规则保证这一步只能在构造函数内部完成,而不是重排序到构造函数外去完成
3. 退出A 构造函数
4. 堆上对象 被引用变量a 指向

happens before 原则

JSR-133:

  1. 程序顺序原则:一个线程中的每个操作,happens-before 与该线程的任意后续操作
  2. 监视器锁原则:对一个锁的解锁 happens-before 于随后对这个锁的加锁
  3. volatile 变量原则:对一个volatile 域的写 happens-before 于任意后续对该域的读
  4. start() 原则:线程A 中通过start() 方法启动线程B,则 A线程的ThreadB.start() happens-before 于线程B 中的任意操作
  5. join() 原则:线程A 中执行ThreadB.join(),线程B 的任意操作 happens-before 于线程A 从ThreadB.start() 返回。

你可能感兴趣的:(多线程,javase,面试,CAS)