多线程知识点
2021-06-07 09:37:32 0 举报
AI智能生成
多线程知识点
作者其他创作
大纲/内容
多线程知识点
线程与进程基础知识
引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。
线程有3个基本状态:执行、就绪、阻塞
线程有5种基本操作:派生、阻塞、激活、 调度、 结束
线程有两个基本类型:用户级线程,系统级线程(核心级线程)
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权
并行:
并行性是指同一时刻内发生两个或多个事件。
并行是在不同实体上的多个事件
并发:
并发性是指同一时间间隔内发生两个或多个事件。
并发是在同一实体上的多个事件
并行是针对进程的,并发是针对线程的。
线程基础问题
使用多线程可能遇到的问题
线程安全问题(数据跟我们想象中运行的结果不一致)
性能问题(没有把握得当,会带来严重的性能问题)
对象的发布与逸出
静态域逸出
public修饰的get方法
方法参数传递
隐式的this(对象未完全初始化时,构造方法上就调用对象的方法了)
逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!
安全发布对象
在静态域中直接初始化:静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
对应的引用保存到volatile或者AtomicReferance引用中:保证了该对象的引用的可见性和原子性
由final修饰:不可变
由锁来保护
简述解决线程安全性的办法
无状态(没有共享变量)
只要我们保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的
使用final使该引用变量不可变
加锁(内置锁,显示Lock锁)
原子性(atomic包下的类)
count++这个操作就不是一个原子性操作!
可见性
volatile是一种轻量级的同步机制:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
一旦你完成写入,任何访问这个字段的线程将会得到最新的值
会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,内存屏障会把之前的写入值都刷新到缓存。
volatile可以防止重排序
使用JDK为我们提供的类来实现线程安全
原子性(atomic)
容器(ConcurrentHashMap等等)
线程池
线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务(而不是销毁)。这样就实现了线程的重用。
使用线程池的原因:
线程生命周期的开销非常高
程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了
线程池API
Executor提供了一种将“任务提交”与“任务执行”分离开来的机制
Executor接口:定义了执行任务的行为
ExcutorService接口:提供了线程池管理生命周期的方法
AbstractExecutorService类:默认实现
ForkJoinPool线程池:其主要的不同在于采用了工作窃取算法(work-stealing)
ThreadPoolExecutor类:最常用的线程池类
ScheduledExecutorService接口:延后与定期执行
Callable和Future
Callable就是Runnable的扩展。Runnable没有返回值,不能抛出受检查的异常,而Callable可以!
Future一般我们认为是Callable的返回值,但他其实代表的是任务的生命周期(当然了,它是能获取得到Callable的返回值的)
ThreadPoolExecutor详解
变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息。
高3位表示线程池状态,低29位表示任务线程的数量
线程状态就定义了好几种:
RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理。
SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理。
STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。
TERMINATED:线程池彻底终止的状态。
线程池有各种的策略:
线程数量策略
如果运行线程的数量少于核心线程数量,则创建新的线程处理请求如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量
线程空闲时间
当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁。
排队策略
同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量
拒绝任务策略
直接抛出异常使用调用者的线程来处理直接丢掉这个任务丢掉最老的任务
已默认实现的池
newFixedThreadPool
它将返回一个corePoolSize和maximumPoolSize相等的线程池
newCachedThreadPool
非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务。
SingleThreadExecutor
单个worker线程的Executor
execute执行方法
如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。
如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,
如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。
线程池关闭
调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP。
shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程
Atomic原子类
原子类可分为4种类型:基本数据类型、数组、引用类型、对象的属性
解决ABA问题可以使用AtomicStampedReference和AtomicMarkableReference类
LongAdder性能比AtomicLong要好
Thread类
Java使用Thread类来表示线程,有两种方式创建线程
继承Thread,重写run方法
实现Runnable接口,重写run方法
一般我们使用实现Runnable接口,可以避免java中的单继承的限制,并发运行任务和运行机制解耦
线程名
主线程叫做main,其他线程是Thread-x
守护线程
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了
使用守护线程需要注意的地方
在线程启动前设置为守护线程,方法是setDaemon(boolean on)
使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
守护线程中产生的新线程也是守护线程
优先级线程
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素!
线程的优先级是高度依赖于操作系统的
线程的生命周期
sleep方法
调用sleep方法会进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态!
yield方法
调用yield方法会先让别的线程执行,但是不确保真正让出
join方法
调用join方法,会等待该线程执行完毕后才执行别的线程~
interrupt方法
interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了。interrupt方法压根是不会对线程的状态造成影响的,它仅仅设置一个标志位罢了
interrupt线程中断还有另外两个方法(检查该线程是否被中断):
静态方法interrupted()-->会清除中断标志位
实例方法isInterrupted()-->不会清除中断标志位
Java锁机制
synchronized锁
简介
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来
synchronized是一种互斥锁:一次只能允许一个线程进入被锁住的代码块
synchronized底层是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
用处
保证了线程的原子性
synchronized还保证了可见性
类锁与对象锁
静态方法获取的是类锁(类的字节码文件对象)
synchronized修饰普通方法或代码块获取的是对象锁
获取了类锁的线程和获取了对象锁的线程是不冲突的
可重入锁
锁的持有者是“线程”,而不是“调用”
公平锁与非公平锁
公平锁:线程将按照它们发出请求的顺序来获取锁
非公平锁:线程发出请求的时可以“插队”获取锁
Lock和synchronize都是默认使用非公平锁的:公平锁会来带一些性能的消耗的
synchronized锁升级
JDK1.6开始Synchronized锁就做了各种的优化
优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。
Lock显示锁
Lock方式来获取锁支持中断、超时不获取、是非阻塞的
提高了语义化,哪里加锁,哪里解锁都得写出来
Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
支持Condition条件对象
允许多个读线程同时访问共享资源(读写锁)
Lock锁底层原理(AbstractQueuedSynchronizer简称为AQS)
AQS其实就是一个可以给我们实现锁的框架
内部实现的关键是:先进先出的队列、state状态
定义了内部类ConditionObject
拥有两种线程模式
独占模式
共享模式
在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
细节
修改state状态值时使用CAS算法来实现
等待列被称为:CLH队列(三个名字组成),是一个双向队列
定义了框架,具体实现由子类来做(模板模式)!
ReentrantLock锁
比synchronized更有伸缩性(灵活)
使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁
ReentrantReadWriteLock锁
在读的时候可以共享,在写的时候是互斥的
读锁不支持条件对象,写锁支持条件对象
读锁不能升级为写锁,写锁可以降级为读锁
在内部定义了两个内部类来代表读锁和写锁,本质上还是AQS实现
它使用state的变量高16位是读锁,低16位是写锁
死锁
在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。
造成死锁的原因
当前线程拥有其他线程需要的资源
当前线程等待其他线程已拥有的资源
都不放弃自己拥有的资源
常见死锁场景
锁顺序死锁(上锁的时候,没有规定顺序)
动态锁顺序死锁(调用时参数位置不同,就可能发生死锁)
协作对象之间发生死锁(互相调用方法时发生死锁)
避免死锁的方法
固定加锁的顺序:得到对应的hash值来固定加锁的顺序
开放调用(针对对象之间协作造成的死锁):在调用某个方法时不需要持有锁,那么这种调用被称为开放调用【其实就是缩小同步的范围!】
使用定时锁
使用显式Lock锁,在获取锁时使用tryLock()方法
死锁检测
JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
Jstack是JDK自带的命令行工具,主要用于线程Dump分析。
ThreadLocal
往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的
常见的应用
管理Connection
更多脑图和最新原创技术文章可关注公众号:Java3y
避免一些参数传递
实现原理
Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是ThreadLocal对象本身,value则是要存储的对象
想要避免内存泄露就要手动remove()掉!
ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题
同步工具类
CountDownLatch(闭锁)
某个线程等待其他线程执行完毕后,它才执行(其他线程等待某个线程执行完毕后,它才执行)
CyclicBarrier(栅栏)
一组线程互相等待至某个状态,这组线程再同时执行。
Semaphore(信号量)
控制一组线程同时执行
ScheduledThreadPoolExecutor类:相当于提供了延后与定期执行的ThreadPoolExecutor
0 条评论
下一页