锁和多线程相关

307 阅读10分钟
  1. Synchronized 内置锁(悲观锁,非公平锁)

    • 对象锁

      1. this
      2. 具体对象
    • 类锁

      1. .class
      2. private static Object obj = new Object();只有一份
    • 可重入锁

      int count;
      //如果是非重入锁,就会自己把自己锁死
      public synchronized void test(){
         count++;
         test();
      }
      
    • 缺点

      1. 无法中断
      2. 无法尝试拿锁,无法尝试10s(一定时间)后拿锁
    • 原理解析

      获取monitor对象的锁

      1. synchronized代码块

        moniterenter moniterexit

      2. synchronized方法

        flags:ACC_SYNCHRONIZED表明当前方法是个同步方法 底层还是monitorenter和monitorexit

      锁的存放位置:对象头

    四种状态:

    ​ 无锁状态(CAS)

    ​ 偏向锁

    ​ 轻量级锁

    ​ 是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

    ​ 重量级锁

    锁可以升级,也可以降级(和虚拟机有关)

    偏向锁是判断对象头的线程id,如果id还是原来线程的,那就直接执行,这个时候偏向锁会比自旋锁更快。

    撤销偏向锁的时候需要stop the world,在有锁的竞争时,偏向锁会多做很多额外操作(比如修改栈帧中的东西),尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。

    ​ 不同锁的比较

  • 注意点

    只要锁的是不同的对象,就可以同时执行

    synchroinzed(null)不行,必须是对象,要存放在对象头中

    多线程能不能共享一把锁:共享锁(可以),排它锁;共享锁又称读锁,排他锁又称写锁

    Synchronized加锁是和具体对象关联的,普通的对象,类对象(.class对象,在虚拟机中一个类只会一个.class对象)

  • 在Java中,我们为什么必须在synchronized方法或同步块中调用wait(),notify()或notifyAll方法

    1. 避免IllegalMonitorStateException

    2. 避免任何在wait和notify之间的潜在竞态条件(也就是外面加了个synchronized锁,避免因为程序的执行顺序影响执行结果)

    3. wait()会释放锁,notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁

    4. 参考 zhuanlan.zhihu.com/p/76625784 leokongwq.github.io/2017/02/24/…

  1. 显示锁

    • 使用规范
    private Lock lock = new ReentrantLock(); //默认是非公平锁,因为非公平锁效率高
    private int count;
    
    public void incr(){
        lock.lock();
        
        //使用显示锁的规范
        try {
            count++;
        }finally {
            lock.unlock();
        }
    
    }
    
    • 非公平锁和公平锁

      1. 若在释放锁的时候总是没有新来的线程来打扰,则非公平锁等于公平锁

      2. 若释放锁的时候,正好有个新线程,而此时位于队列头的线程还没有被唤醒(因为上下文切换时需要 不少开销的),此后后来的线程则优先获得锁,成功打破公平,称为非公平锁

      3. 线程切换耗费一定的时间周期,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定的几率逃离被挂起的开销。

      4. www.jianshu.com/p/f584799f1…

  2. Thread创建有几种方式?

    有且只有两种,可以查看Thread源码

    //Thread
    There are two ways to create a new thread of execution.
    

    通过线程池,或者Callable(最终是包装成futuretask,最后的实现还是Runnable)都不是。

  3. 活锁(避免,需要sleep一小段时间)

  4. 饥饿

  5. ThreadLocal

    如果我们自己实现一个ThreadLocal,内部持有一个Map,key是线程,value是具体的值

    class MyThreadLocal<T> {
      Map<Thread, T> locals = 
        new ConcurrentHashMap<>();
      //获取线程变量  
      T get() {
        return locals.get(
          Thread.currentThread());
      }
      //设置线程变量
      void set(T t) {
        locals.put(
          Thread.currentThread(), t);
      }
    }
    

    但是这样会带来一个问题,多个线程使用的情况下,会对这个Map的竞争比较激烈。

    Java的实现方式

可以看到,Thread有ThreadLocalMap(该类是静态内部类,其实可以认为和外部类没什么关系),然后ThreadLocalMap里面有一个Entry[] table数组,Entry的构造器

Entry(ThreadLocal<?> key, Object value) {  //key是threadlocal,value是具体的值
    super(var1);
    this.value = var2;
}

这样其实就是每个线程里面都有一个threadLocalMap,里面有一个数组,存放不同的threadlocal和对应的值。这样也不会造成对map的竞争激烈问题。而且效率还很高,如下图

还有一个原因是不容易产生内存泄露。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。

ThreadLocal导致的内存泄漏:

​ 在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。

手动释放:

