并发编程(一)Java并发编程的知识点梳理

来源Java并发编程实战&Java并发编程之美&J.U.C&深入理解java虚拟机&码出高效读书汇总知识点笔记
心理背景:

今年注定不平凡,越来越认识到自己的渺小,马上也就3年工作了,却发现活成了自己曾经讨厌的样子,之前的架构师-Tony(现在已经开公司了)对我来京东前说不忘初心,方得始终,而我却没能深刻理解。
并发编程一直是无法逾越的鸿沟,每次总感觉高并发,大数据,高性能。但是实际上只要不是单线程就总会出现并发的问题。也就是线程安全的问题。之前看过阿里*加多的笔记,同样毕业时间远远比我强太多。不是他优秀,而是我在同样的时间走了好多弯路。(他已经出书了并且极客上开课了),差距很大,之前没有系统看过并发j.u.c和并发书籍,这次三本书一起看,归纳出有用的知识点精华,让并发不再那么难。

前言:

1、先大体认识(Google开源框架介绍,几个模块,模块作用,之间联系,每个模块核心类那些?
2、debug三遍
3、时序图和类图
4、对图分析功能类,比如注释----设计原理和使用场景。

考点核心:

基础篇

  • 线程和进程
  • 线程的实现方式
  • 线程的状态
  • 线程的常用方法
  • 多线程的好处
  • 并发和并行

进阶篇

  • Java内存模型
  • 线程安全性-竞态条件
  • 线程安全性-共享和不可变
  • 原子性和非原子性
  • 多线程-死锁

高级篇

  • ThreadLocal
  • J.U.C
  • 线程池

问题篇

  • SimpleDateFormat
  • ConcurrentHashMap
  • Time
  • 线程池的shutDown
  • ThreadLocal问题(见高级篇)
  • FutureTask

一、并发基础篇

点击这里-基础知识点

二、并发进阶篇

线程安全性的考察4维度(如何避免线程不安全):

1、数据单线程内可见(单线程总是安全的,通过限制数据仅在单线程内可见,可以避免数据被其他线程篡改。最典型的就是线程局部变量,它存储在独立虚拟机栈帧的局部变量中,与其他线程毫无瓜葛。ThreadLocal就是采用这种方式来实现线程安全的。)
2、只读对象(允许复制,拒绝写入。一个对象想要拒绝任何写入,必须要满足以下条件,使用final关键字修饰类,避免被继承,使用private final关键字避免属性被中途修改;没有任何更新方法,返回值不能为可变对象)
3、线程安全类
4、同步与锁机制
线程安全的核心理念-------要么只读,要么加锁。
因为共享和可变本身就是核心

并发编程(一)Java并发编程的知识点梳理_第1张图片

无状态

线程安全类认为是一个在并发环境和单线程环境中都不会被破坏的类。
比如无状态的类的操作计算:
既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。
无状态的对象一定是线程安全的
有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象.不能保存数据,是不变类,是线程安全的。

Java内存模型
并发编程(一)Java并发编程的知识点梳理_第2张图片

  • 所有基本类型的本地变量(boolean、byte、short、char、int、long、float、double)都完全存储在线程堆栈中,因此对其他线程不可见。一个线程可以将pritimive变量的副本传递给另一个线程,但它不能共享原始的本地变量本身.
  • 堆包含在Java应用程序中创建的所有对象,而不管创建对象的线程是什么。这包括基本类型的对象版本(例如Byte、Integer、Long等)。无论对象是创建并分配给局部变量,还是作为另一个对象的成员变量创建,对象都仍然存储在堆中
  • 局部变量也可以是对象的引用。在这种情况下,引用(本地变量)存储在线程堆栈中,而对象本身存储在堆中
  • 对象可以包含方法,这些方法可以包含局部变量。这些局部变量也存储在线程堆栈中,即使方法所属的对象存储在堆中
  • 对象的成员变量和对象本身一起存储在堆中。无论是当成员变量是基本类型时,还是当它是对对象的引用时
  • 静态类变量也与类定义一起存储在堆中
  • 堆上的对象可由具有该对象引用的所有线程访问。当一个线程访问一个对象时,它也可以访问该对象的成员变量。如果两个线程同时调用同一对象上的方法,它们都将访问对象的成员变量,但是每个线程都有自己的局部变量副本
    并发编程(一)Java并发编程的知识点梳理_第3张图片
    两个线程有一组局部变量。其中一个局部变量(局部变量2)指向堆上的一个共享对象(对象3)。它们的引用是本地变量,因此存储在每个线程的线程堆栈中。不过,这两个不同的引用指向堆上的同一个对象
    请注意共享对象(对象3)如何将对对象2和对象4的引用作为成员变量(由对象3到对象2和对象4的箭头所示).通过对象3中的这些成员变量引用,两个线程可以访问对象2和对象4

结合具体代码例子来看Java内存模型

//类定义----堆中
public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }
   //方法定义在堆栈中
    public void methodOne() {
    //局部变量定义在堆栈中
        int localVariable1 = 45;
       //局部变量在堆栈中,引用指向堆上的对象
        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;
        methodTwo();
    }
     //方法定义在堆栈中
    public void methodTwo() {
     //局部变量定义在堆栈中,但是Integer对象存储在堆中如上图中的object1和object5
        Integer localVariable1 = new Integer(99);
    }
}

