本文的篇幅比较长,总结了自己在学习中遇到的有关多线程的所有知识点😎
文章目录
创建线程方法
有两种方法:
(1)继承Thread类
(2)实现Runnable接口
(3)Callable方式:结合Future,可以获取线程的执行结果
Thread基础API及线程状态
线程状态
NEW
RUNNABLE——》running(运行态)+ready(就绪态)
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
start()和run()
start()相当于启动一个新线程,在新线程里运行run()方法。调用start()后,线程不会立即执行,是new–》ready,等待cpu调度
run()只是使用类中的run()方法,不会创建新线程
sleep()和yield()
sleep()和yield()都是静态方法
方法 | 状态 |
---|---|
Thread.yield() | 运行态–》就绪态 |
Thread.sleep(long) | 运行态–》TIMED_WAITING |
join()
线程等待:线程对象.join()/join(long)
当前线程由运行态–》WAITING,或TIMED_WAITING
实现线程等待的方式有两种:
(1)join
t.join(2000);//t线程执行完或等待2000ms后(满足其中一个条件即可)执行下面的代码
System.out.println(Thread.currentThread().getName());
(2)结合activeCount()+yield()使用
while(Thread.activeCount()>1){
Thread.yield();
}
线程中断
方法 | 说明 |
---|---|
boolean isInterrupted() | 测试线程是否中断,不清楚标志位 |
static boolean interrupted() | 测试线程是否中断,清除标志位 |
void interrupt() | 中断线程 |
不是真的中断,只是告诉线程需要进行中断,具体是否中断由线程自己决定
1、线程启动后,中断标志位默认是false
2、在线程运行态中,处理线程中断,需要自行通过判断中断标志位,来处理中断的处理逻辑,通过方法判断:(thread.isInterrupted()/Thread.interrupted())
3、线程因调用wait()/join()/sleep()处于阻塞状态时,将线程中断,会造成:
(1)在这三个阻塞方法所在的代码行,直接抛出InterruptedException异常
(2)抛出异常后,重置线程的中断标志位(=true)
4、使用自定义中断标志位无法满足在阻塞状态时的中断操作–》抛出异常
线程安全
原子性、可见性、有序性
原子性:一个操作或多个操作要么全部执行完成且执行过程不被中断,要么就不执行
特殊的原子性代码:
1、n++、n–、++n、–n都不是原子性的
需要分解成3条指令:从内存读取变量到cpu、修改变量、写回内存
2、对象的new操作
Object a=new Object();
分解成3条指令:分配内存对象、初始化对象、将对象赋值给变量
可见性:多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性:程序执行的顺序按照代码的先后顺序执行
synchronized关键字
synchronized关键字可以保证线程安全的三大特性
线程间同步互斥,竞争对象锁,竞争成功–》执行,竞争失败–》进入同步队列
使用方法:
1、静态方法上:对类对象加锁
2、实例方法:对this对象,哪个对象调用,对哪个对象加锁
3、代码块:synchronized(){}
进入synchronized代码行时,需要获取对象锁:
1、获取成功:往下执行
2、获取失败:阻塞在synchronized代码行
退出synchronized代码块,或synchronized方法:
1、退回对象锁
2、通知JVM及系统,其他线程可以竞争这把锁
synchronized加锁关键点:
1、对哪个对象加锁——一个对象只有一把锁
2、只有同一个对象,才有同步互斥作用(以此来保证线程安全的三大特征)
3、对于synchronized内的代码来说,在同一个时间点,只有一个线程在运行(没有并发、并行)
4、运行的线程越多,性能下降的越快(归还对象锁时,就有越多的线程在唤醒、阻塞状态切换)
5、同步代码执行的时间越短,性能下降的越快
volatile关键字
volatile关键字可以(通过建立内存屏障)保证可见性和有序性
作用:
1、禁止指令重排序
2、建立内存屏障
注意:
1、不能保证原子性
2、volatile修饰的变量,进行赋值不能依赖变量(常量赋值可以保证线程安全)
使用场景:
1、volatile可以结合线程加锁的一些手段,提高线程效率
2、只是变量的读取,常量的赋值,可以不加锁,而是使用volatile,可以提高效率
3、使用在成员变量和静态变量中,不能使用在局部变量
线程间通信
锁对象.wait()/wait(long):释放当前对象持有的对象锁,进入阻塞态(WAITING或TIMED_WAITING),直到被notify()/notifyAll()唤醒或时间到
锁对象.notify()/notifyAll():通知调用wait()进入等待队列的线程,来竞争对象锁
notify():随机唤醒一个线程
notifyAll():唤醒全部线程
单例模式
1、volatile关键字修饰变量
2、私有构造方法
3、双重校验锁写法保证线程安全
public class Sington{
//单例模式
private Sington(){}
//双重校验锁
private static volatile SINGTON=null;
public static Sington getInstance(){
if(SINGTON==null){
synchronzied(Sington.class){
if(SIGTON==NULL){//提高效率:变量使用volatile保证可见性
SINGTON=new Sington();
}
}
}
}
}
阻塞队列
%%%%%%%后续补充%%%%%%%%
线程池
通过下面代码,来解释ThreadPoolExecutor的用法以及其中中各个参数的含义
import java.util.concurrent.*;
public class threadpool {
public static void main(String[] args) {
ExecutorService pool=new ThreadPoolExecutor(//线程池--快递公司
3,//核心线程数:创建好线程池,正是员工就开始取快递
//临时工雇佣:正式员工忙不过来就会创建临时工
//临时工解雇:空闲时间超出设置的时间范围,就解雇
5,//最大线程数(最多数量员工:正式员工+临时工)
20,//时间数量
TimeUnit.SECONDS,//时间单位(时间数量+时间单位 表示一定范围的时间)
//阻塞队列:存放包裹的仓库(存放任务的数据结构)
new ArrayBlockingQueue<>(1000 ),
// new ThreadFactory() {
// @Override
// public Thread newThread(Runnable r) {
// return null;
// }
// },//(了解)线程池创建Thread线程的工厂类。没有提供的话,就使用线程池内部默认的创建线程的方式
//拒绝策略
//CallerRunsPolicy:哪个线程(excute代码行所在的线程)让我(快递公司)送快递,让该线程自己去执行
//AbortPolicy:直接抛出异常 RejectedExecutionException
//DiscardPolicy:从阻塞队列丢弃最新的任务,把当前任务放入阻塞队列
//DiscardOldestPolicy:从阻塞队列丢弃最旧的任务
new ThreadPoolExecutor.DiscardOldestPolicy()
);
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("送快递到西安,A");
}
});
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("送快递到杭州,B");
}
});
System.out.println("正在做事情");
}
}
乐观锁和悲观锁
乐观锁和悲观锁是在代码设计层面上来讲的
乐观锁:认为一般情况下不会产生并发冲突,在数据提交更新的时候才会检测是否发生并发冲突。
悲观锁:总是假设最坏的情况,每次拿数据的时候都会上锁。
java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
CAS
Compare And Swap:比较并交换
技术背景:线程执行任务,任务量比较小的时候,线程安全需要使用synchronized(多个线程同时竞争锁对象)加锁,效率比较低(竞争失败的线程很快在阻塞态和被唤醒状态间切换)
使用前提:代码块执行速度非常快
CAS实现:自旋尝试设置值的操作
缺点:
(1)前提(很快执行)满足不了,线程就一直处于运行态循环执行CAS,性能消耗比较大
(2)线程数量比较多,导致前提可能满足不了,或者CPU在很多线程间切换——性能消耗大
目的:在安全的前提下优化效率——使用较多的场景:对变量修改,保存线程安全
原理:使用CAS,不造成线程阻塞(一直处于运行状态)
存在问题:ABA问题
产生原因:当前线程拷贝主存值到工作内存进行修改的时间段,其它线程把主存中的变量值A–》B–》A。相当于已经被其它线程修改过,但当前线程不知道,还在修改
解决方案:采取乐观锁设计,引入版本号控制
jdk中采用CAS实现的API:
(1)java.util.concurrent.atomic,原子性的并发包下
(2)synchronized中,多个线程不同时间点执行同步代码块时,jdk优化会采用CAS
(3)其他的,如1.8版本中ConcurrentHashMap实现,put操作时,节点是空,采取CAS
synchronized原理
实现原理:通过对象头加锁操作 monitor锁机制:编译为字节码时,生成monitorenter、monitorexit
对象头锁状态:
(1)无锁
(2)偏向锁
(3)轻量级锁
(4)重量级锁
JVM对synchronized的优化方案:根据不同场景,使用不同的锁机制
(1)偏向锁:针对同一个线程再次申请已持有的对象锁。 实现原理–》CAS
(2)轻量级锁:大概率在同一个时间点,只有一个线程申请对象锁。实现原理–》CAS
(3)重量级锁:大概率在同一个时间,多个线程竞争同一个对象锁
实现原理:操作系统的mutex锁
缺点:涉及到操作系统的调度、用户态到内核态的切换,开销非常大,线程会阻塞、唤醒
synchronized锁只能升级不能降级
其他优方案:
(1)锁粗化:一个线程对同一个对象锁反复获取释放的操作,中间没有其他受影响的代码时,可以合并为一个锁
(2)锁消除:临界区代码中,没有对象逃逸出当前线程,说明本身是线程安全操作,可以直接删除锁(不使用锁)
死锁
前提条件:
同步的本质在于:一个线程等待另一个线程执行完毕后才可以继续执行,但如果现在几个相关的线程彼此之间相互等待着,就会造成死锁。
至少有两个线程,互相持有对方申请的对象锁,造成互相等待,导致没法继续执行
后果:线程阻塞等待,无法向下执行
造成死锁的4个必要条件
1、互斥使用:资源被一个线程占有后,别的线程不能使用
2、不可抢占:资源请求者不能强制从资源占有者手中夺取资源
3、请求和保持:资源请求者在请求资源的同时,会保持对原有资源的占用
4、循环等待:发生死锁时,所等待的线程必定会形成一个死循环,造成永久阻塞
解决方案:
(1)资源一次性分配(破坏请求与保持条件)
(2)可剥夺资源:在线程满足条件时,释放掉已占有的资源
(3)资源有序分配:系统为每类资源赋予一个编号,每个线程按照编号请求资源,释放则相反
检测死锁的手段:
使用jdk的监控工具,如jconsole、jstack查看线程状态
Lock体系
Lock lock=new ReentrantLock();
lock.lock();//设置当前线程的同步状态,并在队列中保存线程及线程的同步状态
try{
……
}finally{
lock.unlock();//线程出队列
}
Lock锁的实现原理
Lock锁的实现原理:AQS(AbstractQueuedSynchronizer)
AQS:队列式同步器
实现原理:双端队列保存线程及线程同步状态。并通过CAS提供设置同步状态的方法
Lock锁特点
1、提供公平锁和非公平锁 是否按照入队的顺序设置线程的同步状态——多个线程申请加锁操作时,是否按照时间顺序来加锁
2、AQS提供的独占式、共享式设置同步状态(独占锁、共享锁)
独占锁:只允许一个线程获取到锁
共享锁:一定数量的线程共享式获取锁
3、带Reentrant关键字的lock包下的API:可重入锁
允许多次获取同一个Lock对象锁(和synchronized中的偏向锁对应)
ReentrantReadWriteLock
ReentrantReadWriteLock:提供的读写锁API
使用场景:多线程执行某个操所时,允许读-读并发/并行执行,不允许读写、写-写并发/并行执行
读读并发,读写、写写互斥
读锁和写锁之间只能降级,不能升级(只能 写锁–》读锁)和synchronized锁相反,synchronized锁只能升级
优势:针对读读并发执行,提高运行效率
Condition
作用:线程间通信
使用方法:
1、通过Lock对象.newCondition() 获取Condition对象
2、调用Condition对象.await()让当前线程阻塞等待,并释放锁(等同于:synchronized锁对象.wait())
3、调用Condition对象.signal()/signalAll()通知之前await()阻塞的线程(=等同于:synchronized锁对象.notify()/notifyAll())
ThreadLocal
使用场景:隔离线程间的变量,保证每个线程是使用自己线程内的副本,
代码推荐写法:
//定义类变量:
//ThreadLocal多个线程使用的都是同一个,但里面的值是和线程绑定的,线程间互不相干
static ThreadLocal<保存的数据类型> HOLDER=new ThreadLocal<>();
new Thread(()-{
try{
HOLDER.set(设置的值);
}
finally{
HOLDER.remove();//当有线程设置值的时候,在线程结束前,remove
}
})
原理:Thread对象都有自己的ThreadLocalMap,调用ThreadLocal对象设置值set(value),获取值操作get(),删除值remove(),都是对当前线程中ThreadLocalMap对象的操作。所以线程间的变量是隔离开的。
ConcurrentHashMap
HashMap:非线程安全的,jdk1.7基于数组+链表,jdk1.8基于数组+链表+红黑树
Hashtable的实现:线程安全的,1.7和1.8都是数组+链表,全部方法都是基于synchronized加锁,效率非常低
ConcurrentHashMap:线程安全的,并且支持很多场景下并发操作,提高了效率
jdk1.7:
基于数组+链表,本质上基于 Segment分段锁技术,Segment继承了ReentrantLock,不同的Segment之间多线程可以并发操作。同一个Segment是使用Lock加锁
jdk1.8:
基于数组+链表+红黑树,本质上是使用CAS+synchronized加锁实现线程安全,不同节点多线程可以并发操作,如put操作,同一个节点,如果为空使用CAS,如果不为空,使用synchronized加锁
1.7和1.8效果:读写分离可以提高效率:多线程对不同的Node/Segment的插入/删除是可以并发,并行执行,对同一个Node/Segment的写操作是互斥的,读操作都是无锁操作,可以并发,并行执行(读读、读写并发,写写互斥)
和ReentrantReadWriteLock读写锁不同:读读并发,读写、写写互斥