多线程相关

1、并发和并行的区别
  • 并发:单个CPU分时间段处理多个任务,某个时间段只执行一个任务,强调的是能处理多个任务的能力,不一定同时。
  • 并行:同一时间段多个CPU执行多个任务,强调的是同时处理多个任务
2、多线程有哪几种创建方式
  • 继承Thread类,重写run方法,线程无返回值

  • 实现Runnable接口,重写run方法,线程无返回值

  • 实现Callable接口,重写call方法,

    • 使用FutureTask包装Callable对象,
    • thread 里面传的target对象是FutureTask包装Callable对象后的对象
    • 调用FutureTask对象的get方法获得返回值
  • 用线程池创建线程

    • Executor提供的线程池有:
      • newSingleThreadExecutor:单线程线程池,自始至终都是一个线程池
      • newFixedThreadPool:固定线程数量,任务多不会创建新线程,任务少不会销毁线程
      • newCacheThreadPool: 线程数量不固定,我们无法控制创建的线程数量;但是如果第二个任务开始后第一个任务已经结束,则复用第一个任务创建的线程
      • newSingleThreadScheduledExecutor:调度任务在指定时间执行,也是只有一个线程池
      • newScheduledThreadPool:创建拥有固定线程数量,可以定时或者周期性的执行任务的线程池
      • newSingleThreadScheduledPool: 只有一个线程用来调度任务周期性执行或者在指定时间点执行
  • new ThreadPoolExecutor 自己定义创建,线程池参数

    • 参考文章:https://www.cnblogs.com/thisiswhy/p/12690630.html

    • corePoolSize: 核心线程数大小,可以动态调整

    • maximumPoolSize: 最大线程数,可以动态调整

    • keepliveTime: 空闲线程存活时间,默认只有当线程数超过了核心线程数,并且线程空闲时间超过了该值,线程才会销毁

    • TimeUnit:存活时间的单位

    • 工作队列:当核心线程数已满,则将任务放到等待队列中

    • 执行任务流程

      执行任务,当前线程小于核心线程数,则创建线程执行;否则判断阻塞队列是否已满,如果不满则将任务放到阻塞队列中;如果阻塞队列已满,然后当前线程数小于最大线程数,则创建新线程执行当前任务;如果线程数大于等于最大线程数,则执行拒绝策略

    • 线程池被创建后里面就会有线程么

      刚被创建的线程池里面一开始是没有线程的,如果需要预热的话,可以调用2个方法:prestartAllCoreThreads,开启所有core线程;prestartCoreThread:仅开启一个

    • 核心线程会被回收么

      核心线程默认不会被回收,如果想在空闲时间超过某个值后回收线程,可以设置:allowCoreThreadTimeOut

3、java 中wait和sleep的区别和联系
  • sleep是Thread的一个静态方法,wait是object的一个方法
  • sleep是让一个线程进入睡眠状态,不会释放锁;wait是让线程进入阻塞状态,会释放锁
  • sleep可以在非同步代码块中调用, wait必须在同步代码块中调用,通常搭配notify和notify all来一起使用
4、进程和线程的区别
  • 进程是系统分配资源和调度的基本单位
  • 线程是程序执行的最小单位,被包含在进程之内,一个进程中可以并发多个线程,可以理解成系统分配处理器时间资源的基本单位
5、java线程的生命周期
  • new:使用new方法创建出来的线程
  • 就绪:调用start方法后,此时线程处于等待CPU分配资源的阶段,谁先抢到资源,谁先执行
  • 运行:就绪的线程被调度并获得了CPU资源后便进入了此状态
  • 阻塞:调用wait或者sleep方法,此时需要其他机制将处于阻塞状态的线程唤醒,唤醒后的线程不会立刻执行run方法,需要再次等待CPU分配资源进入运行状态
  • 销毁:线程执行完毕后或者提前被强制性终止或者出现异常导致结束,那么线程就会被销毁,释放资源
