多线程的发展史,起始就是一个压榨资源,提升效率的战斗史,促进它发展的根本动力其实在于各种资源(磁盘、内存、网络、CPU)的运行速度不平衡而造成的资源浪费。
站在地主老财的角度,如何让长工们给我种地赚钱 - 陈松
线程(thread):
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB
。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)
描述。TCB
包括以下信息:
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。线程
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
举例论证—为什么要使用多线程?
我们的程序都是一条执行流,一步一步的执行。但其实这种程序对我们计算机的资源的使用上是低效的。例如:
多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的,采用多线程的方式去同时完成几件事情,而不互相干扰。
从程序运行效率的角度来看:单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,从而降低程序整体的效率。
但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了!比方说:
远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。
多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
这是另外一个没有这么明显的优点了。假设有一个大的任务A:
通俗的描述就是
多线程并不是提升整体的工作效率
•同步完成多项任务
•提高资源使用效率
•多线程和多进程
控制方向的反转
线程池中线程数 <= CPU核数 + 2的时候,无需缓存切换则效率最高。
•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加锁。在读取票数执行的时候,则需要保证写线程阻塞。
不追寻细腻化程度
多任务处理是指:
当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一件任务提供服务。因为任务调度机制保证不同任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程。
看起来是多个任务都在做,本质上我们的大脑同一时间依旧只做了一件事情。
进程(Process)
:
在操作系统中运行的程序就是进程,比如你的QQ,播放器,游戏,IDE…
线程
是程序中执行的线程。Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。
Thread
对象时:
当Java虚拟机启动时
通常有一个非守护进程线程(通常调用某些指定类的名为main
的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:
Runtime
类的exit
方法,并且安全管理器已经允许进行退出操作。run
方法还是抛出超出run
方法的run
创建一个新的执行线程有两种方法。 一个是将一个类声明为Thread
的子类。 这个子类应该重写run
类的方法Thread
。 然后可以分配并启动子类的实例。
有三种创建线程的方法:
实现 Runnable
和 Callable
接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。
需要实现接口中的 run() 方法。
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
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实例,且将类对象放入才可以调用线程方法
与 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());
}
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable
接口。
public
//implements Runnable
class Thread implements Runnable {
/* Make sure registerNatives is the first thing does. */
private static native void registerNatives();
static {
registerNatives();
}
当调用 start()
方法启动一个线程时:
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
实现思路
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
实现思路(了解)
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
实现的好处
但过程稍微复杂了一些。
WebDownloader.class
,并写出下载方法downloader()
Thread
类创建)run()
方法—线程运行体start()
方法,一起启动执行1.引入jar包(最好在maven项目中直接引入jar包)
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.6version>
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.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程1.jpg");
TestTread2 t2 = new TestTread2("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程2.jpg");
TestTread2 t3 = new TestTread2("https://www.baidu.com/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.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程1.jpg");
TestTread2 t2 = new TestTread2("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程2.jpg");
TestTread2 t3 = new TestTread2("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png","线程3.jpg");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
下载了文件名为线程2.jpg的文件!
下载了文件名为线程3.jpg的文件!
下载了文件名为线程1.jpg的文件!
输出执行顺序不一——>说明不是顺序而是并发执行
总结:通过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() | 暂停当前正在执行的线程对象,并执行其他线程。 |
思路
实现代码
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
setPriority()
setPriority(int newPriority) | 更改线程的优先级。 |
---|---|
sleep()
sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
---|---|
[sleep](…/…/java/lang/Thread.html#sleep(long, int))(long millis, int nanos) | 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
1s = 1000millis
interruptedException
就绪状态
模拟倒计时
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
join()
也称为插队方法(强制执行)
join() | 等待该线程终止。 |
---|---|
join(long millis) | 等待该线程终止的时间最长为 millis 毫秒。 |
[join](…/…/java/lang/Thread.html#join(long, int))(long millis, int nanos) | 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 |
插队示例代码
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次
isAlive()
isAlive() | 测试线程是否处于活动状态。 |
---|---|
yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
也称之为线程礼让
礼让案例实现代码
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线程完成执行
为什么会出现礼让失败的情况呢?
静态代理模式
结婚案例实现思路
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
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
注!
线程中断或者结束,一旦进去死亡状态就无法再次启用!
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码:
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)
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
/**
* Tests whether the current thread has been interrupted. The
* interrupted status 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).
*
* 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 true
if the current thread has been interrupted;
* false
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 的 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.
*
*
* Sample Usage (Note that the following classes are all
* made-up.)
*
{@code
* interface ArchiveSearcher { String search(String target); }
* class App {
* ExecutorService executor = ...
* ArchiveSearcher searcher = ...
* void showSearch(final String target)
* throws InterruptedException {
* Future future
* = executor.submit(new Callable() {
* public String call() {
* return searcher.search(target);
* }});
* displayOtherThings(); // do other things while searching
* try {
* displayText(future.get()); // use future
* } catch (ExecutionException ex) { cleanup(); return; }
* }
* }}
*
* 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:
* {@code
* FutureTask future =
* new FutureTask(new Callable() {
* public String call() {
* return searcher.search(target);
* }});
* executor.execute(future);}
*
* Memory consistency effects: Actions taken by the asynchronous computation
* happen-before
* actions following the corresponding {@code Future.get()} in another thread.
*
* @see FutureTask
* @see Executor
* @since 1.5
* @author Doug Lea
* @param 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.
*
* 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);
/**
* 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.
*
* First the checkAccess
method of this thread is called
* with no arguments. This may result in throwing a
* SecurityException
.
*
* Otherwise, the priority of this thread is set to the smaller of
* the specified newPriority
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 MIN_PRIORITY
to
* MAX_PRIORITY
.
* @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一下,效果优先级顺序又明显一些,这是为什么呢?
用户线程
和守护线程
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
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
:
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
,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可。存在如下问题:
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
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
总结:
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
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。例如join()等方法
wait()
notify()
notifyAll()
调用 wait()
使得线程等待某个条件满足:
notify()
或者 notifyAll()
来唤醒挂起的线程。 唤醒正在等待改对象监视器的单个线程。如果有任何线程在等待这个对象,其中一个线程将被唤醒。这一选择是任意的,并且发生在实现的自由裁量权上。线程通过调用一个等待方法来等待对象的监视器。
在当前线程放弃该对象上的锁之前,被唤醒的线程将无法继续运行。被唤醒的线程将以通常的方式与任何其它可能正在竞争同步这个对象的线程竞争;
wait()
notify()
notifyAll()
它们都属于 Object
的一部分,而不属于 Thread
。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException
。
使用 wait()
挂起期间:
notify()
或者 notifyAll()
来唤醒挂起的线程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张票
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();
}
}
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.创建资源类,定义属性和操作方法
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相互转换
原因:
中断
和虚假唤醒
是可能的,而且此方法始终在循环中使用因此发生了虚假唤醒问题。
2.原因
那么为什么会发生虚假唤醒?
那是因为**wait有个特点,从哪里等待就从哪里继续执行**
当上述案例,线程小蓝要加1
此时线程小蓝要加1进入等待队列争夺CPU时间片
但是还是没有拿到时间片执行
此时线程小蓝蓝要加1此时已经正确执行完毕,此时num = 1
了
接着就按例调用this.notifyAll()
方法唤醒所有线程
此时小蓝要加1线程又被唤醒了
但是它这次很幸运,抢到了CPU分配的时间片执行
执行了接下来的num++
操作,此时num = 2
3.解决方法
那么知道了原因此时要如何解决?
方法一:保持
num = 0
wait()
方法notifyAll()
才能唤醒notifyAll()
了一定已经执行完+1了,num = 1
了方法二:也就是将接下来的步骤放入到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
...
//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张票!
//发现问题:当多个线程操作同一个资源的情况下:会出现线程不安全!数据紊乱!
当多个线程去并发执行的时候,会出现同一张票被两个或以上个人抢的,那么票也太惨了,撕碎的票肯定过不了检查的!那么怎么办呢?
实际代码:
/**
* @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是不安全的,但是为什么它是不安全的呢?
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)
为什么会出现并发修改异常?
ArrayList
源码发现,其并没有加上synchronized
等安全处理操作通过Vector
解决
List<String> list = new Vector();
通过Collections
解决
通过CopyOnWriteArrayList
解决
一、通过
Vector
来解决
1.示例代码
public class ArrayListUnsafe {
public static void main(String[] args) {
//1.创建集合
// List list = new ArrayList();
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.
*
* 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 list = new ArrayList();
// List 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 list = new ArrayList();
// List list = new Vector();
// List list = Collections.synchronizedList(new ArrayList());
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.好处
这样操作的好处就是既照顾了并发读,也维护了独立写
在线程安全的前提下,进一步提升了效率
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<>();
即可解决并发问题
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++;
}
多线程三大特性
可见性、有序性、原子性(较难实现)
线程:程序内部不同的分支
启动应用程序流程
所以:进程可以看成是一个静态的概念,线程才是真正的动态概念
进程是分配资源的基本单位,线程也是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锁,实际上就是给对象的头添加锁信息。
加了锁实际上就是从原来的多线程并发执行,变成了互斥的序列化执行(一次只能是一个线程进去执行)。
在起了多个线程并不加锁的时候,将j(初始值=0)不断加上9999次,可想而知正确的结果是10000
但是实际上在不断启动新线程,导致多个线程执行相同任务后,那么在原本执行操作结果是10000的过程中,不断创建新的线程,在之前正在加载此数据的旧线程正在改变提交值时候,半道也被其它线程修改了相关任务的值,导致值被多次改变,出现结果与预期的误差。
那么:怎么解决多线程抢夺任务执行造成的误差问题呢?
答案:就是加锁呀!但是怎么加才合理呢?刚开始的从Java6版本前一直使用的是原本操作系统沿用的互斥锁(悲观锁),这样虽然可以解决此问题,但是没有其它线程跟我抢,我也加,这样就会造成CPU资源的浪费,哪怕实现了效率也是低下的。
那么请问:有没有什么办法在多线程环境下保证值的正确性还要在此基础之上提高它的效率呢?
此时:
乐观锁就出现了
JUC
import java.util.concurrent.*;
CAS:也就是乐观锁(别称)的实现方式!
乐观锁也称自旋锁
也就是它总是认为没有其它的线程会跟我抢,只是在执行任务改变值之后在提交的时候,才去判断一下,值此时是否出现了改变,如果有改变就证明有其它线程跟我抢了,并且领先我一步完成任务并且提交,那么此时任务实际上已经完成了,我就不需要再去将它的值再改变。
例如:i++过程
i的初始值为0
1.将i++变成1
2.再到内存中修改相应值
加上CAS(乐观锁实现)就是:
先不上锁,在读取改变值时候先判断值是否=0,
是再将它在内存中的值置换为1;不是就放弃操作
问题
博文地址:https://www.cnblogs.com/wyq178/p/8965615.html
也就是读取和需要判断修改的时候,值都是同一个值,但是其中被修改成别的值过。
也就是读取的时候是A,中间被其它线程改成了B,但是最后再我修改值之后进内存置换判断之前
其它线程又将它改回成了A,这时你读取的还是A但是其中,在你操作过程中它被改变过哦。
如果只是值类型的改变,并无大碍,但是要是改变的是引用类型就会
要了解什么是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多次执行的问题。
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
总共两种,一种就是添加记录版本来判断是否被修改,可查看修改次数,另一种就是用boolean类型,修改就改变不记录。
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,单核就不会加了,总不能自己执行的操作,被自己影响吧。
不是原子性就是一条它中间会分成很多段,这样就容易被其它的CPU或者线程所打断!
所以CAS操作本身就必须具备原子性,其实CAS本身,在CPU中就有原始的指令来支持它。
是不是CAS一定就比悲观锁效率高呢?
不是,要看线程执行的时间,等待线程数量的多和少
什么时候用CAS,什么时候用悲观锁?
具体要看不是,要看线程执行的时间,等待线程数量的多和少
1.线程执行时间短,等待线程数量相对少,用CAS来实现,效率确实要高
2.否则还是用悲观排队锁,至少那么多的线程排队它不会去消耗CPU资源对吧
分析流程
小明要上厕所
1.使用悲观锁的情况:
小明进坑,用悲观锁把门锁上了,外面的小红、小黄等线程一直在外排列队列中等待,等到轮到某个线程时CPU会唤醒它,其实这个等待并不消耗CPU资源。
2.但使用乐观锁就不一样了,乐观锁还有一个别称就是自旋锁。一但小明进坑上锁后,外面等待的线程就会一直执行操作然后最终提交判断有没有改变,也可以想象成外面的小红和小黄等线程一直提着裤子转来转去,等着小明开锁,这个过程是有消耗CPU资源的哦!
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.com/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.net/u010191243/article/details/22993573
注意
举个栗子:如何快速获取汉字的拼音首字母
软件开发过程中经常遇到的场景:
经常需要对函数返回null或者0情况的处理时
代码中出现多分支选择时
需要将通用性的功能从其实现细节中分离出来时
提供服务的对象可能有多个来源或者需要控制对对象的访问时
对内容和容器或者对单复数一视同仁时
设计出现类膨胀时(m x n)
需要将行为和数据分离时
……
识别当前软件(架构)中的臭味:
僵化:很难对系统进行改动,每个改动会导致其他模块跟着改动
脆弱:对系统的细微改动可能会导致系统崩溃
耦合:系统各模块相互耦合,很难对系统提炼出可在其他系统中重用的组件
晦涩:系统代码架构、逻辑非常晦涩难懂,系统很难维护
不必要的复杂性:设计找那个包含有不具有任何直接好处的基础结构
不必要的重复:系统中存在大量重复的结构、代码
粘滞:包括软件和环境的粘滞性
……
每一个(建筑)模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次的使用该方案 而不必做重复劳动。
−− Alexander
设计模式就是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
代码看多了,写多了,人会自然而然去总结和思考。这些总结和思考的结果,就是各种设计模式。
使用设计模式的目的,是为了提高代码的可重用性,让代码容易被同行理解、维护和扩展。
设计模式的四要素:
模式名称:模式的助记名,能够尽可能反馈该模式所解决的问题分类。
应用场景:描述了何时比较适合应用该模式,包括问题的前因后果、前提条件等。
解决方案:描述了设计的组成部分,包括他们之间的相互关系、各自的职责和写作方式。
应用效果:描述了该模式的使用效果以及使用该模式需要的权衡考虑等。
设计的基本原则 -- S O L I D
SRP:单一职责原则:设计职责明确
OCP:开放-封闭原则
LSP:里氏替换原则:子类可以无缝替换父类
ISP:接口隔离原则:面向接口编程(站在客户角度反向实现思想)
DIP:依赖倒置原则:抽象依赖细节,细节依赖抽象
引入新功能的时候,往往会从现有代码内,找个功能相近的类,从它继承,然后把新功能追加进去。
随着项目的进展,新功能的扩充,子类和父类就越来越紧密,很难维护。
合理的做法是:
创建型模式 | 单例、工厂、抽象工厂、建造者、原型 | |
---|---|---|
结构型模式 | 适配器、装饰、桥接、代理、外观、组合、享元 | |
行为型模式1 | 观察者、模版方法、命令、状态、责任链 | |
行为型模式2 | 解释器、策略、迭代器、中介、访客、备忘 |
保证某一个类,只有一份实例。
使用者,只能使用这一份实例。
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);
}
}
需要的时候再初始化,但要承担线程可能会不安全的风险(就不是一个实例了)。
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工厂来产生单例就可以了。
注意点:
多线程的应用程序访问和操作单例的全局单例的时候,必须加锁。
•单例的每个方法,必须加锁。
•加锁一般在单例对象内部实现。
锁太多,小心发生死锁。
创建对象的时候,不是直接new出来对象,而是借助一个单独的类,来创建出对象。
目的是:对使用者,屏蔽对象创建过程的复杂性
有些对象很大,依赖其它小对象,大对象直接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;
}
}
注:主要描述对象与对象的关系
显示孙悟空图片:设计SunWuKong子类继承于JPanel类,用于放置孙悟空图片。
快速复制:Java中Object类已经实现了clone
方法(浅拷贝),SunWuKong子类需要实现Clonable
接口。
Linux进程的创建:
使用fork/vfork/clone快速以当前进程(权限、文件打开列表、堆栈大小等)为原型创建出新进程。
让子进程直接运行(自我复制)或者通过exec函数簇执行新的程序体。
电子账单投递:
行业电子账单格式和内容都是类似的
稍作修改就可以生成个性化电子账单
用于创建相同(重复)或者相似的对象,同时又要保证性能
用途
主要是用于解决类膨胀问题
一个例子
网上商城系统要销售电脑,根据类型有台式机,笔记本,平板等;根据品牌又分为联想,苹果,戴尔等。
原始设计图
•缺陷:
可扩展性差
类膨胀: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; // 处理数据
};
把一个类的接口变换成客户端期待的另一种接口。
适配器模式,又被叫做转换器模式、变压器模式、包装(Wrapper)模式。
系统开发好后,为了扩充功能,会向外部暴露出一些插件接口,方便以后把新的功能模块集成进来。
新的功能模块的对外接口,往往和现有系统要求的接口不一致。
应用场景:模块接口与当前系统不兼容
实现方案:
私有继承(白盒复用):源代码复用
对象组合(黑盒复用):二进制复用
适配你期望的接口
标准模板库STL中的容器queue和stack是在容器deque的基础上施加了一些相应的操作约束,因而本质上并不是容器,而是容器的deque的适配器。
在标准模板库中类似的还有迭代器的适配器insert_iterator/front_insert_iterator/
back_insert_iterator等等。
与桥接模式的区别:
• 适配器模式是将一种接口转换为另外一种接口,
• 桥接模式是把实现和接口分离,让实现和接口独立的变化。
在代理模式中,两个对象参与处理同一个请求,代理把接收到的请求,委托给真实的对象处理。
代理控制请求,代理在客户端和真实对象之间,起到桥梁的作用。
isector* p = new sector_proxy(‘全部A股’);
isector_list* subs = p->get_sub_sectors();
sector_proxy负责检查权限,记录日志,然后取到板块数据。
又称为发布订阅模式
定义了一种一对多的依赖关系:多个观察者同时监听某一个主题对象的变化
主题对象的变化能够及时通知到所有的观察者(以便它们能够及时做出响应)
应用场景非常广泛
GUI应用界面设置(字体、主题、多语种等)的变更
监视目录/文件的变更
衍生行情数据的计算
……
分布式发布订阅机制
分布式系统的基石 -- ZooKeeper
组合模式让用户对单个对象和组合对象的使用,具有一致性。
模糊简单元素和复杂元素的概念。客户程序可以像处理简单元素一样来处理复杂元素。
解耦客户端程序和复杂元素的内部实现。
文件系统由目录和文件组成,目录可以包含子目录和文件。
客户端只需要处理目录和文件两种数据类型,而不必关心目录的底层实现逻辑。
责任链模式(ChainOfResponsibility)
第一个对象有个引用,指向下一个对象。下一个对象,又指向下下一个对象。这样,就形成一个链表。
客户端请求,在这个链表上进行传递,直到有某个对象处理了这个请求。对象不处理请求的话,就出传递给下一个对象。
在不影响客户端的情况下, 责任链模式可以动态增加或减少链中的处理结点。
责任链中的每个处理结点,只处理自己应该处理的业务功能,减少模块间的耦合度。
命令模式,将客户端的请求封装成一个命令对象,然后把命令对象交给请求处理对象。
命令模式作为中间人,分离了客户端请求和处理请求的模块。
把业务功能抽出封装成命令对象,等待客户端调用。
在合适的时刻,执行命令对象命令可以被撤销,回滚命令可以被反复执行。
命令要么成功,要么失败,不能只执行一半。
号称策略模式双胞胎的设计模式——状态模式,如它的名字一样,状态模式最核心的设计思路就是
下面我们就来详细看下它的基本原理和实现过程吧。
状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
状态模式,是对状态的封装,不同状态触发不同的操作
设计模式虽然多,但每个模式都有自己的特定目标,理解起来也不太难。
使用过程中,不要纠结于到底应该用那个模式,不要纠结于模式之间的优劣。
写出简单、清晰、易理解、易维护的代码,才是最终的目标。
《设计模式》小测验
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;
}
}