:Java 多线程

Posted 请叫我东子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了:Java 多线程相关的知识,希望对你有一定的参考价值。

Java 多线程

1、synchronized

  • 修饰代码块
  • 底层实现,通过 monitorenter & monitorexit 标志代码块为同步代码块。
  • 修饰方法
  • 底层实现,通过 ACC_SYNCHRONIZED 标志方法是同步方法。
  • 修饰类 class 对象时,实际锁在类的实例上面。
  • 单例模式
public class Singleton 

    private static volatile Singleton instance = null;

    private Singleton()

    public static Singleton getInstance()
    if (null == instance) 
        synchronized (Singleton.class) 
            if (null == instance) 
            instance = new Singleton();
            
        
      
        return instance;
        

  • 偏向锁,自旋锁,轻量级锁,重量级锁
    • 通过 synchronized 加锁,第一个线程获取的锁为偏向锁,这时有其他线程参与锁竞争,升级为轻量级锁,其他线程通过循环的方式尝试获得锁,称自旋锁。若果自旋的次数达到一定的阈值,则升级为重量级锁。
    • 需要注意的是,在第二个线程获取锁时,会先判断第一个线程是否仍然存活,如果不存活,不会升级为轻量级锁。

2、Lock

  • ReentrantLock
    • 基于 AQS (AbstractQueuedSynchronizer)实现,主要有 state (资源) + FIFO (线程等待队列) 组成。
    • 公平锁与非公平锁:区别在于在获取锁时,公平锁会判断当前队列是否有正在等待的线程,如果有则进行排队。-
    • 使用 lock() 和 unLock() 方法来加锁解锁。
  • ReentrantReadWriteLock
    • 同样基于 AQS 实现,内部采用内部类的形式实现了读锁(共享锁)和写锁 (排它锁)。
  • 非公平锁吞吐量高
    在获取锁的阶段来分析,当某一线程要获取锁时,非公平锁可以直接尝试获取锁,而不是判断当前队列中是否有线程在等待。一定情况下可以避免线程频繁的上下文切换,这样,活跃的线程有可能获得锁,而在队列中的锁还要进行唤醒才能继续尝试获取锁,而且线程的执行顺序一般来说不影响程序的运行。

3、volatile

  • Java 内存模型
  • 在多线程环境下,保证变量的可见性。使用了 volatile 修饰变量后,在变量修改后会立即同步到主存中,每次用这个变量前会从主存刷新。
  • 禁止 JVM 指令重排序。
  • 单例模式双重校验锁变量为什么使用 volatile 修饰?禁止 JVM 指令重排序,new Object()分为三个步骤:申请内存空间,将内存空间引用赋值给变量,变量初始化。如果不禁止重排序,有可能得到一个未经初始化的变量。

4、线程的五种状态

1). New
一个新的线程被创建,还没开始运行。

2). Runnable
一个线程准备就绪,随时可以运行的时候就进入了 Runnable 状态。
Runnable 状态可以是实际正在运行的线程,也可以是随时可以运行的线程。
多线程环境下,每个线程都会被分配一个固定长度的 CPU 计算时间,每个线程运行一会儿就会停止让其他线程运行,这样才能让每个线程公平的运行。这些等待 CPU 和正在运行的线程就处于 Runnable 状态。

3). Blocked
例如一个线程在等待 I/O 资源,或者它要访问的被保护代码已经被其他线程锁住了,那么它就在阻塞 Blocked 状态,这个线程所需的资源到位后就转入 Runnable 状态。

4). Waiting(无限期等待)
如果一个线程在等待其他线程的唤醒,那么它就处于 Waiting 状态。以下方法会让线程进入等待状态:

  • Object.wait()
  • Thread.join()
  • LockSupport.park()

5). Timed Waiting(有期限等待)
无需等待被其他线程显示唤醒,在一定时间后有系统自动唤醒。
以下方法会让线程进入有限等待状态:

  • Thread.sleep(sleeptime)
  • Object.wait(timeout)
  • Thread.join(timeout)
  • LockSupport.parkNanos(timeout)
  • LockSupport.parkUntil(timeout)