6、synchronize锁升级的过程
  • 一个对象由4部分组成:

    • 对象头(header)

      • markWord:32位虚拟机4个字节,64位虚拟机8个字节,主要存储:hashCode、分代年龄,锁标志位,线程持有的锁,偏向线程ID等,是一个非固定的数据结构

        • 32位markWord的数据:
      • 类型指针:指向类型元数据的指针,64位虚拟机中,不开启指针压缩是8个字节,开启指针压缩是4个字节;32位虚拟机就是4个字节

    • 实例数据

    • 数组长度(当对象是数组的时候才会有)

    • 8字节对齐填充

    • 引出一个问题:一个Object对象在内存中占用的字节数为:32位下4+4=8;64位下:8+4或者8+8再加上8字节对齐的要求所以是16

  • 流程:

    • 线程A获取锁对象,发现没有其他线程获得该锁,则会在markWord中通过CAS的方式记录当前线程的ID,将锁标志位置为偏向锁状态
    • A线程还没执行完,此时B线程来获得锁,发现锁状态是偏向锁,而且偏向锁中的线程ID不是线程B的线程ID,线程B通过CAS操作尝试将线程ID刷成线程B的线程ID,尝试一段时间还不成功后,升级为轻量级锁(暂停线程A,撤销偏向锁,升级轻量级锁),若尝试成功,则仍然为偏向锁,只是此时的线程ID刷成线程B的线程ID
    • 升级为轻量级锁后,线程A获得的轻量级锁会先把锁对象的对象头的markWord复制一份,放到线程A的栈帧中,然后使用CAS把对象头中的内容替换为线程A存储markWord副本的地址
    • 如果在线程A复制对象头CAS写地址的同时,线程B也在CAS,并且线程A成功了,那线程B就尝试CAS来等待,当自旋次数太多,或者同时线程C来CAS了则膨胀为重量级锁
  • 重量级锁相关东西(参考:https://blog.csdn.net/javazejian/article/details/72828483):

    • synchronize修饰代码块的时候会生成:monitorenter和monitorexit两条字节码,修饰方法的时候会生成 ACC_SYNCHRONIZED标志位

    • 每一个对象都会有一个monitor对象与之关联,当锁是重量级锁的时候,原来放指针的地方存放的是monitor对象的地址,monitor对象如下:

      ObjectMonitor() {
          _header       = NULL;
          _count        = 0; //记录个数
          _waiters      = 0,
          _recursions   = 0;
          _object       = NULL;
          _owner        = NULL;
          _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
          _WaitSetLock  = 0 ;
          _Responsible  = NULL ;
          _succ         = NULL ;
          _cxq          = NULL ;
          FreeNext      = NULL ;
          _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
          _SpinFreq     = 0 ;
          _SpinClock    = 0 ;
          OwnerIsThread = 0 ;
        }
      
    • monitor中有2个队列:waitSet,EntryList,用来保存ObjectorWaitor对象列表(每个等待锁的线程都会被封装成一个ObjectorWaitor对象),owner指向持有锁的线程的对象

    • 当多个线程竞争锁对象时,首先会进去Entry_List集合,当线程A获取到对象的monitor后进入Owner区域,并把Owner变量设置为当前线程A同时count+1,若调用wait方法,则释放A持有的锁,同时包装A的ObjectorWaitor进入到waitSet中等待被唤醒;唤醒后的线程重新进入Entry_list重新竞争锁

7、ThreadLocal的底层实现形式和实现的数据结构

参考:https://www.zhihu.com/question/341005993

  • threadLocal意义:主要作用是数据隔离,填充的数据只属于当前线程,对其他线程是不可见的
  • 底层实现:
    • 线程类Thread中有一个ThreadLocalMap类型的变量threadLocals,每个线程都实例化自己线程的threadLocals;
    • ThreadLocalMap中有一个Entry和存放Entry的数组table
    • 当我们在某个线程A中new出来一个ThreadLocal对象 threadLocalA调用set方法的时候,当前线程A的threadLocals变量会初始化创建一个ThreadLocalMap变量,并以threadLocalA对象为key, set的值为value创建一个Entry,存储到table数组中
    • 当调用get的时候也是用threadLocalA对象作为key然后从table数组中取
    • 注意点:Entry中的key是一个弱引用、一个线程创建的多个ThreadLocal对象是放在table数组中的,在放到数组中的时候,先做一次hash,获得索引位置,如果出现了hash冲突,直接往后放的
8、synchronize和lock的区别
  • synchronize是java的关键字, lock是一个接口
  • synchronize可重入,不可中断,非公平锁
  • synchronize获得锁后执行完代码块或者发生异常,可以自动释放锁;而lock需要手动释放锁
9、手写DCL的单例,并解释volatile的作用,以及怎么实现禁止指令重排序的
public class DclSingleTon {
    private DclSingleTon() {}
    private static volatile DclSingleTon instance;
    public static DclSingleTon getInstance() {
        if (instance == null) {
            synchronized (DclSingleTon.class) {
                if (instance == null) {
                    instance = new DclSingleTon();
                }
            }
        }
        return instance;
    }
}
  • volatile具有2个功能:禁止指令重拍和修饰的变量发生变化对所有线程是可见的

  • volatile实现指令重排底层是靠内存屏障,memory barrier,即在编译完的指令间插入memory barrier告诉编译器和CPU,不管什么指令都不能喝memory barrier指令重排,也就是说通过内存屏障禁止在内存屏障前后的指令执行重排序优化

  • 实例化一个对象分为3步:

    memory = allocate(); //1.分配对象内存空间
    instance(memory);    //2.初始化对象
    instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null
    

    由于步骤2和步骤3没有数据依赖关系,而且重排序前后在单线程环境中执行结果并未发生改变,因此经过指令重排序后,步骤2和步骤3的顺序是无法保证的,假如步骤3先执行,然后另外有一个线程刚好去判断 instance == null,此时不为空,但是instance尚未初始化,这样就会有线程安全问题,所以需要添加volatile来保证instance初始化的时候不发生指令重排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值