概念:
背景
- 程序,进程,线程,多任务,主线程
- 三高应用(高可用,高性能,高并发)
- 学习理论(守破离,断舍离),
- lambda(JDK8,内部类<静态,局部,匿名,lambda>,函数式编程),
- 定时器(timer,task),
- 类协作(设计模式),代理模式(静态代理<婚庆公司>,动态代理,真实角色,代理角色),单例模式,
- CAS(compare and swap),悲观锁,乐观锁
知识点
- 线程实现(Thread,Runnable,Callable),
- 线程启动(方式,时机),
- 线程状态(new新生,runnable<就绪,运行>,terminated死亡,block阻塞,time_wait时间等待<join,sleep,wait>,wait阻塞等待<wait,notify>),
- 同步synchronize(方法,块,容器),
- 数据可见性和代码有序性(volatile),
- 守护线程,
- ThreadLocal,JUC
- happen before,CPU指令重排机制
实际问题
- 共享资源问题(并发问题),
- 并发案例(龟兔赛跑,12306,电影院,取钱),
- 共享资源(全局对象、变量、方法),
- 死锁(哲学家就餐问题),
- 线程协作,生产者消费者模型(缓存法<wait,notify>,信号灯法),
- 单例模式使用了dubble checked。
线程命题
- 线程是为了解决应用的多任务并行问题。
- 线程是轻量级的进程。
- 线程并行是单CPU模拟并行,所以线程会争夺CPU的使用时间。
- 应用并发是某一功能被多人同时使用。
- 应用并发造成线程并发问题、服务器资源问题。
- 线程并发问题是线程调度机制和操作公共资源共同造成的。
- 线程调度机制衍生出线程状态和改变调度逻辑问题。
- 线程并发问题有三个因素,原子性、可见性、有序性。
- synchronize来解决原子性。
- synchronize可能造成死锁问题。
- synchronize是悲观锁。
- ThreadLocal是乐观锁。
- volatile解决数据可见性和代码有序性。
- volatile用来解决CPU指令重排造成的脏数据问题。
- 生产者消费者模型是解决并发问题的异步方案。
- synchronize是解决并发问题的同步方案。
演化过程
线程的来历
- 计算机简化模型:程序+计算。
- 计算机使用CPU代替人工计算。
- 程序是算法+数据。
- 操作系统也是一种程序,可以运行应用程序。
- 操作系统管理计算机硬件和程序执行过程。
- 计算机只能一次运行一个程序。
- 操作系统通过进程调度机制实现计算机同时运行多个程序。
- 进程是程序的一次执行过程。
- 每个进程有独立内存空间。
- 进程调度机制让多个进程轮流使用CPU。
- 最初一个程序执行一个任务。
- 线程让程序同时执行多个任务。
- 线程调度机制让多个线程轮流使用CPU。
- 线程使用进程的内存空间和CPU时间。
创建线程方式
- 继承Thread类;
- 对象可以直接调用start()启动。
- 实现Runnable接口,线程不可以返回数据和抛异常;
- 对象需要放入Thead代理对象后,代理对象调用start()启动。
- 实现Callable接口,线程可以返回数据和抛异常;
- 对象放入Thead代理对象后,代理对象调用start()启动。
- 对象放入FutureTask代理对象后,使用ExecutorService线程池对象的execute()启动。
线程状态
- 创建状态(NEW):创建一个线程对象。
- 运行状态(RUNNABLE):线程启动后,争取或者获得CPU使用权。
- 死亡状态(TERMINATED):线程逻辑(run方法)运行完毕。
- 时间等待状态(TIME_WAIT):等待指定时间后(sleep/join/yield),才能争取CPU使用权。
- 等待状态(WAIT):等待别人唤醒(notify)。
- 阻塞状态(BLOCK):获取同步锁失败,等待别人释放锁。
线程并发问题(原子性:代码级别)
问题原因:CPU使用权
- 多个线程同时操作公共资源。
- 由于线程调度机制,线程被剥夺了CPU使用权,导致暂停写入步骤。
- 其他线程可能拿到未修改之前的脏数据。
- 从而出现数据不一致问题。
解决方案:悲观锁
- 使用synchronize同步锁机制。
- 操作公共资源之前,先锁定资源使用权,阻止其他人使用资源。
- 将资源操作步骤原子化。
- 所有资源操作步骤都完成后,才释放资源锁。
- 加锁的资源类型:
- 同步方法,等同于锁定this对象(一个线程对象启动多个线程的情况);
- 同步块,等同于锁定数据对象(class对象也可以锁定=锁定new操作);
线程并发问题(可见性:指令级别)
问题原因:CPU寄存器缓存
- 线程指令运行时,会从主存中拷贝数据到寄存器;
- 由于寄存器中的数据拷贝,多线程操作同一个主存数据时;
- 会出现拷贝数据不一致;
解决方案:volatile
- volatile申明的变量,在修改后会标识缓存对象为无效;
- 缓存无效后,CPU会重新加载主存数据;
线程并发问题(有序性:代码级别)
问题原因:JVM指令重排
- JVM在保证代码执行效果一直的情况,进行的指令顺序重新排列;
- 多线程情况下,变量变化顺序,可能造成逻辑判断混乱;
int i = 0; //语句1
boolean flag = false; //语句2
i = 1; //语句3
flag = true; //语句4
执行顺序可能是:1234、1243、2134、2143。
解决方案:volatile
- volatile申明的变量,在编译时,会保证改变量操作前后代码不变;
- 比如5条代码,变量是第3条,那么12不会到3之后,45不会到3之前;
- 只能是:12345,12354,21345,21354
int i = 0; //语句1
boolean flag = false; //语句2
ticket = 99; //语句3
i = 1; //语句4
flag = true; //语句5
火车票并发案例
/**
* 线程同步机制:synchroneze
* 使用同步块的方式,创建一个锁对象lock;
* sleep会继续占用lock;
*
* @author jeadong
*
*/
public class SynchronizeTest {
public static void main(String[] args) {
//启动3个买票线程,模拟并发抢票
new Thread(new Web12306()).start();
new Thread(new Web12306()).start();
new Thread(new Web12306()).start();
}
}
//模拟12306买票操作
class Web12306 implements Runnable{
private static Object lock = new Object();//买票锁,保证减库存操作的线程安全
private static int tikects = 99;//总票数
private boolean flag = true;
@Override
public void run() {
while(flag){
//买票
buyTikect();
}//while end
}
private void buyTikect() {
//买票前,先加锁
synchronized(lock){
if (tikects > 0) {
tikects--;
System.out.println("【"+Thread.currentThread().getName()+"】购票成功!剩余票量:"+tikects);
}else{
flag=false;//票卖光了,就结束买票
}
//模拟买票操作延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}//sycn end
}
}
单例模式案例,double checked
/**
* 双重检测锁:synchronize+volatile
* 创建单例模式
* 1、保证原子性,用synchronize锁定class
* 2、保证可见性和有序性,用volatile直接访问主存对象
* 3、最外层的判空是为了提升访问效率
*
* @author jeadong
*
*/
public class DubbleCheckedTest {
public static void main(String[] args) {
//启动子线程触发第一次访问
new Thread(() -> {
System.out.println(Singleton.getInstance());
}).start();
//主线程触发第二次访问
System.out.println(Singleton.getInstance());
}
}
class Singleton {
//volatile必须加,为了防止指令重排,第一次创建时,内存还没完成创建,就把对象指针暴露出去了
private static volatile Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
//第一层检测,为了第二次访问之后提升效率,不要再次加锁
if (null == instance) {
//锁定class,让其他线程无法创建对象,保证只会执行一次New操作
//如果没有加锁,就能模拟出创建两个对象
synchronized(Singleton.class){
//第二层检测,保证第一次访问顺利完成对象创建
if(null == instance){
//模拟创建对象比较耗时
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建对象
instance = new Singleton();
}
}
}
return instance;
}
}