public class MySharedObject {

    //静态变量指向MySharedObject实例 相当于上图中的object3--存储堆中

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //成员变量对象在堆中
    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}

每个执行methodOne()的线程将在各自的线程堆栈上创建自己的localVariable1和localVariable2副本。localVariable1变量将彼此完全分离,只存在于每个线程的线程堆栈中。一个线程无法看到另一个线程对其localVariable1副本所做的更改
注意MySharedObject类也包含两个成员变量。成员变量本身连同对象一起存储在堆中。这两个成员变量指向另外两个整数对象。这些整数对象对应于上图中的对象2和对象4

硬件内存架构

并发编程(一)Java并发编程的知识点梳理_第4张图片
每个CPU包含一组寄存器,这些寄存器本质上是在CPU内存中。CPU在这些寄存器上执行操作的速度要比在主内存中执行变量的速度快得多。这是因为CPU访问这些寄存器的速度要比访问主内存快得多。
通常,当CPU需要访问主内存时,它会将部分主内存读入CPU缓存。它甚至可以将缓存的一部分读入内部寄存器,然后对其执行操作。当CPU需要将结果写回主内存时,它会将值从内部寄存器刷新到缓存内存,并在某个时候将值刷新回主内存
当CPU需要在高速缓存中存储其他内容时,通常会将存储在高速缓存中的值刷新回主内存。CPU缓存可以一次将数据写入一部分内存,并一次刷新一部分内存。它不必每次更新时都读取/写入完整的缓存。通常,缓存是在称为“缓存线”的较小内存块中更新的。可以将一条或多条高速缓存线读入高速缓存内存,并将一条或多条高速缓存线再次刷新回主内存

Java内存模型和硬件内存体系结构是不同的。硬件内存体系结构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。线程堆栈和堆的部分有时可能出现在CPU缓存和内部CPU寄存器中
并发编程(一)Java并发编程的知识点梳理_第5张图片

当看完Java内存模型,我们可以知道的多线程出现错误的问题点往往来源于:

线程安全性—竞态条件(读取、检查和写入共享变量时的竞争条件)
线程安全性—共享和可变的变量(线程更新(写)到共享变量的可见性)

共享变量引申问题

1、可见性问题—指令重排序
2、失效数据(意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环等)
3、非原子的64位操作
Java内存模型要求,变量的读取操作和写入操作都必须是原子操作。但是非volatile类型的long和double变量,JVM允许64位的读和写操作分解为2个32位的操作。所以在多线程中使用共享且可变的long和double等类型的变量也不安全,除非用volatile或者锁保护。
在这里插入图片描述

i++,和++i的区别

int i = 3;
int a = i++; // a = 3, i = 4
(先返回i的值给a,然后i加1)
int b = ++a; // b = 4, a = 4
(直接在a的基础上加1,然后给b)

Java内存模型中没有volatile修饰的变量可见性问题:

如果两个或多个线程共享一个对象,而没有正确使用volatile声明或同步,那么一个线程对共享对象的更新可能对其他线程不可见
假设共享对象最初存储在主内存中。在CPU 1上运行的线程然后将共享对象读入它的CPU缓存。在这里,它对共享对象进行更改。只要没有将CPU缓存刷新回主内存,在其他CPU上运行的线程就不会看到共享对象的更改版本。这样,每个线程都可能最终拥有自己的共享对象副本,每个副本位于不同的CPU缓存中
下图说明了大致的情况。在左CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其count变量更改为2。此更改对运行在正确CPU上的其他线程不可见,因为尚未将更新刷新回主内存
并发编程(一)Java并发编程的知识点梳理_第6张图片
如果想要解决的话:volatile关键字可以确保直接从主内存读取给定的变量,并在更新时始终将其写回主内存。

volatile关键字(JDK5以上)

在线程操作非易失性变量的多线程应用程序中,出于性能原因,每个线程在处理变量时,都可以将变量从主内存复制到CPU缓存中。如果您的计算机包含多个CPU,每个线程可能运行在不同的CPU上。这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中并发编程(一)Java并发编程的知识点梳理_第7张图片
如果计数器counter变量没有声明为volatile,则无法保证计数器变量的值何时从CPU缓存写入主内存。这意味着,CPU缓存中的计数器变量值可能与主内存中的不一样
并发编程(一)Java并发编程的知识点梳理_第8张图片由于变量的最新值还没有被另一个线程写回主内存,所以线程看不到该变量的最新值,这个问题称为“可见性”问题。一个线程的更新对其他线程不可见。

可见性

可见性的定义:

可见性是指某线程修改共享变量的指令对其他线程来说都是可见的,它反映的是指令执行的实时透明度。
每个线程都有独占的内存区域,如操作栈,本地变量表等。而线程本地内存保存了引用变量在堆内存的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。这里必然有一个时间差,在这个时间差内,该线程对副本的操作,其他线程都是不可见的

可见性问题的解决:

volatile是如何保证的:
当使用volatile修饰变量时候,意味着任何对此便来给你的操作都会在内存中进行,不会产生副本,以保证共享变量的可见性,局部阻止了指令重排序的发生。由此可知,使用单例设计模式即使用双检锁机制也不一定会拿到最新的数据

