面试3.4_Java_线程安全

本文详细解读Java中线程的创建方式(Thread, Runnable, Callable),sleep()与wait()的区别,submit()与execute()的应用,以及线程间通信、volatile与synchronized的对比,涵盖了AQS和ReentrantLock等内容,适合并发编程面试准备。


参考链接

  1. 多线程 面试题
  2. 并发编程面试题
  3. JUC包下的锁和工具
  4. volatile可见性实现原理
  5. synchronized 面试题 深度解析
  6. AQS详解

基础知识

创建线程方式

  1. 继承 Thread ;重写 run 方法;
  2. 实现 Runnable 接口;重写 run 方法;
  3. 实现 Callable 接口;重写call()方法

Thread 、Runnable 、Callable 区别

  1. Runnable接口:

    1. 避免lava单继承特性而带来的局限
    2. 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
    3. 适合多个相同程序代码的线程区处理同一资源的情况。
    4. start()启动线程后,线程被JVM放到就绪队列中,如果有处理机可用,则执行run方法。
  2. Callable接口

    1. 重写call方法,线程执行完毕后会有返回值,其他方法无返回值。

sleep()、wait() 区别

sleep()wait()
Thread类中的静态方法Object类中的方法
可在任何地方调用只能在同步代码块或同步方法中使用(即使用synchronized关键字修饰的)
不会释放对象锁 ,暂停执行指定时间,到时后会自动恢复会**放弃对象锁,进入等锁池,只有notify / notifyAll**能唤醒。

submit()、execute() 区别?

  1. 接收参数submit()可以执行 Runnable 和 Callable 类型的任务。execute()只能执行 Runnable 类型的任务。

  2. 返回值submit()方法可以返回持有计算结果的 Future 对象,而execute()没有

  3. 异常处理submit()方便Exception处理


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

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

synchronized、volatile 的区别

volatilesynchronized
只能修饰变量可修饰类、方法、代码段
防止指令重排
仅能实现变量的修改可见性可见性 + 原子性
不会造成线程的阻塞可能会阻塞

synchronized 、Lock 的区别

区别synchronizedLock
存在层面关键字,JVM托管接口
修饰对象类、方法、代码段代码块
【获取 / 释放】锁不需要手动获取锁、释放锁,不会造成死锁需要自己加锁和释放锁,如果释放锁就会造成死锁
锁状态无法获知可获知
获取锁的方式抢占式自旋获取锁直到获取

atomic 原理

主要利用 CAS + volatile + native 方法来保证原子操作,从而避免 synchronized 的高开销


volatile

volatile作用

  1. 保证可见性
  2. 禁止指令重排
  3. 和 CAS 结合,保证了原子性

volatile保证可见性原理

缓存一致性协议:MESI
  1. Modify:当缓存行中的数据被修改时,该缓存行置为M状态

  2. Exclusive(独占):数据只有一个缓存行使用时,置为E状态

  3. Shared:当其他CPU中也读取某数据到缓存行时,所有持有该数据的缓存行置为S状态

  4. Invalid(无效):当某个缓存行数据修改时,其他持有该数据的缓存行置为I状态

  5. 核心的思想:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

总线嗅探机制

volatile 数组

  1. 可以创建 volatile 类型数组,但只是一个指向数组的引用
  2. 若多个线程同时改变数组的元素,volatile不起作用

volatile 变量、atomic 变量的区别

volatile 变量可确保先行关系,即写操作会发生在后续的读操作之前,不能保证原子性;atomic 方法可以让这种操作具有原子性


volatile原子操作

  1. 如果使用volatile修饰long和double,则读写都是原子操作
  2. 对于64位的引用地址的读写,都是原子操作
  3. 在实现JVM时,可以自由选择是否把读写long和double作为原子操作
  4. 推荐JVM实现为原子操作

synchronized

synchronized 底层实现

  1. 同步方法通过ACC_SYNCHRONIZED调用monitorenter / monitorexit实现
  2. 同步代码块通过monitorenter / monitorexit / monitorexit实现;第二个monitorexit可防止因为异常退出而未释放锁的情况

synchronized 使用场景

  1. 非静态方法,锁对象实例(this)

    public synchronized void method() {}
    
  2. 静态方法,锁类对象

    public static synchronized void method() {}
    
  3. 作用于 Lock.class,锁住的是 Lock 的 Class 对象,也是全局只有一个。

    synchronized (Lock.class) {}
    
  4. 作用于 this,锁住的是对象实例

    synchronized (this) {}
    
  5. 作用于静态成员变量,锁住的是该静态成员变量对象

    public static Object monitor = new Object(); 
    synchronized (monitor) {}
    

synchronize 维护了几个队列?

  1. 3个双向链表
    1. 获取锁失败,_cxq 阻塞链表
    2. _EntryList 链表
    3. 等待链表_WaitSet

synchronized 非公平锁体现在哪些地方?

  1. 当持有锁的线程释放锁时
    1. 锁的持有者 owner 属性赋值为 null
    2. 唤醒等待链表中的一个线程(假定继承者)
  2. 当获取锁失败进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的

JVM 做了哪些锁优化?

偏向锁、轻量级锁、自旋锁、自适应自旋、锁消除、锁粗化·

synchronized 锁升级的原理

  1. 在锁对象的对象头里面有一个 threadid 字段
  2. 第一次访问时 threadid 为空,jvm 让其持有偏向锁,并使 threadid = 线程 id
  3. 再次进入,先判断 threadid == 线程 id ?可以直接使用此对象:升级偏向锁为轻量级锁
  4. 通过自旋循环一定次数来获取锁
  5. 执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

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


Lock体系

ReentrantLock、AQS

CLH:虚拟的双向队列
  1. 虚拟的双向队列:不存在队列实例,仅存在结点之间的关联关系
  2. 将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
共享变量state
  • 通过protected类型的getState,setState,compareAndSetState进行操作

    private volatile int state; // volatile修饰保证线程可见性
    
    //返回同步状态的当前值
    protected final int getState() {  
            return state;
    }
     // 设置同步状态的值
    protected final void setState(int newState) { 
            state = newState;
    }
    //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
    protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
获取锁资源
  1. 每次tryAcquire,传+1
  2. 每次tryRelease,传-1
  3. 新增的线程获取不到锁,则加入CLH队尾,如果前驱为头结点则尝试获取锁,否则获取不到锁之后park进入等待状态
  4. 占锁任务结束后unpark唤醒等待队列中最前边的那个未放弃线程
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

ReadWriteLock:未完成

  1. 继承Sync类实现公平锁、非公平锁

并发容器

ConcurrentHashMap:未完成


CopyOnWriteArrayList:未完成


ThreadLocal

ThreadLocal 实现原理
  1. 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 哪些使用场景?
  1. 数据库连接
  2. session 管理等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值