: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 多线程的主要内容,如果未能解决你的问题,请参考以下文章