上图中的线程1和线程2的原理:
如果线程1写入一个易失性变量,而线程2随后读取相同的易失性变量,那么线程2在写入易失性变量之前可以看到的所有变量,在线程2读取易失性变量之后也可以看到
如果线程1读取易失性变量,那么线程1在读取易失性变量时可见的所有变量也将从主内存中重新读取

指令重排序(JVM为了提升性能的一种机制):

只要指令的语义保持不变,就允许Java VM和CPU出于性能原因对程序中的指令进行重新排序。例如

int a = 1;
int b = 2;
a++;
b++;

这些指令可以按照以下顺序重新排序,而不会丢失程序的语义

int a = 1;
a++;

int b = 2;
b++;

然而,当其中一个变量是易失性变量时,指令重新排序就会带来挑战

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦update()方法将一个值写入days,那么新写入的值也将写入主内存。但是,如果Java VM重新排序了这些指令,结果会怎样呢

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

在修改days变量时,month和years的值仍然写入主内存,但这一次是在将新值写入month和years之前。因此,新值对其他线程不可见。重新排序的指令的语义发生了变化

先行发生原则(Happens-Before)

为了解决指令重新排序的挑战,Java volatile关键字除了提供可视性保证之外,还提供了一个“happens-before”保证。(happens before先行发生原则。是时钟顺序的先后,并不能保证线程交互的可见性。)。
并发编程(一)Java并发编程的知识点梳理_第9张图片

但是依然无法保证比如竞态条件下的第2种情况:

并发编程(一)Java并发编程的知识点梳理_第10张图片
虽然最终会将counter值刷入主内存,但是由于同时读取的时候都是0,即使接下来同步到主内存数据也是错误的。
如果不确定共享变量是否会被多个线程并发写,保险的做法是使用同步代码块来实现线程同步。因为所有的操作都需要同步给内存变量(从主存读取和写入比访问CPU缓存更昂贵),所以volatile一定会使线程的执行速度变慢。因此,只在真正需要强制变量可见性时才使用volatile变量或者简化代码的实现以及同步策略的验证(确保自身状态的可见性,确保所引用对象的状态的可见性,标识一些重要的程序生命周期事件的发生比如初始化或者关闭)。如果是比较复杂操作就不要使用了。

锁确保变量可见性:

但是实现方式和volatile略有不同,线程在得到锁时读入副本,释放时候写回内存,锁的操作尤其符号happen before原则。
volatile解决的是多线程共享变量的可见性问题,类似于synchronized但是不具备synchronized的互斥性。所以对volatile变量的操作并非都具有原子性,这是一个犯错误的地方。
能实现count++原子操作的其他类AtomicLong和LongAdder。JDK8推荐使用LongAdder类,比AtomicLong性能更好,有效的减少了乐观锁的重试次数。如果是一写多读那么volatile则可以修饰变量非常合适。比如Cow奶牛系列的CopyOnWriteArrayList.它在修改数据时候会把整个集合数据全部复制出来。,对写操作加锁,修改完成后,再用setArray()把Array指向新的结合。使用volatile可以使得读线程尽快的感知array的修改,不进行指令重排,操作后及对其他线程可见

竞态条件(Race Condition)
并发编程中,由于不恰当的执行时序出现不正确的结果被人叫做:竞态条件(Race Condition)

竞态条件是临界区内可能发生的一种特殊情况。临界段是由多个线程执行的代码段,其中线程的执行顺序对临界段的并发执行结果有影响。

1、最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”,即通过一个可能失效的观测结果来决定下一步的动作。(消息不同步,别人已经修改了这个变量,但是你获取的是老数据)。
举个例子延迟初始化的竞态条件
比如单例模式中没有加锁
并发编程(一)Java并发编程的知识点梳理_第11张图片
2、复合操作
比如上述提到的先检查后执行和读取-修改-写入等操作必须是原子的。否则这种复合操作就会导致线程不安全。

 public class Counter {

     protected long count = 0;

     public void add(long value){
         this.count = this.count + value;
     }
  }

现在有A和B两个线程,this.count = 0;
A: 读取this.count值到寄存器
B: 读取this.count值到寄存器
B: 添加1值给寄存器
B: 将寄存器1值写入内存 this.count现在等于 1
A: 添加1值给寄存器
A: 将寄存器1值写入内存 this.count现在等于 1

线程死锁(应用中多线程抢夺资源和数据库的事务操作):

死锁-》
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

并发编程(一)Java并发编程的知识点梳理_第12张图片
死锁产生的条件:

互斥条件—线程对已经获取到的资源进行排他性使用,该资源同时只有一个线程可以占用。比如使用sychronized关键字修饰的方法代码块
请求并持有条件—指一个线程已经持有了至少一个资源,又提出新的资源请求。(哲学家就餐问题,5个人,5只筷子)每人拿到一只筷子都想等待另一只筷子放下。
不可剥夺条件—线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才有自己释放该资源
环路等待条件—线程和资源之间形成环形链

如何避免死锁:

需要锁的资源的有序性,大家保持某种顺序(哲学家就餐问题,以顺序放下筷子吃饭)