6). Terminated
一个线程正常执行完毕,或者意外失败,那么就结束了。

5、 wait() 与 sleep()

  • 调用后线程进入 waiting 状态。
  • wait() 释放锁,sleep() 没有释放锁。
  • 调用 wait() 后需要调用 notify() 或 notifyAll() 方法唤醒线程。
  • wait() 方法声明在 Object 中,sleep() 方法声明在 Thread 中。

6、 yield()

调用后线程进入 runnable 状态。
让出 CPU 时间片,之后有可能其他线程获得执行权,也有可能这个线程继续执行。

7、 join()

在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B。
可以保证线程的顺序执行。
join() 方法必须在 线程启动后调用才有意义。
使用 wait() 方法实现。

8、线程池

1)、分类

  • FixThreadPool 固定数量的线程池,适用于对线程管理,高负载的系统
  • SingleThreadPool 只有一个线程的线程池,适用于保证任务顺序执行
  • CacheThreadPool 创建一个不限制线程数量的线程池,适用于执行短期异步任务的小程序,低负载系统
  • ScheduledThreadPool 定时任务使用的线程池,适用于定时任务

2)、线程池的几个重要参数

  • int corePoolSize, 核心线程数
  • int maximumPoolSize, 最大线程数
  • long keepAliveTime, TimeUnit unit, 超过 corePoolSize 的线程的存活时长,超过这个时间,多余的线程会被回收。
  • BlockingQueue workQueue, 任务的排队队列
  • ThreadFactory threadFactory, 新线程的产生方式
  • RejectedExecutionHandler handler) 拒绝策略

3)、线程池线程工作过程

corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略

  • 核心线程在线程池中一直存活,当有任务需要执行时,直接使用核心线程执行任务。
  • 当任务数量大于核心线程数时,加入等待队列。
  • 当任务队列数量达到队列最大长度时,继续创建线程,最多达到最大线程数。
  • 当设置回收时间时,核心线程以外的空闲线程会被回收。
  • 如果达到了最大线程数还不能够满足任务执行需求,则根据拒绝策略做拒绝处理。

4)、线程池拒绝策略(默认抛出异常)

拒绝类型拒绝描述
AbortPolicy抛出 RejectedExecutionException
DiscardPolicy什么也不做,直接忽略
DiscardOldestPolicy丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
CallerRunsPolicy直接由提交任务者执行这个任务

5)、如何根据 CPU 核心数设计线程池线程数量

  • IO 密集型 2nCPU
  • 计算密集型 nCPU+1其中 n 为 CPU 核心数量,可通过 Runtime.getRuntime().availableProcessors() 获得核心数:。
    为什么加 1:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。

9、线程使用方式

  • 继承 Tread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口:带有返回值

10、Runnable 和 Callable 比较

  • 方法签名不同, void Runnable.run() , V Callable.call() throws Exception
  • 是否允许有返回值, Callable 允许有返回值
  • 是否允许抛出异常, Callable 允许抛出异常。
  • 提交任务方式, Callable 使用 Future submit(Callable task) 返回 Future 对象,调用其 get() 方法可以获得返回值, - Runnable 使用 void execute(Runnable command) 。

11、hapens-before

如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

12、ThreadLocal

  • 场景
    主要用途是为了保持线程自身对象和避免参数传递,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
  • 原理
    为每个线程创建变量副本,不同线程之间不可见,保证线程安全。使用 ThreadLocalMap 存储变量副本,以 ThreadLocal 为 K,这样一个线程可以拥有多个 ThreadLocal 对象。
  • 实际
    使用多数据源时,需要根据数据源的名字切换数据源,假设一个线程设置了一个数据源,这个时候就有可能有另一个线程去修改数据源,可以使用 ThreadLocal 维护这个数据源名字,使每个线程持有数据源名字的副本,避免线程安全问题。

以上是关于:Java 多线程的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程之内存可见性

Java多线程和并发总结

java多线程 JMM理解 Volatile

13Java并发性和多线程-Java Volatile关键字

多线程中主存与线程工作空间同步数据的时机

多线程&高并发深入浅出JMM-Java线程内存模型