多线程
前置概念
多线程的发展史,起始就是一个压榨资源,提升效率的战斗史,促进它发展的根本动力其实在于各种资源(磁盘、内存、网络、CPU)的运行速度不平衡而造成的资源浪费。
站在地主老财的角度,如何让长工们给我种地赚钱 - 陈松
线程(thread)
概念
线程(thread):
- 是操作系统能够进行运算调度的最小单位。
- 它被包含在进程之中,是进程中的实际运作单位。
- 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 一个进程可以有很多线程,每条线程并行执行不同的任务。
- 在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB
。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)
描述。TCB
包括以下信息:
- 线程状态。
- 当线程不运行时,被保存的现场资源。
- 一组执行堆栈。
- 存放每个线程的局部变量主存区。
- 访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。线程
共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
举例论证—为什么要使用多线程?
我们的程序都是一条执行流,一步一步的执行。但其实这种程序对我们计算机的资源的使用上是低效的。例如:
- 我们有一个用于计算的程序,主程序计算数据
- 在计算的过程中每得到一个结果就需要将其保存到外部磁盘上
- 那么难道我们的主程序每次都要停止等待CPU将结果保存到磁盘之后,再继续完成计算工作吗?
- 要知道磁盘的速度可是巨慢的(相对内存而言)
- 我们如果能分一个线程去完成磁盘的写入工作,主线程还是继续计算的话,是不是效率更高了呢?
- 其实,并发就是这样的一种思想,使用时间片分发给各个线程CPU的使用时间
- 给人感觉好像程序在同时做多个事情一样
- 这样做的好处主要在于它能够对我们整个的计算机资源有一个充分的利用
- 在多个线程竞争计算机资源不冲突的前提下,充分的利用我们的资源。
多线程的出现解决了什么问题?
1.发挥多核CPU的优势
多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情,而不互相干扰。
2.防止阻塞
从程序运行效率的角度来看:单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,从而降低程序整体的效率。
但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了!比方说:
远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。
多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3.便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务A:
- 单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。
- 但是如果把这个大的任务A 分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
总结叙述
核心概念
- 线程是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如:主线程,gc线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行将由调度器安排调度,
- 调度器是与操作系统紧密相关的,先后顺序是不能人为的干预。
- 对于同一份资源操作时,会存在资源抢占的问题,需要加入并发控制
- 线程会带来额外的开销,例如:CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互时,内存控制不当会造成数据不一致。
通俗总结
通俗的描述就是
- 进程是计算机分配资源的一个基本单位,
- 每个进程中又包含至少一个main线程(不然就没有存在的意义了哈!)的线程集合。
- 可以理解进程是一个一般都会装了很多线程的容器
- 进程实际上是执行程序的一次执行过程,它是一个动态的概念
- 线程又是CPU调度和执行的单位
什么是多线程?
多线程并不是提升整体的工作效率
为什么要用?
•同步完成多项任务
•提高资源使用效率
•多线程和多进程
概述
进程与线程
生命周期
创建
阻塞
挂起
控制方向的反转
线程池中线程数 <= CPU核数 + 2的时候,无需缓存切换则效率最高。
JAVA的并发库
•java.util.concurrent.locks
•java.util.concurrent.atomic
•java.util.concurrent
•线程池ThreadPoolExecutor
•继承Thread类
•实现Runnable接口
多线程中的坑
多线程问题
- 多次写入同一内存
- 脏数据写
多核问题
- 数据缓存隔离
- 脏数据写
锁
经典案例的锁版本
强制杀死 kill -9 进程ID
这样会造成,在操作系统调度层面去杀死进程,也就是说,正常这样的杀死的进程,是不会走到程序清理的步骤。
- 共三个线程
- 一个线程在读的时候,另外一个线程也能读,此时写的线程正在堵塞等待
//import java.util.concurrent.locks.ReentrantLock;
ReentrantLock
显示买票案例
-
假设售票员,和你都是读线程(也就是你想要去买票的时候可以看到有多少票)
-
售票员也可以看到还有多少张票
-
写线程则是系统管理员线程,仅当售票员和你看完了还有多少张票,在你交完钱,售票员提交完购买订单后,才执行写的操作让票数- 1;
-
也就是读线程不涉及到更改数据,可保持数据的一致性一般无需加锁。而写线程需要保持原子性且在执行操作的时候数据更改需要使用ReentrantLock加锁。在读取票数执行的时候,则需要保证写线程阻塞。
任务
不追寻细腻化程度
多任务
多任务处理是指:
- 用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务.Linux、windows就是支持多任务的操作系统,比起单任务系统它的功能增强了许多。
当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一件任务提供服务。因为任务调度机制保证不同任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程。
看起来是多个任务都在做,本质上我们的大脑同一时间依旧只做了一件事情。
多线程
进程(Process)
进程(Process)
:
- 是计算机中的程序关于某数据集合上的一次运行活动,
- 是系统进行资源分配和调度的基本单位,
- 是**操作系统结构的基础**。
- 在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
在操作系统中运行的程序就是进程,比如你的QQ,播放器,游戏,IDE…
实际运行
创建线程
-
线程
是程序中执行的线程。Java虚拟机允许应用程序同时执行多个执行线程。每个线程都有优先权。
- 具有较高优先级的线程优先于优先级较低的线程执行。
- 每个线程可能也可能不会被标记为守护程序。
- 当在某个线程中运行的代码创建一个新的
Thread
对象时:- 新线程的优先级最初设置为等于创建线程的优先级,
- 并且当且仅当创建线程是守护进程时才是守护线程。
当Java虚拟机启动时
通常有一个非守护进程线程(通常调用某些指定类的名为
main
的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:- 已经调用了
Runtime
类的exit
方法,并且安全管理器已经允许进行退出操作。 - 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到
run
方法还是抛出超出run
方法的run
创建一个新的执行线程有两种方法。 一个是将一个类声明为
Thread
的子类。 这个子类应该重写run
类的方法Thread
。 然后可以分配并启动子类的实例。
有三种创建线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable
和 Callable
接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。
实现 Runnable 接口
需要实现接口中的 run() 方法。
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
- 使用 Runnable 实例再创建一个 Thread 实例,
- 然后调用 Thread 实例的 start() 方法来启动线程。
public static void main(String[] args) {
//1.实例化类对象
MyRunnable instance = new MyRunnable();
//2.将类对象放入到thread对象中
Thread thread = new Thread(instance);
//3.通过thread对象调用线程的start方法
thread.start();
}
//简洁点
Thread thread = new Thread(instance).start();
总结:必须要new一个thread实例,且将类对象放入才可以调用线程方法
实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
/**1.注意实现Callable接口是可以有返回值的
* 2.可以指定使用Callable后面+泛型类指定返回类型
* 3.不指定返回为Object类型
*/
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
继承 Thread 类
创建方法
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable
接口。
public
//implements Runnable
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
当调用 start()
方法启动一个线程时:
- 虚拟机会将该线程放入就绪队列中等待被调度,
- 当一个线程被调度时会执行该线程的 run() 方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
实现思路总结
1.
Runnable
实现思路
- 将实现
Runnable
接口的类对象丢到Tread中 - 再
.start()
public class RunnableTest1 implements Runnable {
@Override
public void run() {
System.out.println("线程启动方法");
}
public static void main(String[] args) {
//1.类对象
RunnableTest1 runnableTest1 = new RunnableTest1();
//2.new Thread对象放入类对象,并启动strat()
new Thread(runnableTest1, "小明").start();
}
}
2.
Thread
实现思路
- 继承Thread类
- new类对象
- 直接调用
start()
方法
public class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("线程执行方法");
}
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
myThread1.start();
}
}
注:在IDEA中选择快捷键Alt + insert 实现run方法
3.
Callable
实现思路(了解)
- 实现Callable接口
- 注意带有泛型<给一个返回类型(包装类)>,且返此泛型中的类型与call()方法返回类型保持一致
- 创建main方法,并new一个类对象,在main方法中执行如下操作
- 创建执行服务(创建线程池)
- 提交执行(与run()效果类似)
- 获取结果
- 关闭服务
public class CallableTest1 implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行方法!";
}
public static void main(String[] args) throws Exception{
CallableTest1 t1 = new CallableTest1();
CallableTest1 t2 = new CallableTest1();
CallableTest1 t3 = new CallableTest1();
//1.创建执行服务(创建线程池):
ExecutorService ser = Executors.newFixedThreadPool(3);
//2.提交执行(与run()效果类似)
Future<String> result1 = ser.submit(t1);
Future<String> result2 = ser.submit(t2);
Future<String> result3 = ser.submit(t3);
//3.获取结果
String r1 = result1.get();
String r2 = result2.get();
String r3 = result3.get();
//4.关闭服务
ser.shutdownNow();
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
}
}
注:callable
实现的好处
- 可以定义返回值
- 可以抛出对应异常
但过程稍微复杂了一些。
简单应用
下载网络图片
- 引入相关jar包
- 写出下载器
WebDownloader.class
,并写出下载方法downloader()
- 创建线程(继承
Thread
类创建) - 重新
run()
方法—线程运行体 - 定义变量(url,name),并创建全参方法
- 创建main()主程序入口,实例化总class对象
- 调用线程对象的
start()
方法,一起启动执行
1.引入jar包(最好在maven项目中直接引入jar包)
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
2.下载器
//下载器:下载图片线程的执行体
class WebDownloader {
//下载方法
public void downloader(String url, String name){
try {
//把url图片下载下来转为一个文件
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IOError,downloader()-error!");
}
}
}
3.声明变量,并定义全参构造方法
//请求下载图片的路径
private String url;
//图片下载后保存文件名
private String name;
//全参
public TestTread2(String url,String name){
this.url = url;
this.name = name;
}
4.继承Thread,实现线程执行体run()
//线程执行体
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为" + name + "的文件!");
}
5.在main方法中启用多个线程
public static void main(String[] args) {
TestTread2 t1 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程1.jpg");
TestTread2 t2 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程2.jpg");
TestTread2 t3 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程3.jpg");
//启动线程
t1.start();
t2.start();
t3.start();
}
6.总体代码
package com.hao.Log4j2.controller;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @Author LH
* @Date 2022/6/29 9:34
* @TODO:练习Thread,实现多线程同步下载图片
* @Thinking:
*/
public class TestTread2 extends Thread{
//请求下载图片的路径
private String url;
//图片下载后保存文件名
private String name;
//全参
public TestTread2(String url,String name){
this.url = url;
this.name = name;
}
//线程执行体
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("下载了文件名为" + name + "的文件!");
}
//下载器:下载图片线程的执行体
class WebDownloader {
//下载方法
public void downloader(String url, String name){
try {
//把url图片下载下来转为一个文件
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IOError,downloader()-error!");
}
}
}
//main()启动三个线程
public static void main(String[] args) {
TestTread2 t1 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程1.jpg");
TestTread2 t2 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程2.jpg");
TestTread2 t3 = new TestTread2("https://www.baidu/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程3.jpg");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
下载了文件名为线程2.jpg的文件!
下载了文件名为线程3.jpg的文件!
下载了文件名为线程1.jpg的文件!
输出执行顺序不一——>说明不是顺序而是并发执行
总结:通过start()方法多线程同时执行
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
执行线程
run() and start()
普通方法调用和多线程
普通方法调用和多线程
/**
* 1.继承Thread类创建线程
* 2.实例化当前类对象调用run()或start()进行对比
*/
public class RunAndStartThread1 extends Thread{
//1.重写run方法
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("我正在看代码---" + i);
}
}
//main主线程
public static void main(String[] args){
//2.创建一个线程对象
RunAndStartThread1 thread1 = new RunAndStartThread1();
//3.调用run() 或 start()方法开启线程
thread1.run();
//main()线程运行内容
for (int i = 0; i < 2000; i++) {
System.out.println("我正在学习多线程---" + i);
}
/**输出
*我正在学习多线程---383
*我正在看代码---0
*我正在学习多线程---384
*/
}
}
总结:
1.执行run()方法开启线程:两线程之间按照顺序依次执行
2.执行start()方法执行得出结论:主和创两线程交替执行
3.线程不一定立即执行,CPU安排调度(基本上每次结果都不一样)!
线程方法
方法汇总
activeCount() | 返回当前线程的线程组中活动线程的数目。 | int |
---|---|---|
checkAccess() | 判定当前运行的线程是否有权修改该线程。 | |
currentThread() | 返回对当前正在执行的线程对象的引用。 | |
dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 | |
enumerate(Thread[] tarray) | 将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。 | |
getAllStackTraces() | 返回所有活动线程的堆栈跟踪的一个映射。 | |
getContextClassLoader() | 返回该线程的上下文 ClassLoader。 | |
getDefaultUncaughtExceptionHandler() | 返回线程由于未捕获到异常而突然终止时调用的默认处理程序。 | |
getId() | 返回该线程的标识符。 | long |
getName() | 返回该线程的名称。 | String |
getPriority() | 返回线程的优先级。 | int |
getStackTrace() | 返回一个表示该线程堆栈转储的堆栈跟踪元素数组。 | |
getState() | 返回该线程状态 | |
getThreadGroup() | 返回该线程所属的线程组。 | |
getUncaughtExceptionHandler() | 返回该线程由于未捕获到异常而突然终止时调用的处理程序。 | |
holdsLock(Object obj) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true 。 | |
interrupt() | 中断线程。 | |
interrupted() | 测试当前线程是否已经中断。 | |
isAlive() | 测试线程是否处于活动状态。 | |
isDaemon() | 测试该线程是否为守护线程。 | |
isInterrupted() | 测试线程是否已经中断。 | |
join() join(long millis) [join](…/…/java/lang/Thread.html#join(long, int))(long millis, int nanos) | 等待该线程终止。 等待该线程终止的时间最长为 millis 毫秒。 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 | |
run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 | |
setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 | |
setName(String name) | 改变线程名称,使之与参数 name 相同。 | |
setPriority(int newPriority) | 更改线程的优先级。 | |
sleep(long millis) [sleep](…/…/java/lang/Thread.html#sleep(long, int))(long millis, int nanos) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 | |
start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 | |
toString() | 返回该线程的字符串表示形式,包括线程名称、优先级和线程组。 | |
yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
常用方法
线程停止
思路
- 不建议使用JDK提供的stop()、destroy()方法【已废弃】
- 推荐线程自己停止下来【更安全】
- 建议使用一个标志位进行终止变量
- 也就是当flag = false,则终止线程运行
实现代码
public class ThreadStop1 implements Runnable{
//1.线程中定义线程使用的标识——是否停止线程
private boolean flag = true;
//2.线程停止方法
public void stopTest(){
this.flag = false;
}
@Override
public void run() {
//3.线程体使用该标识
while (flag){
System.out.println("run... Thread");
}
}
public static void main(String[] args) {
ThreadStop1 threadStop1 = new ThreadStop1();
new Thread(threadStop1,"小红").start();
for (int i = 0; i < 1000; i++) {
System.out.println("mian线程--->" + i);
//当i = 900时让线程停止
if(i == 900){
threadStop1.stopTest();
System.out.println("小红线程停止");
}
}
}
}
//当i = 900时,小红线程停止
run... Thread
小红线程停止
mian线程--->901
1.setPriority()
setPriority(int newPriority) | 更改线程的优先级。 |
---|---|
2.sleep()
sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
---|---|
[sleep](…/…/java/lang/Thread.html#sleep(long, int))(long millis, int nanos) | 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
- sleep(时间)指定当前线程阻塞的毫秒数
1s = 1000millis
- sleep存在异常
interruptedException
- sleep时间到达到后线程进入
就绪状态
- sleep可以模拟网络时延、倒计时等
- 每个对象都有一个锁,但sleep不会释放锁!
模拟倒计时
public class ThreadSleep2 implements Runnable{
//模拟倒计时
@Override
public void run() {
int num = 10;
while (num > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSleep2 threadSleep2 = new ThreadSleep2();
new Thread(threadSleep2,"小明").start();
}
}
//输出(打印间隔1s)
10
9
8
7
6
5
4
3
2
1
3.join()
也称为插队方法(强制执行)
join() | 等待该线程终止。 |
---|---|
join(long millis) | 等待该线程终止的时间最长为 millis 毫秒。 |
[join](…/…/java/lang/Thread.html#join(long, int))(long millis, int nanos) | 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 |
- Join合并线程
- 待此线程执行完成后
- 再执行其它线程,此时其它线程阻塞
插队示例代码
public class ThreadJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程的VIP来了——>" + i + "次");
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
Thread thread = new Thread(threadJoin, "线程的VIP");
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main线程" + i);
if(i == 800){
thread.join();
}
}
}
}
//当i=800时,vip线程插队执行完毕后再执行main线程
main线程800
线程的VIP来了——>544次
4.isAlive()
isAlive() | 测试线程是否处于活动状态。 |
---|---|
5.yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
也称之为线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,礼让不一定成功哦!具体得看CPU!
礼让案例实现代码
public class ThreadYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "线程完成执行");
}
public static void main(String[] args) {
ThreadYield threadYield = new ThreadYield();
new Thread(threadYield,"a").start();
new Thread(threadYield,"b").start();
}
}
//礼让成功
a线程开始执行
b线程开始执行
a线程完成执行
b线程完成执行
//礼让失败
a线程开始执行
a线程完成执行
b线程开始执行
b线程完成执行
为什么会出现礼让失败的情况呢?
- 当a礼让出来之后,还是由CPU分配时间片
- 也就是说,还是处于一个同等竞争的关系
- 当CPU分配时间片还是分给了a,那么则礼让失败!
静态代理
静态代理模式
结婚案例实现思路
- 定义结婚接口
- 定义你和婚庆公司两个class对象
- 同时实现结婚接口
- 但是都是站在各自的角度来看不同角色会为结婚这件事做那些方法实现
- 在main方法中,new一个你的需求传入给婚庆公司对象
- 然后婚庆公司做他该做的,你做你要做的
- 完成结婚
public class StaticProxyTest {
public static void main(String[] args) {
//结婚很忙,要new一个新的你去找婚庆公司,你还得忙其它事情!
WeddingCompany weddingCompany = new WeddingCompany(new You());
//婚庆公司干活了
weddingCompany.HappyMarry();
/**
* 输出如下:
* 结婚之前:布置现场!
* 结婚了,你很开心!
* 结婚之后:收婚庆布置尾款!
*/
}
}
/**
* 结婚接口
*/
interface Marry{
void HappyMarry();
}
/**
* 真是角色:你去结婚
*/
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("结婚了,你很开心!");
}
}
/**
* 代理角色:帮助你结婚
*/
class WeddingCompany implements Marry{
private Marry target;
public WeddingCompany(Marry target){
this.target = target;
}
//实现结婚这件事情婚庆公司会帮你做以下这些事情
@Override
public void HappyMarry() {
//结婚之前
before();
this.target.HappyMarry();
//结婚之后
after();
}
private void before() {
System.out.println("结婚之前:布置现场!");
}
private void after() {
System.out.println("结婚之后:收婚庆布置尾款!");
}
}
静态代理模式总结
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实角色
好处:
- 代理对象可以做很多真实对象做不了的事情
- 真实对象可以专注于做自己的事情!
并发
Java 并发相关知识体系详解,包含理论基础,线程基础,synchronized
,volatile
,final
关键字, JUC
框架等内容
线程的状态*
状态观察代码
public class ThreadState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("--------");
});
//观察状态
Thread.State state = thread.getState();
System.out.println(state);//NEW
//观察启动后状态
thread.start();//启动线程
state = thread.getState();
System.out.println(state);//Run
//若线程不处于终止状态则一直执行
while (state != Thread.State.TERMINATED){
Thread.sleep(1000);
//更新线程状态
state = thread.getState();
//输出状态
System.out.println(state);
}
}
}
//创建
NEW
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
--------
TERMINATED
状态说明
线程状态。线程可以处于下列状态之一:
NEW
至今尚未启动的线程处于这种状态。RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERMINATED
已退出的线程处于这种状态。
在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
注!
线程中断或者结束,一旦进去死亡状态就无法再次启用!
中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
示例代码
对于以下代码:
- 在 main() 中启动一个线程之后再中断它
- 由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException
- 从而提前结束线程,不执行之后的语句。
public class ThreadInterrupt {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run,State = " + thread1.getState());
}
}
Main run,State = RUNNABLE
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Multithreading.ThreadMethod.ThreadInterrupt$MyThread1.run(ThreadInterrupt.java:15)
interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
示例代码
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
public class ThreadInterrupted {
private static class MyThread2 extends Thread {
@Override
public void run() {
//如果线程未中断则一直执行
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
//线程中断
thread2.interrupt();
}
}
Thread end
Executor 的中断
调用 Executor 的 shutdown()
方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow()
方法,则相当于调用每个线程的 interrupt()
方法。
示例代码
以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
shutdownNow()
public class ExecutorShutdown {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//调用 Executor 的 `shutdown()` 方法会等待线程都执行完毕之后再关闭
executorService.shutdownNow();
System.out.println("Main run");
}
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Multithreading.ThreadMethod.ExecutorShutdown.lambda$main$0(ExecutorShutdown.java:19)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
一般Executor中会存在多个线程,那么想要中断其中一个怎么办呢?
如果只想中断 Executor 中的一个线程,可以通过使用 submit()
方法来提交一个线程,它会返回一个 Future<?>
对象,通过调用该对象的 cancel(true)
方法就可以中断线程。
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
/**
* A {@code Future} represents the result of an asynchronous
* computation. Methods are provided to check if the computation is
* complete, to wait for its completion, and to retrieve the result of
* the computation. The result can only be retrieved using method
* {@code get} when the computation has completed, blocking if
* necessary until it is ready. Cancellation is performed by the
* {@code cancel} method. Additional methods are provided to
* determine if the task completed normally or was cancelled. Once a
* computation has completed, the computation cannot be cancelled.
* If you would like to use a {@code Future} for the sake
* of cancellability but not provide a usable result, you can
* declare types of the form {@code Future<?>} and
* return {@code null} as a result of the underlying task.
*
* <p>
* <b>Sample Usage</b> (Note that the following classes are all
* made-up.)
* <pre> {@code
* interface ArchiveSearcher { String search(String target); }
* class App {
* ExecutorService executor = ...
* ArchiveSearcher searcher = ...
* void showSearch(final String target)
* throws InterruptedException {
* Future<String> future
* = executor.submit(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* displayOtherThings(); // do other things while searching
* try {
* displayText(future.get()); // use future
* } catch (ExecutionException ex) { cleanup(); return; }
* }
* }}</pre>
*
* The {@link FutureTask} class is an implementation of {@code Future} that
* implements {@code Runnable}, and so may be executed by an {@code Executor}.
* For example, the above construction with {@code submit} could be replaced by:
* <pre> {@code
* FutureTask<String> future =
* new FutureTask<String>(new Callable<String>() {
* public String call() {
* return searcher.search(target);
* }});
* executor.execute(future);}</pre>
*
* <p>Memory consistency effects: Actions taken by the asynchronous computation
* <a href="package-summary.html#MemoryVisibility"> <i>happen-before</i></a>
* actions following the corresponding {@code Future.get()} in another thread.
*
* @see FutureTask
* @see Executor
* @since 1.5
* @author Doug Lea
* @param <V> The result type returned by this Future's {@code get} method
*/
public interface Future<V> {
/**
* Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, has already been cancelled,
* or could not be cancelled for some other reason. If successful,
* and this task has not started when {@code cancel} is called,
* this task should never run. If the task has already started,
* then the {@code mayInterruptIfRunning} parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.
*
* <p>After this method returns, subsequent calls to {@link #isDone} will
* always return {@code true}. Subsequent calls to {@link #isCancelled}
* will always return {@code true} if this method returned {@code true}.
*
* @param mayInterruptIfRunning {@code true} if the thread executing this
* task should be interrupted; otherwise, in-progress tasks are allowed
* to complete
* @return {@code false} if the task could not be cancelled,
* typically because it has already completed normally;
* {@code true} otherwise
*/
boolean cancel(boolean mayInterruptIfRunning);
线程的优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程
- 线程调度器按照优先级决定应该调度哪个程序来执行
- 线程的优先级用数字表示:范围从1-10
- Thread中的默认优先级常量
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
- 使用以下方式改变或获取优先级
getPriority().setPriority(int XXX)
/**
* Changes the priority of this thread.
* <p>
* First the <code>checkAccess</code> method of this thread is called
* with no arguments. This may result in throwing a
* <code>SecurityException</code>.
* <p>
* Otherwise, the priority of this thread is set to the smaller of
* the specified <code>newPriority</code> and the maximum permitted
* priority of the thread's thread group.
*
* @param newPriority priority to set this thread to
* @exception IllegalArgumentException If the priority is not in the
* range <code>MIN_PRIORITY</code> to
* <code>MAX_PRIORITY</code>.
* @exception SecurityException if the current thread cannot modify
* this thread.
* @see #getPriority
* @see #checkAccess()
* @see #getThreadGroup()
* @see #MAX_PRIORITY
* @see #MIN_PRIORITY
* @see ThreadGroup#getMaxPriority()
*/
public final void setPriority(int newPriority) {
ThreadGroup g;
//校验是否安全
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
注:建议优先级的设定在start()调度前!
-
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了。这都是看CPU的调度!
-
一般不会出现“性能倒置的问题”。
-
默认一般线程的优先级都是5,公平竞争!
-
由相关源码可得,设置优先级数值的范围必须是[1,10],否则执行出错
-
Exception in thread "main" java.lang.IllegalArgumentException at java.lang.Thread.setPriority(Thread.java:1089)
-
-
得出想要
示例代码
/**
* @Project_Name alorithms
* @Author LH
* @Date 2022/8/22 11:24
* @TODO:测试线程优先级
* @Thinking:
*/
public class PriorityTest1 extends Thread{
public static void main(String[] args) {
//main线程优先级
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority,"t1");
Thread t2 = new Thread(myPriority,"t2");
Thread t3 = new Thread(myPriority,"t3");
Thread t4 = new Thread(myPriority,"t4");
Thread t5 = new Thread(myPriority,"t5");
//先设置优先级,再启动
//默认的
t1.start();
// //设置线程优先级为-1,超出范围
// t2.setPriority(-1);
// t2.start();
//MIN_PRIORITY = 1
t3.setPriority(Thread.MIN_PRIORITY);
t3.start();
//NORM_PRIORITY = 5
t4.setPriority(Thread.NORM_PRIORITY);
t4.start();
//MAX_PRIORITY = 10
t5.setPriority(Thread.MAX_PRIORITY);
t5.start();
/**注意:并不是设置了优先级高的线程就一定会先执行,具体得看CPU调度情况
* 只是相对来说优先级高的优先执行的频率会比较高!
*/
}
static class MyPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
}
}
}
/**
* main--->5
* t5--->10
* t1--->5
* t4--->5
* t2--->1
* t3--->1
*
* 也就是说,也可以这样
* main--->5
* t1--->5
* t4--->5
* t5--->10
* t3--->1
*/
设置线程的优先级,当顺序执行的时候,设置优先级感觉效果不大,可以解释说它收到CPU调度效果特别强。但是在执行的run()方法中sleep一下,效果优先级顺序又明显一些,这是为什么呢?
守护线程(Daemon)
- 线程分为
用户线程
和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如:后台记录操作日志,监控内存,垃圾回收等待。
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main() 属于非守护线程。
在线程启动之前使用 setDaemon()
方法可以将一个线程设置为守护线程。
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
//设当前thread为守护线程
thread.setDaemon(true);
}
实例代码
/**
* @Project_Name alorithms
* @Author LH
* @Date 2022/8/22 18:57
* @TODO:守护线程
* @Thinking: 案例:上帝守护着你
*/
public class DaemonTest {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
//默认false表示用户线程,正常的线程都是用户线程。通过此方法设置线程为守护线程
thread.setDaemon(true);
//上帝守护线程启动
thread.start();
//你,用户线程启动
new Thread(you).start();
}
}
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("上帝永远保护你!");
}
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你一生都开心的活着!");
}
System.out.println("==========goodbye,world===========");
}
}
/** 输出
* 输出goodbye,world表示用户线程终止
* 此时守护线程将继续执行一段JVM用于停止所消耗的时间。
* ==========goodbye,world===========
* 上帝永远保护你!
* 上帝永远保护你!
* 上帝永远保护你!
* 上帝永远保护你!
* 上帝永远保护你!
*/
基础线程机制
Executor
Executor
管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor
:
CachedThreadPool
:一个任务创建一个线程;FixedThreadPool
:所有任务只能使用固定大小的线程;SingleThreadExecutor
:相当于大小为 1 的FixedThreadPool
。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
线程同步
多个线程操作同一个资源
场景
在处理多线程问题时:多个线程访问同一个对象
- 也就是:同一个资源,多个人都想使用!
- 并且某些线程还想修改这个对象 这时候就需要**线程同步**
概念
线程同步其实就是一种等待机制
- 多个需要同时访问此对象的线程进入这个**对象的等待池**形成队列
- 等待前面线程使用完毕,下一个线程再使用
- 也就是让线程排队
队列和锁
-
每个对象都拥有一把锁
-
利用队列和锁,来解决线程的安全性问题
-
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制
锁
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized
,而另一个是 JDK 实现的 ReentrantLock
。
synchronized
,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可。存在如下问题:- 一个线程持有锁,会导致其它所有需要此锁的线程挂起
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致==优先级倒置==—引起性能问题!
synchronized
同步代码块
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
public void func() {
synchronized (this) {
// ...
}
}
同步时
对于以下代码,使用 ExecutorService
执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
public class SynchronizedExample {
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
交叉时
对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
同步方法
它和同步代码块一样,作用于同一个对象。
public synchronized void func () {
// ...
}
同步类
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
示例
public class SynchronizedExample {
public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
同步静态方法
作用于整个类。
public synchronized static void fun() {
// ...
}
打印问题
先打印短信方法还是邮件方法?
罗列使用synchronized
锁的八种情况
1.标准访问,打印顺序?
AA---> sendSMS
BB---> sendEmail
(一个类对象启动两个线程分别执行上述方法)
2.停4s在短信方法中,打印顺序?
AA---> sendSMS
BB---> sendEmail
(一个类对象启动两个线程分别执行上述方法)
1和2结果相同是因为synchronized
锁的是当前对象(一个对象调用两个加锁的方法)
3.新增普通的hello()方法,打印顺序?
BB---> getHello
AA---> sendSMS
(hello方法并未加上synchronized关键字)
4.两部手机情况,打印顺序?
BB---> sendEmail
AA---> sendSMS
(AA和BB为两个不同类对象启动的线程)
3和4是因为把hello()方法未加锁,当sendSMS执行的时候等待了4s,先执行了hello(),此方法未上锁。
5.两个静态同步方法,1部手机,打印顺序?
AA---> sendSMS
BB---> sendEmail
(此时上述为两个静态方法,一个Phone类对象)
6.两个静态同步方法,2部手机,打印顺序?
AA---> sendSMS
BB---> sendEmail
(上述两个静态方法,两个Phone类对象分别调用线程,调用方法)
两个上锁的静态方法,1个对象调用和2个对象分别调用。static + synchronized
锁的是当前两个方法存在的类的class
7.一个静态同步方法,一个普通同步方法,一部手机,打印顺序?
BB---> sendEmail
AA---> sendSMS
(上述1个静态方法,1个普通方法,一个Phone类对象分别调用线程,调用方法)
8.一个静态同步方法,一个普通同步方法,两部手机,打印顺序?
BB---> sendEmail
AA---> sendSMS
(上述1个静态方法sendSMS(),一个普通方法,两个Phone类对象分别调用线程,调用方法)
sendEmail()中锁大class,sendSMS()中锁当前this
总结:
- synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
- 具体如下3种形式:
- 对于普通同步方法:锁的是当前实例对象(锁this)
- 对应静态方法:锁的是当前类的class对象(锁资源类class)
- 对于同步方法块:锁的是synchronize括号里配置的对象。
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
公平锁与非公平锁
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
上述源码阐述了==ReentrantLock
锁,可以是公平的锁,也可以是不公平的锁==
简单的来说:
非公平锁(能者多劳)–>默认:也就是旱的旱死,涝的涝死!
公平锁(阳光普照):我吃肉,你也可以喝汤!
1.非公平锁进来直接操作!
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2.公平锁,进来先判断这里是否有人,有人就排队,没人就执行
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判断是否有排队的前辈无则执行
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//有则排队
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
比较
synchronized
与lock
synchronized
lock
Lock
是一个接口,而synchronized
是Java中的关键字,synchronized
是内置的语言实现(JVM实现)- 发生异常时:
synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;lock
在发生异常时,如果没有主动通过unlock()
方法去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally块
中释放锁;
- 响应中断:
Lock
可以让等待的线程响应中断synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;
- 知道有没有成功获取锁:
- 通过
Lock
可以知道有没有成功获取锁 - 而
synchronized
却无法办到
- 通过
Lock
可以提高多个线程进行读操作的效率。
在性能上来说:
- 如果竞争不激烈,两者的性能是差不多的
- 而当竞争资源非常激烈时(有大量线程同时竞争),此时
Lock
的性能要远远优于synchronized
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
线程协作
概念
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。例如join()等方法
wait()
notify()
notifyAll()
调用 wait()
使得线程等待某个条件满足:
- 线程在等待时会被挂起
- 当其他线程的运行使得这个条件满足时
- 其它线程会调用
notify()
或者notifyAll()
来唤醒挂起的线程。
wait()
唤醒正在等待改对象监视器的单个线程。如果有任何线程在等待这个对象,其中一个线程将被唤醒。这一选择是任意的,并且发生在实现的自由裁量权上。线程通过调用一个等待方法来等待对象的监视器。
在当前线程放弃该对象上的锁之前,被唤醒的线程将无法继续运行。被唤醒的线程将以通常的方式与任何其它可能正在竞争同步这个对象的线程竞争;
注意事项
wait()
notify()
notifyAll()
它们都属于 Object
的一部分,而不属于 Thread
。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException
。
使用 wait()
挂起期间:
- 线程会释放锁。
- 这是因为,如果没有释放锁
- 那么其它线程就无法进入对象的同步方法或者同步控制块中
- 那么就无法执行
notify()
或者notifyAll()
来唤醒挂起的线程 - 造成死锁。
wait() 和 sleep() 的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
await()
signal()
signalAll()
java.util.concurrent
类库中提供了 Condition
类来实现线程之间的协调,可以在 Condition
上调用 await()
方法使线程等待,其它线程调用 signal() 或 signalAll()
方法唤醒等待的线程。
相比于 wait()
这种等待方式,await()
可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。
示例代码
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
before
after
死锁...
线程池*
线程池是一种多线程处理形式:
- 处理过程中将任务添加到队列
- 然后在创建线程后自动启动这些任务。
线程池线程都是后台线程:
- 每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 如果某个线程在托管代码中空闲(如正在等待某个事件)
- 则线程池将插入另一个辅助线程来使所有处理器保持繁忙
- 如果所有线程池的线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程
- 但线程的数目永远不会超过最大值
- 超过最大值的线程可以排队,但要等到其它线程完成后才启动
线程池本质上是一种池化技术
池化技术是一种资源复用的思想
那么本身参考控制线程创建数量有什么作用呢?
可以避免无休止的创建线程带来的资源利用率过高的问题!
所以能起到资源保护的作用!
常见池化技术
- 连接池
- 内存池
- 对象池
而线程池中复用的是线程资源
因为线程创建会涉及到CPU的上下文切换,以及分配内存这样的一些工作
线程复用技术
线程本身并不是一个受控的。
也就是说:线程本身的生命周期是由任务的运行状态来决定的,无法实现人为控制,
那么为了实现线程的复用,线程池又做了什么呢?
线程池里面用到了**阻塞队列**
- 线程池里面的工作线程处于一直运行状态
- 他会去从阻塞队列中获取待执行的任务
- 一旦队列空了,那么这个工作线程就会被阻塞
- 直到下一个次有新的任务进来
如java.util.concurrent.ThreadPoolExecutor
中的阻塞队列workQueue
/**
* The queue used for holding tasks and handing off to worker
* threads. We do not require that workQueue.poll() returning
* null necessarily means that workQueue.isEmpty(), so rely
* solely on isEmpty to see if the queue is empty (which we must
* do for example when deciding whether to transition from
* SHUTDOWN to TIDYING). This accommodates special-purpose
* queues such as DelayQueues for which poll() is allowed to
* return null even if it may later return non-null when delays
* expire.
*/
private final BlockingQueue<Runnable> workQueue;
线程池中的线程状态
而线程是根据任务的情况来决定阻塞或者唤醒。从而去达到线程复用的一个目的
线程池中的资源限制是通过以下几个关键参数来控制的:
它的目的主要是为了:阻塞队列中任务的处理效率
其中:
核心线程数 | 默认长期存在的工作线程 |
---|---|
最大线程数 | 根据任务的情况动态创建的线程 |
线程池主要源码思想
例如:
- 动态扩容和缩容线程池思想
- 线程复用思想
- 线程回收方法
- ……
编程步骤*
思路
核心思想
高内聚,低耦合
- 创建资源类
- 在资源类创建属性和操作方法(例如:封装工具类)
- 判断
- 执行
- 通知
- 实例化资源类对象,创建多个线程,调用资源类对象中的方法
- 注意防止虚假唤醒问题
示例
卖票案例
3个售票员最终卖出30张票
实现一:synchronized上锁
class Ticket {
//1.创建资源类,定义属性和操作方法
/**
* (资源类)1.1票数
*/
private Integer number = 100;
/**
* (操作方法)1.2卖票
加入synchronized关键字
*/
public synchronized void sale(){
if(number > 0){
System.out.println("当前票数为: " + number-- + Thread.currentThread().getName() + ": 卖出了" + "剩余票数为: " + number);
}
}
}
public class SaleTicket{
//2.创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
//2.1 实例化和含有资源的类对象
Ticket ticket = new Ticket();
//2.2 创建三个线程.调用卖出方法
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}
},"售票员-小红").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}," 小黄").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}," 小美").start();
}
}
实现二:使用ReentrantLock(可重入锁)上锁
class Ticket {
private final ReentrantLock lock = new ReentrantLock();
/**
* (资源类)1.票数
*/
private Integer number = 100;
/**
* (操作方法)卖票
*/
public void sale(){
//1.进入方法上锁
lock.lock();
try {
if(number > 0){
System.out.println("当前票数为: " + number-- + Thread.currentThread().getName() + ": 卖出了" + "剩余票数为: " + number);
}
}finally {
//2.一定记得释放锁
lock.unlock();
}
}
}
public class SaleTicket{
//2.创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
//2.1 实例化和含有资源的类对象
Ticket ticket = new Ticket();
//2.2 创建三个线程.调用卖出方法
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}
},"售票员-小红").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}," 小黄").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}," 小美").start();
}
}
+1和-1
synchronized实现
实现思路
1.创建资源类,定义属性和操作方法
2.在资源类操作方法
2.1 判断
2.2 做事
2.3 通知
3.创建多个线程,调用资源类的操作方法
1.实现第一步
创建资源类,定义属性和操作方法
public class Share {
/**
* 1.1 初始值
*/
private Integer num = 0;
/**
* 1.2 加1的方法
*/
public synchronized void incr() throws InterruptedException {
//2.1 判断
//只有num = 0的时候才需要+1
if(num != 0){
//wait(): 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。
this.wait();
}
2.2 做事: 如果num = 0,执行 + 1操作
num++;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//2.3 通知: notifyAll(): 唤醒所有等待该对象监视器的线程。线程通过调用一个等待方法来等待对象的监视器。
this.notifyAll();
}
/**
* 1.3 减1的方法
*/
public synchronized void decr() throws InterruptedException {
if(num != 1){
this.wait();
}
//- 1
num--;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//通知其它正在等待的线程
this.notifyAll();
}
}
2.实现第三步
public class ShareSynchronized {
/**
* 1.1 初始值
*/
private Integer num = 0;
/**
* 1.2 加1的方法
*/
public synchronized void incr() throws InterruptedException {
//2.1 判断
//只有num = 0的时候才需要+1
if(num != 0){
//wait(): 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。
this.wait();
}
2.2 做事: 如果num = 0,执行 + 1操作
num++;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//2.3 通知: notifyAll(): 唤醒所有等待该对象监视器的线程。线程通过调用一个等待方法来等待对象的监视器。
this.notifyAll();
}
/**
* 1.3 减1的方法
*/
public synchronized void decr() throws InterruptedException {
if(num != 1){
this.wait();
}
//- 1
num--;
System.out.println(Thread.currentThread().getName() + " :: " + num);
//通知其它正在等待的线程
this.notifyAll();
}
}
/**
* 3 实例化资源方法类,创建多个线程,调用操作类的操作方法
*/
class Threadsynchronized{
public static void main(String[] args) {
//实例化资源方法类对象
Share share = new Share();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小红:我要减1").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小蓝:我要加1").start();
}
}
3.最终输出
查看最终输出效果是否符合预期
小蓝:我要加1 :: 1
小红:我要减1 :: 0
小蓝:我要加1 :: 1
小红:我要减1 :: 0
...
以上输出符合预期
虚假唤醒
1.发生
那么如果再加入一个小蓝和小红线程共四个线程分别执行加一和减一操作又会发生什么情况呢?
小红:我要减1 :: 20
小蓝:我要加1 :: 21
小红:我要减1 :: 20
小蓝:我要加1 :: 21
小红:我要减1 :: 20
输出并不符合预期的还是1和0相互转换
原因:
- 查看相关wait()方法API文档中存在这样一句话
- 实现
中断
和虚假唤醒
是可能的,而且此方法始终在循环中使用
因此发生了虚假唤醒问题。
2.原因
那么为什么会发生虚假唤醒?
那是因为**wait有个特点,从哪里等待就从哪里继续执行**
-
当上述案例,线程小蓝要加1
- 已经进入到了方法体内
- 进入if判断此时
- num = 1
- 接着进入并调用wait方法执行了等待
-
此时线程小蓝要加1进入等待队列争夺CPU时间片
-
但是还是没有拿到时间片执行
-
此时线程小蓝蓝要加1此时已经正确执行完毕,此时
num = 1
了 -
接着就按例调用
this.notifyAll()
方法唤醒所有线程 -
此时小蓝要加1线程又被唤醒了
-
但是它这次很幸运,抢到了CPU分配的时间片执行
-
执行了接下来的
num++
操作,此时num = 2
-
3.解决方法
那么知道了原因此时要如何解决?
-
方法一:保持
- 一个线程执行加一方法
- 一个线程执行减一方法
- 为啥?
- 假设此时
num = 0
- -1线程此时执行必然进入if中的
wait()
方法 - 此时就需要一个唤醒,而被谁唤醒呢?当然是+1线程的
notifyAll()
才能唤醒 - 此时执行到
notifyAll()
了一定已经执行完+1了,num = 1
了 - 再去执行-1线程依然可以达到1和0之间转换的目的
- 假设此时
-
方法二:也就是将接下来的步骤放入到if循环中的else中去执行就可以避免
-
/** * 1.2 加1的方法 */ public synchronized void incr() throws InterruptedException { //2.1 判断 //只有num = 0的时候才需要+1 if(num != 0){ //wait(): 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。 this.wait(); }else { 2.2 做事: 如果num = 0,执行 + 1操作 num++; System.out.println(Thread.currentThread().getName() + " :: " + num); //2.3 通知: notifyAll(): 唤醒所有等待该对象监视器的线程。线程通过调用一个等待方法来等待对象的监视器。 this.notifyAll(); } }
-
因为如果不满足条件进入
wait()
方法后,它接着执行也执行不到else
中
-
-
方法三:将
wait()
方法加入到while()
循环中-
为什么这样就可以避免呢?
-
在Java的官方文档中这样写道
-
中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用:
synchronized (obj) { while (<condition does not hold>) obj.wait(); ... // Perform action appropriate to condition }
-
-
也就是如果这个
num
不满足我的条件时一直让此线程执行wait()
方法进入等待状态 -
示例修改后的代码如下:
/** * 1.2 加1的方法 */ public synchronized void incr() throws InterruptedException { //2.1 判断 //只有num = 0的时候才需要+1 while (num != 0){ //wait(): 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。 this.wait(); } 2.2 做事: 如果num = 0,执行 + 1操作 num++; System.out.println(Thread.currentThread().getName() + " :: " + num); //2.3 通知: notifyAll(): 唤醒所有等待该对象监视器的线程。线程通过调用一个等待方法来等待对象的监视器。 this.notifyAll(); }
-
修改if为while
-
-
输出预期结果:
小蓝:我要加1 :: 1
小红:我要减1 :: 0
小蓝:我要加1 :: 1
小红:我要减1 :: 0
...
ReentrantLock实现
//1.资源类
public class ShareReentrantLock {
//共享资源
private Integer num = 0;
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
lock.lock();
try {
while(num != 0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + " :: " + num);
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
lock.lock();
try {
while(num != 1){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + " :: " + num);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
//多个线程调用资源类方法
class ThreadReentrantLock{
public static void main(String[] args) {
ShareReentrantLock share = new ShareReentrantLock();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小红:我要减1").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小红红:我要减1").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小蓝:我要加1").start();
new Thread(() ->{
for (int i = 0; i < 100; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"小蓝蓝:我要加1").start();
}
}
输出预期结果:
小蓝:我要加1 :: 1
小红:我要减1 :: 0
小蓝:我要加1 :: 1
小红:我要减1 :: 0
...
线程定制化通信
1.思路
给每个线程定义一个标志位,通过标志位来实现定制化顺序通信
2.实现
3.实现代码
/**
* 一.资源类
*/
public class ThreadCustom {
/**
* 1.1 共享资源
*/
private Integer flag = 1;
/**
* 1.2 new lock锁
*/
private Lock lock = new ReentrantLock();
/**
* 1.3 new 3个 condition
*/
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
/**
* 1.4 创建依赖flag的方法
*/
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 1){
c1.await();
}
//操作
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " :: " +i+ ":轮数 :" + loop);
}
//通知: 修改标志位
flag = 2;
c1.signalAll();
}finally {
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 2){
c1.await();
}
//操作
for (int i = 6; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " :: " +i+ ":轮数 :" + loop);
}
//通知: 修改标志位
flag = 3;
c1.signalAll();
}finally {
lock.unlock();
}
}
public void print15(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (flag != 3){
c1.await();
}
//操作
for (int i = 11; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + " :: " +i+ ":轮数 :" + loop);
}
//通知: 修改标志位
flag = 0;
c1.signalAll();
}finally {
lock.unlock();
}
}
}
/**
* 3.创建线程调用资源类方法
*/
class ThreadCustoms{
public static void main(String[] args) {
ThreadCustom custom = new ThreadCustom();
new Thread(()->{
try {
for (int i = 1; i <= 10; i++) {
custom.print5(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AA0-5线程").start();
new Thread(()->{
try {
for (int i = 1; i <= 10; i++) {
custom.print10(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BB5-10线程").start();
new Thread(()->{
try {
for (int i = 1; i <= 10; i++) {
custom.print15(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CC10-15线程").start();
}
}
4.输出
实现线程定制顺序化执行
AA0-5线程 :: 1:轮数 :1
AA0-5线程 :: 2:轮数 :1
AA0-5线程 :: 3:轮数 :1
AA0-5线程 :: 4:轮数 :1
AA0-5线程 :: 5:轮数 :1
BB5-10线程 :: 6:轮数 :1
BB5-10线程 :: 7:轮数 :1
BB5-10线程 :: 8:轮数 :1
BB5-10线程 :: 9:轮数 :1
BB5-10线程 :: 10:轮数 :1
CC10-15线程 :: 11:轮数 :1
CC10-15线程 :: 12:轮数 :1
CC10-15线程 :: 13:轮数 :1
CC10-15线程 :: 14:轮数 :1
CC10-15线程 :: 15:轮数 :1
并发问题
线程不安全
火车票问题
//获取当前执行线程的名称
Thread.currentThread().getName()
/**
* @Project_Name alorithms
* @Author LH
* @Date 2022/6/30 11:31
* @TODO:多线程同时操作同一对象
* @Thinking:买火车票问题
1.初识并发问题
2.多个线程同时操作同一个对象!
3.创建三个线程去抢票,并查看是谁抢的票
发现问题:当多个线程操作同一个资源的情况下:会出现线程不安全!数据紊乱!
*/
public class concurrentTest1 implements Runnable{
//火车票数量
private int ticketNums = 10;
@Override
public void run() {
while (true){
//结束条件
if (ticketNums <= 0) break;
//模拟买票操作时间,假设为0.2s
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票!");
}
}
public static void main(String[] args) {
concurrentTest1 ticket = new concurrentTest1();
//执行3个线程
new Thread(ticket,"线程名1").start();
new Thread(ticket,"线程名2").start();
new Thread(ticket,"线程名3").start();
}
}
//线程名3-->拿到了第9张票!
//线程名1-->拿到了第9张票!
//发现问题:当多个线程操作同一个资源的情况下:会出现线程不安全!数据紊乱!
发现问题(线程不安全)
当多个线程去并发执行的时候,会出现同一张票被两个或以上个人抢的,那么票也太惨了,撕碎的票肯定过不了检查的!那么怎么办呢?
龟兔赛跑-Race
- 首先赛道距离,然后离终点越来越近
- 判断比赛是否结束
- 打印胜出者
- 龟兔赛跑开始
- 故事中乌龟是赢的,兔子需要睡觉,所有,模拟兔子睡觉
- 终于,乌龟赢得比赛
实际代码:
/**
* @Author LH
* @Date 2022/7/3 11:55
* @TODO:线程模拟龟兔赛跑
* @Thinking:
*/
public class Race implements Runnable{
//胜利者
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
//模拟兔子中途休息
if(Thread.currentThread().getName().equals("兔子") && i == 50){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判断比赛是否结束
boolean flag = gameOver(i);
//如果比赛结束则停止
if(flag) break;
System.out.println(Thread.currentThread().getName() + "-->当前跑了:" + i + "步");
}
}
//判断是否完成比赛
private boolean gameOver(int steps){
//判断是否有胜出者,此时!=null,即有胜出
if(winner != null){
return true;
}
else{
if (steps >= 100){
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
//两个线程实例
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
乌龟-->当前跑了:99步
winner is 乌龟
实际上,乌龟和兔子的速度明明差的很远的,怎么处理两者不同的速度问题呢?例如兔子要走100步,乌龟可能要走1000步呢?
线程的速度快慢,实际上是根据很多系统指标来衡量的,从另一个角度而言它又是充满了很多不确定的因素。
在Java中可以通过sleep()
方法来具象的控制线程的速度。
控制乌龟线程的sleep()
时间(时间与兔子线程实际运行速度来决定)
上述案例实际上需要控制线程的执行速度来更加生动的模拟出来乌龟和兔子线程的执行速度,那么如何去控制线程的速度呢?
实际上线程的执行速度主要是根据CPU来决定的,具有先天性。所以,理论层面是不可行的。因为当CPU分配时间片给到某一个具体线程,CPU具体执行线程任务的时候就已经在一定程度上决定了它的执行速度。实际上只能在时间层面上去侧面实现此效果,也就是通过设置定时器的方式,可以定位到毫秒甚至更小的精度在指定时刻去执行sleep()等方式,来侧面实现控制线程执行速度!
其它不安全案例
实现Runnable接口,是实现Callable接口
集合不安全
集合中线程不安全案例
ArrayList
都知道ArrayList是不安全的,但是为什么它是不安全的呢?
示例代码
1.利用多个线程,存和放,就有可能出现并发问题
public class ArrayListUnsafe {
public static void main(String[] args) {
//1.创建集合
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
//2.向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
2.输出
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824]
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824, d524a95f, b5ed75ff, ff7038b9, 3b00de3d]
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824, d524a95f, b5ed75ff]
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824, d524a95f]
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824, d524a95f, b5ed75ff, ff7038b9]
[null, c6cc1bd2, f88ea0ed, bdc23c47, 20b9f824]
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at Multithreading.Concurrent.SetNotSecure.ArrayListUnsafe.lambda$main$0(ArrayListUnsafe.java:23)
at java.lang.Thread.run(Thread.java:748)
ConcurrentModificationException
为什么会出现并发修改异常?
- 通过查看
ArrayList
源码发现,其并没有加上synchronized
等安全处理操作 - 当多个线程去执行上述代码,就可能会出现正在存就读取了。
- 所以导致并发修改异常
- 存和读两个操作应该是分开执行,对应两个方法都有做相关安全处理才对
解决方案
-
通过
Vector
解决-
List<String> list = new Vector();
-
-
通过
Collections
解决 -
通过
CopyOnWriteArrayList
解决
一、通过
Vector
来解决
1.示例代码
public class ArrayListUnsafe {
public static void main(String[] args) {
//1.创建集合
// List<String> list = new ArrayList<String>();
List<String> list = new Vector();
for (int i = 0; i < 10; i++) {
new Thread(()->{
//2.向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
2.输出
输出正确未出现异常
3.原因分析
通过查看可以发现,Vector
的相关方法都添加了synchronized
/**
* Adds the specified component to the end of this vector,
* increasing its size by one. The capacity of this vector is
* increased if its size becomes greater than its capacity.
*
* <p>This method is identical in functionality to the
* {@link #add(Object) add(E)}
* method (which is part of the {@link List} interface).
*
* @param obj the component to be added
*/
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
通过这样的方式给所有相关方法都加上了锁,从JDK1.0就存在了。
二、通过
Collections
中的synchronizedList
来解决
1.示例代码
public class ArrayListUnsafe {
public static void main(String[] args) {
//1.创建集合
// List<String> list = new ArrayList<String>();
// List<String> list = new Vector();
List<String> list = Collections.synchronizedList(new ArrayList<String>());
for (int i = 0; i < 100; i++) {
new Thread(()->{
//2.向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
示例解决方案
思考
通过以上两种方式可以解决
那么还有效率更高的方案吗?这种显然效率是不高
三、通过
CopyOnWriteArrayList
解决
1.示例代码
public class ArrayListUnsafe {
public static void main(String[] args) {
//1.创建集合
// List<String> list = new ArrayList<String>();
// List<String> list = new Vector();
// List<String> list = Collections.synchronizedList(new ArrayList<String>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
//2.向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
也能成功解决集合线程不安全问题!那么它为什么能够解决呢?
2.原理分析
//写时复制技术
CopyOnWriteArrayList
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//*
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//*
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
简单来说就是:
- 读的时候支持并发读
- 写的时候独立写
在写的时候:
- 复制一份集合副本
- 此时所有读的线程都在读原始集合
- 在副本集合中独立写入
- 写完之后再覆盖/合并原始集合
3.好处
这样操作的好处就是既照顾了并发读,也维护了独立写
在线程安全的前提下,进一步提升了效率
HashSet
示例异常代码
Set<String> set = new HashSet<String>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
//2.向集合中添加内容
set.add(UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
发生并发问题
Exception in thread "44" java.util.ConcurrentModificationException
解决方案
Set<String> set = new HashSet<String>();
修改为:
Set<String> set = new CopyOnWriteArraySet<>();
即可解决并发问题
HashMap
示例异常代码
Map<String,String> map = new HashMap();
for (int i = 0; i < 100; i++) {
String key = String.valueOf(i);
new Thread(()->{
//2.向集合中添加内容
map.put(key,UUID.randomUUID().toString().substring(0, 8));
//3.从集合获取内容
System.out.println(map);
},String.valueOf(i)).start();
}
输出:
Exception in thread "73" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
解决方案
new ConcurrentHashMap();
内置分段锁式概念,解决并发问题!
同步锁
synchronized{
i++;
}
多线程三大特性
可见性、有序性、原子性(较难实现)
进程、线程、协程
计算机组成
早期应用程序在运行的大致过程
线程:程序内部不同的分支
启动应用程序流程
- 应用程序的数据,在开机前是存放在硬盘中的,当开机以后,会将它放入到内存中(速度快但断电即失)。
- 但此时在内存中它可以看成只有一个线程,也就是main方法的主线程只有找到main主线程,CPU在拿到它的代码才可以运行
- 在运行期间,同一个时间点,CPU只能处理一个线程任务(实际上是CPU在某一个时间点只能执行一条指令,而这条指令是属于某个线程的)
所以:进程可以看成是一个静态的概念,线程才是真正的动态概念
进程是分配资源的基本单位,线程也是CPU调度的基本单位
CPU在同一个时间点上只能执行一个线程
一个应用程序是可以有多个进程
切换
线程的创建
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.concurrent.Callable;
/**
* @Author LH
* @TODO:线程的创建
* @Thinking:
*/
//创建线程方式一:继承Thread重写run方法
public class nie extends Thread{
@Override
public void run() {
super.run();
}
}
//创建线程方式二:实现runnable接口
class thread2 implements Readable{
@Override
public int read(CharBuffer cb) throws IOException {
return 0;
}
}
//创建线程方法三:实现callable,callable可以看成是runnable的补充
class thread3 implements Callable{
@Override
public Object call() throws Exception {
return null;
}
}
synchronized本身是把悲观锁
悲观锁:哪怕没有别的线程跟我抢我也要锁,就是任性!
所谓的给对象上synchronized锁,实际上就是给对象的头添加锁信息。
加了锁实际上就是从原来的多线程并发执行,变成了互斥的序列化执行(一次只能是一个线程进去执行)。
加了synchronize锁之后
测试
在起了多个线程并不加锁的时候,将j(初始值=0)不断加上9999次,可想而知正确的结果是10000
但是实际上在不断启动新线程,导致多个线程执行相同任务后,那么在原本执行操作结果是10000的过程中,不断创建新的线程,在之前正在加载此数据的旧线程正在改变提交值时候,半道也被其它线程修改了相关任务的值,导致值被多次改变,出现结果与预期的误差。
那么:怎么解决多线程抢夺任务执行造成的误差问题呢?
答案:就是加锁呀!但是怎么加才合理呢?刚开始的从Java6版本前一直使用的是原本操作系统沿用的互斥锁(悲观锁),这样虽然可以解决此问题,但是没有其它线程跟我抢,我也加,这样就会造成CPU资源的浪费,哪怕实现了效率也是低下的。
那么请问:有没有什么办法在多线程环境下保证值的正确性还要在此基础之上提高它的效率呢?
此时:
乐观锁就出现了
JUC乐观锁
JUC
import java.util.concurrent.*;
CAS:也就是乐观锁(别称)的实现方式!
乐观锁也称自旋锁
也就是它总是认为没有其它的线程会跟我抢,只是在执行任务改变值之后在提交的时候,才去判断一下,值此时是否出现了改变,如果有改变就证明有其它线程跟我抢了,并且领先我一步完成任务并且提交,那么此时任务实际上已经完成了,我就不需要再去将它的值再改变。
例如:i++过程
i的初始值为0
1.将i++变成1
2.再到内存中修改相应值
加上CAS(乐观锁实现)就是:
先不上锁,在读取改变值时候先判断值是否=0,
是再将它在内存中的值置换为1;不是就放弃操作
流程步骤图
问题
ABA问题
博文地址:https://wwwblogs/wyq178/p/8965615.html
也就是读取和需要判断修改的时候,值都是同一个值,但是其中被修改成别的值过。
也就是读取的时候是A,中间被其它线程改成了B,但是最后再我修改值之后进内存置换判断之前
其它线程又将它改回成了A,这时你读取的还是A但是其中,在你操作过程中它被改变过哦。
如果只是值类型的改变,并无大碍,但是要是改变的是引用类型就会
AbA问题的产生
要了解什么是ABA问题,首先我们来通俗的看一下这个例子,一家火锅店为了生意推出了一个特别活动,凡是在五一期间的老用户凡是卡里余额小于20的,赠送10元,但是这种活动没人只可享受一次。然后火锅店的后台程序员小王开始工作了,很简单就用cas技术,先去用户卡里的余额,然后包装成AtomicInteger,写一个判断,开启10个线程,然后判断小于20的,一律加20,然后就很开心的交差了。可是过了一段时间,发现账面亏损的厉害,老板起先的预支是2000块,因为店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。小王一下就懵逼了,赶紧debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次!
阐述:
假设有个线程A去判断账户里的钱此时是15,满足条件,直接+20,这时候卡里余额是35.但是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,线程B去执行扫描账户的时候,发现它又小于20,又用过cas给它加了20,这样的话就相当于加了两次,这样循环往复肯定把老板的钱就坑没了!
本质:
ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。
解决1:没有记录修改次数,那么就加上一个版本号用来记录累加修改的次数
AtomicStampReference
AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
public V getRerference();
public int getStamp();
public void set(V newReference,int newStamp);
AtomicStampReference在cas的基础上增加了一个标记stamp
解决2:改一次我都受不了,用boolean类型来是判断也就是AtomicMarkableReference
总共两种,一种就是添加记录版本来判断是否被修改,可查看修改次数,另一种就是用boolean类型,修改就改变不记录。
CAS底层如何来完成的呢?
compareAndSwapInt
c++中实际上也有一个类Unsafe.cpp对应Java中的Unsafe.class
实际上看到最底层还是在cmpxchg前加上了lock手动锁
CAS操作支持的c++源代码():
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JfrRiOO-1683369747817)(C:/Users/Administrator/AppData/Roaming/Typora/typora-user-images/image-20211012211318936.png)]
也就是MP表示在后面如果是多核CPU就在代码前加上lock,单核就不会加了,总不能自己执行的操作,被自己影响吧。
CAS的原子性问题
不是原子性就是一条它中间会分成很多段,这样就容易被其它的CPU或者线程所打断!
所以CAS操作本身就必须具备原子性,其实CAS本身,在CPU中就有原始的指令来支持它。
是不是CAS一定就比悲观锁效率高呢?
不是,要看线程执行的时间,等待线程数量的多和少
什么时候用CAS,什么时候用悲观锁?
具体要看不是,要看线程执行的时间,等待线程数量的多和少
1.线程执行时间短,等待线程数量相对少,用CAS来实现,效率确实要高
2.否则还是用悲观排队锁,至少那么多的线程排队它不会去消耗CPU资源对吧
分析流程
小明要上厕所
1.使用悲观锁的情况:
小明进坑,用悲观锁把门锁上了,外面的小红、小黄等线程一直在外排列队列中等待,等到轮到某个线程时CPU会唤醒它,其实这个等待并不消耗CPU资源。
2.但使用乐观锁就不一样了,乐观锁还有一个别称就是自旋锁。一但小明进坑上锁后,外面等待的线程就会一直执行操作然后最终提交判断有没有改变,也可以想象成外面的小红和小黄等线程一直提着裤子转来转去,等着小明开锁,这个过程是有消耗CPU资源的哦!
能使用synchronize解决问题的,优先使用synchronize解决
why?
因为从JDK1.6开始,它就开始不再使用操作系统中的互斥锁了
因为synchronize内部有锁升级的过程:偏向锁——自旋锁(轻量级锁,CAS,无锁)——重量级锁(悲观排队锁)
具体使用哪一个得看它场景适用哪一个。(帮你看自旋锁好不好使,不好使就自动升级为重量级锁)
偏向锁
所谓偏向锁就是应用与第一个线程(第一个过来的人,按上述所说是小明)它上的锁就是偏向锁
严格意义来说偏向锁并不是一把锁,而更像是一个标签把自己的线程ID记录在对象Object头上,再来线程就把这个偏向锁撤销掉
JDK15已经把这个锁撤销了(太复杂),但是为什么要设计这个偏向锁?
因为在工业统计大多数情况下,往往只有一个线程在用锁
例如StringBuffer,底层的方法很多都是加上了synchronize的但是大多数情况下并没有很多的线程在竞争哦,
所以很多时候尽管并没有去手动加锁,但是实际上很多时候都应用到了锁的操作。
统计下,70%-80%的时间实际上只有一个线程在跑。20%-30%的时间是有多线程在竞争。
所以为了这个70%-80%的时间不仅有事没事的先上自旋锁,甚至还要上悲观锁,就有点大惊小怪了。由此引入了偏向锁,将线程ID贴在对象的头上,其实只要知道它有没有多线程抢夺的情况出现,有的话偏向锁撤销就知道这个发生了多线程竞争的情况,再去采取相应的措施。
偏向锁实际上是一个非常复杂的概念(它不仅有JVM的调优、还有锁撤销、批量锁撤销,批量重偏向等一系列操作)
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class ChatGPTApiService {
private final String API_ENDPOINT = "https://api.openai/v1/engines/davinci-codex/completions";
private final OkHttpClient client;
public ChatGPTApiService(String proxyAddress, int proxyPort, String proxyUsername, String proxyPassword) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (proxyAddress != null && !proxyAddress.isEmpty() && proxyPort > 0) {
builder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAddress, proxyPort)));
}
if (proxyUsername != null && !proxyUsername.isEmpty() && proxyPassword != null && !proxyPassword.isEmpty()) {
builder.proxyAuthenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(proxyUsername, proxyPassword);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
});
}
client = builder.build();
}
public String getCompletion(String prompt, int maxTokens) throws IOException {
Request request = new Request.Builder()
.url(API_ENDPOINT)
.header("Content-Type", "application/json")
.addHeader("Authorization", "Bearer YOUR_API_KEY_HERE")
.post(RequestBody.create(
MediaType.parse("application/json; charset=utf-8"),
"{\"prompt\": \"" + prompt + "\", \"max_tokens\": " + maxTokens + "}"
))
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
return response.body().string();
}
}
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
设计模式
《设计模式》
前置理论
为什么要学设计模式?单单是面试中会用到?
当然不是,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性;我们在编写程序的时候用好设计模式可以防范于未然,它们可以很好地提供一种解决问题的方案。
设计模式六大原则
(本来想直接入主题找几个模式大写特写一番的,后来看了看,还是觉得先从整体出发比较好。)
设计模式是优秀的前辈软件工程师、架构师们智慧的精华集结,是编程文化的精髓组成部分。本菜在不断学习和思考了二十几个设计模式后,觉得还是先介绍一下整体的几个原则为妙。因为这些原则是设计模式的思想根基。
1、单一职责。
假设你有一台联想G480,它有上网、玩LOL、逛淘宝、写文章、学习等各项功能,你很满意。
假设你有一个类,它有上网、玩LOL、逛淘宝、写文章、看视频等各项功能,你就不一定满意了。为什么?因为客户要求你开发一款电视软件!!!
所以,在编程的世界里,虽然和生活有着密不可分的联系,但是二者还是有不少区别的。
单一职责原则,字面理解就是尽量只有一个职责。准确解释是,就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起,将导致脆弱的设计。当发生变化时,设计会遭受意想不到的破坏!
如果我们把上述功能全部分开,那么我们无论是开发TV软件还是pad软件,只需要复用必要的单一功能类就OK了。
因此,在类设计中,如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责,就要进行重新设计。
2、开放封闭+依赖倒转+里氏代换
为什么把这三个放在一起,请往下看。
(1)开闭原则是指软件实体(类、模块、函数等)应该开放扩展,关闭修改。我们在设计一个类的时候,应当尽量使它足够好,设计好了就尽量不修改了;有了新的需求,直接添加新类就解决了。
但是!无论类设计的多么封闭,还是会存在一些难以预料的变化。所以设计人员必须对于他设计的类应该对哪种变化封闭做出选择。先猜出最有可能发生变化的类,然后构造抽象来隔离变化。
一旦变化发生,应立即采取行动,即创建抽象类来隔离以后发生的同类变化。
开闭原则是OOD的核心,有可维护、可扩展、可复用、隔离性好等优点。我们在未来的开发设计中,应对程序中频繁变化的部分做出抽象,但不可对每个部分都刻意抽象,因为拒绝不成熟的抽象和抽象本身一样重要。
(2)农民老王是个“宝马迷”,一辈子就想买个宝马。终于有一天,老王走运买了张彩票,中了一千万,全部花光买了一款世界上独一无二的“宝马WD”,实现了宝马梦。但是!很不幸的是,还没开到家,轮胎废了。。。(本故事纯属虚构,如有雷同……)
你问老马该怎么办?凉拌呗!要不就不换,闲着,要不就换了全部的轮子,因为所有的都是独一无二的配件!老王表示没钱了~
如果老王买的是相对常见的X5/X6,换一个轮子倒也能接受。所以,由于老王没有学过依赖倒转,吃了大亏。
依赖倒转原则是说“抽象不应该依赖细节,细节要依赖抽象”,就是要针对接口编程,而不是实现。
用“倒转”一词也是有理由的。比如公司某工程师设计了一款宝马图纸,以后造宝马就按照它来,轮子统一是X型号。然而有一天,一大客户只想要Y型号轮子的宝马,公司表示很无奈,损失严重。如果当时没有规定必须用X型号轮子,只说四个轮子即可(规定了接口),就不会出现今天的问题。
为什么依赖了抽象的接口或抽象类就不怕更改了呢?这就需要里氏代换原则来解释了。
(3)里氏代换:子类型必须能够替换掉他们的父类型。即,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它觉察不出来父类对象和子类对象的区别。
正是因为有了里氏代换,才使得开放封闭成为可能。因为子类能完全替换掉父类,这样就在父类关闭的条件下实现了子类扩展。
同样,依赖倒转中的子类依赖父类接口,高层模块和低层模块都依赖抽象才合理,也就很容易解释了。
3、迪米特。
迪米特法则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三方转发这个调用。
这个原则在以后的设计模式中体现的非常普遍,比如单例、代理、外观等等,以后再详述。
迪米特法则的根本就是要求松耦合。在类的结构设计上,每个类都应当尽量降低成员的访问权限。类之间的耦合越弱,越有利于复用,对类的更改引起的程序变化也越小。
4、合成/聚合复用。
在UML的类图中,我们说过聚合、组合之间的联系与区别。聚合是一种弱关系,体现的是A包含B,B不是A的一部分;组合是一种强关系,体现的是A与B之间的整体-部分关系。
合成/聚合复用原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
它要求我们尽量少用继承(只有在类之间符合 is - a时可以使用),防止类的盲目扩大。在“桥接模式”中体现比较深刻。
这其实也体现了单一职责的思想。尽量减少类的功能,降低耦合。
总结:个人认为,设计模式中的思想是系统化的、各自联系的。各个思想之间都有联系,需要我们在设计过程中仔细思考,分析实际需求,把它们对应到各自适合的具体设计模式中。
原文链接:https://blog.csdn/u010191243/article/details/22993573
目的
- 介绍设计模式的基本知识
- 介绍基本用法和注意事项
- 介绍设计模式的实用性
- 学完以后,工作过程中,能有意识的使用起来
- 查看既有代码的时候,能有意识的识别出,代码作者在使用哪种设计模式,进而帮助你更容易理解其完整代码架构和设计意图
注意
- 不要为了特意使用设计模式,而勉强使用设计模式
- 避免过度设计,优雅的解决用户的(原始)需求才是根本
- 要权衡使用了设计模式后,导致的软件复杂性,以及类个数膨胀的问题
举个栗子:如何快速获取汉字的拼音首字母
简介
场景
软件开发过程中经常遇到的场景:
-
经常需要对函数返回null或者0情况的处理时
-
代码中出现多分支选择时
-
需要将通用性的功能从其实现细节中分离出来时
-
提供服务的对象可能有多个来源或者需要控制对对象的访问时
-
对内容和容器或者对单复数一视同仁时
-
设计出现类膨胀时(m x n)
-
需要将行为和数据分离时
-
……
识别
识别当前软件(架构)中的臭味:
-
僵化:很难对系统进行改动,每个改动会导致其他模块跟着改动
-
脆弱:对系统的细微改动可能会导致系统崩溃
-
耦合:系统各模块相互耦合,很难对系统提炼出可在其他系统中重用的组件
-
晦涩:系统代码架构、逻辑非常晦涩难懂,系统很难维护
-
不必要的复杂性:设计找那个包含有不具有任何直接好处的基础结构
-
不必要的重复:系统中存在大量重复的结构、代码
-
粘滞:包括软件和环境的粘滞性
-
……
每一个(建筑)模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次的使用该方案 而不必做重复劳动。
−− Alexander
什么是设计模式?
设计模式就是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
-
代码看多了,写多了,人会自然而然去总结和思考。这些总结和思考的结果,就是各种设计模式。
-
使用设计模式的目的,是为了提高代码的可重用性,让代码容易被同行理解、维护和扩展。
四要素
设计模式的四要素:
-
模式名称:模式的助记名,能够尽可能反馈该模式所解决的问题分类。
-
应用场景:描述了何时比较适合应用该模式,包括问题的前因后果、前提条件等。
-
解决方案:描述了设计的组成部分,包括他们之间的相互关系、各自的职责和写作方式。
-
应用效果:描述了该模式的使用效果以及使用该模式需要的权衡考虑等。
基本原则
设计的基本原则 -- S O L I D
-
SRP:单一职责原则:设计职责明确
-
OCP:开放-封闭原则
-
LSP:里氏替换原则:子类可以无缝替换父类
-
ISP:接口隔离原则:面向接口编程(站在客户角度反向实现思想)
-
DIP:依赖倒置原则:抽象依赖细节,细节依赖抽象
1.多用组合,少用继承
-
引入新功能的时候,往往会从现有代码内,找个功能相近的类,从它继承,然后把新功能追加进去。
-
随着项目的进展,新功能的扩充,子类和父类就越来越紧密,很难维护。
合理的做法是:
- 把功能相近的方法,合并后,用小类各自实现。
- 然后把这些小类,组合到一个大类内,提供给外部使用。
2.对扩展开放,对修改封闭
- 需求:软件开发过程中,经常需要扩充功能。
- 矛盾:从外界看来,现有代码,只要稍微修改,就能满足需求。但,修改的过程很痛苦,修改的结果是一塌糊涂,错误百出。
- 本来只是星星之火,修改后,已经可以燎原了。
- 期望:在增加新功能的过程中,开发完毕测试完毕的模块,不需要修改。
- 只要通过系统配置或对象组装,就能把新开发的功能模块,插入现有系统。
分类
创建型模式 | 单例、工厂、抽象工厂、建造者、原型 | |
---|---|---|
结构型模式 | 适配器、装饰、桥接、代理、外观、组合、享元 | |
行为型模式1 | 观察者、模版方法、命令、状态、责任链 | |
行为型模式2 | 解释器、策略、迭代器、中介、访客、备忘 |
创建型模式
单例模式(singleton)
-
保证某一个类,只有一份实例。
-
使用者,只能使用这一份实例。
class sector_factory
{
//获取工厂对象的单例
static sector_factory* get();
//创建对象
public sector* create();
……
//全局唯一的实例
static sector_factory* s_factory;
}
饿汉式
常用,前置初始化,JVM保证线程安全。对象一次初始化。多次被引用。
/**
* @Project_Name 项目暂存
* @Author LH
* @Date 2021/12/5 14:10
* @TODO:单例模式【饿汉式(常用)】
* @Thinking:
类加载到内存后,就实例化一个单例,JVM保证线程安全(可以保证相同的类在JVM类加载的过程只被执行一次【双亲委派原则】)
1.简单实用,推荐使用!且拿来即用,响应快!
2.缺点:无论是否用到,在类装载时,就会完成实例化,启动时间长。main函数还未执行就创建!
3.也就是你不用它,你装在它干什么?
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01(){};
public static Mgr01 getInstance(){
return INSTANCE;
}
public void m(){System.out.println("m");}
public static void main(String[] args){
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
//一定是true,因为实例对象指向的都是同一个引用,这也是单例的意义所在
System.out.println(m1 == m2);
}
}
懒汉式(lazy loading)
需要的时候再初始化,但要承担线程可能会不安全的风险(就不是一个实例了)。
package com.Hao.singleton;
/**
* @Project_Name 项目暂存
* @Author LH
* @Date 2021/12/5 14:25
* @TODO:单例模式【懒汉式】lazy_Loading
* @Thinking:
1.虽然达到了按需初始化的目的(启动快),但是为此却需要承担线程可能会不安全的风险
*/
public class Mgr02 {
private static Mgr02 INSTANCE;
private Mgr02(){};
//当使用到了的时候,判断是否为null,如果为null则说明并未初始化
public static Mgr02 getInstance(){
if(INSTANCE == null){
//满足条件初始化一次
/**
注意:在此处可能会出现线程不安全的问题
1.如果线程1执行到此处,此时INSTANCE还没有来得及使用如下代码初始化
2.这时,线程2到了上述判断是否为null,此刻没来得及执行初始化,当然是null
3.这个时候就会造成线程不安全。
*/
INSTANCE = new Mgr02();
}
return INSTANCE;
}
}
测试出不安全的情况
public class Mgr02 {
private static Mgr02 INSTANCE;
private Mgr02(){};
//当使用到了的时候,判断是否为null,如果为null则说明并未初始化
public static Mgr02 getInstance(){
if(INSTANCE == null){
//满足条件初始化一次
/**
注意:在此处可能会出现线程不安全的问题
1.如果线程1执行到此处,此时INSTANCE还没有来得及使用如下代码初始化
2.这时,线程2到了上述判断是否为null,此刻没来得及执行初始化,当然是null
3.这个时候就会造成线程不安全。
*/
//在此增加出现多线程不安全的概率
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
INSTANCE = new Mgr02();
}
return INSTANCE;
}
//测试线程不安全的情况
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// new Thread(new Runnable() {
// @Override
// public void run() {
//
// //1365259960
// //152177599
// //798568271
// //打印结果出现不相同
// System.out.println(Mgr02.getInstance().hashCode());
// }
// }).start();
// new Thread(()->
// System.out.println(Mgr02.getInstance().hashCode())
// ).start();
}
}
}
双重判断完善
双重判断 + synchronized
package com.Hao.singleton;
/**
* @Project_Name 项目暂存
* @Author LH
* @Date 2021/12/5 14:25
* @TODO:单例模式【懒汉式】lazy_Loading
* @Thinking:
1.虽然达到了按需初始化的目的,但是为此却需要承担线程可能会不安全的风险
2.为了解决上述问题,提供了双重检查和synchronized加在判断可能出现线程不安全代码块上
*/
public class Mgr02 {
//注意:如果使用这样的写法就需要加上volatile关键字可以防止指令重排问题
private static volatile Mgr02 INSTANCE;
private Mgr02(){};
//当使用到了的时候,判断是否为null,如果为null则说明并未初始化
public static Mgr02 getInstance(){
//双重检查,是有必要的,因为大多数线程执行到此判断就不会执行内部的代码了,在原有基础上增加了一些效率
if (INSTANCE == null) {
if (INSTANCE == null) {
//满足条件初始化一次
/**
注意:在此处可能会出现线程不安全的问题
1.如果线程1执行到此处,此时INSTANCE还没有来得及使用如下代码初始化
2.这时,线程2到了上述判断是否为null,此刻没来得及执行初始化,当然是null
3.这个时候就会造成线程不安全。
*/
synchronized (Mgr02.class) {
//在此增加出现多线程不安全的概率
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
}
}
return INSTANCE;
}
//测试线程不安全的情况
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//1365259960
//152177599
//798568271
//打印结果出现不相同
System.out.println(Mgr02.getInstance().hashCode());
}
}).start();
// new Thread(()->
// System.out.println(Mgr02.getInstance().hashCode())
// ).start();
}
}
}
静态内部类
较为完美
/**
* @Project_Name 项目暂存
* @Author LH
* @Date 2021/12/5 15:22
* @TODO:静态内部类实现单例
* @Thinking:
1.静态内部类方法
2.JVM保证单例
3.加载外部类时不会加载内部类,这样可以实现懒加载
*/
public class Mgr03 {
private Mgr03(){};
//静态内部类方法实现保证了在需要的时候调用getInstance()方法才会去加载Mgr03Holder类
//这样JVM保证了它的线程安全,只会加载一次
private static class Mgr03Holder{
private final static Mgr03 INSTANCE = new Mgr03();
}
public static Mgr03 getInstance(){
return Mgr03Holder.INSTANCE;
}
public void m(){
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Mgr03.getInstance().hashCode());
}).start();
}
}
}
枚举【相对完美】
package com.Hao.singleton;
/**
* @Project_Name 项目暂存
* @Author LH
* @Date 2021/12/5 15:32
* @TODO:枚举方法
* @Thinking:
1.Java创始人之一列举的实现一种实现方法
2.不仅可以解决线程同步,还可以防止反序列化
*/
public enum Mgr04 {
INSTANCE;
public void m(){};
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mgr04.INSTANCE.hashCode());
}).start();
}
}
}
为什么要防止反序列化?
Java的反射,反射是可以将一个class文件加载到内存并new出一个实例出来,也就是说,除了这个枚举方法,上述其它单例实现方法,都可以通过反射的方法加载并new出一个 新的实例,这样就违背了单例的初衷,所有需要防止基于反射机制的反序列化。
为什么枚举类就可以防止反序列化
枚举类它没有构造方法,即使拿到了它的class文件,也无法构造它的对象,它返回的仅仅只是一个值(上述为INSTANCE)。
单例总结
实际上第一种【饿汉式】较为常用(简介、方便),而最后一种枚举方法较为完美,但是并不常用的原因是一定通过反射机制来加载了就是别人在搞破坏了,暂时可以不考虑进去。各种factory中的实现也是单例模式的实践。
后期可以依靠spring中的bean工厂来产生单例就可以了。
注意点:
多线程的应用程序访问和操作单例的全局单例的时候,必须加锁。
•单例的每个方法,必须加锁。
•加锁一般在单例对象内部实现。
锁太多,小心发生死锁。
工厂模式(Factory)
-
创建对象的时候,不是直接new出来对象,而是借助一个单独的类,来创建出对象。
-
目的是:对使用者,屏蔽对象创建过程的复杂性
建造者模式(Builder)
-
有些对象很大,依赖其它小对象,大对象直接new出来后,没法直接使用。
-
建造者模式准备大对象需要的各种小对象,设置好它们之间的关系,然后才把大对象,返回给调用者。
A_Builder
{
A* build()
{
A* a = new A(); //创建A
B* b = new B(a);//创建B
//关联事件
a->on_start += &b->a_started;
a->start();//启动a
return a;
}
}
注:主要描述对象与对象的关系
原型模式(Prototype)
案例
需求和方案分析:
-
显示孙悟空图片:设计SunWuKong子类继承于JPanel类,用于放置孙悟空图片。
-
快速复制:Java中Object类已经实现了
clone
方法(浅拷贝),SunWuKong子类需要实现Clonable
接口。
实现
浅拷贝和深拷贝
浅拷贝
深拷贝
例
Linux进程的创建:
-
使用fork/vfork/clone快速以当前进程(权限、文件打开列表、堆栈大小等)为原型创建出新进程。
-
让子进程直接运行(自我复制)或者通过exec函数簇执行新的程序体。
电子账单投递:
-
行业电子账单格式和内容都是类似的
-
稍作修改就可以生成个性化电子账单
总结
用于创建相同(重复)或者相似的对象,同时又要保证性能
结构型模式
桥接模式(Bridge)
引入
用途
主要是用于解决类膨胀问题
一个例子
网上商城系统要销售电脑,根据类型有台式机,笔记本,平板等;根据品牌又分为联想,苹果,戴尔等。
设计
原始设计图
不用此模式的缺陷
•缺陷:
可扩展性差
类膨胀:m x n
•违反设计原则:
开闭原则(OCP):
添加代码√,修改代码×。
实现
-
将抽象接口和实现脱耦
-
抽象接口是抽取出来的、能满足业务需要的接口类
-
实现是实现了抽象接口的实现类。
-
桥接模式插在抽象接口和实现之间,不让实现直接从抽象接口派生。
-
这样,实现类就不依赖于抽象接口类。
-
抽象化与实现化的脱耦,可以使得二者独立的变化,将他们之间的强关联变成弱关联。也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。
应用场景
类层次出现多个自由度,桥接模式可以使得m x n à m + n
示例代码
//抽象操作类,供客户端使用:
class Abstraction
{
public:
void Handle()
{
handler->handle();
}
void SetHandler(Handler handler);
protected:
Handler handler;
};
//抽象实现类,供内部实现扩展:
class Handler
{
public:
void handle() abstract; // 处理数据
};
适配器模式(Adapter)
-
把一个类的接口变换成客户端期待的另一种接口。
-
适配器模式,又被叫做转换器模式、变压器模式、包装(Wrapper)模式。
-
系统开发好后,为了扩充功能,会向外部暴露出一些插件接口,方便以后把新的功能模块集成进来。
-
新的功能模块的对外接口,往往和现有系统要求的接口不一致。
应用场景:模块接口与当前系统不兼容
-
实现方案:
-
私有继承(白盒复用):源代码复用
-
对象组合(黑盒复用):二进制复用
适配你期望的接口
标准模板库STL中的容器queue和stack是在容器deque的基础上施加了一些相应的操作约束,因而本质上并不是容器,而是容器的deque的适配器。
在标准模板库中类似的还有迭代器的适配器insert_iterator/front_insert_iterator/
back_insert_iterator等等。
与桥接模式的区别:
• 适配器模式是将一种接口转换为另外一种接口,
• 桥接模式是把实现和接口分离,让实现和接口独立的变化。
代理模式(Proxy)
-
在代理模式中,两个对象参与处理同一个请求,代理把接收到的请求,委托给真实的对象处理。
-
代理控制请求,代理在客户端和真实对象之间,起到桥梁的作用。
- 代理模式使代理对象和真实对象分离开,在代理对象内可以做一些业务逻辑,比如权限检查、加锁、记录日志。
- 这样一来,代理和真实对象的功能被彻底分离开,各自完成自己的业务。
- 代理和真实对象,实现了相同的接口,是面向接口的编程。
isector* p = new sector_proxy(‘全部A股’);
isector_list* subs = p->get_sub_sectors();
sector_proxy负责检查权限,记录日志,然后取到板块数据。
行为型模式
观察者模式(Observer)
-
又称为发布订阅模式
-
定义了一种一对多的依赖关系:多个观察者同时监听某一个主题对象的变化
-
主题对象的变化能够及时通知到所有的观察者(以便它们能够及时做出响应)
应用场景非常广泛
-
GUI应用界面设置(字体、主题、多语种等)的变更
-
监视目录/文件的变更
-
衍生行情数据的计算
-
……
分布式发布订阅机制
分布式系统的基石 -- ZooKeeper
组合模式(Composite)
-
组合模式让用户对单个对象和组合对象的使用,具有一致性。
-
模糊简单元素和复杂元素的概念。客户程序可以像处理简单元素一样来处理复杂元素。
-
解耦客户端程序和复杂元素的内部实现。
示例
-
文件系统由目录和文件组成,目录可以包含子目录和文件。
-
客户端只需要处理目录和文件两种数据类型,而不必关心目录的底层实现逻辑。
责任链模式(ChainOfResponsibility)
-
第一个对象有个引用,指向下一个对象。下一个对象,又指向下下一个对象。这样,就形成一个链表。
-
客户端请求,在这个链表上进行传递,直到有某个对象处理了这个请求。对象不处理请求的话,就出传递给下一个对象。
-
在不影响客户端的情况下, 责任链模式可以动态增加或减少链中的处理结点。
-
责任链中的每个处理结点,只处理自己应该处理的业务功能,减少模块间的耦合度。
命令模式(Command)
-
命令模式,将客户端的请求封装成一个命令对象,然后把命令对象交给请求处理对象。
-
命令模式作为中间人,分离了客户端请求和处理请求的模块。
-
把业务功能抽出封装成命令对象,等待客户端调用。
-
在合适的时刻,执行命令对象命令可以被撤销,回滚命令可以被反复执行。
-
命令要么成功,要么失败,不能只执行一半。
状态模式
号称策略模式双胞胎的设计模式——状态模式,如它的名字一样,状态模式最核心的设计思路就是
- 将对象的状态抽象出一个接口
- 然后根据它的不同状态封装其行为,这样就可以实现状态和行为的绑定
- 最终实现对象和状态的有效解耦。
下面我们就来详细看下它的基本原理和实现过程吧。
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
状态模式,是对状态的封装,不同状态触发不同的操作
策略模式(Strategy)
总结
-
设计模式虽然多,但每个模式都有自己的特定目标,理解起来也不太难。
-
使用过程中,不要纠结于到底应该用那个模式,不要纠结于模式之间的优劣。
-
写出简单、清晰、易理解、易维护的代码,才是最终的目标。
小测验
选择
《设计模式》小测验
1、设计模式一般用来解决什么样的问题?( A )
A.同一问题的不同表相 B不同问题的同一表相
C.不同问题的不同表相 D.以上都不是
2、下列属于面向对象基本原则的是( C )
A.继承 B.封装 C.里氏代换 D都不是
3、设计模式中“开闭原则”的含义是指一个软件实体应当( A )
A.对扩展开放,对修改关闭. B.对修改开放,对扩展关闭
C.对继承开放,对修改关闭 D.以上都不对
4、当我们想创建一个具体的对象而又不希望指定具体的类时,可以使用( A )模式。
A.工厂模式 B.桥接模式 C.策略模式 D.以上都可以
5、要依赖于抽象,不要依赖于具体。即针对接口编程,不要针对实现编程,是对( D )的表述
A.开-闭原则 B.接口隔离原则
C.里氏代换原则 D.依赖倒转原则
6、依据设计模式思想,程序开发中应优先使用的是( A )关系实现复用。
A.组合 B.继承 C.创建 D.以上都不对
7、设计模式的两大主题是( D )
A.系统的维护与开发 B 对象组合与类的继承
C.系统架构与系统开发 D.系统复用与系统扩展
8、单例模式的基本要点是( AB )
A.构造函数私有 B.唯一实例
C.静态工厂方法 D.以上都不对
9、下列模式中,属于行为模式的是( B )
A.工厂模式 B.观察者 C.适配器 D.以上都是
10、“不要和陌生人说话” 是( D )原则的通俗表述
A.接口隔离 B.里氏代换
C.依赖倒转 D.迪米特法则
11、构造者的退化模式是通过合并( C )角色完成退化的。
A.抽象产品 B.产品 C.创建者 D.使用者
12、对象适配器模式是( A )原则的典型应用。
A.组合/聚合复用原则 B.里式代换原则
C.依赖倒转原则 D.迪米特法则
13、在观察者模式中,表述错误的是( C )
A.观察者角色的更新是被动的。
B.被观察者可以通知观察者进行更新
C.观察者可以改变被观察者的状态,再由被观察者通知所有观察者依据被观察者的状态进行。
D.又称为发布订阅模式,即多个观察者可以同时监听同一个被观察者的状态变更。
14、对于违反里式代换原则的两个类A和B,可以采用的候选解决方案描述最恰当的是( D )
A.创建一个新的抽象类C,作为两个具体类的超类,将A 和B 共同的行为移动到C 中,从而解决A和B 行为不完全一致的问题。
B.将B到A的继承关系改组成组合关系。
C.区分是“is-a”还是”has-a”。如果是“is-a”,可以使用继承关系,如果是”has-a”应该改成组合或聚合关系
D.以上方案全部错误
15.对于对象组合的描述不恰当的是( D )
A.容器类仅能通过被包含对象的接口来对其进行访问。
B.黑盒复用,封装性好,因为被包含对象的内部细节对外是不可见。
C.通过获取指向其它的具有相同类型的对象引用,可以在运行期间动态地定义(对象的)组合。
D.造成极其严重的依赖关系。
16.关于继承表述错误的是( D )
A.继承是一种通过扩展一个已有对象的实现,从而获得新功能的复用方法。
B.泛化类(超类)可以显式地捕获那些公共的属性和方法。特殊类(子类)则通过附加属性和方法来进行实现的扩展。
C.破坏了封装性,因为这会将父类的实现细节暴露给子类。
D.继承本质上是“白盒复用”,对父类的修改,不会影响到子类。
17.对于依赖倒转的表述错误的是( E )
A.依赖于抽象而不依赖于具体,也就是针对接口编程。
B.依赖倒转的接口并非语法意义上的接口,而是一个类对其他对象进行调用时所知道的方法集合。
D.实现了同一接口的对象,可以在运行期间顺利地进行替换,而且不必知道所使用的对象是哪个实现类的实例。
E.此题没有正确答案。
18.以下哪些问题通过应用设计模式能够解决( AD )
A.指定对象的接口 B.排除软件BUG
C.确定软件的功能都正确实现 D.设计应支持变化
19.面向对象系统中功能复用的最常用技术是( AB )
A.类继承 B.对象组合 C.使用抽象类 D.使用实现类
20.常用的基本设计模式可分为( A )
A.创建型、结构型和行为型 B.对象型、结构型和行为型
C.过程型、结构型和行为型 D.抽象型、接口型和实现型
21.以下关于创建型模式说法正确的是( A )
A.创建型模式关注的是对象的创建
B.创建型模式关注的是功能的实现
C.创建型模式关注的是组织类和对象的常用方法
D.创建型模式关注的是对象间的协作
22.以下属于创建型模式的是( AC )
A.抽象工厂(Abstract Factory)模式
B.组合(Composite)模式
C.单例(Singleton)模式
D.桥接(Bridge)模式
23.以下哪个模式是利用一个对象,快速地生成一批对象( C )
A.抽象工厂(Abstract Factory)模式 B.组合(Composite)模式
C.原型(Prototype)模式 D.桥接(Bridge)模式
24.在不破坏类封装性的基础上,使得类可以同不曾估计到的系统进行交互主要体现在( AD )
A.适配器(Adapte)模式 B. 组合(Composite)模式
C.原型(Prototype)模式 D.桥接(Bridge)模式
25.结构型模式中最体现扩展性的几种模式是( C )
A.适配器(Adapte)模式 B.组合(Composite)模式
C.装饰(Decorator)模式 D.桥接(Bridge)模式
26.行为类模式使用( C )在类间分派行为?
A.接口 B.继承机制 C.对象组合 D.委托
27.以下属于行为对象模式的是( ABCD )
A.模板方法(Template Method)模式 B.迭代器(Iterator)模式
C.命令(Command)模式 D.观察者(Observer)模式
28.封装分布于多个类之间的行为的模式是( C )
A.观察者(Observer)模式 B.迭代器(Iterator)模式
C.访问者(Visitor)模式 D.策略(Strategy)模式
29.Observer(观察者)模式适用于( C )
A.当一个抽象模型存在两个方面,其中一个方面依赖于另一方面,将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
B.当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变时。
C.当一个对象必须通知其它对象,而它又不能假定其它对象是谁。也就是说你不希望这些对象是紧密耦合的。
D.一个对象结构包含很多类对象,它们有不同的接口,而想对这些对象实施一些依赖于其具体类的操作。
30.高级编程语言(Java/C++等)的异常处理机制可理解为哪一种行为模式 ?( C )
A.观察者(Observer)模式 B.迭代器(Iterator)模式
C.职责链(Chain of Responsibility)模式 D.策略(Strategy)模式
编码
时钟
观察者模式
题目1:设计一个时钟提醒程序:
- 有一个时钟源每秒能够周期提供定时信号指示当前时间。
- 该程序能够实时显示数字时钟,并能够在用户指定的时间能进行日程提醒(文字提示、对话框或者邮件提醒等)。
人机交互
命令模式
题目2:设计一个人机交互Shell,能够通过键盘输入执行匹配的预设命令,能够通过简单扩展增加新的命令扩展功能。
压缩字符串
状态模式
给定一个压缩字符串,字符串压缩规则如下:
- 压缩字符串中连续重复出现的子串, 压缩格式为"字符重复的次数(被压缩字符串)",数字为压缩因子。
- 原字符串中不包含数字、左括号和右括号。
- 括号中除了包含被压缩字符串,还可以进一步包含压缩字符串。
请编写程序输出解压后的原字符串。
示例 1:
输入: "3c(xy) "
输出: 压缩格式非法
示例 2:
输入: "3(xy3(de)dd"
输出: 压缩格式非法
示例 3:
输入: "3(x)6(y)z"
输出: "xxxyyyyyyz"
示例 4:
输入: "xy2(a2(bc)d)ef"
输出: "xyabcbcdabcbcdef"
package cn.com.wind;
import java.util.Scanner;
/**
* @Project_Name Wind.2022Java.hli.lihao
* @Author LH
* @Date 2022/8/23 17:47
* @TODO:字符串解压
* @Thinking:
* 1、压缩字符串中连续重复出现的子串, 压缩格式为"字符重复的次数(被压缩字符串)",数字为压缩因子。
* 2、原字符串中不包含数字、左括号和右括号。
* 3、括号中除了包含被压缩字符串,还可以进一步包含压缩字符串。
*/
public class DecompressionOfString {
/**状态模式-关注字符串本身的状态(1.格式(非法,或正常),2.内容等状态):
* 将复杂的问题状态,拆分为一个一个的小问题状态,分别去解决。
* 1.Decompress当识别出除了数字,大小写和'('和')'以外的情况时,则提示字符串输入字符非法
* 2.当识别到Decompress,左右括号不能一一对应,则提示输入括号非法
* 3.当识别到Decompress,不能对应格式为"字符重复的次数(被压缩字符串)",则提示输入的格式错误
* 4.
*/
private static String decompressString;
static StringBuilder resultSb = new StringBuilder();
/**
* 思路:
* 状态校验完毕后再实现
* 1.遍历输入的字符串
* 2.识别括号前的数:次数s
* 3.如果再去识别'('如果在寻找')'的过程中
* 4.又出现了一个'('记录出现的次数n,对应就要找第n个')'的位置
* 5.然后将括号中的内容识别出来,给Decompress赋值
* 6.append(Decompress),s次
* 7.最终打印sb.toString;
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入解压字符串,例如:3(x)6(y)z");
decompressString = scanner.nextLine();
// decompressString = "3(x)6(y)z";
//识别是否存在其它非法字符
if(!checkString()){
System.out.println("输入字符非法! " + decompressString);
return;
}
//校验括号,是否可以一一对应
if (!checkBrackets()){
System.out.println("输入括号非法! " + decompressString);
return;
}
handle();
//蕴含嵌套则一直重复执行
while (!checkIsResult()){
//处理方法
handle();
}
System.out.println(resultSb.toString());
}
/**
* 实际处理追加字符的方法
*/
private static void handle(){
int num = -1,local = 0,left = -1,right = -1,s = 0;
//遍历识别字符串
for (int i = 0; i < decompressString.length(); i++) {
if(decompressString.charAt(i) == '('){
//重复次数
num = Integer.parseInt(decompressString.substring(local,i));
left = i;
//s用来记录如果中途还有(则累加一次,对应到)则需要找第s个)了
while (decompressString.charAt(i) != ')') i++;
right = i;
local = i + 1;
add(num,decompressString.substring(left,right + 1));
}
}
//后面还有字符再追加
if (decompressString.length() > local){
resultSb.append(decompressString.substring(local,decompressString.length()));
}
//处理完成就变成当前处理的结果
decompressString = resultSb.toString();
}
/**
* 校验当前结果集是否为不含嵌套的最终答案
* @return boolean
*/
private static boolean checkIsResult(){
String result = resultSb.toString();
for (int i = 0; i < result.length(); i++) {
if (result.charAt(i) == '(') return false;
}
return true;
}
/**
* 单个字符追加方法
* @param value 追加次数
* @param str 追加字符串
*/
private static void add(Integer value, String str) {
for (int i = 0; i < value; i++) {
resultSb.append(str.substring(1,str.length() - 1));
}
}
/**
* 识别出是否存在除了数字,大小写和'('和')'以外的情况
*/
private static boolean checkString() {
for (int i = 0; i < decompressString.length(); i++) {
if (decompressString.charAt(i) == '(' || decompressString.charAt(i) == ')' ||
decompressString.charAt(i) >= 'a' && decompressString.charAt(i) <= 'z'
|| decompressString.charAt(i) <= '9' && decompressString.charAt(i) >= '0'
){
}
//其余返回false
else {
return false;
}
}
return true;
}
/**
* 左右括号不能一一对应
* 则提示输入括号非法
*/
private static boolean checkBrackets() {
int left = 0,right = 0;
for (int i = 0; i < decompressString.length(); i++) {
if(decompressString.charAt(i) == '('){
left += 1;
}else if (decompressString.charAt(i) == ')'){
right += 1;
}
}
//差值是否为0
return left - right == 0 ? true : false;
}
}
总结
更多推荐
Java基础——进阶
发布评论