创建线程的3种方式
第一种:继承Thread类,重写run()方法
第二种:实现Runnable接口,重写run()方法
第三种:实现Callable接口,重写run()方法
线程生命周期
1.new新建状态
2.runnable可运行状态
包括:1.ready就绪状态,线程调用start()方法之后,等待jvm的调度
比如:yield()线程让步
2.running运行状态,线程对象得到jvm的调度
3.blocked阻塞状态
4.waiting等待状态
注:调用wait()调用wait会释放掉锁,而sleep不会,join()等方法就会进入等待状态
wait()方法处于等待的线程通过调用notify()和notifyall()方法唤醒等待中的线程
join()是线程插队,调用join的线程会先执行,其他线程等到执行完了才能执行
5.Timed_Waiting定时等待状态,比如sleep()线程睡眠
6.terminated终止状态
重点:线程安全
CPU的三级缓存:每个CPU都有L1,L2,L3三级缓存
CPU查找数据的顺序为CPU->L1->L2->L3->内存->硬盘
L1,L2是cpu中同一核内共享数据
L3是cpu中不同核中共享数据
内存中的缓存行是CPU间共享数据
重点
java中,一切不可变的对象一定是线程安全的!比如,final关键字修饰的对象。
互斥同步:synchronized是java中最基本的互斥同步手段,互斥同步会让没抢到锁的线程挂起,等到能执行时在唤醒,挂起和唤醒过程会非常的耗开销。互斥同步也叫阻塞同步!是一种悲观的并发策略!
非阻塞同步:是基于冲突检测的乐观并发策略,没抢到锁时会再次进行抢锁,成功则成功,没成功会一直自旋!
线程竞争激烈时,悲观并发策略适合,竞争不激烈时,乐观并发策略适合
synchronized的原理(锁升级)
对象头主要包括2部分(mark word标记字段 kiass pointer 类型指针)
标记字段01为无锁态
00为轻量锁 没抢到锁的线程会自旋
10为重量锁(互斥锁) 没抢到锁的线程会挂起
死锁
java产生死锁的4个条件
1.互斥使用
2.不可抢占
3.请求和保持(请求其他资源的时候,保持原有资源的占有)
4.循环等待
解决死锁问题的方法是:synchronized同步代码快和lock显式锁
线程重入:线程获取到锁后,再次获取该锁不会被该锁阻塞!
Lock锁(显式锁)
lock是获取锁,unlock释放锁
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
java并发编程的3大特性
原子性 : 在某一时刻,一进程不能分开被线程执行,只能由一个线程执行到底
可见性:线程和线程之间的数据是否可见(可见会带来一系列安全问题)
有序性:指令乱序重排序的问题(乱序重排序也会带来数据不一致问题)
volatile可以保证可见性和有序性
synchronized和lock可以保证原子性,可见性和有序性
lock锁的原理(Cas Aqs)
cas
cas的思路其实很简单,就是给一个元素赋值的时候,先看看内存里的那个值到底变没变,如果没变我就修改,变了我就不改了,其实这是一种无锁操作,不需要挂起线程,无锁的思路就是先尝试,如果失败了,进行补偿,也就是你可以继续尝试。这样在少量竞争的情况下能很大程度提升性能。
重点:Cas是通过比较期望值和原值,不一致会再次尝试,进行自旋。这样在少量竞争的情况下能很大程度提升性能。Cas保证的是对一个对象写操作的无锁原子性。加syncronized的也具有原子性。
CAS还是有几个缺点:
1.ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A -> B -> A的过程。绝大部分场景我们对ABA不敏感。解决方案:添加版本号作为标识,每次修改变量值时,对应增加版本号; 做CAS操作前需要校验版本号。JDK1.5之后,新增AtomicStampedReference类来处理这种情况。
2.循环时间长开销大。如果有很多个线程并发(高并发时),CAS自旋可能会长时间不成功,会增大CPU的执行开销。
3.只能对一个变量进行原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。
Aqs见下篇