锁的时效性,不能一直持有锁
避免无限期等待:如果两个线程正在等待彼此无限期地使用线程连接,则可能会出现死锁。如果你的线程必须等待另一个线程完成,那么最好在你想要等待线程完成的最长时间内使用join


避免嵌套锁:这是死锁的最常见原因,如果已经存在死锁,请避免锁定另一个资源。如果您只使用一个对象锁,几乎不可能出现死锁情况。例如,这是run()方法的另一个实现,没有嵌套锁,程序运行成功,没有死锁情况


仅限锁定所需内容:您应该仅对您必须处理的资源获取锁定,例如在上面的程序中我锁定了完整的Object资源,但如果我们只对其中一个字段感兴趣,那么我们应该仅锁定特定字段不完整对象。


死锁检测机制,如果检测到可以重新分配优先级
比如编程查找死锁
一组很好的抽象和非常容易使用的多线程类
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); this.scheduler.scheduleAtFixedRate(deadlockCheck, period, period, unit);
一个方法来接收描述处于死锁状态的线程的对象列表
void handleDeadlock(final ThreadInfo[] deadlockedThreads);

public interface DeadlockHandler {
  void handleDeadlock(final ThreadInfo[] deadlockedThreads);
}

public class DeadlockDetector {

  private final DeadlockHandler deadlockHandler;
  private final long period;
  private final TimeUnit unit;
  private final ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  private final ScheduledExecutorService scheduler = 
  Executors.newScheduledThreadPool(1);
  
  final Runnable deadlockCheck = new Runnable() {
    @Override
    public void run() {
      long[] deadlockedThreadIds = DeadlockDetector.this.mbean.findDeadlockedThreads();
    
      if (deadlockedThreadIds != null) {
        ThreadInfo[] threadInfos = 
        DeadlockDetector.this.mbean.getThreadInfo(deadlockedThreadIds);
      
        DeadlockDetector.this.deadlockHandler.handleDeadlock(threadInfos);
      }
    }
  };
  
  public DeadlockDetector(final DeadlockHandler deadlockHandler, 
    final long period, final TimeUnit unit) {
    this.deadlockHandler = deadlockHandler;
    this.period = period;
    this.unit = unit;
  }
  
  public void start() {
    this.scheduler.scheduleAtFixedRate(
    this.deadlockCheck, this.period, this.period, this.unit);
  }
}

创建一个处理程序,以将死锁线程信息输出到System.err。然后我们使用它发送电子邮件

public class DeadlockConsoleHandler implements DeadlockHandler {

  @Override
  public void handleDeadlock(final ThreadInfo[] deadlockedThreads) {
    if (deadlockedThreads != null) {
      System.err.println("Deadlock detected!");
      
      Map stackTraceMap = Thread.getAllStackTraces();
      for (ThreadInfo threadInfo : deadlockedThreads) {
      
        if (threadInfo != null) {
      
          for (Thread thread : Thread.getAllStackTraces().keySet()) {
        
            if (thread.getId() == threadInfo.getThreadId()) {
              System.err.println(threadInfo.toString().trim());
                
              for (StackTraceElement ste : thread.getStackTrace()) {
                  System.err.println("\t" + ste.toString().trim());
              }
            }
          }
        }
      }
    }
  }
}

这将遍历所有堆栈跟踪并打印每个线程信息的堆栈跟踪。这样我们就可以准确地知道每个线程在哪一行上等待,以及哪个锁。这种方法有一个缺点 - 如果其中一个线程正在等待超时(实际上可以看作是临时死锁),它可能会发出错误警报。因此,当我们处理死锁时,原始线程不再存在,并且findDeadlockedThreads将为此类线程返回null。为了避免可能的NullPointerExceptions,我们需要防范这种情况。最后,让我们强制一个简单的死锁,看看我们的系统在运行

DeadlockDetector deadlockDetector = new DeadlockDetector(new DeadlockConsoleHandler(), 5, TimeUnit.SECONDS);
deadlockDetector.start();

final Object lock1 = new Object();
final Object lock2 = new Object();

Thread thread1 = new Thread(new Runnable() {
  @Override
  public void run() {
    synchronized (lock1) {
      System.out.println("Thread1 acquired lock1");
      try {
        TimeUnit.MILLISECONDS.sleep(500);
      } catch (InterruptedException ignore) {
      }
      synchronized (lock2) {
        System.out.println("Thread1 acquired lock2");
      }
    }
  }

});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
  @Override
  public void run() {
    synchronized (lock2) {
      System.out.println("Thread2 acquired lock2");
      synchronized (lock1) {
        System.out.println("Thread2 acquired lock1");
      }
    }
  }
});
thread2.start();

输出: Thread1 
获取lock1 
Thread2获取lock2 
检测到死锁!
“Thread-1”Id = 11由“Thread-0”拥有的java.lang.Object@68ab95e6上的BLOCKED Id = 10 
deadlock.DeadlockTester $ 2.run(DeadlockTester.java:42) 
  java.lang.Thread.run(Thread。 java:662)
“Thread-0”Id = 10 BLOCKED在java.lang.Object@58fe64b9拥有“Thread-1”Id = 11 
  deadlock.DeadlockTester $ 1.run(DeadlockTester.java:28)
  java.lang.Thread。运行(Thread.java:662)

