Java多线程

216 阅读12分钟

Thread

Thread中start和run方法的区别

  • 调用 start() 方法会创建一个新的子线程并启动
  • run() 方法只是Thread的一个普通方法调用

Thread和Runnable是什么关系

public class Test implements Runnable{
    
    @Override
    public void run() {
        
    }
}
public class Test extends Thread {
    
}
  • Thread是实现了Runnable接口的类,使得run支持多线程
  • 因为类的单一继承原则,推荐使用Runnable接口

如何实现处理线程的返回值

  • 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
  • 通过Callable接口实现:FutureTask或线程池获取

线程的6个状态

image.png

sleep和wait的区别

  • sleep 是Thread类下的一个方法,wait 是Object类下的一个方法
  • sleep() 方法可以在任何地方使用,wait() 方法只能在synchronized方法或synchronized块中使用
  • Thread.sleep 只会让出cpu,不会导致锁行为的改变,Object.wait 不仅会让出cpu,还会释放已经占有的同步资源锁

notify和notifyAll的区别

  • 两个概念:锁池 EntryList、等待池 WaitSet

    • 锁池(EntryList):假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想调用这个对象的某个synchronized方法,由于B、C线程在进入对象的synchronized方法之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池

    • 等待池(WaitSet):假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁

  • notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

  • notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会


synchronized

互斥锁的特性

  • 互斥性:

    • 即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这个在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也成为操作的原子性
  • 可见性:

    • 必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致

获取对象锁的两种用法

  • 1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号中的实例对象
public class Test {

    public void doWhat1() {
        synchronized (this) {

        }
    }

    public void doWhat2() {
        synchronized (Test.class) {

        }
    }

}
  • 2.同步非静态方法(synchronized method),锁是当前对象的实例对象
public class Test {

    public synchronized void doWhat3() {

    }
}

获取类锁的两种用法

  • 1.同步代码块(synchronized(类.class)),锁是小括号()中的类对象(class对象)
  • 2.同步静态方法(synchronized static method),锁是当前对象的类对象(class对象)

对象锁和类锁的总结

  • 1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
  • 2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞
  • 3.若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞
  • 4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然
  • 5.同一个类的不同对象的对象锁互不干扰
  • 6.类锁由于也是一种特殊的对象锁,因此表现和上述1、2、3、4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
  • 7.类锁和对象锁互不干扰

synchronized底层实现原理

实现synchronized的基础

    1. java对象头
    1. Monitor

自旋锁和自适应自旋锁

  • 自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU
  • 缺点:若锁被其它线程长时间占用,会带来许多性能上的开销

自适应自旋锁

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

偏向锁:减少同一线程获取锁的代价

核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁及当前线程id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。 偏向锁不适用于锁竞争比较激烈的多线程场合

轻量级锁:

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁 适用场景:线程交替执行同步块

若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

优点缺点使用场景
偏向锁加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块或者同步方法的场景
轻量级锁竞争的线程不会阻塞,提高了响应速度若线程长时间抢不到锁,自旋会消耗CPU性能线程交替执行同步方法的场景
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗追求吞吐量,同步块或者同步方法执行时间较长的场景

synchronized和ReentrantLock的区别

  • ReentrantLock(再入锁),基于AQS实现

  • 能够实现比synchronized更细粒度的控制

  • 性能未必比synchronized高,并且也是可重入的

  • ReentrantLock设置为公平锁时,倾向于将锁赋予等待时间最久的线程

  • synchronized是非公平锁

  • synchronized是关键字,ReentrantLock是类

  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁

  • ReentrantLock可以获取各种锁的信息

  • ReentrantLock可以灵活的实现多路通知

  • synchronized操作Mark Word,lock调用Unsafe类的park()方法

Java内存模型(JMM)

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM中的主内存

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

JMM中的工作内存

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

JMM与Java内存区域划分时不同的概念层次

  • JMM描述的是一组规则,围绕原子性、有序性、可见性展开
  • 相似点:存在共享区域和私有区域

主内存和工作内存的数据存储类型以及操作方式

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

指令重排序需要满足的条件

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序 无法通过happens-before原则推导出来的,才能进行指令的重排序

happens-before的八大原则

  • 1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 2.锁定规则:一个unLock操作先行发生于后面同一个锁的lock操作;
  • 3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  • 4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  • 5.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
  • 6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

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

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

volatile变量为何立即可见

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

volatile如何禁止重排优化

  • 内存屏障(Memory Barrier)

  • 1.保证特定操作的执行顺序

    • 通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
  • 2.保证某些变量的内存可见性

    • 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

CAS(Compare and Swap)一种高效实现线程安全性的方法

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

volatile和synchronized的区别

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

Java线程池

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

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

Work-Stealing算法:某个线程从其它队列里窃取任务来执行

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

J.U.C的三个Executor接口

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

ThreadPoolExecutor构造函数

  • corePoolSize:核心线程数量
  • maximumPoolSize:线程不够用时能够创建的最大线程数
  • keepAliveTime:线程池维护线程所允许的空闲时间
  • workQueue:任务等待队列
  • threadFactory:创建新线程
  • handler:线程池的饱和策略
    • AbortPolicy:直接抛出异常,默认策略
    • CallerRunsPolicy:用调用者所在的线程来执行任务
    • DiscardPolicy:丢弃队列中最靠前的任务,并执行当前任务
    • DiscardOldestPolicy:直接丢弃任务

线程池的状态

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