1.线程的简介
1.1 什么是进程
进程,是正在运行的程序实例,是操作系统进行资源分配的最小单位。每个进程都有它自己的地址空间和系统资源(比如CPU时间,内存空间,磁盘IO等)。多个进程可以同时执行,每个进程在运行时都不会影响其他进程的运行,资源不共享。
程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
当你打开一个软件程序时,计算机会为它分配一个资源区域,并为其分配一定数量的内存,这个程序就是一个进程。举个例子来说,比如你打开了一个文字编辑器,这个编辑器就是一个进程。计算机会给它分配一定数量的内存空间,用于它的运行和存储。通俗点说,进程是操作系统中运行的一个任务(一个应用程序运行在一个进程中)。
电脑就是多进程的。 可以同时启动,视频播放器,音乐播放器,QQ、wechat等各种应用程序。
1.2 什么是线程
线程是进程的一部分,是CPU能够进行运算调度的最小单位。线程不能独立存在,必须依赖于进程。
线程是一个进程中的顺序执行流(执行单元)。一个进程中可以有一个线程,也可能有多个线程。每个线程都有自己的指令指针、堆栈和局部变量等,但它们共享进程的代码、数据和全局变量等资源。
多线程可以实现并发执行,提高程序的效率。
线程使用的场景:
当一个程序中需要同时完成多个任务的情况下,我们可以将每个任务定义成一个线程,使他们得以同时工作。
有些时候,虽然可以使用单一线程完成,但是使用多线程可以更快完成,如下载文件。
1.3 线程和进程的区别
- 进程是操作系统运行的一个任务,线程是进程中运行的一个任务
- 进程是资源分配的最小单位(相互独立),线程是程序执行的最小单位(cpu调度的基本单元)。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。进程之间的通信需要以IPC进行
- 线程是轻量级的进程,同一个进程中可以包含多个线程。多线程共享进程中的数据,使用相同的地址空间,因此,线程之间的通信更方便,CPU切换(或创建)一个线程的开销远比进程要小很多。
- 一个进程结束,其内的所有线程都结束,但不会对另外一个进程造成影响。多线程程序,一个线程结束,有可能会造成其他线程结束。
1.4 CPU核心数
现在,一个CPU都是多核心的。
内核(处理器):一个CPU内核在一个时间片上可以执行一个线程
逻辑处理器:同时可以处理的线程数量。(超线程时使用)
当物理CPU使用了超线程技术后,在CPU的一颗内核中,利用就是利用其中空闲的执行单元,模拟出另外一个核心(并不是真正的物理运算核心),使得CPU的这颗内核有两个逻辑核心,也就是所谓的逻辑CPU,此时物理CPU的一颗内核在一个时间片内理论上可同时执行两个内核线程,从而提高了整个CPU的工作效率,此时逻辑CPU的数量=物理CPU的数量x单个CPU的内核数x2。值得注意的是,一颗内核并不代表只能有一个或者两个逻辑CPU,也可以有4个逻辑CPU或者更多。
1.5 CPU时间片
1.5.1 CPU时间片的概念
在宏观上,我们可以同时打开多个应用程序,每个程序同时运行,互不打扰。但在微观上:由于只有一个CPU(一个内核而言),一次只能运行某一个进程的某一个线程。如何公平处理,一种方法就是引入时间片的概念,每个程序轮流执行。
CPU调度机制算法,会将时间划分成一个个时间片,时间片的大小从几ms到几百ms。
1.5.2 线程调度
线程调度是计算机多线程操作系统中分配CPU时间给各个线程的过程。每个线程代表程序中的一个执行路径,操作系统通过线程调度器分配处理器时间,决定哪个线程将获得执行的机会,以及获得的时间长短。
1.5.3 进程调度
进程调度是操作系统中分配CPU时间给各个进程的过程。进程是系统进行资源分配和调度的独立单位,它包含代码、数据以及分配的系统资源。与线程调度不同,进程调度涉及到的上下文切换成本更高,因为进程间共享的资源更少。
进程调度的算法之一:时间片轮转算法(Round-Robin,RR)
CPU 时间片轮转机制是一种抢占式调度算法,即 CPU 会分配给每个进程一个固定时间片,当一个进程的时间片用尽后,系统会打断该进程并分配给下一个进程。这一过程会一直进行下去,直到所有进程都被执行完毕。
实现原理:
系统将所有的就绪进程按先来先服务的原则,排成一个队列,
每次调度时,把CPU分配给队首进程,并令其执行一个时间片.时间片的大小从几ms到几百ms.
当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;
然后,再把处理器分配给就绪队列中新的队首进程,同时也让它执行一个时间片
Windows 系统中线程轮转时间也就是时间片大约是20ms,如果某个线程所需要的时间小于20ms,那么不到20ms就会切换到其他线程;如果一个线程所需的时间超过20ms,系统也最多只给20ms,除非意外发生(那可能导致整个系统无响应),而Linux/unix中则是5~800ms。
1.6 串行与并发
多个线程“同时运行”只是我们感官上的一种表现。其实,线程是并发运行的。
操作系统将时间划分成很多时间片段,尽可能的均匀分配给每一个线程,获取时间片段的线程被CPU运行,而其他线程处于等待状态。所以这种微观上是走走停停,断断续续的,宏观上都在运行的现象叫并发。但不是绝对意义上的“同时发生”。
2.线程的调度机制
2.1 java线程的状态简介
在Java中,线程(Thread)可以处于以下几种状态:
新建状态(New):线程对象已经创建,但还没有调用start()方法。
就绪状态(Runnable):线程已经调用start()方法,等待CPU调度执行。
运行状态(Running):线程获得CPU时间片,开始执行run()方法里的代码。
阻塞状态(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,直到进入就绪状态。
等待状态(Waiting):线程因为某些条件而进入等待状态,此时不会被分配CPU时间片,直到其他线程显式地唤醒。
超时等待状态(Timed Waiting):线程在指定的时间内等待,时间到后会自动返回到就绪状态。
终止状态(Terminated):线程的run()方法执行完毕或者因异常退出而结束线程的生命周期。
2.2 抢占式调度与协同式调度
JVM线程调度的实现依赖于底层操作系统的支持。由于Java是跨平台的,JVM会利用底层操作系统提供的功能来管理线程的调度。
Java线程调度基本上是抢占式的,在这种模式下,每个线程都有机会获得CPU时间片,操作系统基于线程的优先级来决定哪个线程更应该运行。高优先级的线程会得到更多的运行机会。
而相对的协同式调度则要求线程主动释放控制权,当前运行的线程必须主动让出CPU时间,其他线程才能获得执行机会。但在Java中,由于大多数现代操作系统都采用抢占式调度,协同式调度在Java中并不常见。
2.3 线程的优先级
线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片段的概率。Java线程可以通过setPriority方法设置优先级,优先级较高的线程有更大的几率获得CPU时间片。
线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高。线程提供了三个常量来表示最低,最高,以及默认优先级
-
Thread.MIN_PRIORITY
-
Thread.MAX_PRIORITY
-
Thread.NORM_PRIORITY
2.4 线程生命周期管理
JVM通过Thread类提供的方法来管理线程生命周期,例如start()、sleep()、yield()、join()、wait()等,使得线程在适当的时候运行或暂停
start()方法可以使线程处于就绪状态
yield()方法可以使当前运行的线程让出自己的时间片,但不会阻塞线程,只是将线程从运行状态转移至就绪状态。
join()方法可以让一个线程等待另一个线程完成后再继续执行。
sleep()方法可以使当前线程暂停执行指定时间。
wait()方法是当前线程释放锁,释放cpu等资源,进入等待状态
3. 线程的创建及其常用API
3.1 线程的三种创建方式
3.1.1 第一种
继承Thread类,重写run方法,创建该类对象,调用start方法开启线程。start方法可以将该线程对象纳入可执行线程池中。切记,不是调用run方法。
测试代码如下:
/*
线程的创建:三种方式
第一种: 使用Thread的实现类或者匿名内部类的方式来创建线程对象
*/
public class _01CreateDemo {
public static void main(String[] args) {
//创建一个线程对象,处于新建状态
Thread t1 = new MyThread();
// 启动线程,使其处于就绪状态
t1.start();
System.out.println("main 方法结束");
//使用匿名内部类的方式来实现Thread,创建一个线程对象
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(currentThread().getName()+":"+"100"+i);
}
}
};
t2.start();
}
}
// 实现Thread类型,自定义一个线程,实现1到100的打印
class MyThread extends Thread{
// 重写run方法,run方法就是用来编写线程任务代码的
public void run(){
for (int i = 0; i < 100; i++) {
// Thread.currentThread().getName() 获取当前线程的名字
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
}
}
3.1.2 第二种
实现Runnable接口,重写run方法,创建Thread类对象,将Runnable子类对象传递给Thread类对象。调用start方法开启线程。
第二种方式比第一种要好很多:
将线程对象和线程任务对象分离开。降低了耦合性,利于维护
适合多个相同的程序代码的线程去处理同一个资源
可以避免java中的单继承的限制
增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
测试代码如下:
/*
线程创建的三种方式:
第二种: 使用Runnable接口的实现类或者匿名内部类的方式,来创建线程对象
*/
public class _02CreateDemo {
public static void main(String[] args) {
//获取Runnable的实例对象
Runnable runnable = new MyTask();
//创建线程对象,调用构造器Thread(Runnable runnable),传入任务
Thread t1 = new Thread(runnable);
//启动线程,进入就绪状态
t1.start();
//使用匿名内部类或者lambda表达式,创建线程对象,并计算100以内的奇数之和
Thread t2 = new Thread(()->{
int sum = 0;
for (int i = 1; i < 100; i+=2) {
sum += i;
}
System.out.println("100以内的奇数之和sum:"+sum);
});
t2.start();
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i < 100; i+=2) {
sum += i;
}
System.out.println("100以内的奇数之和sum:"+sum);
}
});
t3.start();
Thread t4 = new Thread(()->{
int sum = 0;
for (int i = 1; i < 100; i+=2) {
sum += i;
}
System.out.println("sum:"+sum);
});
t4.start();
}
}
//自定义实现类,实现Runnable接口,计算100以内偶数之和,并打印
class MyTask implements Runnable{
// 重写run方法:线程的任务代码
public void run() {
int sum = 0;
for (int i = 2; i < 101; i+=2) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + sum);
}
System.out.println("100以内的偶数之和sum = " + sum);
}
}
3.1.3 第三种
-
创建Callable接口的实现类对象,并实现call()方法。该call()方法将作为线程的执行体,该call()方法有返回值。
-
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
-
使用FutureTask对象作为Thread对象的target创建井启动新线程。
-
使用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
第三种使用Runnable功能更加强大的一个子类Callable。这个子类是具有返回值的任务方法。
测试代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
线程创建的三种方式:
第三种:
1.先获取Callable对象,重写里面的call方法。call相当于run,但是call有返回值。
2.再获取FutureTask对象,将上面的Callable对象传入构造器
3.最后在获取Thread对象,传入FutureTask对象。
4. 这种方式可以获取线程里的返回数据,进行其他操作。想比前两种方式功能更强大一些。
*/
public class _03CreateDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Callable是函数式接口,里面的 V call() 相当于线程(Thread或者Runnable)的run方法,即任务代码书写的位置
Callable c1 = ()->{
int sum = 0;
for (int i = 2; i < 101; i+=2) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + sum);
Thread.sleep(100);
}
return sum;
};
//调用FutureTask的构造器,传入Callable对象
FutureTask<Integer> ft = new FutureTask<>(c1);
// 创建Thread线程对象,调用start()方法,进入就绪状态
Thread t1 = new Thread(ft);
t1.start();
// 获取线程执行结束后的结果, 注意: get方法有阻塞所在线程的效果。
int result = ft.get();
System.out.println("result: " + result);
System.out.println("------main结束------");
}
}
3.2 常用的构造器
Thread的常用构造器
1. Thread(Runnable runnable)
2. Thread(Runnable runnable, String name): 使用指定Runnable对象作为参数,
3. Thread(String name) :指定名称为name
Thread(Runnable runnable) 测试代码:
public class _01ConstructorDemo {
public static void main(String[] args) {
/*
使用Thread(Runnable runnable),创建一个线程对象。
任务: 随机五个长度的int元素,放入数组中,并冒牌排序,输出。
*/
Thread t1 = new Thread(()->{
int[] a = new int[5];
for (int i = 0; i < a.length; i++) {
a[i] = (int)(Math.random()*101);
}
for (int i = 0; i < a.length-1; i++) {
for (int j = 0; j < a.length-i-1; j++) {
if(a[j]>a[j+1]) {
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
System.out.println(Thread.currentThread().getName()+"正在执行中:"+Arrays.toString(a));
}
System.out.println(Arrays.toString(a));
},"线程一");
t1.start();
}
}
Thread(Runnable runnable, String name)测试代码:
/*
构造器
Thread(Runnable runnable,String name)
*/
public class _02ConstructorDemo {
public static void main(String[] args) {
Runnable a = new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"说:我喜欢哈哈哈哈哈哈哈");
}
}
};
//调用两个参数的构造器,第二个参数是线程的名称
Thread t1 = new Thread(a,"张三");
t1.start();
}
}
Thread(String name) 测试代码
public class _03ConstructorDemo {
public static void main(String[] args) {
Thread t1 = new Thread("Thread-B"){
@Override
public void run() {
String name = this.getName();
for (int i = 0; i < 10; i++) {
System.out.println(name+"说,我喜欢喝水!"+i);
}
}
};
t1.start();
}
}
3.3 常用的属性方法
方法名 | 用途 |
---|---|
static Thread currentThread() | Thread类的静态方法,可以用于获取运行当前代码片段的线程对象 |
long getId() | 返回该线程的标识符 |
String getName() | 返回该线程的名称 |
int getPriority() | 返回该线程的优先级 |
Thread.State getState() | 获取线程的状态 |
boolean isAlive() | 判断线程是否处于活动状态 |
boolean isInterrupted() | 判断线程是否已经中断 |
boolean isDaemon() | 判断线程是否为守护线程 |
测试代码:
/*
线程常用的属性方法
*/
public class PropertyMethodDemo {
public static void main(String[] args) {
/*
main方法:本质上就是一个线程
*/
// 获取当前线程对象
Thread thread = Thread.currentThread();
//1. 获取当前线程的名字
String name = thread.getName();
//2. 获取当前线程的唯一标识符
long id = thread.getId();
//3. 获取当前线程的优先级
int priority = thread.getPriority();
//4.获取当前线程的状态
Thread.State state = thread.getState();
//5.查看当前线程是否存活
boolean alive = thread.isAlive();
//6.查看当前线程是否被打断
boolean interrupted = thread.isInterrupted();
//7. 查看当前线程是否为守护进程
boolean daemon = thread.isDaemon();
System.out.println("name = " + name);
System.out.println("id = " + id);
System.out.println("priority = " + priority);
System.out.println("state = " + state);
System.out.println("alive = " + alive);
System.out.println("interrupted = " + interrupted);
System.out.println("daemon = " + daemon);
}
}
3.4 守护线程
-
void setDaemon(boolean on)
当参数为true时该线程为守护线程
1.进程要么是守护线程,要么前台线程
2.前台线程: 可以在表面运行的,能看到的。或者是daemon的值为false。
3.后台线程就是守护线程,即daemon的值为true。 注意: 所有的前台线程都结束了,后台线程就会立即结束。
测试代码:
public class DaemonDemo {
public static void main(String[] args) {
//第一个线程:rose 喊10次, i jump, 10次后真跳了
Thread rose = new Thread(()->{
for (int i = 1; i < 11; i++) {
System.out.println(Thread.currentThread().getName()+"说: i jump -----");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("------跳跃中.....-------");
},"rose");
//
Thread jack = new Thread(()->{
for (int i = 1; i < 101; i++) {
System.out.println(Thread.currentThread().getName()+"说: you jump, i jump -----");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"jack");
//jack应该是守护进程,即rose跳水后,jack应该停止喊了
jack.setDaemon(true);
rose.start();
jack.start();
}
}
3.5 声明周期相关方法
3.5.1 sleep()方法
static void sleep(long time)
解析:线程的睡眠方法,可以让当前线程进入休眠状态,进入阻塞状态,不再占用cpu的使用时间。当睡眠结束后,就转为就绪(Runnable)状态。sleep()方法平台移植性好。
时间单位是毫秒。休眠时间一过,立马进入就绪状态(等待调度器分配时间片段)。注意: 休眠时间内,可能会被异常中断,因此要处理异常InterruptedException。
两个参数的方法:
static void sleep(long time, int nanos)
time 单位毫秒, nanos:单位纳秒
测试代码:
public class _01SleepDemo {
public static void main(String[] args) {
Thread t1 = new Thread("Download"){
public void run() {
for (int i = 0; i <= 100; i+=10) {
System.out.println("正在下载视频中-------"+i+"%");
try {
//使用休眠方法,来假装模拟正在下载中
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("下载完成!");
}
};
t1.start();
}
}
3.5.2 yield()方法
static void yield():
解析:
线程的礼让方法,表示让出cpu的使用权,会进入就绪状态。不过,下一个时间片段还可能是该线程的。
测试代码:
public class _02YieldDemo {
public static void main(String[] args) {
Thread t1 = new Thread("Thread-A"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":"+i);
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t2 = new Thread("Thread-B"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//打印5之前让一下时间片段
if (i==5){
//直行道28行时,cpu正在被Thread-B使用
Thread.yield();// 这一行代码表示,Thread-B让出cpu。进入就绪状态,下一个时间片段,可能还会被线程调度器分配给Thread-B。
}
System.out.println(getName()+":"+i);
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t1.start();
t2.start();
}
}
3.5.3 join()方法
void join():
解析: 另一个线程加入方法。此时当前线程就会处于阻塞状态。另一个线程结束后,当前线程就会进入就绪状态。
测试代码:
public class _03JoinDemo {
public static void main(String[] args) {
Thread download = new Thread("download"){
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("图片正在下载中" + i + "%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread show = new Thread("show"){
@Override
public void run() {
try {
//当前线程是显示线程,显示应该在下载线程执行完毕后,在换行,因此要让download加入进来。
//阻塞显示线程
download.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for(int i=1;i<=100;i++){
System.out.println("图片加载中...." + i + "%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
download.start();
show.start();
}
}
3.5.4 interrupted()
void interrupted():
解析:线程中断、打断方法。表示当前线程去打断另外一个线程。
注意:是在当前线程中,用另一个线程的调用打断方法。
测试代码:
public class _04InterruptedDemo {
public static void main(String[] args) {
Thread lin = new Thread("林永健"){
@Override
public void run() {
System.out.println(getName()+"说,开始睡觉");
try {
//线程休眠100秒,模拟林睡着了
Thread.sleep(100000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println(getName()+"说:干嘛呢?干嘛呢!天塌了吗?");
}
}
};
Thread huang = new Thread("黄宏"){
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "说:"+(i+1)+"次八十! ");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(getName()+"说:搞定了");
lin.interrupt();
}
};
lin.start();
huang.start();
}
}
4. 临界资源问题(重点)
4.1 什么是临界资源
临界资源:在一个进程中,多个线程之间是资源共享的。如果一个资源同时被多个线程访问,这个资源就是一个临界资源。
当多个线程并发读写同一个临界资源时,会发生“线程并发安全问题”。
常见的临界资源:
多线程共享实例变量
多线程共享静态公共变量
如果想解决线程安全问题,需要将异步的操作变为同步操作。
异步操作:多线程并发的操作,相当于各干各的
同步操作:有先后顺序的操作,相当于你干完我再干。
引入代码:
public class _01SafeDemo {
public static void main(String[] args) {
Runnable desk = new Desk();
Thread xiaoming = new Thread(desk,"小明");
Thread xiaohong = new Thread(desk,"小红");
xiaoming.start();
xiaohong.start();
}
}
class Desk implements Runnable{
//静态属性:豆子的数量,初始值10个。
private static int beancount = 10;
public void take(){
beancount--;
}
@Override
public void run() {
//定义线程的任务,取走豆子,一个一个的拿走
while(beancount!=0){//桌上只剩下一个豆子时,两个线程恰巧执行到条件判断。 1>0成立,
//那么两个线程都进入循环体,然后各自拿走一个豆子,此时豆子就是-1个了。 明显不合理
//即出现了临界资源的安全隐患问题。
try {
//增加出现安全隐患的概率
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
take();
System.out.println(Thread.currentThread().getName()+"吃了一个豆子,剩下的个数:"+beancount);
}
}
}
结果:
由此看出,这样子是不合理的,这就是多线程的线程并发安全问题。 接下来我们就要说处理方法,也就是锁。
4.2 锁机制
4.2.1 锁机制的简介
java提供了一种内置的锁机制来支持原子性,通过关键字synchronized来进行同步代码块。
同步代码块包含两部分:
一个是充当锁的对象的引用
一个是由这个锁保护的代码块。
语法格式:
synchronized(同步监视器-锁对象引用){
//代码块
}
每个Java对象都可以用作一个实现同步的锁。线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是通过正常途径退出还是通过抛出异常退出都一样。获取内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
所以,上面的问题我们就有了解决方法,如下面代码所示:
public class _02SafeSynchronizedDemo {
public static void main(String[] args) {
Runnable desk1 = new Desk1();
Thread xiaoming = new Thread(desk1,"小明");
Thread xiaohong = new Thread(desk1,"小红");
xiaoming.start();
xiaohong.start();
}
}
class Desk1 implements Runnable {
//静态属性:豆子的数量,初始值10个。
private static int beancount = 1;
public void take() {
beancount--;
}
@Override
public void run() {
//定义线程的任务,取走豆子,一个一个的拿走
//将具有安全隐患问题的代码放入{}。因为两个线程操作的都是同一个桌子,因此this可以作为锁对象
while (beancount != 0) {//桌上只剩下一个豆子时,两个线程恰巧执行到条件判断。 1>0成立,
//那么两个线程都进入循环体,然后各自拿走一个豆子,此时豆子就是-1个了。 明显不合理
//即出现了临界资源的安全隐患问题。
try {
//增加出现安全隐患的概率
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (this) {
if (beancount >0) {
take();
}
}
System.out.println(Thread.currentThread().getName() + "吃了一个豆子,剩下的个数:" + beancount);
}
}
}
结果为:
这样子输出结果就合理了很多。
锁机制的总结:
针对于临界资源安全隐患问题的解决方式,引入锁机制。
1.锁机制的作用: 将异步的代码块变成同步的代码块。
2. 语法:
synchronized(锁对象的地址){
//需要同步的代码块,(如果不同步,就会出现安全隐患问题)。
}3.任何的java对象都可以作为锁,所有的线程看到的都是同一个对象。
4. 同步的代码块在可能的情况下,尽量缩小范围,提高其他代码的并发效率。
5. 运行逻辑:
当一个线程执行到synchronized(锁对象的地址)的{}里,就表示该线程A获取了锁对象,其他线程都必须等待,直到线程A执行完了同步代码块,会自动释放锁对象。其他线程才有机会获取锁对象,谁获取到锁对象,谁就执行同步代码块。
4.2.2 合适的锁对象
使用关键字synchronized同步方法内的部分代码块时,对于充当锁的对象,应该注意:
多个需要同步的线程在访问该同步块时,看到的应该是同一个锁的对象引用,否则不同步。
通常我们会使用this来作为锁对象
4.2.3 Synchronized关键字的作用域
作用域有两种:
第一种,同步方法内的部分代码块,或者全部代码块(相当于给方法直接加锁)
通常情况下锁对象都是this,此时多个线程访问此同步块时会进行同步操作。如果这个对象中有多个同步方法,只要其中一个线程正在访问某一个同步方法,那么其他线程就不能同时访问这个对象中的任何同步方法。注意,不同的对象实例的同步方法互不干扰。
第二种,同步静态方法
当我们对一个静态方法加锁时,该方法的锁对象是类对象。每个类都有唯一的一个类对象。获取类对象的方法为:类名.class
静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。原因就在于,静态方法的锁是类对象,而非静态方法的锁对象是当前方法所属的对象。
总结
1. synchronized可以锁住方法里面的部分代码。
即锁的范围就是同步块的范围
此时,所得对象可以是java的任何引用类型的对象。
2. 非静态方法上锁:就是在方法上添加修饰词synchronized
当一个线程正在访问该方法时,获取了锁对象,其他线程想要访问该方法,就要处于阻塞状态。
前提条件: 多个线程访问对象的实例,必须是同一个对象的,比如对象是桌子,必须是同一张。
(重点) 当一个线程正在访问一个同步方法时,this这个锁对象即被它占用,其他线程想要执行该实例对象的其他的同步方法时,也需要等待,因为this锁已经被占用了。
测试代码为:
public class _01Synchronized {
public static void main(String[] args) {
Desk desk = new Desk();
Thread t1 = new Thread(desk,"小明");
Thread t2 = new Thread(desk,"小红");
t1.start();
t2.start();
}
}
class Desk implements Runnable{
//添加一个非静态属性,充当锁对象。
Object lock = new Object();
private int BeanCount = 10;
// 如果给方法中的所有代码上锁,不如直接在方法上添加关键字synchronized,给方法上锁。
// 此时不需要特意指定锁对象,以锁对象是this
public synchronized void take(){
System.out.println("开始吃豆子");
//给部分代码上锁
// synchronized(lock){
if(BeanCount>0){
BeanCount--;
System.out.println("豆子被"+Thread.currentThread().getName()+"吃了一个,剩余"+BeanCount);
}
// }
}
@Override
public void run() {
while(BeanCount>0){
take();
}
}
}
4.3 单例模式的改进
懒汉式单例,在多线程的环境下,会出现问题。由于临界资源问题的存在,单例对象可能会被实例化多次。解决方案,就是将对象的 null值判断和实例化,放到一个同步代码段中执行。
测试代码:
import java.util.LinkedList;
import java.util.List;
public class _02StaticSynchronizedDemo {
public static void main(String[] args) {
// 创建一个集合对象,用来存储五个线程
List<Thread> pool = new LinkedList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(){
@Override
public void run() {
Boss b = Boss.getBoss();
}
};
pool.add(t);
}
for (Thread t : pool) {
t.start();
}
}
}
/*
单例设计模式的懒汉模式
1. 单例模式: 在整个项目中,获取的对象都是同一个。
2. 饿汉模式: 就是着急获取对象的那个唯一实例。
3. 懒汉模式: 就是不着急获取对象的那个唯一实例。
*/
class Boss{
// 提供一个私有的静态的该类类型的变量。
private static Boss boss;
// 第二步,将构造器初始化
private Boss(){
}
//第三步,提供一个公有的静态的返回该类型的方法
//给静态方法上锁,就是在方法上添加修饰词 synchronized
// 锁对象是类名.class, 这个类对象在整个项目下都是唯一的
public synchronized static Boss getBoss(){
// Boss.class 类对象
// synchronized (Boss.class){
if(boss == null){
boss = new Boss();
}
// }
return boss;
}
}
4.4 死锁
4.4.1 死锁的产生
死锁是指两个或多个线程无限期地等待对方持有的资源而导致的一种阻塞现象。 在 Java多线程编程中,死锁通常是由于多个线程在竞争资源时出现了相互等待的情况,导致所有线程都无法继续执行,从而产生死锁。
在深入了解死锁的成因之前,需要先了解死锁发生的必要条件,包括以下四个条件:
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
只有当这四个条件同时满足时,才可能发生死锁。如果其中任何一个条件不满足,就不会发生死锁。因此,避免死锁的关键是破坏这四个条件中的至少一个。
例如,可以通过使用同步机制来破坏请求和保持条件,避免进程在请求资源时阻塞并持有已获得的资源。同时,可以使用超时机制来破坏不剥夺条件,避免进程长时间持有资源而不释放。另外,可以使用资源分配图等工具来检测循环等待条件,从而避免出现循环等待的情况。
4.4.2 java多线程中死锁产生的原因
1)竞争同一把锁时发生死锁
如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁的时候,就会产生死锁。
2)多个锁的嵌套导致死锁
在 Java 中,如果多个线程在持有一个锁的情况下尝试获取另一个锁,并且在相互等待对方释放锁时,就会出现死锁。这种情况通常是由于多个线程在获取锁的顺序上存在差异,从而导致相互等待。
代码案例:
public class _01DeadLockDemo {
public static void main(String[] args) {
Thread t1 = new Thread("小明"){
public void run(){
synchronized("A"){
for(int i=0;i<50;i++){
System.out.println(getName()+":"+i);
}
synchronized("B"){
for(int i=50;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
}
};Thread t2 = new Thread("小红"){
public void run(){
synchronized("B"){
for(int i=0;i<50;i++){
System.out.println(getName()+":"+i);
}
synchronized("A"){
for(int i=50;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
}
};
t1.start();
t2.start();
}
}
代码解析:
死锁的产生之一:
线程1 先获取锁A,然后再想要获取锁B
线程2 先获取锁B,然后在想要获取锁A
两个线程都占用了对方想要的锁,而对方还占用并不释放。因此都出现了等待现象,无法继续向下执行,这就是死锁。
4.4.3 死锁的解决方案
1)避免使用多把锁
使用多把锁会增加死锁的概率,因此应该尽量避免使用多把锁。可以考虑使用更高级别的同步工具,例如信号量、读写锁、并发集合等。
2)避免嵌套锁
在持有一个锁的情况下,尽量避免获取其他锁,尤其是嵌套锁。如果确实需要嵌套锁,可以考虑使用线程本地变量或者其他的同步工具来避免死锁。
3)统一锁的获取顺序
在多线程中使用多把锁时,为了避免死锁,应该尽量保证所有线程获取锁的顺序是一致的。可以按照某种全局的规则来确定锁的获取顺序,例如按照对象的 hash 值来获取锁。
4)限制锁的持有时间
持有锁的时间越长,发生死锁的概率就越大。因此,可以考虑限制锁的持有时间,避免线程长时间持有锁而导致其他线程无法获取锁的情况。
5)超时等待锁
如果一个线程尝试获取锁时发现已经被其他线程占用,可以设置一个超时时间,超过这个时间就放弃获取锁。这样可以避免线程一直阻塞等待锁而导致死锁。
6)破除循环等待
6.1按顺序获取资源
按顺序获取资源是一种比较常见的破除循环等待的方法。如果所有的线程都按照固定的顺序获取资源,那么就不会出现循环等待的情况。
6.2 统一资源分配器
统一资源分配器是一种能够有效避免死锁的方法。在这种方法中,所有的资源都由一个统一的资源分配器来进行分配和释放,每个线程在需要资源时向资源分配器发出请求,资源分配器根据当前的情况来分配资源。这样就能够避免循环等待和其他死锁问题的发生。
7)检测死锁
可以定期检测系统中是否存在死锁,并且采取相应的措施来解决死锁问题。例如,可以使用 jstack 工具来查看死锁情况,或者使用死锁检测算法来自动检测死锁。
部分的测试(解决上述代码问题):
public class _02DeadLockDemo {
public static void main(String[] args) {
Thread t1 = new Thread("小明"){
public void run(){
synchronized("A"){
for(int i=0;i<50;i++){
System.out.println(getName()+":"+i);
}
synchronized("B"){
for(int i=50;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
}
};Thread t2 = new Thread("小红"){
public void run(){
synchronized("B"){
for(int i=0;i<50;i++){
System.out.println(getName()+":"+i);
}
try {
"B".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized("A"){
for(int i=50;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
}
};
t1.start();
t2.start();
}
}
代码解析:
避免死锁的方式之一:
1. 就是按照顺序加锁。将小红的A、B锁互换。
2. 设置超时等待。 设置了一定时间限制,如果在这个时间范围内没有获取到锁,那就不执行锁里的内容
4.5 锁的常用API
4.5.1 常用API
Object类中有几个方法如下 :
wait()
等待,让当前的线程,释放自己持有的指定的锁标记,进入到等待队列。
等待队列中的线程,不参与CPU时间片的争抢,也不参与锁标记的争抢。
notify()
通知、唤醒。唤醒等待队列中,一个等待这个锁标记的随机的线程。
被唤醒的线程,进入到锁池,开始争抢锁标记。
notifyAll()
通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。 。
被唤醒的线程,进入到锁池,开始争抢锁标记。
总结与锁有关的方法
1. wait(): 释放自己占有的锁对象,进入等待队列中,不参与锁的争抢,也不参与时间片的争抢。 也就是进入等待状态。
直到被其他线程notify或者notifyAll。 注意:是锁调用该方法。
wait(long timeout): 等待一个指定时间,如果超过这个时间,则自动唤醒。
wait(long timeout, int naous): 指定的等待时间更精确一些。
2. notify():
通知,唤醒等待队列中的某一个线程,是随机的。被唤醒的那个线程加入锁池,开始争抢锁对象。
3. notifyAll()
通知,唤醒等待队列中的所有线程。被唤醒的所有线程加入锁池,开始争抢锁对象。注意:
无论是wait()方法,还是notity()/notifyAll()方法,在使用的时候要注意,一定要是自己持有的锁标记,才可以做这个操作。否则会出现 IllegalMonitorStateException 异常。
测试代码:
public class _01LockApiDemo {
public static void main(String[] args) {
// 定义一个对象,充当锁
Object lock = new Object();
//定义一个下载线程
Thread download = new Thread(()->{
System.out.println("开始下载图片");
for (int i = 0; i < 101; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----------"+i+"%");
}
System.out.println("图片下载完成--*");
// 通知等待队列中的某一个线程
synchronized (lock) {
//如果想要调用锁的通知方法,那么耶必须先获取锁对象。
lock.notify();
}
},"下载图片");
Thread show = new Thread(()->{
// 该显示线程,刚要执行任务时,就应该进入等待队列,因为需要等待下载线程结束。
try {
synchronized (lock) {
// 如果想要进入等待队列,必须先获取锁对象,然后在调用锁对象的wait方法。
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当下载线程结束后,该线程得到通知了,然后才有机会获取锁对象,然后获取时间片,才能继续向下执行。
System.out.println("加载图片");
for (int i = 0; i < 101; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----------"+i+"%");
}
System.out.println("图片加载完成--*");
},"加载图片");
download.start();
show.start();
}
}
4.5.2 面试题 wait()和sleep()的区别
1)sleep()方法,在休眠时间结束后,会自动的被唤醒。而wait()进入到的阻塞态,需要被 notify/notifyAll手动唤醒。如果wait()的括号中有时间,时间结束也会自动进入就绪态。
2)wait()会释放自己持有的指定的锁标记,进入到阻塞态。 sleep()进入到阻塞态的时候,不会释放自己持有的锁标记。
4.6 ReentrantLock
4.6.1 可重入锁的简介
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中的一个可重入锁实现,它提供了比synchronized 关键字更灵活、功能更丰富的线程同步机制。ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync(非公平锁)与FairSync(公平锁)类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
1)可重入性
ReentrantLock 是一种可重入锁。这意味着持有锁的线程可以再次获取该锁,而不会发生死锁。每次成功获取锁都会增加锁的持有计数,相应的释放锁操作会减少计数。当计数降至零时,锁才会真正释放给其他等待的线程。这种特性使得在递归调用或嵌套同步块中使用同一线程多次获取同一锁成为可能。
2)公平性
ReentrantLock 默认实现的是非公平锁。同时,ReentrantLock 提供了公平锁和非公平锁两种模式。在构造时可以通过传递布尔参数指定:
公平锁(true):
按照线程请求锁的顺序进行排队,先请求的线程优先获得锁。公平锁倾向于减少线程饥饿现象,但可能降低系统的整体吞吐量。非公平锁(默认,false):
不保证按照线程请求锁的顺序分配锁,允许后来的线程“插队”获取锁。非公平锁在某些场景下可能提供更高的性能,但可能增加线程饥饿的风险。3)显式锁操作
与 synchronized 关键字不同,ReentrantLock 需要显式地调用方法来获取和释放锁:
lock():上锁方法,如果发现已经上锁,当前线程将被阻塞,直到获取锁
unlock():解锁方法。必须确保在持有锁的线程中正确调用此方法,否则可能导致死锁或其他同步问题。
tryLock():尝试非阻塞地获取锁。如果锁可用,立即返回 true;否则返回 false。
tryLock(long timeout, TimeUnit unit):尝试在指定时间内获取锁。如果在超时时间内锁不可用,返回 false。
4)条件变量(Condition)
ReentrantLock 还支持条件变量,通过 newCondition() 方法创建 Condition 对象。条件变量允许线程在满足特定条件时等待,直到其他线程通知它们条件已发生变化。与 Object 类的 wait()、notify() 和 notifyAll() 方法相比,条件变量提供了更精细的线程同步控制:
await():当前线程进入等待状态,释放锁,并在其他线程调用对应 Condition 对象的 signal() 或 signalAll() 方法时唤醒
signal():唤醒一个等待在该 Condition 上的线程,但不释放锁。
signalAll():唤醒所有等待在该 Condition 上的线程,但不释放锁。
4.6.2 案例演示
Reentrantlock: 是一个可以让一个线程多次获取锁对象的类型,里面内置了一个计数器,用于记录当前线程获取锁的次数,可以避免死锁情况。
该类提供了两个子类型:
非公平锁:多个线程获取锁的方式,不是按照线程的请求顺序,而是可能发生"插队"现象。这种锁,性能该,但是可能增加线程饥饿现象(某一个线程可能很久都获取不到锁)。
公平锁:多个线程获取锁的方式,是按照请求顺序获取。谁都能获取到锁。减少了线程饥饿现象,但是系统吞吐量可能不高(性能高的线程也需要排队才能获取到锁)。
如何使用两种锁:
构造器中传入true,表示使用公平锁,传入false或者不指定参数,使用的就是非公平锁。
该类比synchronized更加灵活,但是需要手动上锁和解锁。4方法:
lock(): 上锁方法,锁对象没被其他线程占用时,就会上锁成功,否则当前线程处于阻塞状态。
unlock(): 必须在占有锁的时候,才能进行解锁。否则报异常。
trylock : 尝试获取锁,如果获取不到,并不阻塞,而是执行其他的代码。
获取不到锁,返回false,获取到,返回true。
trylock(long time,TimeUnit unit):
可以指定一定时间内获取锁对象,如果超过这个时间,还没有获取到锁,就返回false,时间内获取到锁,返回true。
简单书写方式:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantlockDemo {
public static void main(String[] args) {
//使用两个线程模拟两个人来使用计数器。
MyCounter mycounter = new MyCounter("秒表");
Thread t1 = new Thread(mycounter,"夏末");
Thread t2 = new Thread(mycounter,"宵宫");
t1.start();
t2.start();
}
}
class MyCounter implements Runnable{
//先创建锁对象 构造器中可以传入true和false,来表示公平和非公平。默认是非公平锁
ReentrantLock rl = new ReentrantLock();
private int count =0;
private String name;
public MyCounter(String name) {
this.name = name;
}
@Override
public void run() {
//调用lock方法,进行上锁。
rl.lock();
for (int i = 0; i < 100; i++) {
count++;
System.out.println(Thread.currentThread().getName()+"使用了秒表进行计数:"+count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String str =null;
System.out.println(str.length());
//同步代码块执行完毕后,再解锁
rl.unlock();
}
}
标准书写方式:(带finally)
import java.util.concurrent.locks.ReentrantLock;
/*
可重入锁的标准写法: 释放锁操作在finally模块中
*/
public class ReentrantlockDemo02 {
public static void main(String[] args) {
//使用两个线程模拟两个人来使用计数器。
MyCounter mycounter = new MyCounter("秒表");
Thread t1 = new Thread(mycounter,"夏末");
Thread t2 = new Thread(mycounter,"宵宫");
t1.start();
t2.start();
}
}
class MyCounter01 implements Runnable{
//先创建锁对象 构造器中可以传入true和false,来表示公平和非公平。默认是非公平锁
ReentrantLock rl = new ReentrantLock();
private int count =0;
private String name;
public MyCounter01(String name) {
this.name = name;
}
@Override
public void run() {
//调用lock方法,进行上锁。
rl.lock();
for (int i = 0; i < 100; i++) {
count++;
System.out.println(Thread.currentThread().getName()+"使用了秒表进行计数:"+count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//在释放锁之前发生了异常,锁就无法释放了,别的线程想要获取锁对象,变成了不可能,所以都会处于阻塞状态
//因此为了不阻塞其他线程对锁的获取,那么当前线程不管是否有异常,那都应该正确的释放锁。
//所以锁的释放应该在try的finally模块儿中
try {
String str =null;
System.out.println(str.length());
} catch (Exception e) {
e.printStackTrace();
} finally {
//同步代码块执行完毕后,再解锁
rl.unlock();
}
}
}
方法测试:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TicketCenterDemo {
public static void main(String[] args) {
TicketCenter center = new TicketCenter(100);
Thread t1 = new Thread(()->{
center.buyOne();
},"宵宫");
Thread t2 = new Thread(()->{
center.buyOne();
},"刻晴");
Thread t3 = new Thread(()->{
center.buyBatch(10);
},"甘雨");
Thread t4 = new Thread(()->{
center.buyBatch(10);
},"凌华");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class TicketCenter{
private int ticketNum;
public TicketCenter(int ticketNum) {
this.ticketNum = ticketNum;
}
//获取一个锁对象
ReentrantLock rsl = new ReentrantLock();
//购买一张票
public void buyOne(){
// 尝试获取锁对象
boolean success = rsl.tryLock();
if(success){
System.out.println(Thread.currentThread().getName()+"当前票的数量是"+ticketNum);
if(ticketNum > 0){
try {
Thread.sleep(5000);//模拟购票过程所需的时间
ticketNum--;
System.out.println(Thread.currentThread().getName()+": 当前票的数量是"+ticketNum);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rsl.unlock();
}
}
}else {
System.out.println(Thread.currentThread().getName()+"没有获取到锁对象,买牛魔酬宾");
}
}
//批量购买高铁票
public void buyBatch(int num){
boolean success = false;
try {
success = rsl.tryLock(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(success){
System.out.println(Thread.currentThread().getName()+"当前票的数量是"+ticketNum);
if(ticketNum > 0 &&num < ticketNum){
try {
Thread.sleep(5000);//模拟购票过程所需的时间
ticketNum-=num;
System.out.println(Thread.currentThread().getName()+": 当前票的数量是"+ticketNum);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rsl.unlock();
}
}
}else {
System.out.println(Thread.currentThread().getName()+"没有获取到锁对象,买牛魔酬宾");
}
}
}
4.6.3 经典面试问题:
ReentrantLock和synchronized有什么不同
使用方法上面的区别
①ReentrantLock可以提供公平+非公平两种特性
当ReentrantLock构造方法中指定了参数为true的时候,这个锁被确定为公平锁。 而synchronized无法提供公平锁的特性。
②ReentrantLock的加锁、解锁操作都是需要手动进行
而synchronized的话可以进行自动的加锁、解锁操作。 synchronized可以有效避免加锁之后忘记解锁的情况。
当代码执行到synchronized修饰的代码块的时候,如果在同步代码块内部发生了异常,没有及时处理的话,会提前退出并且让线程释放锁。而ReentrantLock无法做到立刻解锁,因此,unLock()的解锁操作一定要在finally代码块当中,避免加锁之后忘记解锁的情况。
③synchronized无法提供lock.tryLock()这样的尝试获取锁的特性,而ReentrantLock可以提供。
线程如果在指定的时间之内无法获取到锁,或者锁已经被占用了,那么lock.tryLock()可以有效减少线程阻塞等待的情况,或者减少阻塞等待的时间。
而synchronized只会让无法获取到锁的线程"死等"。直到获取到锁的线程释放锁
④ReentrantLock可以提供中断式加锁。
ReentrantLock在调用lock.lockInterruptibly()时候,可以让获取不到锁,进入阻塞等待的线程被提前"唤醒",但是synchronized不可以。具体的操作已经在上面解释了。
5. 生产者消费者模式设计
5.1 什么是生产者消费者模型
生产者-消费者模型(Producer-Consumer problem)是一个非常经典的多线程并发协作的模型。
比如某个模块负责生产数据,而另一个模块负责处理数据。产生数据的模块就形象地被称为生产者;而处理数据的模块,则被称为消费者。
生产者和消费者在同一段时间内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
如图所示:
5.2 生产者消费者模式的优点
1、解耦
由于有缓冲区的存在,生产者和消费者之间不直接依赖,耦合度降低。
2、支持并发
由于生产者与消费者是两个独立的并发体,它们之间是通过缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区中拿数据接口,这样就不会因为彼此的处理速度而发生阻塞。(通过使用多个生产者和消费者线程,可以实现并发处理,提高系统的吞吐量和响应性)
3、支持忙闲不均
缓冲区还有另一个好处:当数据生产过快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等消费者处理掉其他数据时,再从缓存区中取数据来处理。(通过使用缓冲区可以平衡生产者与消费者之间的速度差异,以及处理能力的不匹配)
5.3 生产者消费者模式所遵循的规则
生产者仅仅在缓冲区未满时生产,缓冲区满则停止生产。
消费者仅仅在缓冲区有产品时才能消费,缓冲区为空则停止消费。
当消费者发现缓冲区没有可消费的产品时会通知生产者。
当生产者生产出可消费的产品时,应该通知等待的消费者去消费。
5.4 案例演示
生产者消费者设计模式:
1. 多线程并发的一种经典设计模式
2. 多个生产者——一个仓库——多个消费者
仓库里数据不足时,生产者生产数据,存储到仓库,通知消费者进行消费。
仓库里数据充足时,消费者消费仓库里的数据,不足时,通知生产者生产数据。
3. 这种模式的优点:
解耦: 消费者和生产者没有耦合。互不相干。
支持并发: 多个生产者和多个消费者可以并发运行。
支持闲忙不均: 因为有仓库,生产过快可以先存仓库中,让消费者慢慢的处理数据。
import java.util.LinkedList;
import java.util.List;
public class PCmodelDemo {
public static void main(String[] args) {
AppleShelf appleShelf = new AppleShelf(60);
Producer p1 = new Producer(appleShelf,20,"网易");
Producer p2 = new Producer(appleShelf,10,"腾讯");
Producer p3 = new Producer(appleShelf,30,"淘宝");
Producer p4 = new Producer(appleShelf,40,"拼多多");
Consumer c1 = new Consumer(appleShelf,10,"甘雨");
Consumer c2 = new Consumer(appleShelf,5,"凌华");
Consumer c3 = new Consumer(appleShelf,15,"宵宫");
Consumer c4 = new Consumer(appleShelf,20,"刻晴");
Consumer c5 = new Consumer(appleShelf,30,"妮露");
Consumer c7 = new Consumer(appleShelf,40,"纳西妲");
p1.start();
p2.start();
p3.start();
p4.start();
c1.start();
c2.start();
c3.start();
c4.start();
c5.start();
c7.start();
}
}
class Consumer extends Thread{
private AppleShelf appleShelf;
private int num;
private String name;
public Consumer(AppleShelf appleShelf, int num,String name) {
super(name);
this.appleShelf = appleShelf;
this.num = num;
}
public void takeApple(){
appleShelf.takeApple(num);
}
@Override
public void run() {
while(true){
takeApple();
}
}
}
class Producer extends Thread{
//要操作一个苹果架子
private AppleShelf appleShelf;
// 生产苹果的数量
private int num;
private String name;
public Producer(AppleShelf appleShelf, int num,String name) {
super(name);
this.appleShelf = appleShelf;
this.num = num;
}
// 生产者的生产代码
public void produce(){
appleShelf.storeApple(num);
}
//编写生产者的任务代码
@Override
public void run() {
while(true){
produce();
}
}
}
//设计一个苹果架子类型,用于储存苹果。
class AppleShelf{
//定义一个容器,用来储存苹果
List<Apple> apples;
//定义一个容量属性
private int capacity;
//定义一个对象,充当锁
Object lock = new Object();
public AppleShelf(int capacity) {
apples = new LinkedList<>();
this.capacity = capacity;
}
/**
* 向苹果架子上放入苹果,该方法让生产者来调用
* @param num
*/
public synchronized void storeApple(int num){
while(apples.size()+num > capacity){
try {
//释放锁,进入等待队列中。
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//可以生产了
for (int i = 0; i < num; i++) {
apples.add(new Apple());
}
System.out.println(Thread.currentThread().getName()+"生产了"+num+"个。剩余: "+apples.size());
//通知其他等待线程
notifyAll();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void takeApple(int num){
// 不能消费的条件
while(num>apples.size()){
// 通知生产者
notifyAll();
try {
//通知等待队列,因为再次被通知唤醒后,还有可能不满足消费的条件,因此下一步要执行条件判断
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < num; i++) {
apples.remove(apples.size()-1);
}
System.out.println(Thread.currentThread().getName()+"消费了"+num+"个。剩余:"+apples.size());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Apple{}
6. 线程池
6.1 线程池的简介
线程池,其实就是一个容器,里面存储了若干个线程。
使用线程池,最主要是解决“复用 “ 的问题。之前的处理方式中,当我们使用到一个线程的时候,需要进行实例化,当这个线程使用结束之后,对这个线程进行销毁。对于需求实现来说没有问题,但是频繁的开辟、销毁线程,其实对CPU来说,是一种负荷。所以要尽量的优化这一点。
可以使用复用机制解决这个问题。当我们需要使用到一个线程的时候,不是直接实例化,而是先去线程池中查找是否有闲置的线程可以使用。如果有,直接拿来用;如果没有,再实例化一个新的线程。并且,当这个线程使用结束之后,并不是马上销毁,而是将其放入线程池中,以便下次使用。
6.2 线程池的开辟
为什么要引入线程池?
因为多线程势必涉及到对象的频繁创建和销毁,这样非常消耗CPU的性能。
什么是线程池?
就是一个线程容器,用来存储固定几个线程对象,当我们想要做任务的时候,只需要在线程池中找到闲置的线程,来处理我们的任务。当任务处理完毕后,将线程归还给线程池,并不是销毁这个线程。这就避免了多线程对象的频繁创建和销毁。
在Java中,使用 ThreadPoolExecutor 类来描述线程池,在这个类的对象实例化的时候,有几个常见的参数:
参数 | 含义 |
---|---|
int corePoolSize | 核心线程的数量 |
int maximumPoolSize | 线程池最大容量(包含了核心线程和临时线程) |
long keepAliveTime | 临时线程可以空闲的时间 |
TimeUnit unit | 临时线程保持存活的时间单位 |
BlockingQueue<Runnable> workQueue | 任务等待队列 |
RejectedExecutionHandler handler | 拒绝访问策略 |
BlockingQueue 常用子类型:
ArrayBlockingQueue
LinkedBlockingQueue
SynchronouseQueue
RejectedExecutionHandler的策略参数:
ThreadPoolExecutor.AbortPolicy:
丢弃新的任务,并抛出异常 RejectedExecutionException
ThreadPoolExecutor.DiscardPolicy:
丢弃新的任务,但是不会抛出异常
ThreadPoolExecutor.DiscardOldestPolicy:
丢弃等待队列中最早的任务
ThreadPoolExecutor.CallerRunsPolicy:
不会开辟新的线程,由调用的线程来处理
代码演示:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class _01ThreadPoolDemo {
public static void main(String[] args) {
//使用标准方式创建一个线程池对象。
/*
int corePoolSize :用于指定核心线程数量
int maximumPoolSize : 用于指定线程池中最大的线程数量(核心线程+临时线程)
long keepAliveTime: 临时线程的闲置时间,超过该时间,临时线程销毁。
TimeUnit unit: 用于指定闲置时间的单位
BlockingQueue<Runnable> workQueue: 用于指定存储用户的任务的阻塞队列,是一个接口
常用子类型:
ArrayBlockingQueue
LinkedBlockingQueue
synchronousQueue
如何查看一个类的继承关系: 选中类名 ctrl + h
RejectedExecutionHandler handler : 用于指定线程池的拒绝访问策略。
AbortPolicy: 默认策略,丢弃新任务,抛异常
DiscardPolicy: 丢弃新任务 ,不抛异常
DiscardOldestPolicy: 丢弃最早的那一个任务
CallerRunsPolicy: 不开辟临时线程,等待核心线程来执行任务
前提条件 阻塞队列需要设置容量
1. 任务数量<= 核心线程数+阻塞队列的容量 这种情况,只有核心线程来处理任务
2. 任务数量> 核心线程数+阻塞队列的容量 并且 <= 线程池最大容量+阻塞队列的容量
这种情况,会启用临时线程
3. 如果任务数量>线程池最大容量+阻塞队列的容量,就会触发拒绝资源策略。
*/
ThreadPoolExecutor pool=new ThreadPoolExecutor(5,
10,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(5),
new ThreadPoolExecutor.AbortPolicy());
//添加任务
for (int i = 0; i < 15; i++) {
Runnable r =new Task("小"+i);
pool.execute(r);
}
// 注意:如果不想使用线程池了,需要调用shutdown方法,该方法是当线程池中的所有任务执行完后,会销毁线程池。
// pool.shutdown();
//立即销毁线程池。
pool.shutdownNow();
}
}
class Task implements Runnable{
String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i <10; i++) {
try {
Thread.sleep(520);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(name+":"+i);
}
}
}
6.3 线程池的工作原理
线程池中的所有线程,可以分为两部分:核心线程 和 临时线程。
核心线程:常驻于线程池中的线程,这些线程,不会被线程池销毁,只有当线程池需要被销毁的时候, 他们才会被关闭销毁。
临时线程:当遇到了临时的高密度的线程需求量,此时,就会临时的开辟一些线程,处理一些逻辑。当 这些线程处理完自己需要处理的任务后,就会闲置。当闲置了指定的时间之后,这个临时线程就会被销毁。
6.4 线程池的常用方法
execute(Runnable runnable) : 将任务提交给线程池,由线程池分配线程来完成。
shutdown(): 向线程池发送一个停止信号,这个操作并不会停止线程中的线程,而是在线程池中所有的任务都执行结束之后,结束线程和线程池。
shutdownNow(): 立即停止线程池中的所有的线程和线程池。
6.5 线程池的工具类
Executors类是一个用来获取线程池对象的工具类,实际应用中,大部分场景下,可以不用前面的构造 方法进行线程池的获取,而是用Executors工具类中的方法进行获取。
线程池工具类里的常用方法:
1. static ExecutorService newSingleThreadPool()
返回只有一个核心线程的线程池,里面的阻塞队列的容量没有指定,可以理解为int的最大值。没有临时线程。
2. static ExecutorService newFixedThreadPool(int nThread)
返回可以指定核心线程个数的线程池。里面的阻塞队列的容量也没有指定,可以理解为int的最大值。没有临时线程。
3. static ExecutorService newCachedThreadPool()
返回只有临时线程的线程池。临时线程的数量为int的最大值,闲置时间为1分钟。
4. static ScheduledExecutorService newScheduledThreadPool()
返回指定核心线程数量的线程池,临时线程的数量(int的最大值-核心数),临 时线程的闲置时间为0。
代码演示:
/*
java中的大部分场景下,线程池的获取,只需要使用线程池工具类即可。
*/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class _02ThreadExcutorsDemo {
public static void main(String[] args) {
/*
线程池工具类里的常用方法:
1. static ExecutorService newSingleThreadPool()
:返回只有一个核心线程的线程池,里面的阻塞队列的容量没有指定,可以理解为int的最大值。
没有临时线程
2. static ExecutorService newFixedThreadPool(int nThread)
:返回可以指定核心线程个数的线程池。里面的阻塞队列的容量也没有指定,可以理解为int的最大值。
没有临时线程
3. static ExecutorService newCachedThreadPool()
:返回只有临时线程的线程池。临时线程的数量为int的最大值,闲置时间为1分钟。
4. static ScheduledExecutorService newScheduledThreadPool()
: 返回指定核心线程数量的线程池,临时线程的数量(int的最大值-核心数),临时线程的闲置时间为0.
*/
//针对于newSingleThreadPool来说: 只有一个核心线程,任务是串行的,下面的案例会执行50秒
//ExecutorService service = Executors.newSingleThreadExecutor();
//针对于newFixedThreadPool来说: 由5个核心线程来接收任务,并发执行,10个任务分两批执行。执行10s
// ExecutorService service = Executors.newFixedThreadPool(5);
//针对于newCachedThreadPool来说: 由10个临时线程来接收任务,并发执行,执行5s
// ExecutorService service = Executors.newCachedThreadPool();
// for (int i = 0; i < 10; i++) {
// service.submit(()->{
// for (int j = 0; j < 10; j++) {
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName()+":Hello world");
// }
// });
// service.shutdown();
/*
调度线程池的方法功能很多,可以设置延迟时间。
注意ScheduledExecutorService是ExecutorService的子类型
schedule(
Runnable command,//任务long delay,
//延迟时间
TimeUnit unit
U延迟时间的单位
);
*/
Runnable runnable =()->{
for (int j = 0; j < 10; j++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":Hello world");
}
};
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 5; i++) {
scheduler.schedule(runnable,5, TimeUnit .SECONDS);
}
}
}