前言

咱们书接上回,既然线程已被创建,其安全问题便应运而生。
产生线程安全问题的主要原因

  • 存在共享数据(临界资源);
  • 存在多条线程共同操作这些资源。

解决方案的共同特性:同一时刻有且仅有一个线程在操作共享数据,其他线程必须等到该线程处理完后再对共享数据进行操作。(互斥锁)

互斥锁的特性

  • 互斥性:同一时间只允许一个线程持有某个对象锁来协调多线程(同一时间只有一个线程对需要同步的代码块进行访问),也称为操作的原子性。
  • 可见性:必须确保在锁释放之前,对共享变量所做的修改,对随后获得该锁的另一个线程是可见的(通俗一点来说就是要保证下一个线程拿到的数据必须是最新数据),不然,下一个线程操作的数据可能就是一个副本数据,造成类似数据库脏读的情况发生。

synchronized锁

synchronized锁的是对象而不是代码

获取的锁的分类:获取对象锁和获取类锁

获取对象锁的方法

  1. 同步代码块,锁的是()中的实例对象;
  2. 同步非静态方法,锁的是当前对象的实例对象。

获取类锁的方法

  1. 同步代码块,锁的是()中的类对象(class);
  2. 同步静态方法,锁是当前对象的类对象(class)。

对象锁和类锁的总结

  1. 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
  2. 若锁住的是同一对象,只有一个线程可访问对象的同步代码块,其余想访问的线程会被阻塞;
  3. 若锁住的是同一对象,只有一个线程可访问对象的同步方法,其余想访问的线程会被阻塞;
  4. 若锁住的是同一对象,只有一个线程可访问对象的同步代码块,其余想访问同步方法的线程会被阻塞,反之也成立;
  5. 同一个类的不同对象的对象锁互不干扰;
  6. 类锁也是一种特别的对象锁不同在于它只有一个对象锁,同一个类的不同对象使用类锁将会是同步的;
  7. 类锁和对象锁互不干扰。

实现synchronized的基础:Java对象头、Monitor(内部锁)

对象在内存中的布局:对象头、实例数据、对齐填充

对象头的结构

  • Mark World:默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息;
  • Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定对象是哪个类的数据。

重入:从互斥锁的设计上看,当一个线程试图操作一个由其他线程持有的对象锁的临界资源是,将会处于阻塞状态,但当一个线程再次请求自己持有对象的临界资源时,称为重入。

自旋锁

  • 通过让线程执行忙循环等待锁的释放,不让出CPU;
  • 缺点:若锁被其他线程长时间占用,会带来性能上的开销。

自适应自旋锁

  • 自旋的次数不再固定;
  • 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁粗化:通过扩大加锁的范围,避免反复加锁和解锁。

锁消除:扫描运行上下文,去除不可能存在竞争的锁。

synchronized的四种状态:无锁、偏向锁、轻量级锁、重量级锁(锁等级从左到右)

偏向锁:减少同一线程获取锁的代价(不适合在多线程场景下使用)
  如果一个线程得到锁则进入偏向模式,Mark World结构变为偏向锁结构,当该线程再次请求锁时,无需再做同步操作(获取锁的过程只要检查Mark World的锁标记位是偏向锁及当前线程的Id与Mark World的ThreadID相等即可,减少大量申请锁的操作)

轻量级锁:一个线程进入同步块获得偏向锁后,第二个线程加入锁争用的情形下,偏向锁会升级为轻量级锁。(线程交替执行同步块)

锁的内存语义

  • 当线程释放锁时,Java内存模型把该线程对应的本地内存的共享变量更新到主内存中;
  • 线程获取锁时,Java内存模型会把该线程的本地内存设置为无效,使被监视器保护的临界区代码必须得读取主内存中的共享变量。

偏向锁、轻量级锁、重量级锁的汇总


优点

缺点

使用场景

偏向锁

加锁和解锁不需要比较再交换的操作,没有额外的性能消耗,和执行非同步方法相比差距极小

在多线程存在锁竞争的情况下,会有额外的消耗(锁撤销)

单线程访问同步块或同步方法

轻量级锁

竞争的线程不会阻塞,响应速度提高

如果长时间抢不到锁,CPU性能会被自旋锁消耗

多线程执行同步块或同步方法

重量级锁

竞争的线程不自旋,CPU不会消耗

线程会阻塞,响应时间慢,多线程下,大量的获取和释放锁操作使性能消耗严重

同步方法、同步块执行时间长、大型项目追求吞吐量、并发数等

ReentrantLock

ReentrantLock(再入锁):基于AQS实现、能够实现比synchronized更细粒度的控制、调用lock()之后须使用unclock()释放锁、可重入但性能不一定比synchronized高。

ReentrantLock公平性的设置

  • ReentrantLock f = new ReentrantLock(true);
  • 参数为true时,倾向于将锁赋予等待时间最久的线程;

公平锁:获取锁的顺序按先后调用lock方法的顺序。

