学习目标
● 掌握线程相关概念
● 掌握线程的基本使用
● 掌握线程池的使用
● 了解解决线程安全方式
1.为什么要学习线程?
● 从1946年2月14日世界上第一台计算机在美国宾夕法尼亚大学诞生到今天,计算和处理的模式早已从单用户单任务的串行模式发展到了多用户多任务的高并发处理模式。计算机处理的任务调度单位就是我们今天讲的进程和线程。
● 随着计算机的发展,CPU的性能越来越高,从早期的20MHZ到现在的2GHZ以上,从单核CPU发展到了多核CPU,性能提升了千万倍。为了充分发挥CPU的计算性能,提高CPU的资源利用率,同时弥补进程调度过于繁重产生的问题,进程内部演进出了并发调度的诉求,于是就发明了线程。
2.有关概念
2.1 进程
● 什么是进程?
● 在windows操作系统中,进程被分为后台进程和应用进程两类。大部分后台进程在系统开始运行时被操作系统启动,完成操作系统的基础服务功能。大部分应用进程由用户启动,完成用户所需要的具体应用功能。比如听音乐,社交聊天,浏览网站等。
● 简单来说,进程就是程序的一次启动执行。每一个进程都是要占用系统资源,包括CPU、内存、磁盘、线程数等。
● 进程与程序是什么关系呢?
● 程序是存放在硬盘中的可执行的文件,主要包括指令和数据。一个进程是一个程序的一次启动和执行,是操作系统将程序装入内存,给程序分配必要的系统资源,并且开始运行程序的指令。
● 同一个程序可以多次启动,对应多个进程。比如,多次打开Chrome浏览器程序,在任务管理器或者Process Explorer中可以看到多个Chrome进程。
● Process Explorer:是由Sysinternals出品的免费产品,是一个轻量级进程管理器。
进程组成部分
● 1、程序段(代码段)
● 2、数据段
● 3、进程控制块(PCB)
● 3.1、进程描述信息
● 3.2、进程调度信息
● 3.3、进程资源信息
● 3.4、进程上下文
● 现代操作系统中,进程是并发执行的,任何进程都可以与其它进程一起执行。在进程内部,代码段和数据段有自己独立的地址空间,不同进程的地址空间是相互隔离的。也可以任务,进程之间是相互隔离的。
对于Java工程师来说,这里有一个问题:什么是Java程序的进程呢?
● Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。
● 在这个JVM进程内部,所有的Java程序代码都是以线程来运行的。JVM找到程序的入口点main(),然后运行main(),这样就产生一个线程。这个线程被称为主线程。当main()方法结束后,主线程运行完成,JVM进程也随即退出。
2.2 线程
● 什么是线程?
● 线程是指"进程代码段"的一次顺序执行流程。线程是CPU调度的最小单位一个进程可以有一个或者多个线程,因此可以说线程隶属于进程。各个线程之间共享进程的内存空间、系统资源。
线程的组成部分
● 一个标准的线程主要由3部分组成:
● 1、线程描述信息:线程的基本信息
● 2、程序计数器:记录线程下一条指令的代码段内存地址
● 3、栈内存:是代码段中局部变量的存储空间,为线程独立拥有,线程之间是不共享的。在JDK1.8中,每个线程在创建时默认分配1MB大小的栈内存(不受GC管理)。
2.3 进程 VS 线程
● 线程是"进程代码段"的一次顺序执行流程。一个进程包含一个或者多个线程,一个进程至少由一个线程。
● 线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程的并发性高。
● 线程是出于高并发的调度需求从进程内部演进而来的。线程的出现充分发挥了CPU的计算性能,又弥补了进程过度笨重的问题。
● 进程之间是相互独立的,但是进程之间的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
● 切换速度不同。线程上下文切换比进程上下文切换要快的多。所以有的时候,线程也会被称为轻量级进程。
● 注意: 虽然一个进程有很多线程,但是在一个CPU的内核中,同一时刻只能有一个线程是正在执行的,该线程也被称为当前线程。
2.4 并行与并发
● 并发(concurrency):指两个或多个事件在同一个时间段内发生。指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果;例如多个请求同时发给服务器,同时得到了处理(一般称为用户级别的并发).
● 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个处理器上同时执行;
● 串行:多个指令一条一条的执行;或者对任务一个一个的进行处理;
2.5 同步与异步
● 同步:一个任务的开始必须等待另外一个任务的结束;例如生活中的 接力棒比赛,当上一个人完成任务之后才能把接力棒传递给下一个人。
● 异步:一个任务的开始 无需等待其他任务的结束;任务之间没有毫无关联,无需相互等待;
2.6 组合
● 与多线程进行关联
● 同步非阻塞: 多个线程同时执行(一个一个进行执行) 不会出现阻塞
● 同步阻塞: 多个线程都不做任务了
● 异步阻塞: 多个线程并发执行 都阻塞了
● 异步非阻塞: 多个线程并发执行 都正常执行
3.Thread
● java.lang.Thread类是Java多线程编程的基础。
● 在Java程序中,一个线程使用一个Thread类对象(实例)进行描述。
3.1 层级
public class Thread extends Object implements Runnable{}
3.2 常用常量
static int MAX_PRIORITY //线程可以拥有的最大优先级(10)。
static int MIN_PRIORITY //线程可以拥有的最低优先级(1)。
static int NORM_PRIORITY //分配给线程的默认优先级(5)。
3.3 常用构造
构造方法 | 描述 |
---|---|
Thread() 创建一个新的线程 | |
Thread(String name) | 创建一个指定名称的线程 |
Thread(Runnable target) | 利用Runnable对象创建线程对象 |
Thread(Runnable target,String name) | 利用Runnable对象创建线程对象,并指定线程名称 |
3.4 常用方法
方法 | 描述 |
---|---|
static Thread currentThread() | 获得当前正在运行的线程对象 |
static void sleep(long millis) | 线程在指定时间内(毫秒数)休眠 |
static void yield() | 当前线程让出使用CPU的使用权,立马处于就绪状态。 |
static boolean interrupted() | 当前线程是否已被中断(如果调用了interrupt()方法 底层清除打断状态) |
long getId() | 获得线程的标识符 |
String getName() | 获得当前线程的名称(未自定义,使用Thread-n) |
int getPriority() | 获得线程 的优先级 |
Thread.State getState() | 获得线程的状态 |
void interrupt() | 中断线程。(其实不能终止线程,底层仅仅是修改了一个属性值) |
boolean isAlive() | 判断线程是否存活 |
boolean isInterrupted() | 当前线程是否已被中断(如果调用了interrupt()方法 底层不清除打断状态) |
void join() | 等待当前线程死亡 |
void join(long millis) | |
setName(String name) | 修改线程的名称 |
setPriority(int newPriority) | 修改线程的优先级别 |
start() | 启动线程 |
3.5 使用方法
public static void main(String[] args) {
Thread thread = Thread.currentThread();//获得当前正在运行的线程
//获得线程相关的信息
System.out.println(thread.getId());//获得线程的唯一标识
System.out.println(thread.getName());//获得线程的名称
System.out.println(thread.getPriority());//获得线程的优先级
thread.setPriority(Thread.MAX_PRIORITY);//修改线程的优先级
thread.setName("main主线程");//修改线程的名称
try {
Thread.sleep(200);//当前线程休眠200毫秒
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("111111111111111111");
}
4.创建线程
4.1 继承Thread类
- 模拟电脑管家,可以同时执行病毒查杀,清理垃圾,等功能。
public class GarbageThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在清理垃圾.....");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public GarbageThread(String name) {
super(name);
}
}
public class HealthCheckThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在电脑体检....");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public HealthCheckThread(String name) {
super(name);
}
}
public class VirusThread extends Thread {
//重写run方法
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在查杀病毒....");
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public VirusThread(String name) {
super(name);
}
}
//在主线程开启多个子线程 cpu进行调度
public class ComputerManager {
public static void main(String[] args) {
//同时执行多个功能
//1.垃圾清理
//2.查找木马
//3.电脑体检
//.....
GarbageThread garbageThread = new GarbageThread("垃圾清理线程");
HealthCheckThread checkThread = new HealthCheckThread("电脑体检线程");
VirusThread virusThread = new VirusThread("病毒查杀线程");
garbageThread.start();//启动线程
checkThread.start();
virusThread.start();
}
}
4.2 实现Runnable接口
- 多线程下载网络小说资源。
public class DownloadNovel implements Runnable {
private String novelPath;
@Override
public void run() {
try{
download();
}catch(IOException e){
e.printStackTrace();
}
}
public DownloadNovel(String novelPath) {
this.novelPath = novelPath;
}
public static void downloadNovel() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(novelPath).openStream()));
String line;
String fileName = "";
//获得文件名+小说名字
while ((line = reader.readLine()) != null) {
if (line.matches("(.*)<title>(.*)")) {
fileName = line.split("_")[0].split(">")[1];
fileName+=".txt";
break;
}
}
BufferedWriter writer = new BufferedWriter(new FileWriter(new File(NOVEL_DIR, fileName)));
while ((line = reader.readLine()) != null) {
if (line.matches("(.*)<main\\s(.*)")) {
line = line.substring(line.indexOf("<p>"), line.lastIndexOf("<p>")).replaceAll("<p>", "\n");
writer.write(line);
writer.newLine();
break;
}
}
writer.close();
reader.close();
}
}
public class ThreadDemo {
public static void main(String[] args) {
//开启多线程 下载小说资源
long begin = System.currentTimeMillis();
List<String> novelPathList = List.of(
"https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/pdyqJ3Rujv62uJcMpdsVgA2/",
"https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/w0Xq2d90qPxOBDFlr9quQA2/",
"https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/o9u2qont57Rp4rPq4Fd4KQ2/",
"https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/V_KClkdhrbvgn4oDUcDQ2/",
"https://read.qidian.com/chapter/D4Pic8a3frqLTMDvzUJZaQ2/Iw-c6HDPysHwrjbX3WA1AA2/"
);
List<Thread> threadList = new ArrayList<>();
for (String path : novelPathList) {
DownloadNovel downloadNovel = new DownloadNovel(path);
threadList.add(new Thread(downloadNovel));
}
threadList.forEach(Thread::start);
try {
for (Thread thread : threadList) {
thread.join();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
}
4.3 实现Callable接口
public class SaleTicket implements Callable<Integer> {
private static int ticketNum = 30;
@Override
public Integer call() throws Exception {
while (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "售卖了第" + ticketNum + "张票");
ticketNum--;
TimeUnit.MILLISECONDS.sleep(500);
}
return ticketNum;
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建3个售票窗口 同时进行卖票
SaleTicket saleTicket = new SaleTicket();
FutureTask<Integer> task1 = new FutureTask<>(saleTicket);
FutureTask<Integer> task2 = new FutureTask<>(saleTicket);
FutureTask<Integer> task3 = new FutureTask<>(saleTicket);
Thread window1 = new Thread(task1, "窗口1");
Thread window2 = new Thread(task2, "窗口2");
Thread window3 = new Thread(task3, "窗口3");
window1.start();
window2.start();
window3.start();
try {
System.out.println(task1.get());
//提前调用get会阻塞主线程 一定要在start之后调用get方法。 避免出现阻塞。
//获得的数据就是重写call方法的返回值数据。
System.out.println(task2.get());
System.out.println(task3.get());
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
4.4 线程池
4.4.1 优势
● 在上面几种方式中,多线程频繁创建和销毁非常消耗性能(用户态和核心态的转换)。导致出现线程资源的浪费,要提高资源利用率。
● 降低资源消耗-重用存在的线程,减少对象创建、消亡的开销,性能好
● 提高响应速度 -可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。当任务到达时,任务可不用等待线程创建就能立即执行
● 提高线程的可管理性-提供定时执行、定期执行、单线程、并发数控制等功能。
● 池化技术。使用线程池提高资源的利用率。线程池里面的线程用来接任务,当线程完成任务,回到池子中继续接任务。 当线程池销毁的时候,内部线程全部销毁。
● 有2种方式可以创建线程池对象:
- Executors
- ThreadPoolExecutor
4.4.2 Executors
● 在jdk中,提供了java.util.concurrent.Executors来快捷的创建线程池对象。
● Executors提供的创建线程池方式如下,了解:
● Executor:运行新任务的简单接口
● ExecutorService:扩展了Executor,添加了用来管理执行器生命周期和任务生命周期的方法
● newSingleThreadExecutor()://创建单个线程的线程池
● newFixedThreadPool()://创建线程数量固定的线程池
● newCachedThreadPool()://创建线程数量可伸缩的线程池
● newScheduledThreadPool()://创建执行延时任务的线程池
private static void demo1() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//创建一个数量固定的线程池,超出的任务会在队列中等待空闲的线程,可用于控制程序的最大并发数.
/*for (int num = 0; num < 5; num++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + "------");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}*/
ExecutorService executorService1 = Executors.newCachedThreadPool();
//短时间内处理大量工作的线程池,会根据任务数量产生对应的线程,并试图缓存线程以便重复使用.
//新的任务提交,没有可用的线程,将会自动创建新的线程并新增至池子中.未在六十秒内使用线程,这些线程将被终止并从缓存中删除。
ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(5);
//创建执行周期性或定时性任务的线程池.
scheduledService.schedule(()->{
System.out.println("3秒之后执行........"+LocalDateTime.now());
},3,TimeUnit.SECONDS);
System.out.println("当前时间:"+ LocalDateTime.now());
}
4.4.3 ThreadPoolExecutor
private static void demo2() {
// public ThreadPoolExecutor(int corePoolSize, //初始化池子中核心线程数量
// int maximumPoolSize, //可允许的最大的线程数量
// long keepAliveTime, //超过core数量的线程的最大存活时间
// TimeUnit unit, //keepAliveTime的时间单位
// BlockingQueue<Runnable> workQueue, //任务队列(维护未执行的新任务)
// ThreadFactory threadFactory, //创建线程对象
// RejectedExecutionHandler handler//拒绝策略 任务量>max+队列任务数量) {}
//1.有界队列 ArrayBlockingQueue 100
//2.无界队列 LinkedBlockingDeque (队列大小无限制) PriorityBlockingQueue 优先队列
//3.同步队列 SynchronousQueue 一个任务的提交必须等待另外一个线程的消费。
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,
100,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(30),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
//提交任务
//poolExecutor.submit()
//poolExecutor.execute();
}
5. 线程安全
线程安全是并发编程中的术语,指某个程序在并发环境中被调用时,能够正确地处理多线程之间的公共变量,使程序能正确完成。
5.1 缓存一致性(了解)
简单了解 即可
● 计算机内存模型:
● CPU 与缓存一致性 :
● 我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。
● 刚开始,CPU的性能与数据读写对比差异不大。但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大, 这就导致CPU每次操作内存都要耗费很多等待时间。
● 可是,不能因为内存的读写速度慢,就不发展CPU技术了,总不能让内存成为计算机处理的瓶颈。
● 因此,人们就提出一个新的概念—>缓存。就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。
● 程序的执行过程: 当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
● 随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存.
● CPU缓存可以分为一级缓存(L1),二级缓存(L2),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分.
● 有了多级缓存之后,程序的执行就变成了: 当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找.
● 单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核,即多核CPU,则每个核都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存.
● 随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响.
● 单线程: cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。
● 单核CPU,多线程:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。
● 多核CPU,多线程: 每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。
● 在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
● CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。
● 除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。
● 任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题.
5.2 并发编程的问题
● 并发编程里面出现的问题,其实与上面所提到的3个问题一致(缓存一致性,处理器优化,指令重排序。)
● 只不过并发编程里面所描述的,是人们使用抽象思维进行定义的。称为:
● 原子性问题
● 可见性问题
● 有序性问题。
● 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不全部执行完成,要不就不执行。
● 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
● 有序性即程序执行的顺序按照代码的先后顺序执行。
● 缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。
5.3 JMM
● 所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型
● 为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范.通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性
● 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障.
● Java内存模型:
● Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
● 提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification
● Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步.
● JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题
5.4 JMM的实现
● 在Java中提供了一系列和并发处理相关的关键字,比如
volatile
、synchronized
、final
、concurren
包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
● 在开发多线程的代码的时候,我们可以直接使用synchronized
等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。
原子性
● 在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit,在Java中对应的关键字就是synchronized。
● 因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性。
可见性
● Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
● Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
● 除了volatile,Java中的synchronized和final两个关键字也可以实现可见性。只不过实现方式不同。
有序性
● 在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:
● volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
synchronized 在并发中,可以保证程序的有序性,可见性,以及原子性。
volatile在并发中,可以保证程序的有序性,可见性,但是不能保证原子性。但是它可以限制指令重排。
5.5 synchronized
public class SaleTicket {
private static int ticketNum = 30;
public static void sale() {
while (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + (ticketNum--) + "票");
try {
TimeUnit.MILLISECONDS.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Test {
public static void main(String[] args) {
new Thread(SaleTicket::sale, "窗口1").start();
new Thread(SaleTicket::sale, "窗口2").start();
new Thread(SaleTicket::sale, "窗口3").start();
}
}
- 问题:出现了一票多卖,或者少卖等情况。多个窗口同时操作同一个变量ticketNum。出现了并发编程里面的问题,没有保证可见性。
- 解决方案:
//在方法签名处使用synchronized关键字(必须是同一个监视器对象)。
public class SaleTicket {
private static int ticketNum = 30;
public static void sale() {
while (ticketNum > 0) {
ticketNum();
try {
TimeUnit.MILLISECONDS.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static synchronized void ticketNum(){
System.out.println(Thread.currentThread().getName() + "正在卖第" + (ticketNum--) + "票");
}
}
- 同步代码块
//在方法体里面 使用synchronized(监视器对象){}
//了解锁对象相关的概念: https://zhuanlan.zhihu.com/p/71156910
public class SaleTicket {
private static int ticketNum = 30;
public static void sale() {
while (ticketNum > 0) {
synchronized (SaleTicket.class){
System.out.println(Thread.currentThread().getName() + "正在卖第" + (ticketNum--) + "票");
}
try {
TimeUnit.MILLISECONDS.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.6 Lock
- 模拟存钱/取钱功能:
public class AccountBalance {
@Getter
private BigDecimal balance = new BigDecimal("1000");
private final String NAME = "张三";
//存钱
public void cunMoney(double money) {
balance = balance.add(BigDecimal.valueOf(money));
System.out.println(Thread.currentThread().getName() + "正在给" + NAME + "转账" + money + ",目前账户的余额:" + balance);
}
//取钱
public void quMoney(double money) {
balance = balance.subtract(BigDecimal.valueOf(money));
System.out.println(Thread.currentThread().getName() + "取了" + NAME + money + ",目前账户的余额:" + balance);
}
}
class Test {
public static void main(String[] args) {
AccountBalance balance = new AccountBalance();
Thread thread1 = new Thread(() -> balance.cunMoney(500), "李四");
Thread thread2 = new Thread(() -> balance.quMoney(1000), "王五");
Thread thread3 = new Thread(() -> balance.cunMoney(1000), "赵六");
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("操作之后的余额还剩下:"+balance.getBalance());
}
}
- 使用Lock加锁解决
public class AccountBalance {
@Getter
private BigDecimal balance = new BigDecimal("1000");
private final String NAME = "张三";
private final Lock lock = new ReentrantLock();//默认是可重入锁,一个线程可以多次获得同一把锁。
//存钱
public void cunMoney(double money) {
try {
lock.lock();
balance = balance.add(BigDecimal.valueOf(money));
System.out.println(Thread.currentThread().getName() + "正在给" + NAME + "转账" + money + ",目前账户的余额:" + balance);
} finally {
lock.unlock();
}
}
//取钱
public void quMoney(double money) {
try {
lock.lock();
balance = balance.subtract(BigDecimal.valueOf(money));
System.out.println(Thread.currentThread().getName() + "取了" + NAME + money + ",目前账户的余额:" + balance);
} finally {
lock.unlock();
}
}
}
5.7 volatile
怎么才能保证程序中的原子性呢?
- 案例: 模拟自动生成id。(id生成器)
public class IdGenerator {
private static int id = 0;//维护id
public static void idIncrement() {
id++;
}
public static int getId() {
return id;
}
}
class IdTest {
@SneakyThrows
public static void main(String[] args) {
List<Thread> threadList = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
threadList.add(new Thread(IdGenerator::idIncrement));
}
threadList.forEach(Thread::start);
for (Thread thread : threadList) {
thread.join();
}
System.out.println("最后的id:" + IdGenerator.getId());
}
}
- 使用synchronized能够保证原子性。 效率偏慢。
public class IdGenerator {
//并发编程里面: 能够保证原子性类
//AtomicInteger AtomicLong....
//private static int id = 0;
// private static AtomicLong id = new AtomicLong(1000);
//LongAdder
private static LongAdder id = new LongAdder();
static {
//设置初始值
id.add(1000);
}
public static void idIncrement() {
// id.incrementAndGet();// CAS 乐观锁
id.increment();
}
public static long getId() {
//return id.get();
return id.longValue();
}
}
● 什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
● 1、比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
● 2、设置:如果是,将A更新为B,结束。如果不是,则什么都不做,循环重试。
● 上面的两步操作是原子性的,可以简单地理解为瞬间完成,在CPU看来就是一步操作。
● 有了CAS,就可以实现一个乐观锁。优点是无锁编程,所以没有上锁和释放锁的性能损失。
● 它允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
解决单例(懒汉模式问题)问题
单例模式: 一个进程(多个线程)有且只有1个对象。
● 饿汉模式:
优点: 单线程/并发环境 对象只有1个。
弊端: 没有体现懒加载。
● 懒汉模式:
优点: 体现懒加载。在单线程环境下,保证对象只有1个。
弊端: 并发环境下,不能保证对象有且只有1个。线程安全的问题。
public class SysUser implements Cloneable, Serializable {
private SysUser() {
}
private volatile static SysUser sysUser;
//并发10个线程 每个线程循环10次
//第一次循环的时候 1个线程执行这些逻辑 其他9个排队等待
//剩下的循环 并发执行
public static SysUser getInstance() {
if (sysUser == null) {
synchronized (SysUser.class) {
if (sysUser == null) {
sysUser = new SysUser();//jvm底层 可能会出现 指令重排
//1. new 开辟空间 5s
//2. SysUser() 初始化成员变量数据 6s
//3. 赋值 2s
// 1 3 5+2
}
}
}
return sysUser;
}
//禁止/避免 克隆打破单例
@Override
public SysUser clone() {
/* try {
SysUser clone = (SysUser) super.clone();
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}*/
return this;
}
public Object readResolve() {
return sysUser;
}
}
class Test1 {
public static void main(String[] args) throws Exception {
/* SysUser instance = SysUser.getInstance();
System.out.println(instance);
SysUser clone = instance.clone();
System.out.println(clone);*/
SysUser instance = SysUser.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("src/a.txt"));
outputStream.writeObject(instance);
System.out.println("instance:" + instance);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("src/a.txt"));
Object object = inputStream.readObject();//自动检测类中是否有 readResolve
System.out.println("obj:" + object);
inputStream.close();
//单例模式: 一个进程里面有且只有1个类对象。
//并发的环境下 获得SysUser类对象 保证SysUser类对象有且只有1个
//10个 每个线程执行10次获得SysUser类对象
// List<Thread> threadList = new ArrayList<>(10);
// for (int i = 0; i < 20; i++) {
// threadList.add(new Thread(() -> {
// System.out.println(Thread.currentThread().getName() + "---" + SysUser.getInstance());
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// }));
// }
// threadList.forEach(Thread::start);
}
//Thread-6---com.java.thread4.SysUser@44c924be
//Thread-0---com.java.thread4.SysUser@43785832
//懒汉模式在并发编程里面 不能保证单例。
//解决: 保证单例
//1. synchronized
// 1.1 同步方法 性能低。 锁粒度比较大 有且只有1个线程执行getInstance方法 其他19个全部排队等待
// 1.2 同步代码块 锁粒度比较小,性能偏低。
// 1.3 建议: DCL double check lock 双重检测锁 if+同步代码块
// 1.4 JIT指令重排序---->程序的有序性。 DCL+volatile
//克隆/序列化/反射,都会打破单例(饿汉/懒汉)
//使用枚举类创建单例---->属性、方法
}
5.8 ThreadLoacal
- 解决SimpleDateFormat问题。
public class ThreadLocalDemo {
//日期格式化 DateFormat---->SimpleDateFormat(Date 与 String之间的相互转换)
private static final String PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(PATTERN);
}
};
/**
* string转Date
*
* @param dateStr 字符串时间
* @return Date类的对象
*/
public static Date strToDate(String dateStr) {
Date date = null;
try {
date = THREAD_LOCAL.get().parse(dateStr);
} catch (ParseException e) {
throw new RuntimeException(e);
}
return date;
}
/**
* date转String
*
* @param date 日期对象
* @return 指定pattern格式下的字符串数据
*/
public static String dateToStr(Date date) {
return THREAD_LOCAL.get().format(date);
}
结论:
- 在dateToStr/strToDate 各自new一次new SimpleDateFormat(PATTERN) 在并发里面没有任何问题。
- 弊端: 5个线程 每个线程调用15次strToDate 创建了30个SimpleDateFormat对象。
- 节约堆内存,是否可以创建1次new SimpleDateFormat(PATTERN)? 作为全局变量 提高内存利用率
- 作为全局,在并发环境下,出现了线程安全的问题。
- 解决线程安全: synchronized
- 弊端: 性能低
- 节约内存,提高性能角度解决以上所有问题。
- 出现线程安全的根本原因: 多个线程共同操作/使用同一个SimpleDateFormat
- 5个线程----> 各自都有各自的SimpleDateFormat (创建5个)-----> ThreadLocal
- 将SimpleDateFormat对象的创建,维护,销毁,交给ThreadLocal管理
public static void main(String[] args) {
//本地线程
//作用: 为每个线程创建各自的数据副本(对象)。
//每个线程都有各自的资源操作。
//需求: 并发: 将多个字符串数据转换成指定的Date类对象
List<String> strList = new ArrayList<>();
//size=15
Collections.addAll(strList,
"2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00",
"2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00",
"2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00",
"2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00", "2020-01-01 12:00:00"
);
//创建2个线程 同时解析strList里面的字符串日期 转换成Date类对象
new Thread(() -> getListStr(strList)).start();
new Thread(() -> getListStr(strList)).start();
new Thread(() -> getListStr(strList)).start();
new Thread(() -> getListStr(strList)).start();
/*String str = "2020-01-01 12:00:00";
Date date = strToDate(str);
System.out.println(date);//Wed Jan 01 12:00:00 CST 2020
Date date1 = new Date();
System.out.println(dateToStr(date1));*/
}
private static void getListStr(List<String> list) {
for (String dateStr : list) {
Date date = strToDate(dateStr);
System.out.println(Thread.currentThread().getName() + "---" + date);
}
}
}
5.9 集合类型
● 感受集合类型所谓线程安全?
Collection
1.1 List
ArrayList LinkedList Vector
1.2 Set
Map
HashMap
HashTable---->synchronized(hash)
ConcurrentHashMap---->CAS(位桶里面指定index的那一个对象)
public class ListDemo {
//模拟: 并发多个线程,同时操作一个List集合对象。新增数据。
//100个线程 每个线程循环100次新增数据 list元素: 10000
//1.使用安全的集合类型
//2.将不安全的集合对象转换成线程安全的集合对象
private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
//private static List<Integer> list =new Vector<>();
public static void insertDataToList(){
List<Thread> threadList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
threadList.add(new Thread(()->{
for (int j = 0; j < 100; j++) {
list.add(j);
}
}));
}
threadList.forEach(Thread::start);
try {
for (Thread thread : threadList) {
thread.join();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("list.size():"+list.size());
}
public static void main(String[] args) {
insertDataToList();
}
}
6. 线程通信
生产者与消费者现象。
● 多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。而多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些通信机制,可以协调它们的工作,以此来帮我们达到多线程共同操作一份数据;
● 线程间通信的常见方式:共享主存 ,等待-通知。
● 等待唤醒机制:这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就是在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。
● 需求: 在双十一,很多用户提交很多订单执行支付功能。支付宝支付/微信支付---->支付成功 订单处理完毕。
生产者: 用户提交(生产)订单-----> 存储缓冲区(容器)
消费者: 淘宝服务器消费用户提交的订单。
只要用户提交一个订单,就可以通知服务器处理订单了。
服务器从缓冲区获得一个订单进行处理,这个时候缓冲区有足够空间维护新的订单,通知用户提交新的订单。
6.1 synchronized+wait+notify
- 同步代码块+wait+notify
public class OrderPool {
private static List<String> pool = Collections.synchronizedList(new ArrayList<>());
//订单池----> 最大数量的限定
private static final int MAX = 10;
//提交订单到池子里
public static void putOrder() {
while (true) {
synchronized (OrderPool.class) {
try {
while (pool.size()==MAX){
//当前生产者线程等待
OrderPool.class.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.add("order");
//唤醒消费者线程---> 随机唤醒一个
OrderPool.class.notify();
System.out.println(Thread.currentThread().getName() + "提交1个订单,池子里面:" + pool.size());
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//从池子里面获得订单进行处理
public static void consumeOrder() {
while (true) {
synchronized (OrderPool.class) {
try {
while (pool.size() == 0) {//池子没有订单
//消费者等待
OrderPool.class.wait(); //自动释放锁
//线程新的状态 WAITING
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.remove(0);//消费
//唤醒生产者
OrderPool.class.notify();
System.out.println(Thread.currentThread().getName() + "处理1个订单,池子里面:" + pool.size());
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- 同步代码块+wait+notify
public class OrderPool {
private static List<String> pool = Collections.synchronizedList(new ArrayList<>());
//订单池----> 最大数量的限定
private static final int MAX = 10;
//提交订单到池子里
public synchronized void putOrder() {
try {
while (pool.size() == MAX) {
//当前生产者线程等待
this.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.add("order");
//唤醒消费者线程---> 随机唤醒一个
this.notify();
System.out.println(Thread.currentThread().getName() + "提交1个订单,池子里面:" + pool.size());
}
//从池子里面获得订单进行处理
public synchronized void consumeOrder() {
try {
while (pool.size() == 0) {//池子没有订单
//消费者等待
this.wait(); //自动释放锁
//线程新的状态 WAITING
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.remove(0);//消费
//唤醒生产者
this.notify();
System.out.println(Thread.currentThread().getName() + "处理1个订单,池子里面:" + pool.size());
}
}
- 测试
public static void main(String[] args) {
OrderPool orderPool = new OrderPool();
//有很多生产者线程---->用户线程生成订单---->池子
List<Thread> threadList1 = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
threadList1.add(new Thread(()->{
while (true){
orderPool.putOrder();
try {
TimeUnit.MILLISECONDS.sleep(700);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "用户" + i));
}
//有很多消费者线程----->服务器处理订单
List<Thread> threadList2 = new ArrayList<>();
for (int i = 1; i <= 2; i++) {
threadList2.add(new Thread(()->{
while (true){
orderPool.consumeOrder();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "服务器" + i));
}
threadList1.forEach(Thread::start);
threadList2.forEach(Thread::start);
//并发8个线程
}
6.2 Lock+Condition
public class OrderPool {
private static List<String> pool = Collections.synchronizedList(new ArrayList<>());
//订单池----> 最大数量的限定
private static final int MAX = 10;
private static final Lock myLock = new ReentrantLock();
private static final Condition condition = myLock.newCondition();
//提交订单到池子里
public void putOrder() {
try {
myLock.lock();
try {
while (pool.size() == MAX) {
//当前生产者线程等待
condition.await();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.add("order");
//唤醒消费者线程---> 随机唤醒一个
condition.signal();
System.out.println(Thread.currentThread().getName() + "提交1个订单,池子里面:" + pool.size());
} finally {
myLock.unlock();
}
}
//从池子里面获得订单进行处理
public void consumeOrder() {
try {
myLock.lock();
try {
while (pool.size() == 0) {//池子没有订单
//消费者等待
//线程新的状态 WAITING
condition.await();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.remove(0);//消费
//唤醒生产者
condition.signal();
System.out.println(Thread.currentThread().getName() + "处理1个订单,池子里面:" + pool.size());
} finally {
myLock.unlock();
}
}
}
7. 线程状态
public enum State {
NEW,// new 线程对象 新建状态
RUNNABLE,// run方法 运行状态
BLOCKED,//阻塞状态----> 释放锁对象---> notify
WAITING,//等待 notify
TIMED_WAITING,//时间内等待
TERMINATED;//死亡状态
}