ExecutorService es;
ThreadLocal tl;
es.execute(()->{
  //ThreadLocal增加变量
  tl.set(obj);
  try {
    // 省略业务逻辑代码
  }finally {
    //手动清理ThreadLocal 
    tl.remove();
  }
});
  1. CAS(Compare And Swap)自旋锁(乐观锁),现代CPU提供了一个专门的CAS指令:是一条原子指令,比较并执行

    1. 用其他锁也可以实现原子操作,但是过于笨重,所有人都可以同时操作就是乐观锁,悲观是必须排队一个一个来。

    2. 普通线程,一旦阻塞或者等待,就会发生上下文切换,时间周期5000~20000 3~5ms CAS指令 普通指令0.6ns / CAS指令几百纳秒 不会进入阻塞状态

    3. CAS存在的问题(也是为什么锁还存在)

      1. ABA问题(必问的问题) 添加一个比较版本

      解决:ABA问题用AtomicStampedReference,内部加了时间戳,比较的时候不仅比较值,还看时间戳是不是一样,类似版本号

      1. 开销问题 如果长期执行不成功会耗费CPU

      解决:改用加锁

      1. 只能保证一个共享变量的原子操作

      解决:打包到一个对象里面,可以用AtomicReference

    4. 什么时候使用CAS呢?

      普通的累加

      比较简单的操作

    5. 适应性自旋锁,次数控制在10次左右,一个线程上下文切换的时间,超过这个时间不再执行(避免引入重量级锁造成上下文切换)

    6. markable改没改过

      stamped改过几次

  2. 线程池

    阻塞队列

    • ArrayBlockingQueue:一个由数组构成的有界阻塞队列。(常用)

    • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。(常用)

    • PriorityBlickingQueue:一个支持优先级排序的无界阻塞队列。

    • DelayQueue:一个使用优先级队列实现的无阻塞队列。

      ​ Delay时间到了才能取出,可以做一个单机的缓存系统。

    • SynchronousQueue:一个不存储元素的阻塞队列。

      ​ 不存储元素的阻塞队列(解耦)Okhttp ​ 存取的时候必须有一个立马拿走,不然就不能继续存了。

    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 transfer方法 没有消费者消费,反复阻塞 tryTransfer() 看看有没有消费者消费,尝试,马上返回

    • LinkedBlockingDeque:一个由链接结构组成的双向阻塞队列。

    阻塞队列不完全是阻塞方法,take()和put()等才是真正的阻塞方法。

    线程池

    核心线程池 -> 阻塞队列 -> 非核心线程池

    拒绝策略

    • 直接丢弃最老的那个

    • 抛出异常(默认策略)

    • 让调用者线程自己复制

    • 把最新提交的任务扔掉

      自定义拒绝策略:

      ​ 比如,阻塞等待策略,利用take等方法

    执行

    execute(Runnable runnable)

    submit(Runnable runnable): Future 需要拿到返回结果的时候

    关闭

    ​ shutDown()中断没有执行任务的线程

    ​ shutDownNow()尝试中断正在执行的任务,线程的中断,协作机制所以不一定能中断(读取字节流的操作 就不能中断)

    合理配置线程池中线程大小

    任务特性:

    CPU密集型 计算 机器的CPU核心数+1 一般磁盘一部分当做虚拟内存,但是磁盘的读写速度很慢,可能会产生页缺失,页缺失情况下,为了让CPU充分利用起来,所以要+1

    IO密集型 网络,读写磁盘 (磁盘和网络的读取速度远远低于CPU的读取速度) CPU核心数*2

    混合型

    如果相差不大,考虑拆分成两个线程池

    如果相差很大,自己判断,性能测试

    其余概念

    DMA机制(直接内存访问)

    OS 内核空间,用户空间

    零拷贝概念 mmap

    允许你向我申请一段单独的空间,允许你直接访问这一段空间,不再有拷贝过程

    CPU去操作磁盘的时候,通知磁盘操作,磁盘操作后发送一个中断消息(硬件方面)通知CPU,所以

    IO操作基本不怎么耗费CPU

    sleep(0)可以看成一个运行态的进程产生一个中断,由运行态直接转入就绪态。这样做是给其他就绪态进程使用时间片的机会。

  3. AQS原理

    AbstractQueuedSynchronizer 用来构建一个同步组件

    同步工具类的内部,内部类 继承AQS 功能

    1. AQS的设计模式:模板方法

    固定步骤,具体的步骤实现在子类中。

    Android中模板方法:onDraw,dispatchDraw这两个方法

    因为使用了模板方法模式,所以我们自己实现一个锁很简单,到架构师级别,可能需要自己写个锁,实现一个同步机制。

    1. 了解其中的方法,state
    2. 实现一个类型的ReentranLock的锁

    AQS的基本思想CLH队列锁

  4. 自己实现锁的可重入

  5. Java内存模型(Java Memory Model,也称JMM)

工作内存和主内存只是一个抽象的概念,不是一个真实的群体。

count = count+1  不是原子性操作    操作过程可能被打断

流水线和重排序   intel 十级流水线    提供执行效率

多条指令
  1. volatile的实现原理

    有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令

    • 将当前处理器缓存行的数据写回到系统内存

    • 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效

    volitale+CAS 替换synchroinzed 比如concurrentHashMap 为什么一个线程写,多个线程读,也要加volitale关键字呢? (可见性很重要)

  2. 问题:Java锁

    更详细的并发编程面试题参考并发编程笔记。

参考文章:

www.jianshu.com/p/f584799f1…

leokongwq.github.io/2017/02/24/…

zhuanlan.zhihu.com/p/76625784