非公平锁:抢占的顺序不一定(synchronized是非公平锁)。

ReentrantLock将锁对象化

  • 判断是否有线程,或者某个特定线程,在排队等待获取锁;
  • 带超时的获取锁的尝试;
  • 感知有没有成功获取锁。

ReentrantLock和synchronized的区别

  • synchronized是关键字,ReentrantLock是类;
  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁;
  • ReentrantLock可以获取各种锁的信息;
  • ReentrantLock可以灵活地实现多路通知;
  • sync操作Mark Word,lock调用Unsafe类的park()方法。

JMM(java memory model)

Java内存模型JMM:JMM只是一个概念,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段、静态字段和构成数组对象的元素等)的访问方式。

JMM中的主内存

  • 存储Java实例对象;
  • 包括成员变量、类信息、常量、静态变量等;
  • 属于数据共享的区域,多线程并发操作时会引发线程安全问题。

JMM中的工作内存

  • 存储当前方法的所有本地变量信息,本地变量对其他线程不可见;
  • 字节码行号指示器、Native方法信息;
  • 属于线程私有数据区域,不存在线程安全问题。

JMM主内存和工作内存的数据存储类型、操作方式总结

  • 方法中的基本数据类型本地变量将直接存储在工作内存的栈帧结构中;
  • 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中;
  • 成员变量、static变量、类信息均会被存储在主内存中;
  • 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。

指令重排序的条件:
  1、在单线程环境下不能改变程序运行的结果
  2、存在数据依赖关系的不允许重排序

volatile

volatile:JVM提供的轻量级同步机制

  • 保证被volatile修饰的共享变量对所有线程总是可见的;
  • 禁止指令重排序优化。

volatile的可见性实现原理

  • 当写volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取volatile变量时,JMM会把该线程对应的工作区内存设置为无效。

volatile禁止重排序采用的方法

  • 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化;
  • 强制刷出CPU缓存数据,让任何线程都能读取到最新的数据。

内存屏障(Memory Barrier)

  1. 保证特定操作的执行顺序;
  2. 保证某些变量的内存可见性。

volatile和synchronized的区别

  1. volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止;
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性;
  4. volatile不会造成线程阻塞;synchronized可能会造成线程的阻塞;
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

CAS

CAS(Compare and Swap):用于实现线程安全性

  • 支持原子更新操作,适用于计数器、序列发生器等场景;
  • 属于乐观锁机制;
  • CAS操作失败时由开发者决定继续尝试或执行别的操作。

CAS缺点:

  • 若循环时间长,则开销很大;
  • 只能保证一个共享变量的原子操作;
  • ABA问题:取数两次都为A但其中可能有数据变动为B,线程会认为没有变化(解决方案:AtomicStampedReference)。

Java线程池

java 多线程设置并发数 java多线程并发控制机制_java 多线程设置并发数


利用Executors创建不同的线程池满足不同场景的需求

  1. newFixedThreadPool(int nThreads)指定工作线程数量的线程池;
  2. newCachedThreadPool()处理大量短时间工作任务的线程池
  3. newSingleThreadExecutor()创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它;
    (1)试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
    (2)如果线程闲置的时间超过阈值,则会被终止并移除缓存;
    (3)系统长时间闲置的时候,不会消耗什么资源。
  4. newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize) 定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程;
  5. newWorkStealingPool()内部构建ForkJoinPool,利用working-straling算法,并行地处理任务,不保证处理顺序。

Fork/Join框架:把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架。

working-straling:某个线程从其他队列里窃取任务来执行。

为什么要使用线程池

  • 降低资源消耗;
  • 提高线程的可管理性。

接口

  • Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;
  • ExecutorService:具有管理执行器和任务生命周期的方法,提交任务机制更完善;
  • ScheduledExecutorService:支持Future和定期任务执行。

ThreadPoolExecutor的构造函数

  • corePoolSize:核心线程数量;
  • maximumPoolSize:线程不够用时能够创建的最大线程数;
  • workQueue:任务等待队列;
  • keepAliveTime:抢占的顺序不一定;
  • threadFactory:创建新线程且同一优先级。

handler:线程池的饱和策略

  • AbortPolicy:直接抛出异常(默认);
  • CallerRunsPolicy:用调用者所在的线程来执行任务;
  • DiscardOldestPolicy:丢弃队列中最靠前的任务,并执行当前任务;
  • DiscardPolicy:直接丢弃任务。

线程池的状态

  • RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务;
  • SHUTDOWN:不再接受新提交的任务,但可以处理存量任务;
  • STOP:不再接受新提交的任务,也不处理存量任务;
  • TIDYING:所有任务都已终止;
  • TERMINATED:terminated()方法执行完后进入该状态。

线程池大小选定策略

  • CPU密集型:线程数=按照核数 or 核数+1设定;
  • I/O密集型:线程数=CPU核数*(1 + 平均等待时间/平均工作时间)。