注意:死锁检测可能是一项昂贵的操作,建议间隔至少几分钟,因为检查死锁的频率并不比这更频繁
并发编程(一)Java并发编程的知识点梳理_第13张图片
Java线程转储
定义:Java线程转储是JVM中活动的所有线程的列表
作用:Java线程转储非常有助于分析应用程序和死锁情况中的瓶颈。
并发编程(一)Java并发编程的知识点梳理_第14张图片并发编程(一)Java并发编程的知识点梳理_第15张图片
如何分析死锁并在Java中避免它
dump举例:

2012-12-27 19:08:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.5-b02 mixed mode):

"Attach Listener" daemon prio=5 tid=0x00007fb0a2814000 nid=0x4007 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" prio=5 tid=0x00007fb0a2801000 nid=0x1703 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" prio=5 tid=0x00007fb0a204b000 nid=0x4d07 waiting for monitor entry [0x000000015d971000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t2" prio=5 tid=0x00007fb0a1073000 nid=0x4207 waiting for monitor entry [0x000000015d209000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t1" prio=5 tid=0x00007fb0a1072000 nid=0x5503 waiting for monitor entry [0x000000015d86e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"Service Thread" daemon prio=5 tid=0x00007fb0a1038000 nid=0x5303 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0a1037000 nid=0x5203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0a1016000 nid=0x5103 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007fb0a4003000 nid=0x5003 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007fb0a4800000 nid=0x3f03 in Object.wait() [0x000000015d0c0000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177)

"Reference Handler" daemon prio=5 tid=0x00007fb0a4002000 nid=0x3e03 in Object.wait() [0x000000015cfbd000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75320> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x000000013de75320> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00007fb0a2049800 nid=0x3d03 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0a300d800 nid=0x3503 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0a2001800 nid=0x3603 runnable 

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0a2003800 nid=0x3703 runnable 

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0a2004000 nid=0x3803 runnable 

"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0a2005000 nid=0x3903 runnable 

"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0a2005800 nid=0x3a03 runnable 

"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0a2006000 nid=0x3b03 runnable 

"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0a2006800 nid=0x3c03 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007fb0a1015000 nid=0x5403 waiting on condition 

JNI global references: 114


Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x00007fb0a1074b08 (object 0x000000013df2f658, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fb0a1010f08 (object 0x000000013df2f668, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x00007fb0a1012360 (object 0x000000013df2f678, a java.lang.Object),
  which is held by "t3"

Java stack information for the threads listed above:
===================================================
"t3":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t1":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t2":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.


dump分析:线程转储输出清楚地显示了死锁情况以及涉及的线程和资源导致死锁情况。 为了分析死锁,我们需要注意状态为BLOCKED的线程,然后是等待锁定的资源。每个资源都有一个唯一的ID,我们可以使用它找到哪个线程已经在对象上持有锁。例如,线程“t3”正在等待锁定0x000000013df2f658,但它已被线程“t1”锁定。

死锁的另一种形式:嵌套锁

定义线程A和线程B同时执行下面的锁,lock和unlock,当线程A执行了unlock释放了monitorObject 的锁,但是并没有释放this。这是时候线程A仍然持有this。那么线程B一直无限期等待线程A释放this对象。而线程A一直等待线程B释放monitorObject

public class Lock{
  protected MonitorObject monitorObject = new MonitorObject();
  protected boolean isLocked = false;

  public void lock() throws InterruptedException{
    synchronized(this){
      while(isLocked){
        synchronized(this.monitorObject){
            this.monitorObject.wait();
        }
      }
      isLocked = true;
    }
  }

  public void unlock(){
    synchronized(this){
      this.isLocked = false;
      synchronized(this.monitorObject){
        this.monitorObject.notify();
      }
    }
  }
}

两者区别:

在死锁中,两个线程互相等待释放锁
在嵌套锁中,线程1持有锁a,并等待线程2发出的信号。线程2需要锁a将信号发送给线程1

死锁的另一种表现形式—重入锁定

如果一个线程两次调用lock()而没有在两次调用之间调用unlock(),那么第二次调用lock()就会阻塞。发生了再入锁定

public class Lock{

  private boolean isLocked = false;

  public synchronized void lock()
  throws InterruptedException{
    while(isLocked){
      wait();
    }
    isLocked = true;
  }

  public synchronized void unlock(){
    isLocked = false;
    notify();
  }
}

如何避免:

避免编写重新进入锁的代码
使用可重入锁
锁的超时时间
异常finally中unlock

考察点:滑动条件

滑动条件的意思是,从线程检查某个条件到对其进行操作,该条件已被另一个线程更改,因此第一个线程的操作是错误的

package com.xiaochengxinyizhan.interview.threadcoding;

/**
 * @author xcxyz
 * @create 2019-03-26 11:22
 * 滑动条件
 **/

public class SlippedConditions4Lock {
    private boolean isLocked = true;
/**
 *
 * 假设isLocked为false,
 * 两个线程同时调用lock()。
 * 如果进入第一个同步块的第一个线程恰好在第一个同步块之后被抢占,
 * 这个线程将检查isLocked并将其标记为false。
 * 如果现在允许第二个线程执行,并因此进入第一个同步块,
 * 那么这个线程也将看到isLocked为false。
 * 现在,两个线程都将条件读取为false。然后两个线程将进入第二个同步块,并将其锁定为true,
 * 然后继续
 *这种情况是条件下滑的一个例子。两个线程都测试条件,然后退出同步块,
 * 从而允许其他线程在前两个线程中的任何一个更改后续线程的条件之前测试条件。
 * 换句话说,从检查条件开始,直到线程将其更改为后续线程,条件才会发生变化
 * 为了避免滑入条件,条件的测试和设置必须由执行测试和设置的线程自动执行,
 * 这意味着没有其他线程可以在测试和设置条件之间通过第一个线程检查条件
 */
    public void lock(){
        synchronized(this){
            while(isLocked){
                try{
                    this.wait();
                } catch(InterruptedException e){
                    //do nothing, keep waiting
                }
            }
            //解决方案
//            isLocked = true;
        }
//        解决方案很简单。只需移动isLocked = true;进入第一个同步块,正好在while循环之后
        synchronized(this){
            isLocked = true;
        }
    }

    public synchronized void unlock(){
        isLocked = false;
        this.notify();
    }
}

考察维度实战如何保证线程安全:

1、并发原子类
java.util.concurrent.atomic包中的类
点击这里J.U.C的世界(2019.3.22留的尾巴)

2、加锁机制
内置锁synchronized虽然可以防止并发但是导致的结果是性能会比之前下降很多。因为在加锁的类,对象,代码块,在同一个时间只能由一个线程持有,其他都得阻塞等待。
一种解决办法就是拆分代码块,只针对需要共享变量的地方进行同步锁。
重入性意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方法,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时候,这个锁就被认为是没有被任何线程持有,当线程请求一个未被持有的锁时候,JVM记下锁的持有者,并将获取值设为1.如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块,计数器会相应的减少。当计数值为0,锁被释放。
并发编程(一)Java并发编程的知识点梳理_第16张图片
并发编程(一)Java并发编程的知识点梳理_第17张图片

3、信号量同步共享数据:
信号量是一个线程同步结构,可以用来在线程之间发送信号以避免丢失同步信息,也可以像使用锁一样保护关键代码
CountDownLatch,Semaphore。CyclicBarrier是基于同步到达某个点的信号量触发机制。
无论性能还是安全性,尽可能使用并发包中的信号同步类,避免使用对象wait()和notify()方式来进行同步。
简单的实现:

public class Semaphore {
  private boolean signal = false;

  public synchronized void take() {
    this.signal = true;
    this.notify();
  }

  public synchronized void release() throws InterruptedException{
    while(!this.signal) wait();
    this.signal = false;
  }

}

上面提到了3种解决办法:
原子操作数据类型和同步锁。那么混用会如何呢?一般不建议混用,混用两种锁机制,结果不会给性能或者安全性带来任何提升。
对于每一个共享和可变的变量都应该只有一个锁来保护,从而使维护人员知道是哪一个锁。

避免使用锁的场景执行时间较长的计算或者可能无法快速完成的操作时候(例如网络I/O或控制台I/O)一定不要持有锁

4、封闭线程,不共享数据变量
1、栈封闭—》基本数据类型(由栈管理),线程内部使用或者线程局部使用,只能通过局部变量才能访问对象。他们位于执行线程的栈中,其他线程无法访问这个栈。
2、Ad-hoc线程封闭(链接–点击这里)
维护线程封闭性的职责完全由程序实现类承担。尽量用栈封闭或者ThreadLocal类来替代它。
3、ThreadLocal类提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时候设置的最新值。
4、可变对象变为不可变对象。
5、用final关键字
6、维护容器对象包含多个状态变量的不变性,并使用volatile维护可见性
7、静态初始化

5、发布与逸出问题
发布:实例化一个对象。其他地方对当前的对象引用
逸出:当已经实例化的对象,被重新引用覆盖,这种情况就是逸出。—解决这个问题就是封装
eg:特殊案例:
安全的对象构造----this引用在构造函数中逸出。
常见的错误就是构造函数中启动一个线程,当对象在器构造函数中创建一个线程,无论显式还是隐式,this引用都会被新创建的线程共享。在对象尚未构造完,新的线程就可以看见。
解决办法:私有构造函数和一个公共的工厂方法。
在静态初始化函数中初始化一个对象引用。(public static Holder holder =new Holder(42))----静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在同步机制
将对象的引用保存到volatile类型的域或者AtomicReferance对象
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中

设计线程安全的类

前提要素:
找出构成对象状态的所有变量。(变量)
找出约束状态变量的不变性条件。(共享变量可能会并发)
建立对象状态的并发访问管理策略。(如何确保并发安全)

原子性和封装性是帮助满足状态变量的有效值或状态转换上的约束条件。

通过现有库中的类阻塞队列Blocking Queue或信号量Semaphore来实现依赖状态的行为。

无限制的创建线程有哪些问题:

1、线程生命周期的开销
2、资源消耗
3、稳定性
并发编程(一)Java并发编程的知识点梳理_第18张图片
线程池的使用如何确定线程池的最合适大小:
并发编程(一)Java并发编程的知识点梳理_第19张图片

引申::单例模式看线程安全

一个类只能有一个实例称之为单例。
步骤三步:
1、将构造函数私有化
2、在类的内部创建实例
3、提供获取唯一实例的方法
使用静态类.doSomething()体现的是基于对象,而使用单例设计模式体现的是面向对象。

单例模式-懒汉式


public class Java3y {

    // 1.将构造函数私有化,不可以通过new的方式来创建对象
    private Java3y(){}

    // 2.1先不创建对象,等用到的时候再创建
    private static Java3y java3y = null;

    // 2.1调用到这个方法了,证明是要被用到的了
    public static Java3y getJava3y() {

        // 3. 如果这个对象引用为null,我们就创建并返回出去
        if (java3y == null) {
            java3y = new Java3y();
        }

        return java3y;
    }
}

单线程可以,多线程环境下不安全

加锁
public class Java3y {

    // 1.将构造函数私有化,不可以通过new的方式来创建对象
    private Java3y(){}

    // 2.1先不创建对象,等用到的时候再创建
    private static Java3y java3y = null;

    // 2.1调用到这个方法了,证明是要被用到的了
    public static synchronized Java3y getJava3y() {

        // 3. 如果这个对象引用为null,我们就创建并返回出去
        if (java3y == null) {
            java3y = new Java3y();
        }

        return java3y;
    }
}
优点:
1、线程安全性得到保证。
2、客户端应用可以传递参数
3、实现了懒惰初始化
缺点:
1、由于锁定开销导致性能下降。
2、初始化实例变量后不需要的不必要的同步

那么如何提高性能?

双重检测机制(DCL)懒汉式
public class Java3y {


    private Java3y() {
    }

    private static Java3y java3y = null;


    public static Java3y getJava3y() {
        if (java3y == null) {
            // 将锁的范围缩小,提高性能
            synchronized (Java3y.class) {
                java3y = new Java3y();
            }
        }
        return java3y;
    }
}
线程A和线程B同时调用getJava3y()方法,他们同时判断java==null,得出的结果都是为null,所以进入了if代码块了
此时线程A得到CPU的控制权-->进入同步代码块-->创建对象-->返回对象
线程A完成了以后,此时线程B得到了CPU的控制权。同样是-->进入同步代码块-->创建对象-->返回对象
很明显的是:Java3y类返回了不止一个实例!所以上面的代码是不行的!



public class Java3y {


    private Java3y() {
    }

    private static Java3y java3y = null;

    public static Java3y getJava3y() {
        if (java3y == null) {

            // 将锁的范围缩小,提高性能
            synchronized (Java3y.class) {

                // 再判断一次是否为null
                if (java3y == null) {
                    java3y = new Java3y();
                }
            }
        }
        return java3y;
    }
}
重排序问题


public class Java3y {
    private Java3y() {
    }

    private static volatile Java3y java3y = null;

    public static Java3y getJava3y() {
        if (java3y == null) {

            // 将锁的范围缩小,提高性能
            synchronized (Java3y.class) {

                // 再判断一次是否为null
                if (java3y == null) {
                    java3y = new Java3y();
                }
            }
        }
        return java3y;
    }
}
volatile 实现可见性

package com.designpatterns;

public class ASingleton {

	private static volatile ASingleton instance;
	private static Object mutex = new Object();

	private ASingleton() {
	}

	public static ASingleton getInstance() {
		ASingleton result = instance;
		if (result == null) {
			synchronized (mutex) {
				result = instance;
				if (result == null)
					instance = result = new ASingleton();
			}
		}
		return result;
	}

}
局部变量result似乎没必要。但它可以提高我们代码的性能。在实例已经初始化的情况下(大多数情况下),volatile字段只被访问一次(由于“return result;”而不是“return instance;”)。这可以将方法的整体性能提高多达25%

单例模式-饿汉式


public class Java3y {

    // 1.将构造函数私有化,不可以通过new的方式来创建对象
    private Java3y(){}

    // 2.在类的内部创建自行实例
    private static final Java3y java3y = new Java3y();

    // 3.提供获取唯一实例的方法
    public static Java3y getJava3y() {
        return java3y;
    }
}
一上来就创建对象了,如果该实例从始至终都没被使用过,则会造成内存浪费
单线程可以,多线程环境下不安全

在类加载时创建实例变量
优点:
1、线程安全无需同步
2、易于实施
缺点:
1、早期创建可能未在应用程序中使用的资源。
2、客户端应用程序无法传递任何参数,因此我们无法重用它。例如,具有用于数据库连接的通用单例类,其中客户端应用程序提供数据库服务器属性,枚举类排除掉
并发编程(一)Java并发编程的知识点梳理_第20张图片
并发编程(一)Java并发编程的知识点梳理_第21张图片

在if循环和volatile变量中使用synchronized块

package com.designpatterns;

public class ASingleton {

	private static volatile ASingleton instance;
	private static Object mutex = new Object();

	private ASingleton() {
	}

	public static ASingleton getInstance() {
		ASingleton result = instance;
		if (result == null) {
			synchronized (mutex) {
				result = instance;
				if (result == null)
					instance = result = new ASingleton();
			}
		}
		return result;
	}

}
优点:
1、线程安全性得到保证
2、客户端应用可以传递参数
3、实现了懒惰初始化
4、同步开销很小,仅当变量为null时才适用于前几个线程
缺点:
1、额外的条件

三、高并发安全性保证以后,线程在单线程程序中的活跃性问题

不同线程的事件发生时序,开发和测试难以发觉的问题。比如死锁,饥饿,活锁。所以活跃性意味着某件正确的事情最终会发生。

死锁----哲学家就餐问题

解决 办法:
支持定时的锁
通过线程转储信息来分析死锁
UNIX平台kill -3
减少锁的竞争,减少锁的持有时间,降低锁的请求频率,使用带有协调机制的独占锁,缩小锁的范围,锁分段,避免热点域(经常修改的地方)
ReadWriteLock和volatile的使用,CAS的使用
使用Lock接口的实现ReentrantLock。由于加锁和释放锁都需要自己操作,所以不能忘记unlock否则将出现锁无法释放问题。
ReentrantLocak在java6以后性能渐渐不如内置锁sychronized
并发编程(一)Java并发编程的知识点梳理_第22张图片
什么时候使用ReentrantLock:在一些内置锁无法满足需求的情况下,ReentrantLokc可以作为一种高级工具,当需要一些高级功能时候才应该使用ReentrantLock.比如可定时的,可轮训与可中断的锁,公平队列和非块结构的锁。
sychronized性能已经优于Reentrantlock,并且在线程转储中能够给出哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。

ReadWriteLock:

释放优先:当一个写入操作释放写入锁时候,并且队列中同时存在读线程和写线程,那么应该优先原则读?写还是发出请求的线程
读线程插队:如果锁是由读线程持有,但写线程正在等待,那么新到达的读线程能否立即获得访问权
重入性
降级:写入锁降级为读取锁
升级:读取锁升级为写入锁

ReentrantReadWriteLock
并发编程(一)Java并发编程的知识点梳理_第23张图片

线程池和信号量----资源死锁

无法获取CPU资源

四、活跃性保证以后,性能问题

服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,可伸缩性较低。
我们通常说多线程不是就为了提高线程的性能么?

这里想提到的是设计良好的并发应用程序,线程能提高程序的性能,但是随之而来也会带来某种程度的运行时开销。在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程,在并发基础那里我们知道,他是在不断获取抢夺CPU时间切片,所以他就会做上下文切换操作,这样的操作就会带来极大的开销。
保存和恢复执行上下文,丢失局部性,并且CPU时间会更多花在线程调度而不是线程运行。
当线程共享数据时候,必须使用同步机制或者可见性问题,这些机制往往会抑制某些编译器的优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。

五、我不想学线程可以么?

实际上只要你使用java应用程序,就伴随线程,当JVM启动时候,就会为JVM的内部任务(垃圾收集、终结操作等)创建后台线程,并创建一个主线程来运行main方法。而servlet和rmi都会创建线程池并调用这些线程中的方法。

问题篇

SimpleDateFormat
并发编程(一)Java并发编程的知识点梳理_第24张图片
线程不安全的原因:calendar本身就是不安全的,私有属性也是不安全的比如fields,time等。
解决办法:
@1、每个线程都new一次实例,开销内存占用比较大
@2、加锁synchronized,性能下降
@3、使用ThreadLocal,非常好,但是注意remove释放引用

Timer的使用
当timer执行多个timeTask的时候有一个抛出异常,其余都停止。
并发编程(一)Java并发编程的知识点梳理_第25张图片
解决办法:ScheduledThreadPoolExecutor既可以多生产单消费,也可以多生产多消费。异常catch掉就可以

BeanUtil.cloneBean深拷贝解决引用被封装方法体内的下游修改的问题。

创建线程和线程池任务指定与业务相关的线程名

线程池shutdown,如果不释放关闭,则线程池一直持有资源不释放

线程池使用FutureTask注意点:当拒绝策略为DiscardPolicy和DiscardOldesPolicy时候,当被拒绝的任务FutureTask对象上调用get()方法会导致调用线程一直阻塞。所以日常使用,用带超时的get方法。
或者重写Future的拒绝策略并设置FuturreTask的状态为NORMAL。

日志的输出为了减少rt(响应时间),可以改为异步输出

六、来自【梁飞(虚极)】原dubbo创始人的Java并发编程建议!

并发编程(一)Java并发编程的知识点梳理_第26张图片
并发编程(一)Java并发编程的知识点梳理_第27张图片
并发编程(一)Java并发编程的知识点梳理_第28张图片
并发编程(一)Java并发编程的知识点梳理_第29张图片
并发编程(一)Java并发编程的知识点梳理_第30张图片
并发编程(一)Java并发编程的知识点梳理_第31张图片
并发编程(一)Java并发编程的知识点梳理_第32张图片
并发编程(一)Java并发编程的知识点梳理_第33张图片并发编程(一)Java并发编程的知识点梳理_第34张图片并发编程(一)Java并发编程的知识点梳理_第35张图片
并发编程(一)Java并发编程的知识点梳理_第36张图片并发编程(一)Java并发编程的知识点梳理_第37张图片并发编程(一)Java并发编程的知识点梳理_第38张图片并发编程(一)Java并发编程的知识点梳理_第39张图片并发编程(一)Java并发编程的知识点梳理_第40张图片并发编程(一)Java并发编程的知识点梳理_第41张图片并发编程(一)Java并发编程的知识点梳理_第42张图片

你可能感兴趣的:(职场@多线程高并发@Java)