前端技术
HTML
CSS
Javascript
前端框架和UI库
VUE
ReactJS
AngularJS
JQuery
NodeJS
JSON
Element-UI
Bootstrap
Material UI
服务端和客户端
Java
Python
PHP
Golang
Scala
Kotlin
Groovy
Ruby
Lua
.net
c#
c++
后端WEB和工程框架
SpringBoot
SpringCloud
Struts2
MyBatis
Hibernate
Tornado
Beego
Go-Spring
Go Gin
Go Iris
Dubbo
HessianRPC
Maven
Gradle
数据库
MySQL
Oracle
Mongo
中间件与web容器
Redis
MemCache
Etcd
Cassandra
Kafka
RabbitMQ
RocketMQ
ActiveMQ
Nacos
Consul
Tomcat
Nginx
Netty
大数据技术
Hive
Impala
ClickHouse
DorisDB
Greenplum
PostgreSQL
HBase
Kylin
Hadoop
Apache Pig
ZooKeeper
SeaTunnel
Sqoop
Datax
Flink
Spark
Mahout
数据搜索与日志
ElasticSearch
Apache Lucene
Apache Solr
Kibana
Logstash
数据可视化与OLAP
Apache Atlas
Superset
Saiku
Tesseract
系统与容器
Linux
Shell
Docker
Kubernetes
[Java参数传递机制]的搜索结果
这里是文章列表。热门标签的颜色随机变换,标签颜色没有特殊含义。
点击某个标签可搜索标签相关的文章。
点击某个标签可搜索标签相关的文章。
Java
Java中的值传递和引用传递是个啥玩意儿? 嗨,大家好!今天咱们来聊聊Java里的一个经典问题——值传递和引用传递。这事儿我以前也是一头雾水,但经过一番探索,终于有点眉目了。现在就让我们一起深入了解一下吧! 1. 值传递和引用传递的基础概念 首先,咱们得明白这两个概念到底是什么意思。 - 值传递(Pass by Value):在方法调用时,实际参数的值被复制一份,传递给形式参数。方法内部对形式参数的操作不会影响到实际参数。 - 引用传递(Pass by Reference):在方法调用时,传递的是实际参数的引用(即内存地址),方法内部通过这个引用可以访问到实际参数的内容。因此,方法内部对参数的修改会影响到实际参数。 2. Java中到底是值传递还是引用传递? Java中的参数传递机制其实挺简单的,那就是所有的参数都是按值传递的。但是这里的“值”有点特殊,对于对象类型的参数,传递的是对象的引用。因此,我们可以说Java是按值传递,但传递的是对象引用的副本。 举个栗子: java public class Main { public static void main(String[] args) { String str = "Hello"; changeString(str); System.out.println(str); // 输出 "Hello" StringBuilder sb = new StringBuilder("Hello"); changeStringBuilder(sb); System.out.println(sb.toString()); // 输出 "Changed" } public static void changeString(String s) { s = "Changed"; } public static void changeStringBuilder(StringBuilder sb) { sb.append(" Changed"); } } 在这个例子中,changeString方法尝试改变str的值,但由于字符串是不可变的,所以实际上并没有改变。在changeStringBuilder方法里,虽然传入的是StringBuilder对象的引用,但实际上你在方法里面对它的修改会反映到外面的那个实际参数上。换句话说,你就是在直接操作那个原本的对象,所以任何改动都会在外面体现出来。 3. 理解背后的原理 为啥会有这种现象呢?这得从JVM的工作机制说起。在Java里,像int和double这样的基本类型就直接存数值,但对象就不一样了,它们住在堆内存这片大天地里,而你声明的变量其实存的是一个指针,指向那个对象所在的地址。所以啊,在调用方法的时候,基本类型的数据就像传递钞票一样,直接给一份拷贝过去;而对象类型的数据则是传递一个指向这个数据的地址,类似于给你一张地图,告诉你东西放在哪儿。 这个过程就像你在厨房里烤蛋糕,如果我把一块蛋糕给你,你吃掉它并不会影响到我的蛋糕。要是我把蛋糕店的地图给你,让你去买一块新鲜出炉的蛋糕,那你拿回来我就有口福了,可以美美地吃上一口。 4. 实际开发中的应用 了解这些概念对我们实际编程有什么帮助呢?首先,这有助于我们更好地理解代码的行为。比如说,当我们想改变某个对象的状态时,就得把对象的引用递给函数,而不是它的具体值。这样我们才能真正地修改原对象,而不是弄出个新对象来。其次,这也提醒我们在编写代码时要注意副作用,尤其是在处理共享资源时。 举个例子,如果你在多线程环境中操作同一个对象,那么你需要特别小心,确保线程安全。否则,可能会出现意想不到的问题。 结语 好了,今天的分享就到这里啦!希望这篇文章能帮到你理解Java中的值传递和引用传递。记得,理论知识要结合实践,多写代码才能真正掌握这些概念。如果你有任何疑问或者想讨论的话题,欢迎随时留言交流哦! 加油,码农们!
2025-01-20 15:57:53
116
月下独酌_
Groovy
...ovy , 一种基于Java虚拟机的动态脚本语言,继承了Java的语法结构同时增加了许多新特性,如动态类型、闭包支持以及本文提到的方法参数传递方式。Groovy的设计目标是在保留Java强大功能的同时提升开发效率,特别适合用于快速开发、测试驱动开发以及构建复杂的脚本任务。文中提到Groovy通过按值传递和按引用传递等方式实现了灵活的方法参数处理,并且支持可变参数和默认参数值,这些特性显著提升了代码的可读性和简洁性。 按引用传递 , 一种参数传递机制,当方法接收到的是对象引用而不是对象本身时,对该引用的操作会直接影响到原始对象的状态。在Groovy中,由于对象本质上是以引用形式存储的,因此当我们传递一个对象到方法中并对该对象的属性进行修改时,这种修改会在方法外部可见。例如文中提到的Person类实例,在modifyPerson方法内对其name属性的更改会同步反映到原始对象上,这是因为Groovy直接操作的是对象的内存地址。 可变参数 , 一种允许方法接受不定数量参数的功能,通常表现为方法签名中的最后一个参数被声明为数组类型。在Groovy中,使用可变参数可以让方法适应不同数量的输入,从而避免了为各种可能的情况单独定义多个重载方法的需求。例如文中展示的sum方法,它可以通过接收任意数量的数字参数并计算它们的总和,极大地提高了代码的通用性和复用率。这种特性对于处理动态数据集尤其有用。
2025-03-15 15:57:01
101
林中小径
Struts2
... Struts2中“Java.lang.NullPointerException in Action class while executing method 'execute'”问题解析与解决方案 在我们深入使用Struts2框架进行Java Web开发时,偶尔会遇到一种常见的运行时异常——Java.lang.NullPointerException,尤其在Action类执行execute方法时。这篇东西,咱们就来点儿接地气的,从实际动手干的视角,一边瞅着代码实例,一边掰扯这个问题是怎么冒出来的、怎么把它摆平的,还有怎样提前给它上个“紧箍咒”,预防它再出来闹腾。 1. 异常现象分析 首先,让我们通过一个示例来直观感受一下这个问题。假设我们有一个简单的Struts2 Action类: java public class UserAction extends ActionSupport { private UserService userService; // 这是一个依赖注入的对象 public String execute() { User user = userService.getUserById(1); // 假设这里调用服务层获取用户信息 // ... 其他业务逻辑 return SUCCESS; } // getter 和 setter 方法省略... } 当执行上述execute方法时,如果出现NullPointerException,则意味着在执行userService.getUserById(1)这行代码时,userService对象未被正确初始化,其值为null。 2. 问题根源探究 原因一:依赖注入失败 在Struts2中,我们通常利用框架的依赖注入功能来实现Action和Service之间的解耦。就像刚才举的例子那样,如果咱们没有给userService这个家伙喂饱饭(也就是没有正确注入它),或者在喂饭的过程中出了岔子,那么到执行execute方法的时候,userService就会变成一个空肚子(null),这样一来,就难免会闹肚子(引发异常)了。 原因二:实例化时机不当 另一种可能的情况是,尽管在配置文件中设置了依赖注入,但可能由于某些原因(例如配置错误或加载顺序问题),导致注入的服务对象尚未初始化完成,此时访问也会抛出空指针异常。 3. 解决方案及示例 解决方案一:确保依赖注入生效 在Struts2的配置文件中(通常是struts.xml),我们需要明确指定Action类中需要注入的属性和服务对象的关系: xml /success.jsp userServiceBean 解决方案二:检查并修正实例化顺序 如果确认了依赖注入配置无误,但仍出现空指针异常,则应检查应用启动过程中相关Bean的加载顺序,确保在Action类执行execute方法之前,所有依赖的对象已经成功初始化。 解决方案三:防御性编程 无论何种情况,我们在编码时都应当遵循防御性编程原则,对可能为null的对象进行判空处理: java public class UserAction extends ActionSupport { private UserService userService; public String execute() { if (userService != null) { // 防御性判空 User user = userService.getUserById(1); // ... 其他业务逻辑 } else { System.out.println("userService is not initialized correctly!"); // 打印日志或采取其他容错处理 } return SUCCESS; } // getter 和 setter 方法省略... } 4. 总结与思考 面对“Java.lang.NullPointerException in Action class while executing method 'execute'”这样的问题,我们需要从多方面进行排查和解决。不仅仅是对Struts2框架的依赖注入机制了如指掌,更要像侦探一样时刻保持警惕,做好咱们的防御性编程工作。为啥呢?这就像是给程序穿上防弹衣,能有效防止那些突如其来的运行时异常搞崩我们的程序,让程序稳稳当当地跑起来,不尥蹶子。在实际做项目的时候,把这些技巧学懂了、用溜了,那咱们的开发速度和代码质量绝对会嗖嗖往上涨,没跑儿!
2023-06-26 11:07:11
69
青春印记
转载文章
...令存在语法错误,包括参数数量,参数名等等(编译器错误)。 在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。 2.4.2 在执行 exec 之后发生错误 比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。 最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下, 只有错误的命令没有被执行,但是其他命令没有受到影响。 这个显然不符合我们对原子性的定义,也就是我们没办法用 Redis 的这种事务机制来实现原子性,保证数据的一致。 3、Lua脚本 Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类似。 使用 Lua 脚本来执行 Redis 命令的好处: 1、一次发送多个命令,减少网络开销。 2、Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。 3、对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。 3.1 在Redis中调用Lua脚本 使用 eval /ɪ’væl/ 方法,语法格式: redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....] eval代表执行Lua语言的命令。 lua-script代表Lua语言脚本内容。 key-num表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。 [key1key2key3…]是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来。 [value1 value2 value3 …]这些参数传递给 Lua 语言,它们是可填可不填的。 示例,返回一个字符串,0 个参数: redis> eval "return 'Hello World'" 0 3.2 在Lua脚本中调用Redis命令 使用 redis.call(command, key [param1, param2…])进行操作。语法格式: redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value command是命令,包括set、get、del等。 key是被操作的键。 param1,param2…代表给key的参数。 注意跟 Java 不一样,定义只有形参,调用只有实参。 Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。 3.2.1 设置键值对 在 Redis 中调用 Lua 脚本执行 Redis 命令 redis> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 gupao 2673 redis> get gupao 以上命令等价于 set gupao 2673。 在 redis-cli 中直接写 Lua 脚本不够方便,也不能实现编辑和复用,通常我们会把脚本放在文件里面,然后执行这个文件。 3.2.2 在 Redis 中调用 Lua 脚本文件中的命令,操作 Redis 创建 Lua 脚本文件: cd /usr/local/soft/redis5.0.5/src vim gupao.lua Lua 脚本内容,先设置,再取值: cd /usr/local/soft/redis5.0.5/src redis-cli --eval gupao.lua 0 得到返回值: root@localhost src] redis-cli --eval gupao.lua 0 "lua666" 3.2.3 案例:对 IP 进行限流 需求:在 X 秒内只能访问 Y 次。 设计思路:用 key 记录 IP,用 value 记录访问次数。 拿到 IP 以后,对 IP+1。如果是第一次访问,对 key 设置过期时间(参数 1)。否则判断次数,超过限定的次数(参数 2),返回 0。如果没有超过次数则返回 1。超过时间, key 过期之后,可以再次访问。 KEY[1]是 IP, ARGV[1]是过期时间 X,ARGV[2]是限制访问的次数 Y。 -- ip_limit.lua-- IP 限流,对某个 IP 频率进行限制 ,6 秒钟访问 10 次 local num=redis.call('incr',KEYS[1])if tonumber(num)==1 thenredis.call('expire',KEYS[1],ARGV[1])return 1elseif tonumber(num)>tonumber(ARGV[2]) thenreturn 0 elsereturn 1 end 6 秒钟内限制访问 10 次,调用测试(连续调用 10 次): ./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 6 10 app:ip:limit:192.168.8.111 是 key 值 ,后面是参数值,中间要加上一个空格和一个逗号,再加上一个空格 。 即:./redis-cli –eval [lua 脚本] [key…]空格,空格[args…] 多个参数之间用一个空格分割 。 代码:LuaTest.java 3.2.4 缓存 Lua 脚本 为什么要缓存 在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端, 会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。 如何缓存 Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script. Please use EVAL.” 127.0.0.1:6379> script load "return 'Hello World'" "470877a599ac74fbfda41caa908de682c5fc7d4b"127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0 "Hello World" 3.2.5 自乘案例 Redis 有 incrby 这样的自增命令,但是没有自乘,比如乘以 3,乘以 5。我们可以写一个自乘的运算,让它乘以后面的参数: local curVal = redis.call("get", KEYS[1]) if curVal == false thencurVal = 0 elsecurVal = tonumber(curVal)endcurVal = curVal tonumber(ARGV[1]) redis.call("set", KEYS[1], curVal) return curVal 把这个脚本变成单行,语句之间使用分号隔开 local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal script load ‘命令’ 127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal' "be4f93d8a5379e5e5b768a74e77c8a4eb0434441" 调用: 127.0.0.1:6379> set num 2OK127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6 (integer) 12 3.2.6 脚本超时 Redis 的指令执行本身是单线程的,这个线程还要执行客户端的 Lua 脚本,如果 Lua 脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢? eval 'while(true) do end' 0 为了防止某个脚本执行时间过长导致 Redis 无法提供服务,Redis 提供了 lua-time-limit 参数限制脚本的最长运行时间,默认为 5 秒钟。 lua-time-limit 5000(redis.conf 配置文件中) 当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。 Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端: script kill 如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的。 127.0.0.1:6379> eval "redis.call('set','gupao','666') while true do end" 0 因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。 127.0.0.1:6379> script kill(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the scripttermination or kill the server in a hard way using the SHUTDOWN NOSAVE command. 遇到这种情况,只能通过 shutdown nosave 命令来强行终止 redis。 shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。 4、Redis 为什么这么快? 4.1 Redis到底有多快? 根据官方的数据,Redis 的 QPS 可以达到 10 万左右(每秒请求数)。 4.2 Redis为什么这么快? 总结:1)纯内存结构、2)单线程、3)多路复用 4.2.1 内存 KV 结构的内存数据库,时间复杂度 O(1)。 第二个,要实现这么高的并发性能,是不是要创建非常多的线程? 恰恰相反,Redis 是单线程的。 4.2.2 单线程 单线程有什么好处呢? 1、没有创建线程、销毁线程带来的消耗 2、避免了上线文切换导致的 CPU 消耗 3、避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等 4.2.3 异步非阻塞 异步非阻塞 I/O,多路复用处理并发连接。 4.3 Redis为什么是单线程的? 不是白白浪费了 CPU 的资源吗? 因为单线程已经够用了,CPU 不是 redis 的瓶颈。Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。 4.4 单线程为什么这么快? 因为 Redis 是基于内存的操作,我们先从内存开始说起。 4.4.1 虚拟存储器(虚拟内存 Vitual Memory) 名词解释:主存:内存;辅存:磁盘(硬盘) 计算机主存(内存)可看作一个由 M 个连续的字节大小的单元组成的数组,每个字节有一个唯一的地址,这个地址叫做物理地址(PA)。早期的计算机中,如果 CPU 需要内存,使用物理寻址,直接访问主存储器。 这种方式有几个弊端: 1、在多用户多任务操作系统中,所有的进程共享主存,如果每个进程都独占一块物理地址空间,主存很快就会被用完。我们希望在不同的时刻,不同的进程可以共用同一块物理地址空间。 2、如果所有进程都是直接访问物理内存,那么一个进程就可以修改其他进程的内存数据,导致物理地址空间被破坏,程序运行就会出现异常。 为了解决这些问题,我们就想了一个办法,在 CPU 和主存之间增加一个中间层。CPU 不再使用物理地址访问,而是访问一个虚拟地址,由这个中间层把地址转换成物理地址,最终获得数据。这个中间层就叫做虚拟存储器(Virtual Memory)。 具体的操作如下所示: 在每一个进程开始创建的时候,都会分配一段虚拟地址,然后通过虚拟地址和物理地址的映射来获取真实数据,这样进程就不会直接接触到物理地址,甚至不知道自己调用的哪块物理地址的数据。 目前,大多数操作系统都使用了虚拟内存,如 Windows 系统的虚拟内存、Linux 系统的交换空间等等。Windows 的虚拟内存(pagefile.sys)是磁盘空间的一部分。 在 32 位的系统上,虚拟地址空间大小是 2^32bit=4G。在 64 位系统上,最大虚拟地址空间大小是多少? 是不是 2^64bit=10241014TB=1024PB=16EB?实际上没有用到 64 位,因为用不到这么大的空间,而且会造成很大的系统开销。Linux 一般用低 48 位来表示虚拟地址空间,也就是 2^48bit=256T。 cat /proc/cpuinfo address sizes : 40 bits physical, 48 bits virtual 实际的物理内存可能远远小于虚拟内存的大小。 总结:引入虚拟内存,可以提供更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单。并且可以对物理内存进行隔离,不同的进程操作互不影响。还可以通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享。 4.4.2 用户空间和内核空间 为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space)/ˈkɜːnl /,一部分是用户空间(User-space)。 内核是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中,都是对物理地址的映射。 在 Linux 系统中, 内核进程和用户进程所占的虚拟内存比例是 1:3。 当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。 进程在内核空间以执行任意命令,调用系统的一切资源;在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。 top 命令: us 代表 CPU 消耗在 User space 的时间百分比; sy 代表 CPU 消耗在 Kernel space 的时间百分比。 4.4.3 进程切换(上下文切换) 多任务操作系统是怎么实现运行远大于 CPU 数量的任务个数的? 当然,这些任务实际上并不是真的在同时运行,而是因为系统通过时间片分片算法,在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。 为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。 什么叫上下文? 在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好 CPU 寄存器和程序计数器(ProgramCounter),这个叫做 CPU 的上下文。 而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。 在切换上下文的时候,需要完成一系列的工作,这是一个很消耗资源的操作。 4.4.4 进程的阻塞 正在运行的进程由于提出系统服务请求(如 I/O 操作),但因为某种原因未得到操作系统的立即响应,该进程只能把自己变成阻塞状态,等待相应的事件出现后才被唤醒。 进程在阻塞状态不占用 CPU 资源。 4.4.5 文件描述符 FD Linux 系统将所有设备都当作文件来处理,而 Linux 用文件描述符来标识每个文件对象。 文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行 I/O 操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件。 Linux 系统里面有三个标准文件描述符。 0:标准输入(键盘); 1:标准输出(显示器); 2:标准错误输出(显示器)。 4.4.6 传统 I/O 数据拷贝 以读操作为例: 当应用程序执行 read 系统调用读取文件描述符(FD)的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。(两次拷贝,两次 user 和 kernel 的上下文切换)。 I/O 的阻塞到底阻塞在哪里? 4.4.7 Blocking I/O 当使用 read 或 write 对某个文件描述符进行过读写时,如果当前 FD 不可读,系统就不会对其他的操作做出响应。从设备复制数据到内核缓冲区是阻塞的,从内核缓冲区拷贝到用户空间,也是阻塞的,直到 copy complete,内核返回结果,用户进程才解除 block 的状态。 为了解决阻塞的问题,我们有几个思路。 1、在服务端创建多个线程或者使用线程池,但是在高并发的情况下需要的线程会很多,系统无法承受,而且创建和释放线程都需要消耗资源。 2、由请求方定期轮询,在数据准备完毕后再从内核缓存缓冲区复制数据到用户空间 (非阻塞式 I/O),这种方式会存在一定的延迟。 能不能用一个线程处理多个客户端请求? 4.4.8 I/O 多路复用(I/O Multiplexing) I/O 指的是网络 I/O。 多路指的是多个 TCP 连接(Socket 或 Channel)。 复用指的是复用一个或多个线程。它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。 客户端在操作的时候,会产生具有不同事件类型的 socket。在服务端,I/O 多路复用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。 多路复用有很多的实现,以 select 为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有 socket,当任何一个 socket 的数据准备好了,多路复用器就会返回。这时候用户进程再调用 read 操作,把数据从内核缓冲区拷贝到用户空间。 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select() 函数就可以返回。 Redis 的多路复用, 提供了 select, epoll, evport, kqueue 几种选择,在编译的时 候来选择一种。 evport 是 Solaris 系统内核提供支持的; epoll 是 LINUX 系统内核提供支持的; kqueue 是 Mac 系统提供支持的; select 是 POSIX 提供的,一般的操作系统都有支撑(保底方案); 源码 ae_epoll.c、ae_select.c、ae_kqueue.c、ae_evport.c 5、内存回收 Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回 收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory) 触发内存淘汰。 5.1 过期策略 要实现 key 过期,我们有几种思路。 5.1.1 定时过期(主动淘汰) 每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的 数据,从而影响缓存的响应时间和吞吐量。 5.1.2 惰性过期(被动淘汰) 只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。 例如 String,在 getCommand 里面会调用 expireIfNeeded server.c expireIfNeeded(redisDb db, robj key) 第二种情况,每次写入 key 时,发现内存不够,调用 activeExpireCycle 释放一部分内存。 expire.c activeExpireCycle(int type) 5.1.3 定期过期 源码:server.h typedef struct redisDb { dict dict; / 所有的键值对 /dict expires; / 设置了过期时间的键值对 /dict blocking_keys; dict ready_keys; dict watched_keys; int id;long long avg_ttl;list defrag_later; } redisDb; 每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。 Redis 中同时使用了惰性过期和定期过期两种过期策略。 5.2 淘汰策略 Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。 5.2.1 最大内存设置 redis.conf 参数配置: maxmemory <bytes> 如果不设置 maxmemory 或者设置为 0,64 位系统不限制内存,32 位系统最多使用 3GB 内存。 动态修改: redis> config set maxmemory 2GB 到达最大内存以后怎么办? 5.2.2 淘汰策略 https://redis.io/topics/lru-cache redis.conf maxmemory-policy noeviction 先从算法来看: LRU,Least Recently Used:最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。 LFU,Least Frequently Used,最不常用,4.0 版本新增。 random,随机删除。 如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random、 volatile-ttl 相当于 noeviction(不做内存回收)。 动态修改淘汰策略: redis> config set maxmemory-policy volatile-lru 建议使用 volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的 key。 5.2.3 LRU 淘汰原理 问题:如果基于传统 LRU 算法实现 Redis LRU 会有什么问题? 需要额外的数据结构存储,消耗内存。 Redis LRU 对传统的 LRU 算法进行了改良,通过随机采样来调整算法的精度。如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个), 随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。所以采样参数m配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低。 问题:如何找出热度最低的数据? Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值。 但是不是获取系统当前的时间戳,而是设置为全局变量 server.lruclock 的值。 源码:server.h typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS;int refcount;void ptr; } robj; server.lruclock 的值怎么来的? Redis 中有个定时处理的函数 serverCron,默认每 100 毫秒调用函数 updateCachedTime 更新一次全局变量的 server.lruclock 的值,它记录的是当前 unix 时间戳。 源码:server.c void updateCachedTime(void) { time_t unixtime = time(NULL); atomicSet(server.unixtime,unixtime); server.mstime = mstime();struct tm tm; localtime_r(&server.unixtime,&tm);server.daylight_active = tm.tm_isdst; } 问题:为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗? 这样函数 lookupKey 中更新数据的 lru 热度值时,就不用每次调用系统函数 time,可以提高执行效率。 OK,当对象里面已经有了 LRU 字段的值,就可以评估对象的热度了。 函数 estimateObjectIdleTime 评估指定对象的 lru 热度,思想就是对象的 lru 值和全局的 server.lruclock 的差值越大(越久没有得到更新),该对象热度越低。 源码 evict.c / Given an object returns the min number of milliseconds the object was never requested, using an approximated LRU algorithm. /unsigned long long estimateObjectIdleTime(robj o) {unsigned long long lruclock = LRU_CLOCK(); if (lruclock >= o->lru) {return (lruclock - o->lru) LRU_CLOCK_RESOLUTION; } else {return (lruclock + (LRU_CLOCK_MAX - o->lru)) LRU_CLOCK_RESOLUTION;} } server.lruclock 只有 24 位,按秒为单位来表示才能存储 194 天。当超过 24bit 能表 示的最大时间的时候,它会从头开始计算。 server.h define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) / Max value of obj->lru / 在这种情况下,可能会出现对象的 lru 大于 server.lruclock 的情况,如果这种情况 出现那么就两个相加而不是相减来求最久的 key。 为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。而 Redis LRU 算法在 sample 为 10 的情况下,已经能接近传统 LRU 算法了。 问题:除了消耗资源之外,传统 LRU 还有什么问题? 如图,假设 A 在 10 秒内被访问了 5 次,而 B 在 10 秒内被访问了 3 次。因为 B 最后一次被访问的时间比 A 要晚,在同等的情况下,A 反而先被回收。 问题:要实现基于访问频率的淘汰机制,怎么做? 5.2.4 LFU server.h typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS;int refcount;void ptr; } robj; 当这 24 bits 用作 LFU 时,其被分为两部分: 高 16 位用来记录访问时间(单位为分钟,ldt,last decrement time) 低 8 位用来记录访问频率,简称 counter(logc,logistic counter) counter 是用基于概率的对数计数器实现的,8 位可以表示百万次的访问频率。 对象被读写的时候,lfu 的值会被更新。 db.c——lookupKey void updateLFU(robj val) {unsigned long counter = LFUDecrAndReturn(val); counter = LFULogIncr(counter);val->lru = (LFUGetTimeInMinutes()<<8) | counter;} 增长的速率由,lfu-log-factor 越大,counter 增长的越慢 redis.conf 配置文件。 lfu-log-factor 10 如果计数器只会递增不会递减,也不能体现对象的热度。没有被访问的时候,计数器怎么递减呢? 减少的值由衰减因子 lfu-decay-time(分钟)来控制,如果值是 1 的话,N 分钟没有访问就要减少 N。 redis.conf 配置文件 lfu-decay-time 1 6、持久化机制 https://redis.io/topics/persistence Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis 提供了两种持久化的方案,一种是 RDB 快照(Redis DataBase),一种是 AOF(Append Only File)。 6.1 RDB RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢复数据。 什么时候写入 rdb 文件? 6.1.1 RDB 触发 1、自动触发 a)配置规则触发。 redis.conf, SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。 如果不需要 RDB 方案,注释 save 或者配置成空字符串""。 save 900 1 900 秒内至少有一个 key 被修改(包括添加) save 300 10 400 秒内至少有 10 个 key 被修改save 60 10000 60 秒内至少有 10000 个 key 被修改 注意上面的配置是不冲突的,只要满足任意一个都会触发。 RDB 文件位置和目录: 文件路径,dir ./ 文件名称dbfilename dump.rdb 是否是LZF压缩rdb文件 rdbcompression yes 开启数据校验 rdbchecksum yes 问题:为什么停止 Redis 服务的时候没有 save,重启数据还在? RDB 还有两种触发方式: b)shutdown 触发,保证服务器正常关闭。 c)flushall,RDB 文件是空的,没什么意义(删掉 dump.rdb 演示一下)。 2、手动触发 如果我们需要重启服务或者迁移数据,这个时候就需要手动触 RDB 快照保存。Redis 提供了两条命令: a)save save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果内存中的数据比较多,会造成 Redis 长时间的阻塞。生产环境不建议使用这个命令。 为了解决这个问题,Redis 提供了第二种方式。 执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求。 具体操作是 Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在 fork 阶段,一般时间很短。 用 lastsave 命令可以查看最近一次成功生成快照的时间。 6.1.2 RDB 数据的恢复(演示) 1、shutdown 持久化添加键值 添加键值 redis> set k1 1 redis> set k2 2 redis> set k3 3 redis> set k4 4 redis> set k5 5 停服务器,触发 save redis> shutdown 备份 dump.rdb 文件 cp dump.rdb dump.rdb.bak 启动服务器 /usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf 啥都没有: redis> keys 3、通过备份文件恢复数据停服务器 redis> shutdown 重命名备份文件 mv dump.rdb.bak dump.rdb 启动服务器 /usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf 查看数据 redis> keys 6.1.3 RDB 文件的优势和劣势 一、优势 1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。 2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。 3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。 二、劣势 1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。 2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。 如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。 6.2 AOF Append Only File AOF:Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。 Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。 6.2.1 AOF 配置 配置文件 redis.conf 开关appendonly no 文件名appendfilename "appendonly.aof" AOF 文件的内容(vim 查看): 问题:数据都是实时持久化到磁盘吗? 由于操作系统的缓存机制,AOF 数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到 AOF 文件? 问题:文件越来越大,怎么办? 由于 AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。 例如 set xxx 666,执行 1000 次,结果都是 xxx=666。 为了解决这个问题,Redis 新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。 可以使用命令 bgrewriteaof 来重写。 AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。 重写触发机制 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb 问题:重写过程中,AOF 文件被更改了怎么办? 另外有两个与 AOF 相关的参数: 6.2.2 AOF 数据恢复 重启 Redis 之后就会进行 AOF 文件的恢复。 6.2.3 AOF 优势与劣势 优点: 1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。 缺点: 1、对于具有相同数据的的 Redis,AOF 文件通常会比 RDB 文件体积更大(RDB 存的是数据快照)。 2、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证。 6.3 两种方案比较 那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢? 如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。 否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。 本篇文章为转载内容。原文链接:https://blog.csdn.net/zhoutaochun/article/details/120075092。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2024-03-18 12:25:04
541
转载
转载文章
...n.net/grd_java/article/details/113116244。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。 网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 如果右边没有就找找左边 上一节:JUC锁,一些面试题和源码讲解 1、引用 java引用共4种,强软弱虚 强引用:我们普通的new一个对象,就是强引用,只有它指向为空了,或者已经没用了,才会被回收 软引用:JVM内存不够了,就回收软引用 弱引用:只要碰见垃圾回收器(System.gc()),就被回收 虚引用:对象当被回收时,会将其放在队列中 1、软引用 / 软引用 软引用是用来描述一些还有用但并非必须的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。 -Xmx20M/import java.lang.ref.SoftReference;public class T02_SoftReference {public static void main(String[] args) {SoftReference<byte[]> m = new SoftReference<>(new byte[1024102410]);//创建软引用,分配10M//m = null;System.out.println(m.get());//获取System.gc();//垃圾回收try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(m.get());//再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用干掉byte[] b = new byte[1024102415];System.out.println(m.get());} }//软引用非常适合缓存使用 2、弱引用 public class M {@Overrideprotected void finalize() throws Throwable {System.out.println("finalize");} } 上图中,tl对象强引用指向ThreadLocal,map中key弱引用指向ThreadLocal,当tl=null时,强引用消失,此时弱引用也将自动被回收,但是此时key=null,value指向10M这个就永远访问不到,既内存泄露 下图中,18行到20行为解决内存泄露问题的,那就是通过remove()将它消除了 / 弱引用遭到gc就会回收/import java.lang.ref.WeakReference;public class T03_WeakReference {public static void main(String[] args) {WeakReference<M> m = new WeakReference<>(new M());System.out.println(m.get());System.gc();System.out.println(m.get());ThreadLocal<M> tl = new ThreadLocal<>();tl.set(new M());tl.remove();} } 3、虚引用 虚引用 虚引用不是给开发人员用的,一般是给写JVM(java虚拟机,没有它java程序运行不了),Netty等技术大牛用的 虚引用,对象当被回收时,会将其放在队列中,此时我们监听到队列中有新值了,就知道有虚引用被回收了 此时我们要做相应的处理,虚引用指向的值,是无法直接get()获取的 虚引用使用场景 一般情况(其它情况暂时没什么用),虚引用指向堆外内存(直接被操作系统管理的内存),JVM无法对其回收 当虚引用对象被回收时,JVM的垃圾回收无法自动回收堆外内存, 但是此时,虚引用对象被回收,会将其放在队列中 操作人员,看到队列中有对象被回收,就进行相应操作,回收堆内存 如何回收堆外内存 C和C++有函数可以用 java现在也提供了Unsafe类可以操作堆外内存,具体请参考上一篇博客,总之,JDK1.8只能通过反射来用,JDK1.9以上可以通过new Unsafe对象来用 Unsafe类的方法有: copyMemory():直接访问内存 allocateMemory():直接分配内存,这就必须手动回收内存了 freeMemory():回收内存 下面是一个虚引用例子,自己看吧,懂得自然懂,现在看不懂的,先收藏或者保存上,以后回来看 / 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响, 也无法通过虚引用来获取一个对象的实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象, 那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null, 弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。 jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存, 而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念), 所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后, 会在堆内存分配一个对象保存这个堆外内存的引用, 这个对象被垃圾收集器管理,一旦这个对象被回收, 相应的用户线程会收到通知并对直接内存进行清理工作。 事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放, DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。/import java.lang.ref.PhantomReference;import java.lang.ref.Reference;import java.lang.ref.ReferenceQueue;import java.util.LinkedList;import java.util.List;public class T04_PhantomReference {private static final List<Object> LIST = new LinkedList<>();private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();public static void main(String[] args) {PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);new Thread(() -> {while (true) {LIST.add(new byte[1024 1024]);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();Thread.currentThread().interrupt();}System.out.println(phantomReference.get());} }).start();new Thread(() -> {while (true) {Reference<? extends M> poll = QUEUE.poll();if (poll != null) {System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);} }}).start();try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();} }} 2、容器 1、发展历史(一定要了解) map容器你需要了解的历史 JDK早期,java提供了Vector和Hashtable两个容器,这两个容器,很多操作都加了锁Synchronized,对于某些不需要用锁的情况下,就显得十分影响性能,所以现在基本没人用这两个容器,但是面试经常问这两个容器里面的数据结构等内容 后来,出现了HashMap,此容器完全不加锁,是用的最多的容器 但是完全不加锁未免不完善,所以java提供了如下方式,将HashMap变为加锁的 //通过Collections.synchronizedMap(HashMap)方法,将其变为加锁Map集合,其中泛型随意,UUID只是举例。static Map<UUID, UUID> m = Collections.synchronizedMap(new HashMap<UUID, UUID>()); 通过阅读源码发现,上面方法将HashMap变为加锁,也是使用Synchronized,只是锁的内容更细,但并不比HashTable效率高多少 所以衍生除了新的容器ConcurrentHashMap ConcurrentHashMap 此容器,插入效率不如上面的,因为它做了各种判断和CAS,但是差距不是特别大 读取效率很高,100个线程同时访问,每个线程读取一百万次实测 Hashtable 39s ,SynchronizedHashMap 38s ,ConcurrentHashMap 1.7s 前两个将近40秒,ConcurrentHashMap只需要不到2s,由此可见此容器读取效率极高 2、为什么推荐使用Queue来做高并发 为什么推荐Queue(队列) Queue接口提供了很多针对多线程非常友好的API(offer ,peek和poll,其中BlockingQueue还添加了put和take可以阻塞),可以说专门为多线程高并发而创造的接口,所以一般我们使用Queue而不用List 以下代码分别使用链表LinkList和ConcurrentQueue,对比一下速度 LinkList用了5s多,ConcurrentQueue几乎瞬间完成 Concurrent接口就是专为多线程设计,多线程设计要多考虑Queue(高并发用)的使用,少使用List / 有N张火车票,每张票都有一个编号 同时有10个窗口对外售票 请写一个模拟程序 分析下面的程序可能会产生哪些问题? 重复销售?超量销售? 使用Vector或者Collections.synchronizedXXX 分析一下,这样能解决问题吗? 就算操作A和B都是同步的,但A和B组成的复合操作也未必是同步的,仍然需要自己进行同步 就像这个程序,判断size和进行remove必须是一整个的原子操作 @author 马士兵/import java.util.LinkedList;import java.util.List;import java.util.concurrent.TimeUnit;public class TicketSeller3 {static List<String> tickets = new LinkedList<>();static {for(int i=0; i<1000; i++) tickets.add("票 编号:" + i);}public static void main(String[] args) {for(int i=0; i<10; i++) {new Thread(()->{while(true) {synchronized(tickets) {if(tickets.size() <= 0) break;try {TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("销售了--" + tickets.remove(0));} }}).start();} }} 队列 import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;public class TicketSeller4 {static Queue<String> tickets = new ConcurrentLinkedQueue<>();static {for(int i=0; i<1000; i++) tickets.add("票 编号:" + i);}public static void main(String[] args) {for(int i=0; i<10; i++) {new Thread(()->{while(true) {String s = tickets.poll();if(s == null) break;else System.out.println("销售了--" + s);} }).start();} }} 3、多线程常用容器 1、ConcurrentHashMap(无序)和ConcurrentSkipListMap(有序,链表,使用跳表数据结构,让查询更快) 跳表:http://blog.csdn.net/sunxianghuang/article/details/52221913 import java.util.;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentSkipListMap;import java.util.concurrent.CountDownLatch;public class T01_ConcurrentMap {public static void main(String[] args) {Map<String, String> map = new ConcurrentHashMap<>();//Map<String, String> map = new ConcurrentSkipListMap<>(); //高并发并且排序//Map<String, String> map = new Hashtable<>();//Map<String, String> map = new HashMap<>(); //Collections.synchronizedXXX//TreeMapRandom r = new Random();Thread[] ths = new Thread[100];CountDownLatch latch = new CountDownLatch(ths.length);long start = System.currentTimeMillis();for(int i=0; i<ths.length; i++) {ths[i] = new Thread(()->{for(int j=0; j<10000; j++) map.put("a" + r.nextInt(100000), "a" + r.nextInt(100000));latch.countDown();});}Arrays.asList(ths).forEach(t->t.start());try {latch.await();} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println(end - start);System.out.println(map.size());} } 2、CopyOnWriteList(写时复制)和CopyOnWriteSet 适用于,高并发是,读的多,写的少的情况 当我们写的时候,将容器复制,让写线程去复制的线程写(写的时候加锁) 而读线程依旧去读旧的(读的时候不加锁) 当写完,将对象指向复制后的已经写完的容器,原来容器销毁 大大提高读的效率 / 写时复制容器 copy on write 多线程环境下,写时效率低,读时效率高 适合写少读多的环境 @author 马士兵/import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.Random;import java.util.Vector;import java.util.concurrent.CopyOnWriteArrayList;public class T02_CopyOnWriteList {public static void main(String[] args) {List<String> lists = //new ArrayList<>(); //这个会出并发问题!//new Vector();new CopyOnWriteArrayList<>();Random r = new Random();Thread[] ths = new Thread[100];for(int i=0; i<ths.length; i++) {Runnable task = new Runnable() {@Overridepublic void run() {for(int i=0; i<1000; i++) lists.add("a" + r.nextInt(10000));} };ths[i] = new Thread(task);}runAndComputeTime(ths);System.out.println(lists.size());}static void runAndComputeTime(Thread[] ths) {long s1 = System.currentTimeMillis();Arrays.asList(ths).forEach(t->t.start());Arrays.asList(ths).forEach(t->{try {t.join();} catch (InterruptedException e) {e.printStackTrace();} });long s2 = System.currentTimeMillis();System.out.println(s2 - s1);} } 3、synchronizedList和ConcurrentLinkedQueue package com.mashibing.juc.c_025;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Queue;import java.util.concurrent.ConcurrentLinkedQueue;public class T04_ConcurrentQueue {public static void main(String[] args) {List<String> strsList = new ArrayList<>();List<String> strsSync = Collections.synchronizedList(strsList);//加锁ListQueue<String> strs = new ConcurrentLinkedQueue<>();//Concurrent链表队列,就是读快for(int i=0; i<10; i++) {strs.offer("a" + i); //add添加,但是不同点是,此方法会返回一个布尔值}System.out.println(strs);System.out.println(strs.size());System.out.println(strs.poll());//取出,取完后将元素去除System.out.println(strs.size());System.out.println(strs.peek());//取出,但是不会将元素从队列删除System.out.println(strs.size());//双端队列Deque} } 4、LinkedBlockingQueue 链表阻塞队列(无界链表,可以一直装东西,直到内存满(其实,也不是无限,其长度Integer.MaxValue就是上限,毕竟最大就这么大)) 主要体现在put和take方法,put添加的时候,如果队列满了,就阻塞当前线程,直到队列有空位,继续插入。take方法取的时候,如果没有值,就阻塞,等有值了,立马去取 import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.TimeUnit;public class T05_LinkedBlockingQueue {static BlockingQueue<String> strs = new LinkedBlockingQueue<>();static Random r = new Random();public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 100; i++) {try {strs.put("a" + i); //如果满了,当前线程就会等待(实现阻塞),等多会有空位,将值插入TimeUnit.MILLISECONDS.sleep(r.nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();} }}, "p1").start();for (int i = 0; i < 5; i++) {new Thread(() -> {for (;;) {try {System.out.println(Thread.currentThread().getName() + " take -" + strs.take()); //取内容,如果空了,当前线程就会等待(实现阻塞)} catch (InterruptedException e) {e.printStackTrace();} }}, "c" + i).start();} }} 5、ArrayBlockingQueue 有界阻塞队列(因为Array需要指定长度) import java.util.Random;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.concurrent.TimeUnit;public class T06_ArrayBlockingQueue {static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);static Random r = new Random();public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {strs.put("a" + i);}//strs.put("aaa"); //满了就会等待,程序阻塞//strs.add("aaa");//strs.offer("aaa");strs.offer("aaa", 1, TimeUnit.SECONDS);System.out.println(strs);} } 6、特殊的阻塞队列1:DelayQueue 延时队列(按时间进行调度,就是隔多长时间运行,谁隔的少,谁先) 以下例子中,我们添加线程到队列顺序为t12345,正常情况下,会按照顺序运行,但是这里有了延时时间,也就是时间越短,越先执行 步骤很简单,拿到延时队列 指定构造方法 继承 implements Delayed 重写 compareTo和getDelay import java.util.Calendar;import java.util.Random;import java.util.concurrent.BlockingQueue;import java.util.concurrent.DelayQueue;import java.util.concurrent.Delayed;import java.util.concurrent.TimeUnit;public class T07_DelayQueue {static BlockingQueue<MyTask> tasks = new DelayQueue<>();static Random r = new Random();static class MyTask implements Delayed {String name;long runningTime;MyTask(String name, long rt) {this.name = name;this.runningTime = rt;}@Overridepublic int compareTo(Delayed o) {if(this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))return -1;else if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) return 1;else return 0;}@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);}@Overridepublic String toString() {return name + " " + runningTime;} }public static void main(String[] args) throws InterruptedException {long now = System.currentTimeMillis();MyTask t1 = new MyTask("t1", now + 1000);MyTask t2 = new MyTask("t2", now + 2000);MyTask t3 = new MyTask("t3", now + 1500);MyTask t4 = new MyTask("t4", now + 2500);MyTask t5 = new MyTask("t5", now + 500);tasks.put(t1);tasks.put(t2);tasks.put(t3);tasks.put(t4);tasks.put(t5);System.out.println(tasks);for(int i=0; i<5; i++) {System.out.println(tasks.take());//获取的是toString方法返回值} }} 7、特殊的阻塞队列2:PriorityQueque 优先队列(二叉树算法,就是排序) import java.util.PriorityQueue;public class T07_01_PriorityQueque {public static void main(String[] args) {PriorityQueue<String> q = new PriorityQueue<>();q.add("c");q.add("e");q.add("a");q.add("d");q.add("z");for (int i = 0; i < 5; i++) {System.out.println(q.poll());} }} 8、特殊的阻塞队列3:SynchronusQueue 同步队列(线程池用处非常大) 此队列容量为0,当插入元素时,必须同时有个线程往外取 就是说,当你往这个队列里面插入一个元素,它就拿着这个元素站着(阻塞),直到有个取元素的线程来,它就把元素交给它 就是用来同步数据的,也就是线程间交互数据用的一个特殊队列 package com.mashibing.juc.c_025;import java.util.concurrent.BlockingQueue;import java.util.concurrent.SynchronousQueue;public class T08_SynchronusQueue { //容量为0public static void main(String[] args) throws InterruptedException {BlockingQueue<String> strs = new SynchronousQueue<>();new Thread(()->{//这个线程就是消费者,来取值try {System.out.println(strs.take());//和同步队列要值} catch (InterruptedException e) {e.printStackTrace();} }).start();strs.put("aaa"); //阻塞等待消费者消费,就拿着aaa站着,等线程来取//strs.put("bbb");//strs.add("aaa");System.out.println(strs.size());} } 9、特殊的阻塞队列4:TransferQueue 传递队列 此队列加入了一个方法transfer()用来向队列添加元素 但是和put()方法不同的是,put添加完元素就走了 而这个方法,添加完自己就阻塞了,直到有人将这个元素取走,它才继续工作(省去我们手动阻塞) import java.util.concurrent.LinkedTransferQueue;public class T09_TransferQueue {public static void main(String[] args) throws InterruptedException {LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();new Thread(() -> {try {System.out.println(strs.take());} catch (InterruptedException e) {e.printStackTrace();} }).start();strs.transfer("aaa");//放东西到队列,同时阻塞等待消费者线程,取走元素//strs.put("aaa");//如果用put就和普通队列一样,放完东西就走了/new Thread(() -> {try {System.out.println(strs.take());} catch (InterruptedException e) {e.printStackTrace();} }).start();/} } 3、线程池 线程池 由于单独创建线程,十分影响效率,而且无法对线程集中管理,一旦疏落,可能线程无限执行,浪费资源 线程池就是一个存储线程的游泳池,而每个线程就是池子里面的赛道 池子里的线程不执行任何任务,只是提供一个资源 而谁提交了任务,比如我想来游泳,那么池子就给你一个赛道,让你游泳 比如它想练憋气,那么给它一个赛道练憋气 当他们用完,走了,那么后面其它人再过来继续用 这就是线程池,始终只有这几个线程,不做实现,而是借用这几个线程的用户,自己掌控用这些线程资源做什么(提交任务给线程,线程空闲就帮他们完成任务) 线程池的两种类型(两类,不是两个) ThreadPoolExecutor(简称TPE) ForkJoinPool(分解汇总任务(将任务细化,最后汇总结果),少量线程执行多个任务(子任务,TPE做不到先执行子任务),CPU密集型) Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 1、常用类 Executor ExecutorService 扩展了execute方法,具有一个返回值 规定了异步执行机制,提供了一些执行器方法,比如shutdown()关闭等 但是它不知道执行器中的线程何时执行完 Callable 对Runnable进行了扩展,实现Callable的调用,可以有返回值,表示线程的状态 但是无法返回线程执行结果 Future 获得未来线程执行结果 由此,我们可以得知线程池基本的一个使用步骤 其中service.submit():为异步提交,也就是说,主线程该干嘛干嘛,我是异步执行的,和同步不一样(当前线程执行完,主线程才能继续执行,叫同步) futuer.get():获取结果集结果,此时因为异步,主线程执行到这里,结果集可能还没封装好,所以此时如果没有值,就阻塞,直到结果集出来 public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<String> c = new Callable() {@Overridepublic String call() throws Exception {return "Hello Callable";} };ExecutorService service = Executors.newCachedThreadPool();Future<String> future = service.submit(c); //异步System.out.println(future.get());//阻塞service.shutdown();} 2、FutureTask 可充当任务的结果集 上面我们介绍Future是用来得到任务的执行结果的 而FutureTask,可以当做一个任务用,并且返回任务的结果,也就是可以跑线程,然后还可以得到线程结果 public static void main(String[] args) throws InterruptedException, ExecutionException {FutureTask<Integer> task = new FutureTask<>(()->{TimeUnit.MILLISECONDS.sleep(500);return 1000;}); //new Callable () { Integer call();}new Thread(task).start();System.out.println(task.get()); //阻塞} 3、CompletableFuture 非常灵活的任务结果集 一个非常灵活的结果集 他可以将很多执行不同任务的线程的结果进行汇总 比如一个网站,它可以启动多个线程去各大电商网站,比如淘宝,京东,收集某些或某一个商品的价格 最后,将获取的数据进行整合封装 最终,客户就可以通过此网站,获取某类商品在各网站的价格信息 / 假设你能够提供一个服务 这个服务查询各大电商网站同一类产品的价格并汇总展示 @author 马士兵 http://mashibing.com/import java.io.IOException;import java.util.Random;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;public class T06_01_CompletableFuture {public static void main(String[] args) throws ExecutionException, InterruptedException {long start, end;/start = System.currentTimeMillis();priceOfTM();priceOfTB();priceOfJD();end = System.currentTimeMillis();System.out.println("use serial method call! " + (end - start));/start = System.currentTimeMillis();CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(()->priceOfTM());CompletableFuture<Double> futureTB = CompletableFuture.supplyAsync(()->priceOfTB());CompletableFuture<Double> futureJD = CompletableFuture.supplyAsync(()->priceOfJD());CompletableFuture.allOf(futureTM, futureTB, futureJD).join();//当所有结果集都获取到,才汇总阻塞CompletableFuture.supplyAsync(()->priceOfTM()).thenApply(String::valueOf).thenApply(str-> "price " + str).thenAccept(System.out::println);end = System.currentTimeMillis();System.out.println("use completable future! " + (end - start));try {System.in.read();} catch (IOException e) {e.printStackTrace();} }private static double priceOfTM() {delay();return 1.00;}private static double priceOfTB() {delay();return 2.00;}private static double priceOfJD() {delay();return 3.00;}/private static double priceOfAmazon() {delay();throw new RuntimeException("product not exist!");}/private static void delay() {int time = new Random().nextInt(500);try {TimeUnit.MILLISECONDS.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}System.out.printf("After %s sleep!\n", time);} } 4、TPE型线程池1:ThreadPoolExecutor 原理及其参数 线程池由两个集合组成,一个集合存储线程,一个集合存储任务 存储线程:可以规定大小,最多可以有多少个,以及指定核心线程数量(不会被回收) 任务队列:存储任务 细节:初始线程池没有线程,当有一个任务来,线程池起一个线程,又有一个任务来,再起一个线程,直到达到核心线程数量 核心线程数量达到时,新来的任务将存储到任务队列中等待核心线程处理完成,直到任务队列也满了 当任务队列满了,此时再次启动一个线程(非核心线程,一旦空闲,达到指定时间将会消失),直到达到线程最大数量 当线程容器和任务容器都满了,又来了线程,将会执行拒绝策略 上面的细节涉及的所有步骤内容,均由创建线程池的参数执行 下面是ThreadPoolExecutor构造方法参数的源码注释 / 用给定的初始值,创建一个新的线程池 @param corePoolSize 核心线程数量 @param maximumPoolSize 最大线程数量 @param keepAliveTime 当线程数大于核心线程数量时,空闲的线程可生存的时间 @param unit 时间单位 @param workQueue 任务队列,只能包含由execute提交的Runnable任务 @param threadFactory 工厂,用于创建线程给线程池调度的工厂,可以自定义 @param handler 拒绝策略(可以自定义,JDK默认提供4种),当线程边界和队列容量已经满了,新来线程被阻塞时使用的处理程序/public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) JDK提供的4种拒绝策略,不常用,一般都是自己定义拒绝策略 Abort:抛异常 Discard:扔掉,不抛异常 DiscardOldest:扔掉排队时间最久的(将队列中排队时间最久的扔掉,然后让新来的进来) CallerRuns:调用者处理任务(谁通过execute方法提交任务,谁处理) ThreadPoolExecutor继承关系 继承关系:ThreadPoolExecutor->AbstractExectorService类->ExectorService接口->Exector接口 Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 看看下面创建线程池,哪里用到了它 使用实例 import java.io.IOException;import java.util.concurrent.;public class T05_00_HelloThreadPool {static class Task implements Runnable {private int i;public Task(int i) {this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " Task " + i);try {System.in.read();} catch (IOException e) {e.printStackTrace();} }@Overridepublic String toString() {return "Task{" +"i=" + i +'}';} }public static void main(String[] args) {ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,60, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());//创建线程池,核心2个,最大4个,空闲线程存活时间60s,任务队列容量4,使用默认线程工程,创建线程。拒绝策略是JDK提供的for (int i = 0; i < 8; i++) {tpe.execute(new Task(i));//供提交8次任务}System.out.println(tpe.getQueue());//查看任务队列tpe.execute(new Task(100));//提交新的任务System.out.println(tpe.getQueue());tpe.shutdown();//关闭线程池} } 5、TPE型线程池2:SingleThreadPool 单例线程池(只有一个线程) 为什么有单例线程池 有任务队列,有线程池管理机制 Executors(注意这后面有s) 它可以说是线程池工厂类,我们一般通过它创建线程池,并且它为我们封装了线程 看看下面哪里用到了它 /创建单例线程池,扔5个任务进去,查看输出结果,看看有几个线程执行任务/import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class T07_SingleThreadPool {public static void main(String[] args) {ExecutorService service = Executors.newSingleThreadExecutor();for(int i=0; i<5; i++) {final int j = i;service.execute(()->{System.out.println(j + " " + Thread.currentThread().getName());});} }} 6、TPE型线程池3:CachedPool 缓存,存储线程池 此线程池没有核心线程,来一个任务启动一个线程(最多Integer.MaxValue,不会放在任务队列,因为任务队列容量为0),每个线程空闲后,只能活60s 实例 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class T07_SingleThreadPool {public static void main(String[] args) {ExecutorService service = Executors.newSingleThreadExecutor();//通过Executors获取池子for(int i=0; i<5; i++) {final int j = i;service.execute(()->{//提交任务System.out.println(j + " " + Thread.currentThread().getName());});}service.shutdown();} } 7、TPE型线程池4:FixedThreadPool 固定线程池 此线次池,用于创建一个固定线程数量的线程池,不会回收 实例 import java.util.ArrayList;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;public class T09_FixedThreadPool {public static void main(String[] args) throws InterruptedException, ExecutionException {//并发执行long start = System.currentTimeMillis();getPrime(1, 200000); long end = System.currentTimeMillis();System.out.println(end - start);//输出并发执行耗费时间final int cpuCoreNum = 4;//并行执行ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);MyTask t1 = new MyTask(1, 80000); //1-5 5-10 10-15 15-20MyTask t2 = new MyTask(80001, 130000);MyTask t3 = new MyTask(130001, 170000);MyTask t4 = new MyTask(170001, 200000);Future<List<Integer>> f1 = service.submit(t1);Future<List<Integer>> f2 = service.submit(t2);Future<List<Integer>> f3 = service.submit(t3);Future<List<Integer>> f4 = service.submit(t4);start = System.currentTimeMillis();f1.get();f2.get();f3.get();f4.get();end = System.currentTimeMillis();System.out.println(end - start);//输出并行耗费时间}static class MyTask implements Callable<List<Integer>> {int startPos, endPos;MyTask(int s, int e) {this.startPos = s;this.endPos = e;}@Overridepublic List<Integer> call() throws Exception {List<Integer> r = getPrime(startPos, endPos);return r;} }static boolean isPrime(int num) {for(int i=2; i<=num/2; i++) {if(num % i == 0) return false;}return true;}static List<Integer> getPrime(int start, int end) {List<Integer> results = new ArrayList<>();for(int i=start; i<=end; i++) {if(isPrime(i)) results.add(i);}return results;} } 8、TPE型线程池5:ScheduledPool 预定,延时线程池 根据延时时间(隔多长时间后运行),排序,哪个线程先执行,用户只需要指定核心线程数量 此线程池返回的池对象,和提交任务方法都不一样,比较涉及到时间 import java.util.Random;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class T10_ScheduledPool {public static void main(String[] args) {ScheduledExecutorService service = Executors.newScheduledThreadPool(4);service.scheduleAtFixedRate(()->{//提交延时任务try {TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName());}, 0, 500, TimeUnit.MILLISECONDS);//指定延时时间和单位,第一个任务延时0毫秒,之后的任务,延时500毫秒} } 9、手写拒绝策略小例子 import java.util.concurrent.;public class T14_MyRejectedHandler {public static void main(String[] args) {ExecutorService service = new ThreadPoolExecutor(4, 4,0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6),Executors.defaultThreadFactory(),new MyHandler());//将手写拒绝策略传入}static class MyHandler implements RejectedExecutionHandler {//1、继承RejectedExecutionHandler@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {//2、重写方法//log("r rejected")//伪代码,表示通过log4j.log()报一下日志,拒绝的时间,线程名//save r kafka mysql redis//可以尝试保存队列//try 3 times //可以尝试几次,比如3次,重新去抢队列,3次还不行就丢弃if(executor.getQueue().size() < 10000) {//尝试条件,如果size>10000了,就执行拒绝策略//try put again();//如果小于10000,尝试将其放到队列中} }} } 10、ForkJoinPool线程池1:ForkJoinPool 前面我们讲过线程分为两大类,TPE和FJP ForkJoinPool(分解汇总任务(将任务细化,最后汇总结果),少量线程执行多个任务(子任务,TPE做不到先执行子任务),CPU密集型) 适合将大任务切分成多个小任务运行 两个方法,fork():分子任务,将子任务分配到线程池中 join():当前任务的计算结果,如果有子任务,等子任务结果返回后再汇总 下面实例实现,一百万个随机数求和,由两种方法实现,一种ForkJoinPool分任务并行,一种使用单线程做 import java.io.IOException;import java.util.Arrays;import java.util.Random;import java.util.concurrent.ForkJoinPool;import java.util.concurrent.RecursiveAction;import java.util.concurrent.RecursiveTask;public class T12_ForkJoinPool {//1000000个随机数求和static int[] nums = new int[1000000];//一堆数static final int MAX_NUM = 50000;//分任务时,每个任务的操作量不能多于50000个,否则就继续细分static Random r = new Random();//使用随机数将数组初始化static {for(int i=0; i<nums.length; i++) {nums[i] = r.nextInt(100);}System.out.println("---" + Arrays.stream(nums).sum()); //stream api 单线程就这么做,一个一个加}//分任务,需要继承,可以继承RecursiveAction(不需要返回值,一般用在不需要返回值的场景)或//RecursiveTask(需要返回值,我们用这个,因为我们需要最后获取求和结果)两个更好实现的类,//他俩继承与ForkJoinTaskstatic class AddTaskRet extends RecursiveTask<Long> {private static final long serialVersionUID = 1L;int start, end;AddTaskRet(int s, int e) {start = s;end = e;}@Overrideprotected Long compute() {if(end-start <= MAX_NUM) {//如果任务操作数小于规定的最大操作数,就进行运算,long sum = 0L;for(int i=start; i<end; i++) sum += nums[i];return sum;//返回结果} //如果分配的操作数大于规定,就继续细分(简单的重中点分,两半)int middle = start + (end-start)/2;//获取中间值AddTaskRet subTask1 = new AddTaskRet(start, middle);//传入起始值和中间值,表示一个子任务AddTaskRet subTask2 = new AddTaskRet(middle, end);//中间值和结尾值,表示一个子任务subTask1.fork();//分任务subTask2.fork();//分任务return subTask1.join() + subTask2.join();//最后返回结果汇总} }public static void main(String[] args) throws IOException {/ForkJoinPool fjp = new ForkJoinPool();AddTask task = new AddTask(0, nums.length);fjp.execute(task);/ForkJoinPool fjp = new ForkJoinPool();//创建线程池AddTaskRet task = new AddTaskRet(0, nums.length);//创建任务fjp.execute(task);//传入任务long result = task.join();//返回汇总结果System.out.println(result);//System.in.read();} } 11、ForkJoinPool线程池2:WorkStealingPool 任务偷取线程池 原来的线程池,都是有一个任务队列,而这个不同,它给每个线程都分配了一个任务队列 当某一个线程的任务队列没有任务,并且自己空闲,它就去其它线程的任务队列中偷任务,所以叫任务偷取线程池 细节:当线程自己从自己的任务队列拿任务时,不需要加锁,但是偷任务时,因为有两个线程,可能发生同步问题,需要加锁 此线程继承FJP 实例 import java.io.IOException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;public class T11_WorkStealingPool {public static void main(String[] args) throws IOException {ExecutorService service = Executors.newWorkStealingPool();System.out.println(Runtime.getRuntime().availableProcessors());service.execute(new R(1000));service.execute(new R(2000));service.execute(new R(2000));service.execute(new R(2000)); //daemonservice.execute(new R(2000));//由于产生的是精灵线程(守护线程、后台线程),主线程不阻塞的话,看不到输出System.in.read(); }static class R implements Runnable {int time;R(int t) {this.time = t;}@Overridepublic void run() {try {TimeUnit.MILLISECONDS.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(time + " " + Thread.currentThread().getName());} }} 12、流式API:ParallelStreamAPI 不懂的请参考:https://blog.csdn.net/grd_java/article/details/110265219 实例 import java.util.ArrayList;import java.util.List;import java.util.Random;public class T13_ParallelStreamAPI {public static void main(String[] args) {List<Integer> nums = new ArrayList<>();Random r = new Random();for(int i=0; i<10000; i++) nums.add(1000000 + r.nextInt(1000000));//System.out.println(nums);long start = System.currentTimeMillis();nums.forEach(v->isPrime(v));long end = System.currentTimeMillis();System.out.println(end - start);//使用parallel stream apistart = System.currentTimeMillis();nums.parallelStream().forEach(T13_ParallelStreamAPI::isPrime);//并行流,将任务切分成子任务执行end = System.currentTimeMillis();System.out.println(end - start);}static boolean isPrime(int num) {for(int i=2; i<=num/2; i++) {if(num % i == 0) return false;}return true;} } 13、总结 总结 Callable相当于一Runnable但是它有返回值 Future:存储执行完产生的结果 FutureTask 相当于Future+Runnable,既可以执行任务,又能获取任务执行的Future结果 CompletableFuture 可以多任务异步,并对多任务控制,整合任务结果,细化完美,比如可以一个任务完成就可以整合结果,也可以所有任务完成才整合结果 4、ThreadPoolExecutor源码解析 依然只讲重点,实际还需要大家按照上篇博客中看源码的方式来看 1、常用变量的解释 // 1. ctl,可以看做一个int类型的数字,高3位表示线程池状态,低29位表示worker数量private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// 2. COUNT_BITS,Integer.SIZE为32,所以COUNT_BITS为29private static final int COUNT_BITS = Integer.SIZE - 3;// 3. CAPACITY,线程池允许的最大线程数。1左移29位,然后减1,即为 2^29 - 1private static final int CAPACITY = (1 << COUNT_BITS) - 1;// runState is stored in the high-order bits// 4. 线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATEDprivate static final int RUNNING = -1 << COUNT_BITS;private static final int SHUTDOWN = 0 << COUNT_BITS;private static final int STOP = 1 << COUNT_BITS;private static final int TIDYING = 2 << COUNT_BITS;private static final int TERMINATED = 3 << COUNT_BITS;// Packing and unpacking ctl// 5. runStateOf(),获取线程池状态,通过按位与操作,低29位将全部变成0private static int runStateOf(int c) { return c & ~CAPACITY; }// 6. workerCountOf(),获取线程池worker数量,通过按位与操作,高3位将全部变成0private static int workerCountOf(int c) { return c & CAPACITY; }// 7. ctlOf(),根据线程池状态和线程池worker数量,生成ctl值private static int ctlOf(int rs, int wc) { return rs | wc; }/ Bit field accessors that don't require unpacking ctl. These depend on the bit layout and on workerCount being never negative./// 8. runStateLessThan(),线程池状态小于xxprivate static boolean runStateLessThan(int c, int s) {return c < s;}// 9. runStateAtLeast(),线程池状态大于等于xxprivate static boolean runStateAtLeast(int c, int s) {return c >= s;} 2、构造方法 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {// 基本类型参数校验if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();// 空指针校验if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;// 根据传入参数unit和keepAliveTime,将存活时间转换为纳秒存到变量keepAliveTime 中this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;} 3、提交执行task的过程 public void execute(Runnable command) {if (command == null)throw new NullPointerException();/ Proceed in 3 steps: 1. If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task. The call to addWorker atomically checks runState and workerCount, and so prevents false alarms that would add threads when it shouldn't, by returning false. 2. If a task can be successfully queued, then we still need to double-check whether we should have added a thread (because existing ones died since last checking) or that the pool shut down since entry into this method. So we recheck state and if necessary roll back the enqueuing if stopped, or start a new thread if there are none. 3. If we cannot queue task, then we try to add a new thread. If it fails, we know we are shut down or saturated and so reject the task./int c = ctl.get();// worker数量比核心线程数小,直接创建worker执行任务if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// worker数量超过核心线程数,任务直接进入队列if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();// 线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()操作。// 这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。if (! isRunning(recheck) && remove(command))reject(command);// 这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。// 这儿有3点需要注意:// 1. 线程池不是运行状态时,addWorker内部会判断线程池状态// 2. addWorker第2个参数表示是否创建核心线程// 3. addWorker返回false,则说明任务执行失败,需要执行reject操作else if (!addWorker(command, false))reject(command);} 4、addworker源码解析 private boolean addWorker(Runnable firstTask, boolean core) {retry:// 外层自旋for (;;) {int c = ctl.get();int rs = runStateOf(c);// 这个条件写得比较难懂,我对其进行了调整,和下面的条件等价// (rs > SHUTDOWN) || // (rs == SHUTDOWN && firstTask != null) || // (rs == SHUTDOWN && workQueue.isEmpty())// 1. 线程池状态大于SHUTDOWN时,直接返回false// 2. 线程池状态等于SHUTDOWN,且firstTask不为null,直接返回false// 3. 线程池状态等于SHUTDOWN,且队列为空,直接返回false// Check if queue empty only if necessary.if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;// 内层自旋for (;;) {int wc = workerCountOf(c);// worker数量超过容量,直接返回falseif (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;// 使用CAS的方式增加worker数量。// 若增加成功,则直接跳出外层循环进入到第二部分if (compareAndIncrementWorkerCount(c))break retry;c = ctl.get(); // Re-read ctl// 线程池状态发生变化,对外层循环进行自旋if (runStateOf(c) != rs)continue retry;// 其他情况,直接内层循环进行自旋即可// else CAS failed due to workerCount change; retry inner loop} }boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);final Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;// worker的添加必须是串行的,因此需要加锁mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.// 这儿需要重新检查线程池状态int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {// worker已经调用过了start()方法,则不再创建workerif (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();// worker创建并添加到workers成功workers.add(w);// 更新largestPoolSize变量int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;} } finally {mainLock.unlock();}// 启动worker线程if (workerAdded) {t.start();workerStarted = true;} }} finally {// worker线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown相关操作if (! workerStarted)addWorkerFailed(w);}return workerStarted;} 5、线程池worker任务单元 private final class Workerextends AbstractQueuedSynchronizerimplements Runnable{/ This class will never be serialized, but we provide a serialVersionUID to suppress a javac warning./private static final long serialVersionUID = 6138294804551838833L;/ Thread this worker is running in. Null if factory fails. /final Thread thread;/ Initial task to run. Possibly null. /Runnable firstTask;/ Per-thread task counter /volatile long completedTasks;/ Creates with given first task and thread from ThreadFactory. @param firstTask the first task (null if none)/Worker(Runnable firstTask) {setState(-1); // inhibit interrupts until runWorkerthis.firstTask = firstTask;// 这儿是Worker的关键所在,使用了线程工厂创建了一个线程。传入的参数为当前workerthis.thread = getThreadFactory().newThread(this);}/ Delegates main run loop to outer runWorker /public void run() {runWorker(this);}// 省略代码...} 6、核心线程执行逻辑-runworker final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;w.firstTask = null;// 调用unlock()是为了让外部可以中断w.unlock(); // allow interrupts// 这个变量用于判断是否进入过自旋(while循环)boolean completedAbruptly = true;try {// 这儿是自旋// 1. 如果firstTask不为null,则执行firstTask;// 2. 如果firstTask为null,则调用getTask()从队列获取任务。// 3. 阻塞队列的特性就是:当队列为空时,当前线程会被阻塞等待while (task != null || (task = getTask()) != null) {// 这儿对worker进行加锁,是为了达到下面的目的// 1. 降低锁范围,提升性能// 2. 保证每个worker执行的任务是串行的w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted. This// requires a recheck in second case to deal with// shutdownNow race while clearing interrupt// 如果线程池正在停止,则对当前线程进行中断操作if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();// 执行任务,且在执行前后通过beforeExecute()和afterExecute()来扩展其功能。// 这两个方法在当前类里面为空实现。try {beforeExecute(wt, task);Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);} } finally {// 帮助gctask = null;// 已完成任务数加一 w.completedTasks++;w.unlock();} }completedAbruptly = false;} finally {// 自旋操作被退出,说明线程池正在结束processWorkerExit(w, completedAbruptly);} } 本篇文章为转载内容。原文链接:https://blog.csdn.net/grd_java/article/details/113116244。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-07-21 16:19:45
328
转载
Java
...结构是线程安全的。在Java中,通过synchronized关键字可以实现对共享资源的互斥访问,从而达到线程安全的目的。 Future模式 , Future模式是Java并发编程中的一个设计模式,它提供了一种异步编程机制,允许主线程提交任务后继续执行其他操作,而非等待该任务完成。通过调用ExecutorService的submit()方法提交一个Callable任务,系统会返回一个Future对象,这个对象代表了该异步计算的结果。主线程可以在任何时候通过调用Future.get()方法来获取计算结果,如果结果尚未准备好,该方法将会阻塞直到结果可用。 Callback模式(回调函数) , Callback模式是一种设计模式,常用于异步编程场景中处理异步操作的结果。在这种模式下,一个对象(通常是客户端)注册一个方法给另一个对象(服务端),当服务端完成特定的操作或事件发生时,会调用预先注册的方法(即回调函数)通知客户端,并将相关结果作为参数传递。在Java中,可以通过接口实现回调逻辑,使得异步任务完成后能够以非阻塞的方式通知并处理结果。例如,在网络请求、I/O操作等场景中广泛应用回调模式进行异步处理。
2023-05-04 14:50:34
410
码农
MyBatis
...给SQL预编译语句塞参数的时候蹦出来,就是当你给索引的位置安排得太多,超出了实际参数的个数,就像是你手里只有三个苹果,却偏偏要按四个位置来放,这不就出问题了吗?这篇东西,咱们会手把手通过实实在在的代码例子、一步步的问题剖析,还有应对招数,一起把这个难题掰扯清楚,同时还会琢磨出怎么才能巧妙地躲开这个问题的小窍门儿。 2. 问题现象与背景理解 --- 想象一下,你正在编写一个使用MyBatis进行数据库操作的服务方法,例如下面这段简单的示例代码: java @Mapper public interface UserMapper { @Update("UPDATE user SET username={username} WHERE id={userId}") int updateUsername(@Param("userId") Integer userId, @Param("username") String username); } @Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public void updateUser(Integer userId, String username) { // 假设此处由于疏忽,只传入了一个参数 userMapper.updateUsername(userId); // 此处应该传入两个参数,但实际只传了userId } } 在上述场景中,我们意图更新用户信息,但不幸的是,在调用updateUsername方法时,仅传入了userId参数,而忽略了username参数。运行此段代码,MyBatis将会抛出StatementParameterIndexOutOfRange异常,提示“Prepared statement parameter index is out of range”。 3. 异常原因剖析 --- 该异常的本质是我们在执行SQL预编译语句时,为占位符(如:{username}和{userId})提供的参数数量与占位符的数量不匹配导致的。在MyBatis的工作原理里,它会根据SQL语句里那些小问号(参数占位符)的数量,亲手打造一个PreparedStatement对象。然后呢,就像我们玩拼图一样,按照顺序把每个参数塞到对应的位置上。当尝试访问不存在的参数时,自然就会引发这样的错误。 4. 解决方案及预防措施 --- 面对StatementParameterIndexOutOfRange异常,解决的关键在于确保传递给映射方法的参数数量与SQL语句中的参数占位符数量相匹配。回到上面的示例代码,正确的做法应该是: java public void updateUser(Integer userId, String username) { userMapper.updateUsername(userId, username); // 正确地传入两个参数 } 同时,为了预防此类问题的发生,我们可以采取以下几种策略: - 代码审查:在团队协作开发过程中,对于涉及SQL语句的方法调用,应仔细检查参数是否齐全。 - 单元测试:编写完善的单元测试用例,覆盖所有可能的参数组合情况,确保SQL语句在各种情况下都能正确执行。 - IDE辅助:利用IDE(如IntelliJ IDEA)的代码提示功能,当方法需要的参数缺失时,IDE通常会在编辑器中给出警告提示。 5. 总结与思考 --- 尽管StatementParameterIndexOutOfRange异常看似简单,但它提醒我们在使用MyBatis等ORM框架时,务必细心对待SQL语句中的参数传递。每个程序员在高强度的编程赶工中,都免不了会犯些小马虎。重点在于,得学会怎样火眼金睛般快速揪出问题所在,同时呢,也得通过一些实实在在的预防招数,让这类小错误尽量少地冒泡儿。因此,养成良好的编程习惯,提高代码质量,是我们每一位开发者在追求技术进步道路上的重要一课。
2024-01-24 12:47:10
114
烟雨江南
HessianRPC
...ception:传入参数不合法”的问题 在分布式系统开发中,HessianRPC作为一种轻量级的远程过程调用协议被广泛应用。然而,在实际动手开发的时候,我们免不了会撞上一个常见的小插曲:“IllegalArgumentException”这个家伙,它跑出来告诉你:喂喂,你传的参数不达标,比如说,方法签名跟我期待的样子对不上号。这篇东西会手把手地,用详尽的步骤解析和实实在在的例子演示,让大家都能更接地气地理解,更能轻松上手解决这个问题。 1. HessianRPC简介 首先,让我们回顾一下HessianRPC的基本概念。Hessian是一种高效、紧凑的二进制RPC协议,由Caucho公司开发,特别适合于互联网传输。这个东西超级实用,它能够让Java和其他一些好兄弟语言(比如.NET、Python这些)毫无障碍地远程互相调用对方的方法,就跟在本地调用一样方便。你只需要稍微捣鼓一下配置,写点简单的代码,就能轻松实现服务端和客户端的顺畅交流啦! 2. 方法签名的重要性 在HessianRPC中,每个远程方法都有其独特的“方法签名”,它包括方法名以及参数类型列表。当客户端调用服务器端的方法时,Hessian会根据这个签名来匹配和校验参数。如果客户端传过来的参数“不按套路出牌”,跟服务器端方法要求的参数类型或数量对不上号,那可就得闹脾气了,会直接抛出一个“IllegalArgumentException”异常。 java // 服务器端接口示例 public interface MyService { String process(String input, int num); } // 客户端错误调用示例 MyService service = (MyService) hessianProxyFactory.create(MyService.class, serverUrl); String result = service.process("Hello", "World"); // 这里第二个参数应该是int类型,而非String类型,会导致IllegalArgumentException 3. “IllegalArgumentException:传入参数不合法”问题解析 上述代码中的客户端尝试以一个字符串参数代替整型参数去调用process方法,这就导致了"IllegalArgumentException"。在进行序列化和反序列化的时候,Hessian这家伙发现传过来的参数类型跟预先给定的方法签名对不上号儿,于是它就毫不客气地抛出了一个异常。 4. 解决方案及预防措施 面对这种问题,我们需要从以下几个方面着手: 4.1 检查并确保参数类型正确 在编写客户端调用代码时,应仔细核对每个参数是否符合服务端方法签名的要求。比如上例中,我们需要将第二个参数修改为整型数值: java String result = service.process("Hello", 123); // 正确的调用方式 4.2 强化代码审查与测试 在项目开发过程中,建议采用自动化测试工具和单元测试,覆盖所有RPC方法调用,确保参数类型的准确无误。同时,代码审查也是防止此类问题的有效手段。 4.3 提供清晰的API文档 对于对外提供的服务接口,应该编写详尽且易于理解的API文档,明确指出每个方法的签名,包括方法名、参数类型和返回值类型,以便开发者在调用时有据可依。 4.4 利用IDE的智能提示 现代集成开发环境(IDE)如IntelliJ IDEA或Eclipse都具有强大的智能提示功能,能自动识别和匹配方法签名,利用好这些特性也能有效避免参数类型不匹配的问题。 总结起来,遭遇HessianRPC的“IllegalArgumentException:传入参数不合法”异常,本质上是对方法签名的理解和使用不到位的结果。在编程实战中,只要我们足够细心、步步为营,像侦探破案那样运用各种工具和策略,完全可以把这些潜在问题扼杀在摇篮里,让系统的运行稳如磐石。记住了啊,解决任何技术难题都得像咱们看侦探小说那样,得瞪大眼睛仔仔细细地观察,用脑子冷静地分析推理,动手实践去验证猜想,最后才能拨开层层迷雾,看到那片晴朗的蓝天。
2024-01-16 09:18:32
542
风轻云淡
SpringBoot
...ing Boot这个Java Web框架吗?它可是个超级好用的小工具!为什么这么说呢?因为它超级简洁,上手快,部署起来也特别方便,所以很多搞程序的大佬们都特别喜欢用它来开发项目。就像是你去超市买菜,选了个特别省事儿的购物车,推起来既轻松又快捷,Spring Boot就是那个购物车,让你的编程之旅更顺畅,效率更高!本文将详细讲解如何使用Spring Boot进行文件上传,包括配置、编码示例以及一些最佳实践。 1. 配置文件上传 在开始之前,确保你的项目中包含了必要的依赖。通常,Spring Boot会自动配置文件上传功能,但为了明确和控制,我们可以通过application.properties或application.yml文件来设置文件上传的目录和大小限制。 properties application.properties spring.servlet.multipart.max-file-size=2MB spring.servlet.multipart.max-request-size=10MB upload.path=/path/to/upload/files 这里,我们设置了单个文件的最大大小为2MB,整个请求的最大大小为10MB,并指定了上传文件的保存路径。 2. 创建Controller处理文件上传 接下来,在你的Spring Boot项目中创建一个控制器(Controller)来处理文件上传请求。下面是一个简单的例子: java import org.springframework.core.io.InputStreamResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; @Controller public class FileUploadController { @PostMapping("/upload") public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) { try { // 检查文件是否存在 if (file.isEmpty()) { return ResponseEntity.badRequest().body("Failed to upload empty file."); } // 获取文件名和类型 String fileName = file.getOriginalFilename(); String contentType = file.getContentType(); // 保存文件到指定路径 File targetFile = new File(upload.path + fileName); Files.copy(file.getInputStream(), Paths.get(targetFile.getAbsolutePath())); return ResponseEntity.ok("File uploaded successfully: " + fileName); } catch (IOException e) { return ResponseEntity.internalServerError().body("Failed to upload file: " + e.getMessage()); } } } 3. 测试文件上传功能 在完成上述配置和编码后,你可以通过Postman或其他HTTP客户端向/upload端点发送一个包含文件的POST请求。确保在请求体中正确添加了文件参数,如: json { "file": "path/to/your/file" } 4. 处理异常与错误 在实际应用中,文件上传可能会遇到各种异常情况,如文件过大、文件类型不匹配、服务器存储空间不足等。在这次的案例里,我们已经用了一段 try-catch 的代码来应对一些常见的错误情况了。就像你在日常生活中遇到小问题时,会先尝试解决,如果解决不了,就会求助于他人或寻找其他方法一样。我们也是这样,先尝试执行一段代码,如果出现预料之外的问题,我们就用 catch 部分来处理这些意外状况,确保程序能继续运行下去,而不是直接崩溃。对于更复杂的场景,例如检查文件类型或大小限制,可以引入更精细的逻辑: java @PostMapping("/upload") public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) { if (!isValidFileType(file)) { return ResponseEntity.badRequest().body("Invalid file type."); } if (!isValidFileSize(file)) { return ResponseEntity.badRequest().body("File size exceeds limit."); } // ... } private boolean isValidFileType(MultipartFile file) { // Check file type logic here } private boolean isValidFileSize(MultipartFile file) { // Check file size logic here } 结语 通过以上步骤,你不仅能够实现在Spring Boot应用中进行文件上传的基本功能,还能根据具体需求进行扩展和优化。记住,良好的错误处理和用户反馈是提高用户体验的关键。希望这篇文章能帮助你更好地理解和运用Spring Boot进行文件上传操作。嘿,兄弟!你听过这样一句话吗?“实践出真知”,尤其是在咱们做项目的时候,更是得这么干!别管你是编程高手还是设计大师,多试错,多调整,才能找到最适合那个场景的那套方案。就像是做菜一样,不试试加点这个,少放点那个,怎么知道哪个味道最对路呢?所以啊,提升技能,咱们就得在实际操作中摸爬滚打,这样才能把技术玩儿到炉火纯青的地步!
2024-09-12 16:01:18
85
寂静森林
Java
...out循环传入第三个参数,真的是闭包的实现吗? 在JavaScript的世界里,setTimeout函数配合其第三个参数常常被用来实现异步控制流和模拟“延时循环”,而关于它是否真正实现了闭包的概念,一直以来都是开发者们热议的话题。然而,在Java的世界里,虽然找不到一个和setTimeout一模一样的功能可以直接用,但是我们完全可以借助一些有趣的工具,比如Lambda表达式、Future以及ScheduledExecutorService这些小玩意儿,来探索类似场景下闭包现象的玩法。那么,让我们一起通过深入剖析和实例代码来揭晓这个谜底。 1. JavaScript中的setTimeout与闭包 首先,我们先回顾一下JavaScript中的setTimeout函数。在JS中,你可以这样做: javascript for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); } 这段代码预期是每隔一秒打印出一个递增的数字,但实际上会连续打印出5个5,这是因为setTimeout回调函数是在循环结束后才执行,此时变量i的值已经变为5,这就是闭包的经典应用场景:外部函数的变量被内部函数引用并保持了其状态。 为了解决这个问题,通常我们会利用立即执行函数或者let声明来创建一个新的作用域: javascript for (let i = 0; i < 5; i++) { setTimeout(function(i) { return function() { console.log(i); }; }(i), 1000); } 这里,每个循环迭代都会生成一个新的闭包,捕获当前的i值,从而达到预期效果。 2. Java中的“模拟setTimeout”与闭包现象 在Java中,虽然没有原生的setTimeout,但我们可以使用ScheduledExecutorService来模拟定时任务,同样也能观察到闭包的现象: java import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); for (int i = 0; i < 5; i++) { final int copyOfI = i; // 使用final关键字创建局部变量副本 executor.schedule(() -> System.out.println(copyOfI), 1, TimeUnit.SECONDS); } executor.shutdown(); } } 在这段Java代码中,我们通过ScheduledExecutorService来实现定时任务,为了能在匿名内部类(Lambda表达式)中正确访问到循环变量i的值,我们创建了一个final局部变量copyOfI作为i的副本。其实,这就是闭包的一个生活化应用场景:想象一下,尽管executor.schedule这招数是在循环跑完之后才正式启动,但是Lambda表达式却像个小机灵鬼,能牢牢地记住每一次循环时copyOfI的不同数值。这就揭示了闭包的核心秘密——它能够持续掌握并访问外部环境变量的能力,就像你的朋友记得你所有的喜好一样自然而又神奇。 3. 结论与思考 综上所述,无论是JavaScript中的setTimeout还是Java中的ScheduledExecutorService结合Lambda表达式的使用,都涉及到了闭包的应用。虽然它们在语法和具体实现上各有各的不同,但当你看到它们如何处理函数和它所在外部环境的关系时,你会发现一个共通的、像超级英雄般的核心概念——闭包。这个概念就像是,即使函数已经完成了它的任务并准备“下班”,但它依然能牢牢地记住并掌握那些原本属于外部环境的变量,就像拥有了一种神奇的力量。 因此,即使在Java中,我们在模拟setTimeout行为时所采用的策略,本质上也是闭包的一种体现,只不过这种闭包机制并非像JavaScript那样显式且直观,而是通过Java特有的方式(如Lambda表达式、内部类对局部变量的捕获)予以实现。
2023-05-05 15:35:33
280
灵动之光_
转载文章
在深入理解了Java中this关键字在Android开发中的具体应用以及Android系统中Context机制之后,开发者们可以进一步关注以下几方面的 首先,对于Android开发的新动态,Google在Android 12版本中对Context的使用引入了一些新特性与优化。例如,为了提高隐私和安全,限制了通过Context获取设备信息的能力,要求开发者更明确地声明权限。此外,针对Context的生命周期管理,Google也推出了一些最佳实践指南,以帮助开发者更好地遵循架构组件设计原则,避免内存泄漏等问题。 其次,深入探究Android组件间的交互方式,尤其是Activity、Service和BroadcastReceiver等组件如何通过Context进行通信,是提升Android开发技能的重要一环。近期有关消息传递机制(如Jetpack库中的WorkManager、LiveData)的博客文章和技术分享,能为开发者提供更加高效且符合现代Android架构规范的Context使用范例。 再者,从设计模式角度解读Context在MVC、MVP、MVVM等不同架构中的角色转变,有助于理解其在整个应用架构中的核心地位。一些经典的软件设计书籍和专家解析文章对此有独到见解,值得深入研读。 最后,结合实际项目案例分析,了解大型项目中Context的最佳实践及常见问题处理方案。许多技术社区和开源项目会分享他们在处理多模块间Context共享、Context引用导致的内存泄漏等问题时的具体解决方案和经验总结,这对于开发者来说具有极高的实战参考价值。
2023-09-27 17:37:26
93
转载
转载文章
...obs可以做成标准的Java组件或 EJBs。Quartz 是个开源的作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。Quartz 允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz 的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。虽然可以通过属性文件(在属性文件中可以指定 JDBC 事务的数据源、全局作业和/或触发器侦听器、插件、线程池,以及更多)配置 Quartz,但它根本没有与应用程序服务器的上下文或引用集成在一起。结果就是作业不能访问 Web 服务器的内部函数;例如,在使用 WebSphere 应用服务器时,由 Quartz 调度的作业并不能影响服务器的动态缓存和数据源。 二、java中实现定时任务分类 从实现的技术上来分类,目前主要有三种技术(或者说有三种产品): Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少,这篇文章将不做详细介绍。 使用Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂,稍后会详细介绍。 Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多,稍后会介绍。 从作业类的继承方式来讲,可以分为两类: 作业类需要继承自特定的作业类基类,如Quartz中需要继承自org.springframework.scheduling.quartz.QuartzJobBean;java.util.Timer中需要继承自java.util.TimerTask。 作业类即普通的java类,不需要继承自任何基类。 注:个人推荐使用第二种方式,因为这样所以的类都是普通类,不需要事先区别对待。 从任务调度的触发时机来分,这里主要是针对作业使用的触发器,主要有以下两种: 每隔指定时间则触发一次,在Quartz中对应的触发器为:org.springframework.scheduling.quartz.SimpleTriggerBean 每到指定时间则触发一次,在Quartz中对应的调度器为:org.springframework.scheduling.quartz.CronTriggerBean 注:并非每种任务都可以使用这两种触发器,如java.util.TimerTask任务就只能使用第一种。Quartz和spring task都可以支持这两种触发条件。 三、Quartz与Spring的集成 第一种,作业类继承自特定的基类:org.springframework.scheduling.quartz.QuartzJobBean。 第一步:定义作业类 Java代码 import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; public class Job1 extends QuartzJobBean { private int timeout; private static int i = 0; //调度工厂实例化后,经过timeout时间开始执行调度 public void setTimeout(int timeout) { this.timeout = timeout; } / 要调度的具体任务 / @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { System.out.println("定时任务执行中…"); } } 第二步:spring配置文件中配置作业类JobDetailBean Xml代码 <bean name="job1" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.gy.Job1" /> <property name="jobDataAsMap"> <map> <entry key="timeout" value="0" /> </map> </property> </bean> 说明:org.springframework.scheduling.quartz.JobDetailBean有两个属性,jobClass属性即我们在java代码中定义的任务类,jobDataAsMap属性即该任务类中需要注入的属性值。 第三步:配置作业调度的触发方式(触发器) Quartz的作业触发器有两种,分别是 org.springframework.scheduling.quartz.SimpleTriggerBean org.springframework.scheduling.quartz.CronTriggerBean 第一种SimpleTriggerBean,只支持按照一定频度调用任务,如每隔30分钟运行一次。 配置方式如下: Xml代码 <bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job1" /> <property name="startDelay" value="0" /><!-- 调度工厂实例化后,经过0秒开始执行调度 --> <property name="repeatInterval" value="2000" /><!-- 每2秒调度一次 --> </bean> 第二种CronTriggerBean,支持到指定时间运行一次,如每天12:00运行一次等。 配置方式如下: Xml代码 <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail" ref="job1" /> <!—每天12:00运行一次 --> <property name="cronExpression" value="0 0 12 ?" /> </bean> 关于cronExpression表达式的语法参见附录。 第四步:配置调度工厂 Xml代码 <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="cronTrigger" /> </list> </property> </bean> 说明:该参数指定的就是之前配置的触发器的名字。 第五步:启动你的应用即可,即将工程部署至tomcat或其他容器。 第二种,作业类不继承特定基类。 Spring能够支持这种方式,归功于两个类: org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean 这两个类分别对应spring支持的两种实现任务调度的方式,即前文提到到java自带的timer task方式和Quartz方式。这里我只写MethodInvokingJobDetailFactoryBean的用法,使用该类的好处是,我们的任务类不再需要继承自任何类,而是普通的pojo。 第一步:编写任务类 Java代码 public class Job2 { public void doJob2() { System.out.println("不继承QuartzJobBean方式-调度进行中..."); } } 可以看出,这就是一个普通的类,并且有一个方法。 第二步:配置作业类 Xml代码 <bean id="job2" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject"> <bean class="com.gy.Job2" /> </property> <property name="targetMethod" value="doJob2" /> <property name="concurrent" value="false" /><!-- 作业不并发调度 --> </bean> 说明:这一步是关键步骤,声明一个MethodInvokingJobDetailFactoryBean,有两个关键属性:targetObject指定任务类,targetMethod指定运行的方法。往下的步骤就与方法一相同了,为了完整,同样贴出。 第三步:配置作业调度的触发方式(触发器) Quartz的作业触发器有两种,分别是 org.springframework.scheduling.quartz.SimpleTriggerBean org.springframework.scheduling.quartz.CronTriggerBean 第一种SimpleTriggerBean,只支持按照一定频度调用任务,如每隔30分钟运行一次。 配置方式如下: Xml代码 <bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job2" /> <property name="startDelay" value="0" /><!-- 调度工厂实例化后,经过0秒开始执行调度 --> <property name="repeatInterval" value="2000" /><!-- 每2秒调度一次 --> </bean> 第二种CronTriggerBean,支持到指定时间运行一次,如每天12:00运行一次等。 配置方式如下: Xml代码 <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> <property name="jobDetail" ref="job2" /> <!—每天12:00运行一次 --> <property name="cronExpression" value="0 0 12 ?" /> </bean> 以上两种调度方式根据实际情况,任选一种即可。 第四步:配置调度工厂 Xml代码 <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="cronTrigger" /> </list> </property> </bean> 说明:该参数指定的就是之前配置的触发器的名字。 第五步:启动你的应用即可,即将工程部署至tomcat或其他容器。 到此,spring中Quartz的基本配置就介绍完了,当然了,使用之前,要导入相应的spring的包与Quartz的包,这些就不消多说了。 其实可以看出Quartz的配置看上去还是挺复杂的,没有办法,因为Quartz其实是个重量级的工具,如果我们只是想简单的执行几个简单的定时任务,有没有更简单的工具,有! 四、Spring-Task 上节介绍了在Spring 中使用Quartz,本文介绍Spring3.0以后自主开发的定时任务工具,spring task,可以将它比作一个轻量级的Quartz,而且使用起来很简单,除spring相关的包外不需要额外的包,而且支持注解和配置文件两种 形式,下面将分别介绍这两种方式。 第一种:配置文件方式 第一步:编写作业类 即普通的pojo,如下: Java代码 import org.springframework.stereotype.Service; @Service public class TaskJob { public void job1() { System.out.println(“任务进行中。。。”); } } 第二步:在spring配置文件头中添加命名空间及描述 Xml代码 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:task="http://www.springframework.org/schema/task" 。。。。。。 xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> 第三步:spring配置文件中设置具体的任务 Xml代码 <task:scheduled-tasks> <task:scheduled ref="taskJob" method="job1" cron="0 ?"/> </task:scheduled-tasks> <context:component-scan base-package=" com.gy.mytask " /> 说明:ref参数指定的即任务类,method指定的即需要运行的方法,cron及cronExpression表达式,具体写法这里不介绍了,详情见上篇文章附录。 <context:component-scan base-package="com.gy.mytask" />这个配置不消多说了,spring扫描注解用的。 到这里配置就完成了,是不是很简单。 第二种:使用注解形式 也许我们不想每写一个任务类还要在xml文件中配置下,我们可以使用注解@Scheduled,我们看看源文件中该注解的定义: Java代码 @Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Scheduled { public abstract String cron(); public abstract long fixedDelay(); public abstract long fixedRate(); } 可以看出该注解有三个方法或者叫参数,分别表示的意思是: cron:指定cron表达式 fixedDelay:官方文档解释:An interval-based trigger where the interval is measured from the completion time of the previous task. The time unit value is measured in milliseconds.即表示从上一个任务完成开始到下一个任务开始的间隔,单位是毫秒。 fixedRate:官方文档解释:An interval-based trigger where the interval is measured from the start time of the previous task. The time unit value is measured in milliseconds.即从上一个任务开始到下一个任务开始的间隔,单位是毫秒。 下面我来配置一下。 第一步:编写pojo Java代码 import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component(“taskJob”) public class TaskJob { @Scheduled(cron = "0 0 3 ?") public void job1() { System.out.println(“任务进行中。。。”); } } 第二步:添加task相关的配置: Xml代码 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd" default-lazy-init="false"> <context:annotation-config /> <!—spring扫描注解的配置 --> <context:component-scan base-package="com.gy.mytask" /> <!—开启这个配置,spring才能识别@Scheduled注解 --> <task:annotation-driven scheduler="qbScheduler" mode="proxy"/> <task:scheduler id="qbScheduler" pool-size="10"/> 说明:理论上只需要加上<task:annotation-driven />这句配置就可以了,这些参数都不是必须的。 Ok配置完毕,当然spring task还有很多参数,我就不一一解释了,具体参考xsd文档http://www.springframework.org/schema/task/spring-task-3.0.xsd。 附录: cronExpression的配置说明,具体使用以及参数请百度google 字段 允许值 允许的特殊字符 秒 0-59 , - / 分 0-59 , - / 小时 0-23 , - / 日期 1-31 , - ? / L W C 月份 1-12 或者 JAN-DEC , - / 星期 1-7 或者 SUN-SAT , - ? / L C 年(可选) 留空, 1970-2099 , - / - 区间 通配符 ? 你不想设置那个字段 下面只例出几个式子 CRON表达式 含义 "0 0 12 ?" 每天中午十二点触发 "0 15 10 ? " 每天早上10:15触发 "0 15 10 ?" 每天早上10:15触发 "0 15 10 ? " 每天早上10:15触发 "0 15 10 ? 2005" 2005年的每天早上10:15触发 "0 14 ?" 每天从下午2点开始到2点59分每分钟一次触发 "0 0/5 14 ?" 每天从下午2点开始到2:55分结束每5分钟一次触发 "0 0/5 14,18 ?" 每天的下午2点至2:55和6点至6点55分两个时间段内每5分钟一次触发 "0 0-5 14 ?" 每天14:00至14:05每分钟一次触发 "0 10,44 14 ? 3 WED" 三月的每周三的14:10和14:44触发 "0 15 10 ? MON-FRI" 每个周一、周二、周三、周四、周五的10:15触发 Cron 表达式包括以下 7 个字段: 秒 分 小时 月内日期 月 周内日期 年(可选字段) 特殊字符 Cron 触发器利用一系列特殊字符,如下所示: 反斜线(/)字符表示增量值。例如,在秒字段中“5/15”代表从第 5 秒开始,每 15 秒一次。 问号(?)字符和字母 L 字符只有在月内日期和周内日期字段中可用。问号表示这个字段不包含具体值。所以,如果指定月内日期,可以在周内日期字段中插入“?”,表示周内日期值无关紧要。字母 L 字符是 last 的缩写。放在月内日期字段中,表示安排在当月最后一天执行。在周内日期字段中,如果“L”单独存在,就等于“7”,否则代表当月内周内日期的最后一个实例。所以“0L”表示安排在当月的最后一个星期日执行。 在月内日期字段中的字母(W)字符把执行安排在最靠近指定值的工作日。把“1W”放在月内日期字段中,表示把执行安排在当月的第一个工作日内。 井号()字符为给定月份指定具体的工作日实例。把“MON2”放在周内日期字段中,表示把任务安排在当月的第二个星期一。 星号()字符是通配字符,表示该字段可以接受任何可能的值。 字段 允许值 允许的特殊字符 秒 0-59 , - / 分 0-59 , - / 小时 0-23 , - / 日期 1-31 , - ? / L W C 月份 1-12 或者 JAN-DEC , - / 星期 1-7 或者 SUN-SAT , - ? / L C 年(可选) 留空, 1970-2099 , - / 表达式意义 "0 0 12 ?" 每天中午12点触发 "0 15 10 ? " 每天上午10:15触发 "0 15 10 ?" 每天上午10:15触发 "0 15 10 ? " 每天上午10:15触发 "0 15 10 ? 2005" 2005年的每天上午10:15触发 "0 14 ?" 在每天下午2点到下午2:59期间的每1分钟触发 "0 0/5 14 ?" 在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14,18 ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0-5 14 ?" 在每天下午2点到下午2:05期间的每1分钟触发 "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 "0 15 10 ? MON-FRI" 周一至周五的上午10:15触发 "0 15 10 15 ?" 每月15日上午10:15触发 "0 15 10 L ?" 每月最后一日的上午10:15触发 "0 15 10 ? 6L" 每月的最后一个星期五上午10:15触发 "0 15 10 ? 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? 63" 每月的第三个星期五上午10:15触发 每天早上6点 0 6 每两个小时 0 /2 晚上11点到早上8点之间每两个小时,早上八点 0 23-7/2,8 每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点 0 11 4 1-3 1月1日早上4点 0 4 1 1 本篇文章为转载内容。原文链接:https://zhanghaiyang.blog.csdn.net/article/details/51397459。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-10-27 18:50:19
344
转载
转载文章
...队消息 出队消息 二Java使用JMS监听并处理Oracle AQ队列创建连接参数类 创建消息转换类 主类进行消息处理 三监控表记录变化通知Java创建表 创建存储过程 创建触发器 环境说明 本实验环境基于Oracle 12C和JDK1.8,其中Oracle 12C支持多租户特性,相较于之前的Oracle版本,使用‘C用户名‘表示用户,例如如果数据库用户叫kevin,则登陆时使用Ckevin进行登陆。 一、Oracle高级消息队列AQ Oracle AQ是Oracle中的消息队列,是Oracle中的一种高级应用,每个版本都在不断的加强,使用DBMS_AQ系统包进行相应的操作,是Oracle的默认组件,只要安装了Oracle数据库就可以使用。使用AQ可以在多个Oracle数据库、Oracle与Java、C等系统中进行数据传输。 下面分步骤说明如何创建Oracle AQ 1. 创建消息负荷payload Oracle AQ中传递的消息被称为有效负荷(payloads),格式可以是用户自定义对象或XMLType或ANYDATA。本例中我们创建一个简单的对象类型用于传递消息。 create type demo_queue_payload_type as object (message varchar2(4000)); 2. 创建队列表 队列表用于存储消息,在入队时自动存入表中,出队时自动删除。使用DBMS_AQADM包进行数据表的创建,只需要写表名,同时设置相应的属性。对于队列需要设置multiple_consumers为false,如果使用发布/订阅模式需要设置为true。 begin dbms_aqadm.create_queue_table( queue_table => 'demo_queue_table', queue_payload_type => 'demo_queue_payload_type', multiple_consumers => false ); end; 执行完后可以查看oracle表中自动生成了demo_queue_table表,可以查看影响子段(含义比较清晰)。 3. 创建队列并启动 创建队列并启动队列: begin dbms_aqadm.create_queue ( queue_name => 'demo_queue', queue_table => 'demo_queue_table' ); dbms_aqadm.start_queue( queue_name => 'demo_queue' ); end; 至此,我们已经创建了队列有效负荷,队列表和队列。可以查看以下系统创建了哪些相关的对象: SELECT object_name, object_type FROM user_objects WHERE object_name != 'DEMO_QUEUE_PAYLOAD_TYPE'; OBJECT_NAME OBJECT_TYPE ------------------------------ --------------- DEMO_QUEUE_TABLE TABLE SYS_C009392 INDEX SYS_LOB0000060502C00030$$ LOB AQ$_DEMO_QUEUE_TABLE_T INDEX AQ$_DEMO_QUEUE_TABLE_I INDEX AQ$_DEMO_QUEUE_TABLE_E QUEUE AQ$DEMO_QUEUE_TABLE VIEW DEMO_QUEUE QUEUE 我们看到一个队列带出了一系列自动生成对象,有些是被后面直接用到的。不过有趣的是,创建了第二个队列。这就是所谓的异常队列(exception queue)。如果AQ无法从我们的队列接收消息,将记录在该异常队列中。 消息多次处理出错等情况会自动转移到异常的队列,对于异常队列如何处理目前笔者还没有找到相应的写法,因为我使用的场景并不要求消息必须一对一的被处理,只要起到通知的作用即可。所以如果消息转移到异常队列,可以执行清空队列表中的数据 delete from demo_queue_table; 4. 队列的停止和删除 如果需要删除或重建可以使用下面的方法进行操作: BEGIN DBMS_AQADM.STOP_QUEUE( queue_name => 'demo_queue' ); DBMS_AQADM.DROP_QUEUE( queue_name => 'demo_queue' ); DBMS_AQADM.DROP_QUEUE_TABLE( queue_table => 'demo_queue_table' ); END; 5. 入队消息 入列操作是一个基本的事务操作(就像往队列表Insert),因此我们需要提交。 declare r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T; r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; v_message_handle RAW(16); o_payload demo_queue_payload_type; begin o_payload := demo_queue_payload_type('what is you name ?'); dbms_aq.enqueue( queue_name => 'demo_queue', enqueue_options => r_enqueue_options, message_properties => r_message_properties, payload => o_payload, msgid => v_message_handle ); commit; end; 通过SQL语句查看消息是否正常入队: select from aq$demo_queue_table; select user_data from aq$demo_queue_table; 6. 出队消息 使用Oracle进行出队操作,我没有实验成功(不确定是否和DBMS_OUTPUT的执行权限有关),代码如下,读者可以进行调试: declare r_dequeue_options DBMS_AQ.DEQUEUE_OPTIONS_T; r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; v_message_handle RAW(16); o_payload demo_queue_payload_type; begin DBMS_AQ.DEQUEUE( queue_name => 'demo_queue', dequeue_options => r_dequeue_options, message_properties => r_message_properties, payload => o_payload, msgid => v_message_handle ); DBMS_OUTPUT.PUT_LINE( ' Browse message is [' || o_payload.message || ']' ); end; 二、Java使用JMS监听并处理Oracle AQ队列 Java使用JMS进行相应的处理,需要使用Oracle提供的jar,在Oracle安装目录可以找到:在linux中可以使用find命令进行查找,例如 find pwd -name 'jmscommon.jar' 需要的jar为: app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/jmscommon.jar app/oracle/product/12.1.0/dbhome_1/jdbc/lib/ojdbc7.jar app/oracle/product/12.1.0/dbhome_1/jlib/orai18n.jar app/oracle/product/12.1.0/dbhome_1/jlib/jta.jar app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/aqapi_g.jar 1. 创建连接参数类 实际使用时可以把参数信息配置在properties文件中,使用Spring进行注入。 package org.kevin.jms; / @author 李文锴 连接参数信息 / public class JmsConfig { public String username = "ckevin"; public String password = "a111111111"; public String jdbcUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl"; public String queueName = "demo_queue"; } 2. 创建消息转换类 因为消息载荷是Oracle数据类型,需要提供一个转换工厂类将Oracle类型转换为Java类型。 package org.kevin.jms; import java.sql.SQLException; import oracle.jdbc.driver.OracleConnection; import oracle.jdbc.internal.OracleTypes; import oracle.jpub.runtime.MutableStruct; import oracle.sql.CustomDatum; import oracle.sql.CustomDatumFactory; import oracle.sql.Datum; import oracle.sql.STRUCT; / @author 李文锴 数据类型转换类 / @SuppressWarnings("deprecation") public class QUEUE_MESSAGE_TYPE implements CustomDatum, CustomDatumFactory { public static final String _SQL_NAME = "QUEUE_MESSAGE_TYPE"; public static final int _SQL_TYPECODE = OracleTypes.STRUCT; MutableStruct _struct; // 12表示字符串 static int[] _sqlType = { 12 }; static CustomDatumFactory[] _factory = new CustomDatumFactory[1]; static final QUEUE_MESSAGE_TYPE _MessageFactory = new QUEUE_MESSAGE_TYPE(); public static CustomDatumFactory getFactory() { return _MessageFactory; } public QUEUE_MESSAGE_TYPE() { _struct = new MutableStruct(new Object[1], _sqlType, _factory); } public Datum toDatum(OracleConnection c) throws SQLException { return _struct.toDatum(c, _SQL_NAME); } public CustomDatum create(Datum d, int sqlType) throws SQLException { if (d == null) return null; QUEUE_MESSAGE_TYPE o = new QUEUE_MESSAGE_TYPE(); o._struct = new MutableStruct((STRUCT) d, _sqlType, _factory); return o; } public String getContent() throws SQLException { return (String) _struct.getAttribute(0); } } 3. 主类进行消息处理 package org.kevin.jms; import java.util.Properties; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueConnectionFactory; import javax.jms.Session; import oracle.jms.AQjmsAdtMessage; import oracle.jms.AQjmsDestination; import oracle.jms.AQjmsFactory; import oracle.jms.AQjmsSession; / @author 李文锴 消息处理类 / public class Main { public static void main(String[] args) throws Exception { JmsConfig config = new JmsConfig(); QueueConnectionFactory queueConnectionFactory = AQjmsFactory.getQueueConnectionFactory(config.jdbcUrl, new Properties()); QueueConnection conn = queueConnectionFactory.createQueueConnection(config.username, config.password); AQjmsSession session = (AQjmsSession) conn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); conn.start(); Queue queue = (AQjmsDestination) session.getQueue(config.username, config.queueName); MessageConsumer consumer = session.createConsumer(queue, null, QUEUE_MESSAGE_TYPE.getFactory(), null, false); consumer.setMessageListener(new MessageListener() { @Override public void onMessage(Message message) { System.out.println("ok"); AQjmsAdtMessage adtMessage = (AQjmsAdtMessage) message; try { QUEUE_MESSAGE_TYPE payload = (QUEUE_MESSAGE_TYPE) adtMessage.getAdtPayload(); System.out.println(payload.getContent()); } catch (Exception e) { e.printStackTrace(); } } }); Thread.sleep(1000000); } } 使用Oracle程序块进行入队操作,在没有启动Java时看到队列表中存在数据。启动Java后,控制台正确的输出的消息;通过Oracle程序块再次写入消息,发现控制台正确处理消息。Java的JMS监听不是立刻进行处理,可能存在几秒中的时间差,时间不等。 三、监控表记录变化通知Java 下面的例子创建一个数据表,然后在表中添加触发器,当数据变化后触发器调用存储过程给Oracle AQ发送消息,然后使用Java JMS对消息进行处理。 1. 创建表 创建student表,包含username和age两个子段,其中username时varchar2类型,age时number类型。 2. 创建存储过程 创建send_aq_msg存储过程,因为存储过程中调用dbms数据包,系统包在存储过程中执行需要进行授权(使用sys用户进行授权): grant execute on dbms_aq to ckevin; 注意存储过程中包含commit语句。 create or replace PROCEDURE send_aq_msg (info IN VARCHAR2) as r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T; r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T; v_message_handle RAW(16); o_payload demo_queue_payload_type; begin o_payload := demo_queue_payload_type(info); dbms_aq.enqueue( queue_name => 'demo_queue', enqueue_options => r_enqueue_options, message_properties => r_message_properties, payload => o_payload, msgid => v_message_handle ); commit; end send_aq_msg; 3. 创建触发器 在student表中创建触发器,当数据写入或更新时,如果age=18,则进行入队操作。需要调用存储过程发送消息,但触发器中不能包含事物提交语句,因此需要使用pragma autonomous_transaction;声明自由事物: CREATE OR REPLACE TRIGGER STUDENT_TR AFTER INSERT OR UPDATE OF AGE ON STUDENT FOR EACH ROW DECLARE pragma autonomous_transaction; BEGIN if :new.age = 18 then send_aq_msg(:new.username); end if; END; 创建完触发器后向执行插入或更新操作: insert into student (username,age) values ('jack.lee.3k', 18); update student set age=18 where username='jack003'; Java JMS可以正确的处理消息。 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_42309178/article/details/115241521。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-12-17 14:22:22
138
转载
转载文章
...对象的作用 类似于 Java 中使用类访问静态成员的语法。因为 Kotlin 取消了 static 关键字,所以 Kotlin 引入伴生对象来弥补没有静态成员的不足。可见,伴生对象的主要作用就是为其所在的外部类模拟静态成员。 在 Java 代码中调用伴生对象 如何在 Java 代码中调用 Kotlin 的伴生对象呢? public static void main(String[] args) { 如果声明伴生对象有名称,则使用: 类名.伴生对象名.方法名() 类名.半生对象名.属性的setter,getter方法 如果声明伴生对象无名称,则采用 Companion 关键字调用: .Companion.方法名() @JvmField 和 @JvmStatic 的使用 在上面的例子中,我们知道了可以在 Java 代码中调用 Kotlin 中伴生对象的成员,类似于 Java 类中的静态成员。但是看上去和 Java 中的还是略有区别,因为类名和方法名/属性setter,getter方法名之间多了个伴生对象的名称或者 Companion 关键字。如何使其在调用的时候与 Java 中的调用看上去一样呢? Kotlin 为我们提供了 @JvmField 和 @JvmStatic 两个注解。@JvmField 使用在属性上,@JvmStatic 使用在方法上。如: class Test { 这样我们在 Java 代码中调用的时候就和 Java 类调用静态成员的形式一致了,Kotlin 代码调用方式不变: System.out.println(Test.flag); System.out.println(Test.add(1, 2)); const 关键字 在伴生对象中,我们可能需要声明一个常量,目的是等同于 Java 中的静态常量。有两种方式,一种是上面所提到的使用 @JvmField 注解,另一种则是使用 const 关键字修饰。这两种声明方式都等同于 Java 中 static final 所修饰的变量。如下代码: companion 扩展属性和扩展方法 扩展函数 Kotlin的扩展函数可以让你作为一个类成员进行调用的函数,但是是定义在这个类的外部。这样可以很方便的扩展一个已经存在的类,为它添加额外的方法 下面我们为String添加一个toInt的方法 package com.binzi.kotlin 在这个扩展函数中,你可以直接访问你扩展的类的函数和属性,就像定义在这个类中的方法一样,但是扩展函数并不允许你打破封装。跟定义在类中方法不同,它不能访问那些私有的、受保护的方法和属性。 扩展函数的导入 我们直接在包里定义扩展函数。这样我们就可以在整个包里面使用这些扩展,如果我们要使用其他包的扩展,我们就需要导入它。导入扩展函数跟导入类是一样的方式。 import 有时候,可能你引入的第三方包都对同一个类型进行了相同函数名扩展,为了解决冲突问题,你可以使用下面的方式对扩展函数进行改名 import com.binzi.kotlin.toInt as toInteger 扩展函数不可覆盖 扩展方法的原理 Kotlin 中类的扩展方法并不是在原类的内部进行拓展,通过反编译为Java代码,可以发现,其原理是使用装饰模式,对源类实例的操作和包装,其实际相当于我们在 Java中定义的工具类方法,并且该工具类方法是使用调用者为第一个参数的,然后在工具方法中操作该调用者 如: fun String?.toInt(): 反编译为对应的Java代码: public 扩展属性 类的扩展属性原理其实与扩展方法是一样的,只是定义的形式不同,扩展属性必须定义get和set方法 为MutableList扩展一个firstElement属性: var 反编译后的java代码如下: public static final Object getFirstElement(@NotNull List $this$firstElement) { 内部类 kotlin的内部类与java的内部类有点不同java的内部类可以直接访问外部类的成员,kotlin的内部类不能直接访问外部类的成员,必须用inner标记之后才能访问外部类的成员 没有使用inner标记的内部类 class A{ 反编译后的java代码 public 用inner标记的内部类 class A{ 反编译后的java代码 public 从上面可以看出,没有使用inner标记的内部类最后生成的是静态内部类,而使用inner标记的生成的是非静态内部类 匿名内部类 匿名内部类主要是针对那些获取抽象类或者接口对象而来的。最常见的匿名内部类View点击事件: //java,匿名内部类的写法 上面这个是java匿名内部类的写法,kotlin没有new关键字,那么kotlin的匿名内部类该怎么写呢? object : View.OnClickListener{ 方法的参数是一个匿名内部类,先写object:,然后写你的参数类型View.OnClickListener{} kotlin还有一个写法lambda 表达式,非常之方便: print( 数据类 在Java中没有专门的数据类,常常是通过JavaBean来作为数据类,但在Kotlin中提供了专门的数据类。 Java public 从上面的例子中可以看到,如果要使用数据类,需要手动写相应的setter/getter方法(尽管IDE也可以批量生成),但是从代码阅读的角度来说,在属性较多的情况下,诸多的seeter/getter方法还是不利于代码的阅读和维护。 Kotlin 在Kotlin中,可以通过关键字data来生成数据类: data 即在class关键字之前添加data关键字即可。编译器会根据主构造函数中的参数生成相应的数据类。自动生成setter/getter、toString、hashCode等方法 要声明一个数据类,需要满足: 主构造函数中至少有一个参数 主构造函数中所有参数需要标记为val或var 数据类不能是抽象、开发、密封和内部的 枚举类 枚举类是一种特殊的类,kotlin可以通过enum class关键字定义枚举类。 枚举类可以实现0~N个接口; 枚举类默认继承于kotlin.Enum类(其他类最终父类都是Any),因此kotlin枚举类不能继承类; 非抽象枚举类不能用open修饰符修饰,因此非抽象枚举类不能派生子类; 抽象枚举类不能使用abstract关键字修饰enum class,抽象方法和抽象属性需要使用; 枚举类构造器只能使用private修饰符修饰,若不指定,则默认为private; 枚举类所有实例在第一行显式列出,每个实例之间用逗号隔开,整个声明以分号结尾; 枚举类是特殊的类,也可以定义属性、方法、构造器; 枚举类应该设置成不可变类,即属性值不允许改变,这样更安全; 枚举属性设置成只读属性后,最好在构造器中为枚举类指定初始值,如果在声明时为枚举指定初始值,会导致所有枚举值(或者说枚举对象)的该属性都一样。 定义枚举类 / 定义一个枚举类 / 枚举类实现接口 枚举值分别实现接口的抽象成员 enum 枚举类统一实现接口的抽象成员 enum 分别实现抽象枚举类抽象成员 enum 委托 委托模式 是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承。 Java中委托: interface Printer { Kotlin: interface Printer { by表示 p 将会在 PrintImpl 中内部存储, 并且编译器将自动生成转发给 p 的所有 Printer 的方法。 委托属性 有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括: 延迟属性(lazy properties): 其值只在首次访问时计算; 可观察属性(observable properties): 监听器会收到有关此属性变更的通知; 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。 为了涵盖这些(以及其他)情况,Kotlin 支持 委托属性 。 委托属性的语法是: var : 在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。 标准委托: Kotlin 标准库为几种有用的委托提供了工厂方法。 延迟属性 Lazy lazy() 接受一个 lambda 并返回一个 Lazy 实例的函数,返回的实例可以作为实现延迟属性的委托:第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。例如: val lazyValue: String 可观察属性 Observable Delegates.observable() 接受两个参数:初始值和修改时处理程序(handler)。每当我们给属性赋值时会调用该处理程序(在赋值后执行)。它有三个参数:被赋值的属性、旧值和新值: class User { 如果想拦截赋的新值,并根据你是不是想要这个值来决定是否给属性赋新值,可以使用 vetoable() 取代 observable(),接收的参数和 observable 一样,不过处理程序 返回值是 Boolean 来决定是否采用新值,即在属性被赋新值生效之前 会调用传递给 vetoable 的处理程序。例如: class User { 把属性存在map 中 一个常见的用例是在一个映射(map)里存储属性的值。这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。 例如: class User(map: Map 在上例中,委托属性会从构造函数传入的map中取值(通过字符串键——属性的名称),如果遇到声明的属性名在map 中找不到对应的key 名,或者key 对应的value 值的类型与声明的属性的类型不一致,会抛出异常。 内联函数 当一个函数被声明为inline时,它的函数体是内联的,也就是说,函数体会被直接替换到函数被调用地方 inline函数(内联函数)从概念上讲是编译器使用函数实现的真实代码来替换每一次的函数调用,带来的最直接的好处就是节省了函数调用的开销,而缺点就是增加了所生成字节码的尺寸。基于此,在代码量不是很大的情况下,我们是否有必要将所有的函数定义为内联?让我们分两种情况进行说明: 将普通函数定义为内联:众所周知,JVM内部已经实现了内联优化,它会在任何可以通过内联来提升性能的地方将函数调用内联化,并且相对于手动将普通函数定义为内联,通过JVM内联优化所生成的字节码,每个函数的实现只会出现一次,这样在保证减少运行时开销的同时,也没有增加字节码的尺寸;所以我们可以得出结论,对于普通函数,我们没有必要将其声明为内联函数,而是交给JVM自行优化。 将带有lambda参数的函数定义为内联:是的,这种情况下确实可以提高性能;但在使用的过程中,我们会发现它是有诸多限制的,让我们从下面的例子开始展开说明: inline 假如我们这样调用doSomething: fun main(args: Array<String>) { 上面的调用会被编译成: fun main(args: Array<String>) { 从上面编译的结果可以看出,无论doSomething函数还是action参数都被内联了,很棒,那让我们换一种调用方式: fun main(args: Array<String>) { 上面的调用会被编译成: fun main(args: Array<String>) { doSomething函数被内联,而action参数没有被内联,这是因为以函数型变量的形式传递给doSomething的lambda在函数的调用点是不可用的,只有等到doSomething被内联后,该lambda才可以正常使用。 通过上面的例子,我们对lambda表达式何时被内联做一下简单的总结: 当lambda表达式以参数的形式直接传递给内联函数,那么lambda表达式的代码会被直接替换到最终生成的代码中。 当lambda表达式在某个地方被保存起来,然后以变量形式传递给内联函数,那么此时的lambda表达式的代码将不会被内联。 上面对lambda的内联时机进行了讨论,消化片刻后让我们再看最后一个例子: inline 上面的例子是否有问题?是的,编译器会抛出“Illegal usage of inline-parameter”的错误,这是因为Kotlin规定内联函数中的lambda参数只能被直接调用或者传递给另外一个内联函数,除此之外不能作为他用;那我们如果确实想要将某一个lambda传递给一个非内联函数怎么办?我们只需将上述代码这样改造即可: inline 很简单,在不需要内联的lambda参数前加上noinline修饰符就可以了。 以上便是我对内联函数的全部理解,通过掌握该特性的运行机制,相信大家可以做到在正确的时机使用该特性,而非滥用或因恐惧弃而不用。 Kotlin下单例模式 饿汉式实现 //Java实现 懒汉式 //Java实现 上述代码中,我们可以发现在Kotlin实现中,我们让其主构造函数私有化并自定义了其属性访问器,其余内容大同小异。 如果有小伙伴不清楚Kotlin构造函数的使用方式。请点击 - - - 构造函数 不清楚Kotlin的属性与访问器,请点击 - - -属性和字段 线程安全的懒汉式 //Java实现 大家都知道在使用懒汉式会出现线程安全的问题,需要使用使用同步锁,在Kotlin中,如果你需要将方法声明为同步,需要添加@Synchronized注解。 双重校验锁式 //Java实现 哇!小伙伴们惊喜不,感不感动啊。我们居然几行代码就实现了多行的Java代码。其中我们运用到了Kotlin的延迟属性 Lazy。 Lazy内部实现 public 观察上述代码,因为我们传入的mode = LazyThreadSafetyMode.SYNCHRONIZED, 那么会直接走 SynchronizedLazyImpl,我们继续观察SynchronizedLazyImpl。 Lazy接口 SynchronizedLazyImpl实现了Lazy接口,Lazy具体接口如下: public 继续查看SynchronizedLazyImpl,具体实现如下: SynchronizedLazyImpl内部实现 private 通过上述代码,我们发现 SynchronizedLazyImpl 覆盖了Lazy接口的value属性,并且重新了其属性访问器。其具体逻辑与Java的双重检验是类似的。 到里这里其实大家还是肯定有疑问,我这里只是实例化了SynchronizedLazyImpl对象,并没有进行值的获取,它是怎么拿到高阶函数的返回值呢?。这里又涉及到了委托属性。 委托属性语法是:val/var : by 。在 by 后面的表达式是该 委托, 因为属性对应的 get()(和 set())会被委托给它的 getValue() 和 setValue() 方法。属性的委托不必实现任何的接口,但是需要提供一个 getValue() 函数(和 setValue()——对于 var 属性)。 而Lazy.kt文件中,声明了Lazy接口的getValue扩展函数。故在最终赋值的时候会调用该方法。 internal.InlineOnly 静态内部类式 //Java实现 静态内部类的实现方式,也没有什么好说的。Kotlin与Java实现基本雷同。 补充 在该篇文章结束后,有很多小伙伴咨询,如何在Kotlin版的Double Check,给单例添加一个属性,这里我给大家提供了一个实现的方式。(不好意思,最近才抽出时间来解决这个问题) class SingletonDemo private constructor( 其中关于?:操作符,如果 ?: 左侧表达式非空,就返回其左侧表达式,否则返回右侧表达式。请注意,当且仅当左侧为空时,才会对右侧表达式求值。 Kotlin 智能类型转换 对于子父类之间的类型转换 先看这样一段 Java 代码 public 尽管在 main 函数中,对 person 这个对象进行了类型判断,但是在使用的时候还是需要强制转换成 Student 类型,这样是不是很不智能? 同样的情况在 Kotlin 中就变得简单多了 fun main(args: Array<String>) { 在 Kotlin 中,只要对类型进行了判断,就可以直接通过父类的对象去调用子类的函数了 安全的类型转换 还是上面的那个例子,如果我们没有进行类型判断,并且直接进行强转,会怎么样呢? public static void main(String[] args) { 结果就只能是 Exception in thread "main" java.lang.ClassCastException 那么在 Kotlin 中是不是会有更好的解决方法呢? val person: Person = Person() 在转换操作符后面添加一个 ?,就不会把程序 crash 掉了,当转化失败的时候,就会返回一个 null 在空类型中的智能转换 需要提前了解 Kotlin 类型安全的相关知识(Kotlin 中的类型安全(对空指针的优化处理)) String? = aString 在定义的时候定义成了有可能为 null,按照之前的写法,我们需要这样写 String? = 但是已经进行了是否为 String 类型的判断,所以就一定 不是 空类型了,也就可以直接输出它的长度了 T.()->Unit 、 ()->Unit 在做kotlin开发中,经常看到一些系统函数里,用函数作为参数 public .()-Unit与()->Unit的区别是我们调用时,在代码块里面写this,的时候,两个this代表的含义不一样,T.()->Unit里的this代表的是自身实例,而()->Unit里,this代表的是外部类的实例。 推荐阅读 对 Kotlin 与 Java 编程语言的思考 使用 Kotlin 做开发一个月后的感想 扫一扫 关注我的公众号如果你想要跟大家分享你的文章,欢迎投稿~ 本篇文章为转载内容。原文链接:https://blog.csdn.net/weixin_39611037/article/details/109984124。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-06-23 23:56:14
470
转载
转载文章
...te 类型是在早期 Java 中 java.util.Date 类基础上构建的。为此,Date 类型使用 UTC(Coordinated Universal Time,国际协调时间[又称世界统一时间])1970 年 1 月 1 日午夜(零时)开始经过的毫秒来保存日期。在使用这种数据存储格式的条件下,Date 类型保存的日期能够精确到 1970 年 1 月 1 日之前或之后的 285616 年。 创建一个日期对象,使用 new 运算符和 Date 构造方法(构造函数)即可。 var box = new Date(); // 创建一个日期对象 在调用 Date 构造方法而不传递参数的情况下,新建的对象自动获取当前的时间和日期。 alert(box); // 不同浏览器显示不同 ECMAScript 提供了两个方法,Date.parse()和 Date.UTC()。Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应的毫秒数。ECMA-262 没有定义 Date.parse()应该支持哪种日期格式,因此方法的行为因实现而异,因地区而异。默认通常接收的日期格式如下: ‘月/日/年’,如 6/13/2011; ‘英文月名 日, 年’,如 May 25, 2004; ‘英文星期几 英文月名 日 年 时:分:秒 时区’,如 Tue May 25 2004 00:00:00 GMT-070 alert(Date.parse('6/13/2011')); // 1307894400000 如果 Date.parse()没有传入或者不是标准的日期格式,那么就会返回 NaN。 alert(Date.parse()); // NaN 如果想输出指定的日期,那么把 Date.parse()传入 Date 构造方法里。 var box = new Date(Date.parse('6/13/2011')); // Mon Jun 13 2011 00:00:00 GMT+0800var box = new Date('6/13/2011'); // 直接传入,Date.parse()后台被调用 Date 对象及其在不同浏览器中的实现有许多奇怪的行为。其中有一种倾向是将超出的范围的值替换成当前的值,以便生成输出。例如,在解析“January 32, 2007”时,有的浏览器会将其解释为“February 1, 2007”。而 Opera 则倾向与插入当前月份的当前日期。 Date.UTC()方法同样也返回表示日期的毫秒数,但它与 Date.parse()在构建值时使用不同的信息。(年份,基于 0 的月份[0 表示 1 月,1 表示 2 月],月中的哪一天[1-31],小时数[0-23] ,分钟,秒以及毫秒)。只有前两个参数是必须的。如果没有提供月数,则天数为 1;如果省略其他参数,则统统为 0。 alert(Date.UTC(2011,11)); // 1322697600000 如果 Date.UTC()参数传递错误,那么就会出现负值或者 NaN 等非法信息。 alert(Date.UTC()); // 负值或者 NaN 如果要输出指定日期,那么直接把 Date.UTC()传入 Date 构造方法里即可。 var box = new Date(Date.UTC(2011,11, 5, 15, 13, 16)); 通用的方法 与其他类型一样,Date 类型也重写了 toLocaleString()、toString()和 valueOf()方法;但这些方法返回值与其他类型中的方法不同。 var box = new Date(Date.UTC(2011,11, 5, 15, 13, 16));alert('toString:' + box.toString());alert('toLocaleString:' + box.toLocaleString()); // 按本地格式输出 这两个方法在不同浏览器显示的效果又不一样,但不用担心,这两个方法只是在调试比较有用,在显示时间和日期上,没什么价值。valueOf()方法显示毫秒数。 日期格式化方法 Date 类型还有一些专门用于将日期格式化为字符串的方法。 var box = new Date();alert(box.toDateString()); // 以特定的格式显示星期几、月、日和年alert(box.toTimeString()); // 以特定的格式显示时、分、秒和时区alert(box.toLocaleDateString()); // 以特定地区格式显示星期几、月、日和年alert(box.toLocaleTimeString()); // 以特定地区格式显示时、分、秒和时区alert(box.toUTCString()); // 以特定的格式显示完整的 UTC 日期 组件方法 组件方法,是为我们单独获取你想要的各种时间/日期而提供的方法。需要注意的时候 ,这些方法中,有带 UTC 的,有不带 UTC 的。UTC 日期指的是在没有时区偏差的情况下的日期值。 alert(box.getTime()); // 获取日期的毫秒数,和 valueOf()返回一致alert(box.setTime(100)); // 以毫秒数设置日期,会改变整个日期alert(box.getFullYear()); // 获取四位年份alert(box.setFullYear(2012)); // 设置四位年份,返回的是毫秒数alert(box.getMonth()); // 获取月份,没指定月份,从 0 开始算起alert(box.setMonth(11)); // 设置月份alert(box.getDate()); // 获取日期alert(box.setDate(8)); // 设置日期,返回毫秒数alert(box.getDay()); // 返回星期几,0 表示星期日,6 表示星期六alert(box.setDay(2)); // 设置星期几alert(box.getHours()); // 返回时alert(box.setHours(12)); // 设置时alert(box.getMinutes()); // 返回分钟alert(box.setMinutes(22)); // 设置分钟alert(box.getSeconds()); // 返回秒数alert(box.setSeconds(44)); // 设置秒数alert(box.getMilliseconds()); // 返回毫秒数alert(box.setMilliseconds()); // 设置毫秒数alert(box.getTimezoneOffset()); // 返回本地时间和 UTC 时间相差的分钟数 以上方法除了 getTimezoneOffset(),其他都具有 UTC 功能,例如 setDate()及 getDate()获取星期几,那么就会有 setUTCDate()及getUTCDate(),表示世界协调时间。 2、正则表达式 假设用户需要在 HTML 表单中填写姓名、地址、出生日期等。那么在将表单提交到服务器进一步处理前,JavaScript 程序会检查表单以确认用户确实输入了信息并且这些信息是符合要求的。 什么是正则表达式 正则表达式(regular expression)是一个描述字符模式的对象。ECMAScript 的 RegExp 类表示正则表达式,而 String 和 RegExp 都定义了使用正则表达式进行强大的模式匹配和文本检索与替换的函数。 正则表达式主要用来验证客户端的输入数据。用户填写完表单单击按钮之后,表单就会被发送到服务器,在服务器端通常会用 PHP、ASP.NET 等服务器脚本对其进行进一步处理 。因为客户端验证,可以节约大量的服务器端的系统资源,并且提供更好的用户体验。 创建正则表达式 创建正则表达式和创建字符串类似,创建正则表达式提供了两种方法,一种是采用 new 运算符,另一个是采用字面量方式。 两种创建方式 var box = new RegExp('box'); // 第一个参数字符串var box = new RegExp('box', 'ig'); // 第二个参数可选模式修饰符 模式修饰符的可选参数 参数 含义 i 忽略大小写 g 全局匹配 m 多行匹配 var box = /box/; // 直接用两个反斜杠var box = /box/ig; // 在第二个斜杠后面加上模式修饰符 测试正则表达式 RegExp 对象包含两个方法:test()和 exec(),功能基本相似,用于测试字符串匹配。test()方法在字符串中查找是否存在指定的正则表达式并返回布尔值,如果存在则返回 true,不存在则返回 false。exec()方法也用于在字符串中查找指定正则表达式,如果 exec()方法执行成功,则返回包含该查找字符串的相关信息数组。如果执行失败,则返回 null。 RegExp 对象的方法 方法 功能 test 在字符串中测试模式匹配,返回 true 或 false exec 在字符串中执行匹配搜索,返回结果数组 // 使用 new 运算符的 test 方法示例var pattern = new RegExp('box', 'i'); // 创建正则模式,不区分大小写var str = 'This is a Box!'; // 创建要比对的字符串alert(pattern.test(str)); // 通过 test()方法验证是否匹配// 使用字面量方式的 test 方法示例var pattern = /box/i; // 创建正则模式,不区分大小写var str = 'This is a Box!';alert(pattern.test(str));// 使用一条语句实现正则匹配alert(/box/i.test('This is a Box!')); // 模式和字符串替换掉了两个变量// 使用 exec 返回匹配数组var pattern = /box/i;var str = 'This is a Box!';alert(pattern.exec(str)); // 匹配了返回数组,否则返回 null 使用字符串的正则表达式方法 除了 test()和 exec()方法,String 对象也提供了 4 个使用正则表达式的方法。 String 对象中的正则表达式方法 方法 含义 match(pattern) 返回 pattern 中的子串或 null replace(pattern, replacement) 用 replacement 替换 pattern search(pattern) 返回字符串中 pattern 开始位置 split(pattern) 返回字符串按指定 pattern 拆分的数组 // 使用 match 方法获取获取匹配数组var pattern = /box/ig; // 全局搜索var str = 'This is a Box!,That is a Box too';alert(str.match(pattern)); // 匹配到两个 Box,Boxalert(str.match(pattern).length); // 获取数组的长度// 使用 search 来查找匹配数据var pattern = /box/ig;var str = 'This is a Box!,That is a Box too';alert(str.search(pattern)); // 查找到返回位置,否则返回-1 因为 search 方法查找到即返回,也就是说无需 g 全局。 // 使用 replace 替换匹配到的数据var pattern = /box/ig;var str = 'This is a Box!,That is a Box too';alert(str.replace(pattern, 'Tom')); // 将 Box 替换成了 Tom// 使用 split 拆分成字符串数组var pattern = / /ig;var str = 'This is a Box!,That is a Box too';alert(str.split(pattern)); // 将空格拆开分组成数组 RegExp 对象的静态属性 属性 短名 含义 input $_ 当前被匹配的字符串 lastMatch $& 最后一个匹配字符串 lastParen $+ 最后一对圆括号内的匹配子串 leftContext $ 最后一次匹配前的子串 multiline $ 用于指定是否所有的表达式都用于多行的布尔值 rightContext $’ 在上次匹配之后的子串 // 使用静态属性var pattern = /(g)oogle/;var str = 'This is google!';pattern.test(str); // 执行一下alert(RegExp.input); // This is google!alert(RegExp.leftContext); // This isalert(RegExp.rightContext); // !alert(RegExp.lastMatch); // googlealert(RegExp.lastParen); // galert(RegExp.multiline); // false Opera 不支持 input、lastMatch、lastParen 和 multiline 属性。IE 不支持 multiline 属性。所有的属性可以使用短名来操作。RegExp.input 可以改写成 RegExp['$_'],依次类推。但 RegExp.input 比较特殊,它还可以写成 RegExp.$_。 RegExp 对象的实例属性 属性 含义 global Boolean 值,表示 g 是否已设置 ignoreCase Boolean 值,表示 i 是否已设置 lastIndex 整数,代表下次匹配将从哪里字符位置开始 multiline Boolean 值,表示 m 是否已设置 Source 正则表达式的源字符串形式 // 使用实例属性var pattern = /google/ig;alert(pattern.global); // true,是否全局了alert(pattern.ignoreCase); // true,是否忽略大小写alert(pattern.multiline); // false,是否支持换行alert(pattern.lastIndex); // 0,下次的匹配位置alert(pattern.source); // google,正则表达式的源字符串var pattern = /google/g;var str = 'google google google';pattern.test(str); // google,匹配第一次alert(pattern.lastIndex); // 6,第二次匹配的位 以上基本没什么用。并且 lastIndex 在获取下次匹配位置上 IE 和其他浏览器有偏差 ,主要表现在非全局匹配上。lastIndex 还支持手动设置,直接赋值操作。 获取控制 正则表达式元字符是包含特殊含义的字符。它们有一些特殊功能,可以控制匹配模式的方式。反斜杠后的元字符将失去其特殊含义。 字符类:单个字符和数字 元字符/元符号 匹配情况 . 匹配除换行符外的任意字符 [a-z0-9] 匹配括号中的字符集中的任意字符 [^a-z0-9] 匹配任意不在括号中的字符集中的字符 \d 匹配数字 \D 匹配非数字,同[^0-9]相同 \w 匹配字母和数字及_ \W 匹配非字母和数字及_ 字符类:空白字符 元字符/元符号 匹配情况 \0 匹配 null 字符 \b 匹配空格字符 \f 匹配进纸字符 \n 匹配换行符 \r 匹配回车字符 \t 匹配制表符 \s 匹配空白字符、空格、制表符和换行符 \S 匹配非空白字符 字符类:锚字符 元字符/元符号 匹配情况 ^ 行首匹配 $ 行尾匹配 \A 只有匹配字符串开始处 \b 匹配单词边界,词在[]内时无效 \B 匹配非单词边界 \G 匹配当前搜索的开始位置 \Z 匹配字符串结束处或行尾 \z 只匹配字符串结束处 字符类:重复字符 元字符/元符号 匹配情况 x? 匹配 0 个或 1 个 x x 匹配 0 个或任意多个 x x+ 匹配至少一个 x (xyz)+ 匹配至少一个(xyz) x{m,n} 匹配最少 m 个、最多 n 个 x 字符类:替代字符 元字符/元符号 匹配情况 this where 字符类:记录字符 元字符/元符号 匹配情况 (string) 用于反向引用的分组 \1 或$1 匹配第一个分组中的内容 \2 或$2 匹配第二个分组中的内容 \3 或$3 匹配第三个分组中的内容 // 使用点元字符var pattern = /g..gle/; // .匹配一个任意字符var str = 'google';alert(pattern.test(str));// 重复匹配var pattern = /g.gle/; // .匹配 0 个一个或多个var str = 'google'; //,?,+,{n,m}alert(pattern.test(str));// 使用字符类匹配var pattern = /g[a-zA-Z_]gle/; // [a-z]表示任意个 a-z 中的字符var str = 'google';alert(pattern.test(str));var pattern = /g[^0-9]gle/; // [^0-9]表示任意个非 0-9 的字符var str = 'google';alert(pattern.test(str));var pattern = /[a-z][A-Z]+/; // [A-Z]+表示 A-Z 一次或多次var str = 'gOOGLE';alert(pattern.test(str));// 使用元符号匹配var pattern = /g\wgle/; // \w匹配任意多个所有字母数字_var str = 'google';alert(pattern.test(str));var pattern = /google\d/; // \d匹配任意多个数字var str = 'google444';alert(pattern.test(str));var pattern = /\D{7,}/; // \D{7,}匹配至少 7 个非数字var str = 'google8';alert(pattern.test(str));// 使用锚元字符匹配var pattern = /^google$/; // ^从开头匹配,$从结尾开始匹配var str = 'google';alert(pattern.test(str));var pattern = /goo\sgle/; // \s 可以匹配到空格var str = 'goo gle';alert(pattern.test(str));var pattern = /google\b/; // \b 可以匹配是否到了边界var str = 'google';alert(pattern.test(str));// 使用或模式匹配var pattern = /google|baidu|bing/; // 匹配三种其中一种字符串var str = 'google';alert(pattern.test(str));// 使用分组模式匹配var pattern = /(google){4,8}/; // 匹配分组里的字符串 4-8 次var str = 'googlegoogle';alert(pattern.test(str));var pattern = /8(.)8/; // 获取 8..8 之间的任意字符var str = 'This is 8google8';str.match(pattern);alert(RegExp.$1); // 得到第一个分组里的字符串内容var pattern = /8(.)8/;var str = 'This is 8google8';var result = str.replace(pattern,'<strong>$1</strong>'); // 得到替换的字符串输出document.write(result);var pattern = /(.)\s(.)/;var str = 'google baidu';var result = str.replace(pattern, '$2 $1'); // 将两个分组的值替换输出document.write(result); 贪婪 惰性 + +? ? ?? ? {n} {n}? {n,} {n,}? {n,m} {n,m}? // 关于贪婪和惰性var pattern = /[a-z]+?/; // ?号关闭了贪婪匹配,只替换了第一个var str = 'abcdefjhijklmnopqrstuvwxyz';var result = str.replace(pattern, 'xxx');alert(result);var pattern = /8(.+?)8/g; // 禁止了贪婪,开启的全局var str = 'This is 8google8, That is 8google8, There is 8google8';var result = str.replace(pattern,'<strong>$1</strong>');document.write(result);var pattern = /8([^8])8/g; // 另一种禁止贪婪var str = 'This is 8google8, That is 8google8, There is 8google8';var result = str.replace(pattern,'<strong>$1</strong>');document.write(result);// 使用 exec 返回数组var pattern = /^[a-z]+\s[0-9]{4}$/i;var str = 'google 2012';alert(pattern.exec(str)); // 返回整个字符串var pattern = /^[a-z]+/i; // 只匹配字母var str = 'google 2012';alert(pattern.exec(str)); // 返回 googlevar pattern = /^([a-z]+)\s([0-9]{4})$/i; // 使用分组var str = 'google 2012';alert(pattern.exec(str)[0]); // google 2012alert(pattern.exec(str)[1]); // googlealert(pattern.exec(str)[2]); // 2012// 捕获性分组和非捕获性分组var pattern = /(\d+)([a-z])/; // 捕获性分组var str = '123abc';alert(pattern.exec(str));var pattern = /(\d+)(?:[a-z])/; // 非捕获性分组var str = '123abc';alert(pattern.exec(str));// 使用分组嵌套var pattern = /(A?(B?(C?)))/; // 从外往内获取var str = 'ABC';alert(pattern.exec(str));// 使用前瞻捕获var pattern = /(goo(?=gle))/; // goo 后面必须跟着 gle 才能捕获var str = 'google';alert(pattern.exec(str));// 使用特殊字符匹配var pattern = /\.\[\/b\]/; // 特殊字符,用\符号转义即可var str = '.[/b]';alert(pattern.test(str));// 使用换行模式var pattern = /^\d+/mg; // 启用了换行模式var str = '1.baidu\n2.google\n3.bing';var result = str.replace(pattern, '');alert(result); 常用的正则 检查邮政编码 var pattern = /[1-9][0-9]{5}/; // 共 6 位数字,第一位不能为 0var str = '224000';alert(pattern.test(str)); 检查文件压缩包 var pattern = /[\w]+\.zip|rar|gz/; // \w 表示所有数字和字母加下划线var str = '123.zip'; // \.表示匹配.,后面是一个选择alert(pattern.test(str)); 删除多余空格 var pattern = /\s/g; // g 必须全局,才能全部匹配var str = '111 222 333';var result = str.replace(pattern,''); // 把空格匹配成无空格alert(result); 删除首尾空格 var pattern = /^\s+/; // 强制首var str = ' goo gle ';var result = str.replace(pattern, '');pattern = /\s+$/; // 强制尾result = result.replace(pattern, '');alert('|' + result + '|');var pattern = /^\s(.+?)\s$/; // 使用了非贪婪捕获var str = ' google ';alert('|' + pattern.exec(str)[1] + '|');var pattern = /^\s(.+?)\s$/;var str = ' google ';alert('|' + str.replace(pattern, '$1') + '|'); // 使用了分组获取 简单的电子邮件验证 var pattern = /^([a-zA-Z0-9_\.\-]+)@([a-zA-Z0-9_\.\-]+)\.([a-zA-Z]{2,4})$/;var str = 'yc60.com@gmail.com';alert(pattern.test(str));var pattern = /^([\w\.\-]+)@([\w\.\-]+)\.([\w]{2,4})$/;var str = 'yc60.com@gmail.com';alert(pattern.test(str)); 3、Function类型 在 ECMAScript 中,Function(函数)类型实际上是对象。每个函数都是 Function 类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针。 函数的声明方式 普通的函数声明 function box(num1, num2) {return num1+ num2;} 使用变量初始化函数 var box= function(num1, num2) {return num1 + num2;}; 使用 Function 构造函数 var box= new Function('num1', 'num2' ,'return num1 + num2'); 第三种方式我们不推荐,因为这种语法会导致解析两次代码(第一次解析常规 ECMAScript 代码,第二次是解析传入构造函数中的字符串),从而影响性能。但我们可以通过这种语法来理解"函数是对象,函数名是指针"的概念。 作为值的函数 ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。 function box(sumFunction, num) {return sumFunction(num); // someFunction}function sum(num) {return num + 10;}var result = box(sum, 10); // 传递函数到另一个函数里 函数内部属性 在函数内部,有两个特殊的对象:arguments 和 this。arguments 是一个类数组对象,包含着传入函数中的所有参数,主要用途是保存函数参数。但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。 function box(num) {if (num <= 1) {return 1;} else {return num box(num-1); // 一个简单的的递归} } 对于阶乘函数一般要用到递归算法,所以函数内部一定会调用自身;如果函数名不改变是没有问题的,但一旦改变函数名,内部的自身调用需要逐一修改。为了解决这个问题,我们可以使用 arguments.callee 来代替。 function box(num) {if (num <= 1) {return 1;} else {return num arguments.callee(num-1); // 使用 callee 来执行自身} } 函数内部另一个特殊对象是 this,其行为与 Java 和 C中的 this 大致相似。换句话说 ,this 引用的是函数据以执行操作的对象,或者说函数调用语句所处的那个作用域。当在全局作用域中调用函数时,this 对象引用的就是 window。 // 便于理解的改写例子window.color = '红色的'; // 全局的,或者 var color = '红色的';也行alert(this.color); // 打印全局的 colorvar box = {color : '蓝色的', // 局部的 colorsayColor : function () {alert(this.color); // 此时的 this 只能 box 里的 color} };box.sayColor(); // 打印局部的 coloralert(this.color); // 还是全局的// 引用教材的原版例子window.color = '红色的'; // 或者 var color = '红色的';也行var box = {color : '蓝色的'};function sayColor() {alert(this.color); // 这里第一次在外面,第二次在 box 里面}getColor();box.sayColor = sayColor; // 把函数复制到 box 对象里,成为了方法box.sayColor(); 函数属性和方法 ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性 :length 和 prototype。其中,length 属性表示函数希望接收的命名参数的个数。 function box(name, age) {alert(name + age);}alert(box.length); // 2 对于 prototype 属性,它是保存所有实例方法的真正所在,也就是原型。这个属性 ,我们将在面向对象一章详细介绍。而 prototype 下有两个方法:apply()和 call(),每个函数都包含这两个非继承而来的方法。这两个方法的用途都在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。 function box(num1, num2) {return num1 + num2; // 原函数}function sayBox(num1, num2) {return box.apply(this, [num1, num2]); // this 表示作用域,这里是 window} // []表示 box 所需要的参数function sayBox2(num1, num2) {return box.apply(this, arguments); // arguments 对象表示 box 所需要的参数}alert(sayBox(10,10)); // 20alert(sayBox2(10,10)); // 20 call()方法于 apply()方法相同,他们的区别仅仅在于接收参数的方式不同。对于 call()方法而言,第一个参数是作用域,没有变化,变化只是其余的参数都是直接传递给函数的。 function box(num1, num2) {return num1 + num2;}function callBox(num1, num2) {return box.call(this, num1, num2); // 和 apply 区别在于后面的传参}alert(callBox(10,10)); 事实上,传递参数并不是 apply()和 call()方法真正的用武之地;它们经常使用的地方是能够扩展函数赖以运行的作用域。 var color = '红色的'; // 或者 window.color = '红色的';也行var box = {color : '蓝色的'};function sayColor() {alert(this.color);}sayColor(); // 作用域在 windowsayColor.call(this); // 作用域在 windowsayColor.call(window); // 作用域在 windowsayColor.call(box); // 作用域在 box,对象冒充 这个例子是之前作用域理解的例子修改而成,我们可以发现当我们使用 call(box)方法的时候,sayColor()方法的运行环境已经变成了 box 对象里了。 使用 call()或者 apply()来扩充作用域的最大好处,就是对象不需要与方法发生任何耦合关系(耦合,就是互相关联的意思,扩展和维护会发生连锁反应)。也就是说,box 对象和 sayColor()方法之间不会有多余的关联操作,比如 box.sayColor = sayColor;。 本篇文章为转载内容。原文链接:https://blog.csdn.net/gongxifacai_believe/article/details/108286196。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-01-24 13:01:25
529
转载
转载文章
...t/luoyang_java/article/details/84953241。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。 1、分布式事务出现场景 场景描述:支付宝转账余额宝 分布式事务必须满足的条件: 1、远程RPC调用,支付宝和余额宝存在接口调用 2、支付宝和余额宝使用不同的数据库 如图: 2、分布式事务解决方案 1、基于数据库XA协议的两段提交 XA协议是数据库支持的一种协议,其核心是一个事务管理器用来统一管理两个分布式数据库,如图 事务管理器负责跟支付宝数据库和余额宝数据库打交道,一旦有一个数据库连接失败,另一个数据库的操作就不会进行,一个数据库操作失败就会导致另一个数据库回滚,只有他们全部成功两个数据库的事务才会提交。 基于XA协议的两段和三段提交是一种严格的安全确认机制,其安全性是非常高的,但是保证安全性的前提是牺牲了性能,这个就是分布式系统里面的CAP理论,做任何架构的前提需要有取舍。所以基于XA协议的分布式事务并发性不高,不适合高并发场景。 2、基于activemq的解决方案 如图: 1、支付宝扣款成功时往message表插入消息 2、message表有message_id(流水id,标识夸系统的一次转账操作),status(confirm,unconfirm) 3、timer扫描message表的unconfirm状态记录往activemq插入消息 4、余额宝收到消息消费消息时先查询message表如果有记录就不处理如果没记录就进行数据库增款操作 5、如果余额宝数据库操作成功往余额宝message表插入消息,表字段跟支付宝message一致 6、如果5操作成功,回调支付宝接口修改message表状态,把unconfirm状态转换成confirm状态 问题描述: 1、支付宝设计message表的目的 如果支付宝往activemq插入消息而余额宝消费消息异常,有可能是消费消息成功而事务操作异常,有可能是网络异常等等不确定因素。如果出现异常而activemq收到了确认消息的信号,这时候activemq中的消息是删除了的,消息丢失了。设置message表就是有一个消息存根,activemq中消息丢失了message表中的消息还在。解决了activemq消息丢失问题 2、余额宝设计message表的目的 当余额宝消费成功并且数据库操作成功时,回调支付宝的消息确认接口,如果回调接口时出现异常导致支付宝状态修改失败还是unconfirm状态,这时候还会被timer扫描到,又会往activemq插入消息,又会被余额宝消费一边,但是这条消息已经消费成功了的只是回调失败而已,所以就需要有一个这样的message表,当余额宝消费时先插入message表,如果message根据message_id能查询到记录就说明之前这条消息被消费过就不再消费只需要回调成功即可,如果查询不到消息就消费这条消息继续数据库操作,数据库操作成功就往message表插入消息。 这样就解决了消息重复消费问题,这也是消费端的幂等操作。 基于消息中间件的分布式事务是最理想的分布式事务解决方案,兼顾了安全性和并发性! 接下来贴代码: 支付宝代码: @Controller@RequestMapping("/order")public class OrderController {/ @Description TODO @param @return 参数 @return String 返回类型 @throws userID:转账的用户ID amount:转多少钱/@Autowired@Qualifier("activemq")OrderService orderService;@RequestMapping("/transfer")public @ResponseBody String transferAmount(String userId,String messageId, int amount) {try {orderService.updateAmount(amount,messageId, userId);}catch (Exception e) {e.printStackTrace();return "===============================transferAmount failed===================";}return "===============================transferAmount successfull===================";}@RequestMapping("/callback")public String callback(String param) {JSONObject parse = JSONObject.parseObject(param);String respCode = parse.getString("respCode");if(!"OK".equalsIgnoreCase(respCode)) {return null;}try {orderService.updateMessage(param);}catch (Exception e) {e.printStackTrace();return "fail";}return "ok";} } public interface OrderService {public void updateAmount(int amount, String userId,String messageId);public void updateMessage(String param);} @Service("activemq")@Transactional(rollbackFor = Exception.class)public class OrderServiceActivemqImpl implements OrderService {Logger logger = LoggerFactory.getLogger(getClass());@AutowiredJdbcTemplate jdbcTemplate;@AutowiredJmsTemplate jmsTemplate;@Overridepublic void updateAmount(final int amount, final String messageId, final String userId) {String sql = "update account set amount = amount - ?,update_time=now() where user_id = ?";int count = jdbcTemplate.update(sql, new Object[]{amount, userId});if (count == 1) {//插入到消息记录表sql = "insert into message(user_id,message_id,amount,status) values (?,?,?,?)";int row = jdbcTemplate.update(sql,new Object[]{userId,messageId,amount,"unconfirm"});if(row == 1) {//往activemq中插入消息jmsTemplate.send("zg.jack.queue", new MessageCreator() {@Overridepublic Message createMessage(Session session) throws JMSException {com.zhuguang.jack.bean.Message message = new com.zhuguang.jack.bean.Message();message.setAmount(Integer.valueOf(amount));message.setStatus("unconfirm");message.setUserId(userId);message.setMessageId(messageId);return session.createObjectMessage(message);} });} }}@Overridepublic void updateMessage(String param) {JSONObject parse = JSONObject.parseObject(param);String messageId = parse.getString("messageId");String sql = "update message set status = ? where message_id = ?";int count = jdbcTemplate.update(sql,new Object[]{"confirm",messageId});if(count == 1) {logger.info(messageId + " callback successfull");} }} activemq.xml <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:amq="http://activemq.apache.org/schema/core"xmlns:jms="http://www.springframework.org/schema/jms"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.1.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-4.1.xsdhttp://www.springframework.org/schema/jmshttp://www.springframework.org/schema/jms/spring-jms-4.1.xsdhttp://activemq.apache.org/schema/corehttp://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"><context:component-scan base-package="com.zhuguang.jack" /><mvc:annotation-driven /><amq:connectionFactory id="amqConnectionFactory"brokerURL="tcp://192.168.88.131:61616"userName="system"password="manager" /><!-- 配置JMS连接工长 --><bean id="connectionFactory"class="org.springframework.jms.connection.CachingConnectionFactory"><constructor-arg ref="amqConnectionFactory" /><property name="sessionCacheSize" value="100" /></bean><!-- 定义消息队列(Queue) --><bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"><!-- 设置消息队列的名字 --><constructor-arg><value>zg.jack.queue</value></constructor-arg></bean><!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 --><bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"><property name="connectionFactory" ref="connectionFactory" /><property name="defaultDestination" ref="demoQueueDestination" /><property name="receiveTimeout" value="10000" /><!-- true是topic,false是queue,默认是false,此处显示写出false --><property name="pubSubDomain" value="false" /></bean></beans> spring-dispatcher.xml <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:tx="http://www.springframework.org/schema/tx"xmlns:util="http://www.springframework.org/schema/util" xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/utilhttp://www.springframework.org/schema/util/spring-util-3.2.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-3.2.xsdhttp://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-3.0.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"><!-- 引入同文件夹下的redis属性配置文件 --><!-- 解决springMVC响应数据乱码 text/plain就是响应的时候原样返回数据--><import resource="../activemq/activemq.xml"/><!--<context:property-placeholder ignore-unresolvable="true" location="classpath:config/core/core.properties,classpath:config/redis/redis-config.properties" />--><bean id="propertyConfigurerForProject1" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"><property name="order" value="1" /><property name="ignoreUnresolvablePlaceholders" value="true" /><property name="location"><value>classpath:config/core/core.properties</value></property></bean><mvc:annotation-driven><mvc:message-converters register-defaults="true"><bean class="org.springframework.http.converter.StringHttpMessageConverter"><property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" /></bean></mvc:message-converters></mvc:annotation-driven><!-- 避免IE执行AJAX时,返回JSON出现下载文件 --><bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"><property name="supportedMediaTypes"><list><value>text/html;charset=UTF-8</value></list></property></bean><!-- 开启controller注解支持 --><!-- 注:如果base-package=com.avicit 则注解事务不起作用 TODO 读源码 --><context:component-scan base-package="com.zhuguang"></context:component-scan><mvc:view-controller path="/" view-name="redirect:/index" /><beanclass="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /><bean id="handlerAdapter"class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"></bean><beanclass="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"><property name="mediaTypes"><map><entry key="json" value="application/json" /><entry key="xml" value="application/xml" /><entry key="html" value="text/html" /></map></property><property name="viewResolvers"><list><bean class="org.springframework.web.servlet.view.BeanNameViewResolver" /><bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"><property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /><property name="prefix" value="/" /><property name="suffix" value=".jsp" /></bean></list></property></bean><!-- 支持上传文件 --> <!-- 控制器异常处理 --><bean id="exceptionResolver"class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"><property name="exceptionMappings"><props><prop key="java.lang.Exception">error</prop></props></property></bean><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"><property name="driverClass"><value>${jdbc.driverClassName}</value></property><property name="jdbcUrl"><value>${jdbc.url}</value></property><property name="user"><value>${jdbc.username}</value></property><property name="password"><value>${jdbc.password}</value></property><property name="minPoolSize" value="10" /><property name="maxPoolSize" value="100" /><property name="maxIdleTime" value="1800" /><property name="acquireIncrement" value="3" /><property name="maxStatements" value="1000" /><property name="initialPoolSize" value="10" /><property name="idleConnectionTestPeriod" value="60" /><property name="acquireRetryAttempts" value="30" /><property name="breakAfterAcquireFailure" value="false" /><property name="testConnectionOnCheckout" value="false" /><property name="acquireRetryDelay"><value>100</value></property></bean><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource"></property></bean><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" /><aop:aspectj-autoproxy expose-proxy="true"/></beans> logback.xml <?xml version="1.0" encoding="UTF-8"?><!--scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。--><configuration scan="false" scanPeriod="60 seconds" debug="false"><!-- 定义日志的根目录 --><!-- <property name="LOG_HOME" value="/app/log" /> --><!-- 定义日志文件名称 --><property name="appName" value="netty"></property><!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 --><appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"><Encoding>UTF-8</Encoding><!--日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度%logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 --> <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender"><Encoding>UTF-8</Encoding><!-- 指定日志文件的名称 --> <file>${appName}.log</file><!--当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。--><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 %i:当文件大小超过maxFileSize时,按照i进行文件滚动--><fileNamePattern>${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern><!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。--><MaxHistory>365</MaxHistory><!-- 当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy--><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy></rollingPolicy><!--日志输出格式:%d表示日期时间,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 %msg:日志消息,%n是换行符--> <encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern></encoder></appender><!-- logger主要用于存放日志对象,也可以定义日志类型、级别name:表示匹配的logger类型前缀,也就是包的前半部分level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERRORadditivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,false:表示只用当前logger的appender-ref,true:表示当前logger的appender-ref和rootLogger的appender-ref都有效--><!-- <logger name="edu.hyh" level="info" additivity="true"><appender-ref ref="appLogAppender" /></logger> --><!-- root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 --><root level="debug"><appender-ref ref="stdout" /><appender-ref ref="appLogAppender" /></root></configuration> 2、余额宝代码 package com.zhuguang.jack.controller;import com.alibaba.fastjson.JSONObject;import com.zhuguang.jack.service.OrderService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller@RequestMapping("/order")public class OrderController {/ @Description TODO @param @return 参数 @return String 返回类型 @throws 模拟银行转账 userID:转账的用户ID amount:转多少钱/@AutowiredOrderService orderService;@RequestMapping("/transfer")public @ResponseBody String transferAmount(String userId, String amount) {try {orderService.updateAmount(Integer.valueOf(amount), userId);}catch (Exception e) {e.printStackTrace();return "===============================transferAmount failed===================";}return "===============================transferAmount successfull===================";} } 消息监听器 package com.zhuguang.jack.listener;import com.alibaba.fastjson.JSONObject;import com.zhuguang.jack.service.OrderService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.client.RestTemplate;import javax.jms.JMSException;import javax.jms.Message;import javax.jms.MessageListener;import javax.jms.ObjectMessage;@Service("queueMessageListener")public class QueueMessageListener implements MessageListener {private Logger logger = LoggerFactory.getLogger(getClass());@AutowiredOrderService orderService;@Transactional(rollbackFor = Exception.class)@Overridepublic void onMessage(Message message) {if (message instanceof ObjectMessage) {ObjectMessage objectMessage = (ObjectMessage) message;try {com.zhuguang.jack.bean.Message message1 = (com.zhuguang.jack.bean.Message) objectMessage.getObject();String userId = message1.getUserId();int count = orderService.queryMessageCountByUserId(userId);if (count == 0) {orderService.updateAmount(message1.getAmount(), message1.getUserId());orderService.insertMessage(message1.getUserId(), message1.getMessageId(), message1.getAmount(), "ok");} else {logger.info("异常转账");}RestTemplate restTemplate = createRestTemplate();JSONObject jo = new JSONObject();jo.put("messageId", message1.getMessageId());jo.put("respCode", "OK");String url = "http://jack.bank_a.com:8080/alipay/order/callback?param="+ jo.toJSONString();restTemplate.getForObject(url,null);} catch (JMSException e) {e.printStackTrace();throw new RuntimeException("异常");} }}public RestTemplate createRestTemplate() {SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();simpleClientHttpRequestFactory.setConnectTimeout(3000);simpleClientHttpRequestFactory.setReadTimeout(2000);return new RestTemplate(simpleClientHttpRequestFactory);} } package com.zhuguang.jack.service;public interface OrderService {public void updateAmount(int amount, String userId);public int queryMessageCountByUserId(String userId);public int insertMessage(String userId,String messageId,int amount,String status);} package com.zhuguang.jack.service;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.client.SimpleClientHttpRequestFactory;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.client.RestTemplate;@Service@Transactional(rollbackFor = Exception.class)public class OrderServiceImpl implements OrderService {private Logger logger = LoggerFactory.getLogger(getClass());@AutowiredJdbcTemplate jdbcTemplate;/ 更新数据库表,把账户余额减去amountd/@Overridepublic void updateAmount(int amount, String userId) {//1、农业银行转账3000,也就说农业银行jack账户要减3000String sql = "update account set amount = amount + ?,update_time=now() where user_id = ?";int count = jdbcTemplate.update(sql, new Object[] {amount, userId});if (count != 1) {throw new RuntimeException("订单创建失败,农业银行转账失败!");} }public RestTemplate createRestTemplate() {SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();simpleClientHttpRequestFactory.setConnectTimeout(3000);simpleClientHttpRequestFactory.setReadTimeout(2000);return new RestTemplate(simpleClientHttpRequestFactory);}@Overridepublic int queryMessageCountByUserId(String messageId) {String sql = "select count() from message where message_id = ?";int count = jdbcTemplate.queryForInt(sql, new Object[]{messageId});return count;}@Overridepublic int insertMessage(String userId, String message_id,int amount, String status) {String sql = "insert into message(user_id,message_id,amount,status) values(?,?,?)";int count = jdbcTemplate.update(sql, new Object[]{userId, message_id,amount, status});if(count == 1) {logger.info("Ok");}return count;} } activemq.xml <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:amq="http://activemq.apache.org/schema/core"xmlns:jms="http://www.springframework.org/schema/jms"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.1.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-4.1.xsdhttp://www.springframework.org/schema/jmshttp://www.springframework.org/schema/jms/spring-jms-4.1.xsdhttp://activemq.apache.org/schema/corehttp://activemq.apache.org/schema/core/activemq-core-5.12.1.xsd"><context:component-scan base-package="com.zhuguang.jack" /><mvc:annotation-driven /><amq:connectionFactory id="amqConnectionFactory"brokerURL="tcp://192.168.88.131:61616"userName="system"password="manager" /><!-- 配置JMS连接工长 --><bean id="connectionFactory"class="org.springframework.jms.connection.CachingConnectionFactory"><constructor-arg ref="amqConnectionFactory" /><property name="sessionCacheSize" value="100" /></bean><!-- 定义消息队列(Queue) --><bean id="demoQueueDestination" class="org.apache.activemq.command.ActiveMQQueue"><!-- 设置消息队列的名字 --><constructor-arg><value>zg.jack.queue</value></constructor-arg></bean><!-- 显示注入消息监听容器(Queue),配置连接工厂,监听的目标是demoQueueDestination,监听器是上面定义的监听器 --><bean id="queueListenerContainer"class="org.springframework.jms.listener.DefaultMessageListenerContainer"><property name="connectionFactory" ref="connectionFactory" /><property name="destination" ref="demoQueueDestination" /><property name="messageListener" ref="queueMessageListener" /></bean><!-- 配置JMS模板(Queue),Spring提供的JMS工具类,它发送、接收消息。 --><bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"><property name="connectionFactory" ref="connectionFactory" /><property name="defaultDestination" ref="demoQueueDestination" /><property name="receiveTimeout" value="10000" /><!-- true是topic,false是queue,默认是false,此处显示写出false --><property name="pubSubDomain" value="false" /></bean></beans> OK~~~~~~~~~~~~大功告成!!!, 如果大家觉得满意并且对技术感兴趣请加群:171239762, 纯技术交流群,非诚勿扰。 本篇文章为转载内容。原文链接:https://blog.csdn.net/luoyang_java/article/details/84953241。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-04-16 22:34:52
499
转载
转载文章
...透明度,大小,位置等参数,合成为一个数据buffer,传递HWComposer或者OpenGL处理,最终给显示器。 在显示过程中使用到了bufferqueue,surfaceflinger作为consumer方,比如windowmanager管理的surface作为生产方产生页面,交由surfaceflinger进行合成。 VSYNC Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSYNC是一种在PC上很早就有应用,可以理解为一种定时中断技术。 tearing 问题: 早期的 Android 是没有 vsync 机制的,CPU 和 GPU 的配合也比较混乱,这也造成著名的 tearing 问题,即 CPU/GPU 直接更新正在显示的屏幕 buffer 造成画面撕裂。 后续 Android 引入了双缓冲机制,但是 buffer 的切换也需要一个比较合适的时机,也就是屏幕扫描完上一帧后的时机,这也就是引入 vsync 的原因。 早先一般的屏幕刷新率是 60fps,所以每个 vsync 信号的间隔也是 16ms,不过随着技术的更迭以及厂商对于流畅性的追求,越来越多 90fps 和 120fps 的手机面世,相对应的间隔也就变成了 11ms 和 8ms。 VSYNC信号种类: 1.屏幕产生的硬件VSYNC:硬件VSYNC是一种脉冲信号,起到开关和触发某种操作的作用。 2.由SurfaceFlinger将其转成的软件VSYNC信号,经由Binder传递给Choreographer Choreographer: 编舞者,用于注册VSYNC信号并接收VSYNC信号回调,当内部接收到这个信号时最终会调用到doFrame进行帧的绘制操作。 Choreographer在系统中流程: 如何通过Choreographer计算掉帧情况:原理就是: 通过给Choreographer设置FrameCallback,在每次绘制前后看时间差是16.6ms的多少倍,即为前后掉帧率。 使用方式如下: //Application.javapublic void onCreate() {super.onCreate();//在Application中使用postFrameCallbackChoreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));}public class FPSFrameCallback implements Choreographer.FrameCallback {private static final String TAG = "FPS_TEST";private long mLastFrameTimeNanos = 0;private long mFrameIntervalNanos;public FPSFrameCallback(long lastFrameTimeNanos) {mLastFrameTimeNanos = lastFrameTimeNanos;mFrameIntervalNanos = (long)(1000000000 / 60.0);}@Overridepublic void doFrame(long frameTimeNanos) {//初始化时间if (mLastFrameTimeNanos == 0) {mLastFrameTimeNanos = frameTimeNanos;}final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;if (jitterNanos >= mFrameIntervalNanos) {final long skippedFrames = jitterNanos / mFrameIntervalNanos;if(skippedFrames>30){//丢帧30以上打印日志Log.i(TAG, "Skipped " + skippedFrames + " frames! "+ "The application may be doing too much work on its main thread.");} }mLastFrameTimeNanos=frameTimeNanos;//注册下一帧回调Choreographer.getInstance().postFrameCallback(this);} } UI绘制全路径分析: 有了前面几个概念,这里我们让SurfaceFlinger结合View的绘制流程用一张图来表达整个绘制流程: 生产者:APP方构建Surface的过程。 消费者:SurfaceFlinger UI绘制全路径分析卡顿原因: 接下来,我们逐个分析,看看都会有哪些原因可能造成卡顿: 1.渲染流程 1.Vsync 调度:这个是起始点,但是调度的过程会经过线程切换以及一些委派的逻辑,有可能造成卡顿,但是一般可能性比较小,我们也基本无法介入; 2.消息调度:主要是 doframe Message 的调度,这就是一个普通的 Handler 调度,如果这个调度被其他的 Message 阻塞产生了时延,会直接导致后续的所有流程不会被触发 3.input 处理:input 是一次 Vsync 调度最先执行的逻辑,主要处理 input 事件。如果有大量的事件堆积或者在事件分发逻辑中加入大量耗时业务逻辑,会造成当前帧的时长被拉大,造成卡顿,可以尝试通过事件采样的方案,减少 event 的处理 4.动画处理:主要是 animator 动画的更新,同理,动画数量过多,或者动画的更新中有比较耗时的逻辑,也会造成当前帧的渲染卡顿。对动画的降帧和降复杂度其实解决的就是这个问题; 5.view 处理:主要是接下来的三大流程,过度绘制、频繁刷新、复杂的视图效果都是此处造成卡顿的主要原因。比如我们平时所说的降低页面层级,主要解决的就是这个问题; 6.measure/layout/draw:view 渲染的三大流程,因为涉及到遍历和高频执行,所以这里涉及到的耗时问题均会被放大,比如我们会降不能在 draw 里面调用耗时函数,不能 new 对象等等; 7.DisplayList 的更新:这里主要是 canvas 和 displaylist 的映射,一般不会存在卡顿问题,反而可能存在映射失败导致的显示问题; 8.OpenGL 指令转换:这里主要是将 canvas 的命令转换为 OpenGL 的指令,一般不存在问题 9.buffer 交换:这里主要指 OpenGL 指令集交换给 GPU,这个一般和指令的复杂度有关 10.GPU 处理:顾名思义,这里是 GPU 对数据的处理,耗时主要和任务量和纹理复杂度有关。这也就是我们降低 GPU 负载有助于降低卡顿的原因; 11.layer 合成:Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。 可以选择降低Surface层级来优化卡顿。 12.光栅化/Display:这里暂时忽略,底层系统行为; Buffer 切换:主要是屏幕的显示,这里 buffer 的数量也会影响帧的整体延迟,不过是系统行为,不能干预。 2.系统负载 内存:内存的吃紧会直接导致 GC 的增加甚至 ANR,是造成卡顿的一个不可忽视的因素; CPU:CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争,比如 1.降频会直接导致应用卡顿; 2.后台活动进程太多导致系统繁忙,cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 ,这种情况比较常见,可以使用dumpsys cpuinfo查看当前设备的cpu使用情况: 3.主线程调度不到 , 处于 Runnable 状态,这种情况比较少见 4.System 锁:system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿 GPU:GPU 的影响见渲染流程,但是其实还会间接影响到功耗和发热; 功耗/发热:功耗和发热一般是不分家的,高功耗会引起高发热,进而会引起系统保护,比如降频、热缓解等,间接的导致卡顿。 如何监控卡顿 线下监控: 我们知道卡顿问题的原因错综复杂,但最终都可以反馈到CPU使用率上来 1.使用dumpsys cpuinfo命令 这个命令可以获取当时设备cpu使用情况,我们可以在线下通过重度使用应用来检测可能存在的卡顿点 A8S:/ $ dumpsys cpuinfoLoad: 1.12 / 1.12 / 1.09CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 14:53:30.866):2% 1053/scanserver: 0.2% user + 1.7% kernel0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor0.4% 564/signserver: 0% user + 0.4% kernel0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults: 54 minor0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 1175 minor0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 minor0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor0% 1774/com.android.nfc: 0% user + 0% kernel0% 172/kworker/1:2: 0% user + 0% kernel0% 145/irq/24-70900000: 0% user + 0% kernel0% 575/thermald: 0% user + 0% kernel / faults: 300 minor... 2.CPU Profiler 这个工具是AS自带的CPU性能检测工具,可以在PC上实时查看我们CPU使用情况。 AS提供了四种Profiling Model配置: 1.Sample Java Methods:在应用程序基于Java的代码执行过程中,频繁捕获应用程序的调用堆栈 获取有关应用程序基于Java的代码执行的时间和资源使用情况信息。 2.Trace java methods:在运行时对应用程序进行检测,以在每个方法调用的开始和结束时记录时间戳。收集时间戳并进行比较以生成方法跟踪数据,包括时序信息和CPU使用率。 请注意与检测每种方法相关的开销会影响运行时性能,并可能影响性能分析数据。对于生命周期相对较短的方法,这一点甚至更为明显。此外,如果您的应用在短时间内执行大量方法,则探查器可能会很快超过其文件大小限制,并且可能无法记录任何进一步的跟踪数据。 3.Sample C/C++ Functions:捕获应用程序本机线程的示例跟踪。要使用此配置,您必须将应用程序部署到运行Android 8.0(API级别26)或更高版本的设备。 4.Trace System Calls:捕获细粒度的详细信息,使您可以检查应用程序与系统资源的交互方式 您可以检查线程状态的确切时间和持续时间,可视化CPU瓶颈在所有内核中的位置,并添加自定义跟踪事件进行分析。在对性能问题进行故障排除时,此类信息可能至关重要。要使用此配置,您必须将应用程序部署到运行Android 7.0(API级别24)或更高版本的设备。 使用方式: Debug.startMethodTracing("");// 需要检测的代码片段...Debug.stopMethodTracing(); 优点:有比较全面的调用栈以及图像化方法时间显示,包含所有线程的情况 缺点:本身也会带来一点的性能开销,可能会带偏优化方向 火焰图:可以显示当前应用的方法堆栈: 3.Systrace Systrace在前面一篇分析启动优化的文章讲解过 这里我们简单来复习下: Systrace用来记录当前应用的系统以及应用(使用Trace类打点)的各阶段耗时信息包括绘制信息以及CPU信息等。 使用方式: Trace.beginSection("MyApp.onCreate_1");alt(200);Trace.endSection(); 在命令行中: python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html 记录的方法以及CPU中的耗时情况: 优点: 1.轻量级,开销小,CPU使用率可以直观反映 2.右侧的Alerts能够根据我们应用的问题给出具体的建议,比如说,它会告诉我们App界面的绘制比较慢或者GC比较频繁。 4.StrictModel StrictModel是Android提供的一种运行时检测机制,用来帮助开发者自动检测代码中不规范的地方。 主要和两部分相关: 1.线程相关 2.虚拟机相关 基础代码: private void initStrictMode() {// 1、设置Debug标志位,仅仅在线下环境才使用StrictModeif (DEV_MODE) {// 2、设置线程策略StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode.detectDiskReads().detectDiskWrites().detectNetwork() // or .detectAll() for all detectable problems.penaltyLog() //在Logcat 中打印违规异常信息// .penaltyDialog() //也可以直接跳出警报dialog// .penaltyDeath() //或者直接崩溃.build());// 3、设置虚拟机策略StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()// 给NewsItem对象的实例数量限制为1.setClassInstanceLimit(NewsItem.class, 1).detectLeakedClosableObjects() //API等级11.penaltyLog().build());} } 线上监控: 线上需要自动化的卡顿检测方案来定位卡顿,它能记录卡顿发生时的场景。 自动化监控原理: 采用拦截消息调度流程,在消息执行前埋点计时,当耗时超过阈值时,则认为是一次卡顿,会进行堆栈抓取和上报工作 首先,我们看下Looper用于执行消息循环的loop()方法,关键代码如下所示: / Run the message queue in this thread. Be sure to call {@link quit()} to end the loop./public static void loop() {...for (;;) {Message msg = queue.next(); // might blockif (msg == null) {// No message indicates that the message queue is quitting.return;// This must be in a local variable, in case a UI event sets the loggerfinal Printer logging = me.mLogging;if (logging != null) {// 1logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}...try {// 2 msg.target.dispatchMessage(msg);dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} finally {if (traceTag != 0) {Trace.traceEnd(traceTag);} }...if (logging != null) {// 3logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);} 在Looper的loop()方法中,在其执行每一个消息(注释2处)的前后都由logging进行了一次打印输出。可以看到,在执行消息前是输出的">>>>> Dispatching to “,在执行消息后是输出的”<<<<< Finished to ",它们打印的日志是不一样的,我们就可以由此来判断消息执行的前后时间点。 具体的实现可以归纳为如下步骤: 1、首先,我们需要使用Looper.getMainLooper().setMessageLogging()去设置我们自己的Printer实现类去打印输出logging。这样,在每个message执行的之前和之后都会调用我们设置的这个Printer实现类。 2、如果我们匹配到">>>>> Dispatching to "之后,我们就可以执行一行代码:也就是在指定的时间阈值之后,我们在子线程去执行一个任务,这个任务就是去获取当前主线程的堆栈信息以及当前的一些场景信息,比如:内存大小、电脑、网络状态等。 3、如果在指定的阈值之内匹配到了"<<<<< Finished to ",那么说明message就被执行完成了,则表明此时没有产生我们认为的卡顿效果,那我们就可以将这个子线程任务取消掉。 这里我们使用blockcanary来做测试: BlockCanary APM是一个非侵入式的性能监控组件,可以通过通知的形式弹出卡顿信息。它的原理就是我们刚刚讲述到的卡顿监控的实现原理。 使用方式: 1.导入依赖 implementation 'com.github.markzhai:blockcanary-android:1.5.0' Application的onCreate方法中开启卡顿监控 // 注意在主进程初始化调用BlockCanary.install(this, new AppBlockCanaryContext()).start(); 3.继承BlockCanaryContext类去实现自己的监控配置上下文类 public class AppBlockCanaryContext extends BlockCanaryContext {....../ 指定判定为卡顿的阈值threshold (in millis), 你可以根据不同设备的性能去指定不同的阈值 @return threshold in mills/public int provideBlockThreshold() {return 1000;}....} 4.在Activity的onCreate方法中执行一个耗时操作 try {Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();} 5.结果: 可以看到一个和LeakCanary一样效果的阻塞可视化堆栈图 那有了BlockCanary的方法耗时监控方式是不是就可以解百愁了呢,呵呵。有那么容易就好了 根据原理:我们拿到的是msg执行前后的时间和堆栈信息,如果msg中有几百上千个方法,就无法确认到底是哪个方法导致的耗时,也有可能是多个方法堆积导致。 这就导致我们无法准确定位哪个方法是最耗时的。如图中:堆栈信息是T2的,而发生耗时的方法可能是T1到T2中任何一个方法甚至是堆积导致。 那如何优化这块? 这里我们采用字节跳动给我们提供的一个方案:基于 Sliver trace 的卡顿监控体系 Sliver trace 整体流程图: 主要包含两个方面: 检测方案: 在监控卡顿时,首先需要打开 Sliver 的 trace 记录能力,Sliver 采样记录 trace 执行信息,对抓取到的堆栈进行 diff 聚合和缓存。 同时基于我们的需要设置相应的卡顿阈值,以 Message 的执行耗时为衡量。对主线程消息调度流程进行拦截,在消息开始分发执行时埋点,在消息执行结束时计算消息执行耗时,当消息执行耗时超过阈值,则认为产生了一次卡顿。 堆栈聚合策略: 当卡顿发生时,我们需要为此次卡顿准备数据,这部分工作是在端上子线程中完成的,主要是 dump trace 到文件以及过滤聚合要上报的堆栈。分为以下几步: 1.拿到缓存的主线程 trace 信息并 dump 到文件中。 2.然后从文件中读取 trace 信息,按照数据格式,从最近的方法栈向上追溯,找到当前 Message 包含的全部 trace 信息,并将当前 Message 的完整 trace 写入到待上传的 trace 文件中,删除其余 trace 信息。 3.遍历当前 Message trace,按照(Method 执行耗时 > Method 耗时阈值 & Method 耗时为该层堆栈中最耗时)为条件过滤出每一层函数调用堆栈的最长耗时函数,构成最后要上报的堆栈链路,这样特征堆栈中的每一步都是最耗时的,且最底层 Method 为最后的耗时大于阈值的 Method。 之后,将 trace 文件和堆栈一同上报,这样的特征堆栈提取策略保证了堆栈聚合的可靠性和准确性,保证了上报到平台后堆栈的正确合理聚合,同时提供了进一步分析问题的 trace 文件。 可以看到字节给的是一整套监控方案,和前面BlockCanary不同之处就在于,其是定时存储堆栈,缓存,然后使用diff去重的方式,并上传到服务器,可以最大限度的监控到可能发生比较耗时的方法。 开发中哪些习惯会影响卡顿的发生 1.布局太乱,层级太深。 1.1:通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。 1.2:用 ViewStub 替代在启动过程中不需要显示的 UI 控件。 1.3:使用自定义 View 替代复杂的 View 叠加。 2.主线程耗时操作 2.1:主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。 2.2:sharepreference尽量使用apply,少使用commit,可以使用MMKV框架来代替sharepreference。 2.3:网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。 2.4:不要在activity的onResume和onCreate中进行耗时操作,比如大量的计算等。 2.5:不要在 draw 里面调用耗时函数,不能 new 对象 3.过度绘制 过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。 4.列表 RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。 5.对象分配和回收优化 自从Android引入 ART 并且在Android 5.0上成为默认的运行时之后,对象分配和垃圾回收(GC)造成的卡顿已经显著降低了,但是由于对象分配和GC有额外的开销,它依然又可能使线程负载过重。 在一个调用不频繁的地方(比如按钮点击)分配对象是没有问题的,但如果在在一个被频繁调用的紧密的循环里,就需要避免对象分配来降低GC的压力。 减少小对象的频繁分配和回收操作。 好了,关于卡顿优化的问题就讲到这里,下篇文章会对卡顿中的ANR情况的处理,这里做个铺垫。 如果喜欢我的文章,欢迎关注我的公众号。 点击这看原文链接: 参考 Android卡顿检测及优化 一文读懂直播卡顿优化那些事儿 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解! 深入探索Android卡顿优化(上) 西瓜卡顿 & ANR 优化治理及监控体系建设 5376)] 参考 Android卡顿检测及优化 一文读懂直播卡顿优化那些事儿 “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解! 深入探索Android卡顿优化(上) 西瓜卡顿 & ANR 优化治理及监控体系建设 本篇文章为转载内容。原文链接:https://blog.csdn.net/yuhaibing111/article/details/127682399。 该文由互联网用户投稿提供,文中观点代表作者本人意见,并不代表本站的立场。 作为信息平台,本站仅提供文章转载服务,并不拥有其所有权,也不对文章内容的真实性、准确性和合法性承担责任。 如发现本文存在侵权、违法、违规或事实不符的情况,请及时联系我们,我们将第一时间进行核实并删除相应内容。
2023-03-26 08:05:57
214
转载
JQuery
...中的一个常见概念,在JavaScript及jQuery中广泛应用。在本文语境下,回调函数是指作为参数传递给另一个函数的函数,这个函数会在特定条件满足或者某个事件发生时由该函数进行调用。在jQuery插件开发中,回调函数通常用于将用户的自定义逻辑注入到插件的标准执行流程中,以适应不同场景下的个性化需求,增强了插件的灵活性和可定制性。 $.extend , $.extend是jQuery提供的一个内置方法,主要用于合并(或扩展)两个或多个对象的属性,并返回合并后的新对象。在本文给出的示例中,$.extend方法被用来合并默认选项对象与用户传入的options参数,确保即使用户未提供某些配置项(如回调函数),插件也有默认的设置可以使用。通过这种方式,$.extend帮助jQuery插件开发者构建更加灵活和易用的API接口,让使用者可以根据自身需求覆盖或添加新的配置选项。
2023-09-01 17:58:02
405
逻辑鬼才
VUE
... 是一个开源的渐进式JavaScript框架,用于构建用户界面。它采用声明式渲染和组件化开发模式,允许开发者通过简单的模板语法创建可复用的组件,并通过响应式数据绑定实现视图与数据模型之间的自动同步更新。在本文语境中,Vue.js 作为前端框架提供了路由系统$router来管理应用程序中的页面导航和交互。 路由系统($router) , Vue.js 的路由系统是一个内置的核心功能模块,通过$router对象实现。这个系统允许开发者定义应用程序中各个页面的路由规则和对应的组件,从而实现在单页面应用(SPA)中模拟多页面应用的切换效果。开发者可以通过调用$router的方法(如push、replace等)进行页面跳转,同时支持动态参数传递、查询参数设置以及hash值操作,使得页面间的导航更流畅且具备良好的数据通信能力。 单页面应用程序(SPA) , 单页面应用程序是一种特殊的Web应用程序设计模式,其特点是整个网站或应用程序只有一个完整的HTML页面,内容的切换和加载并非通过传统的页面刷新方式完成,而是利用JavaScript和Ajax技术动态替换或添加页面片段(DOM元素)。在Vue.js中,结合路由系统$router,可以方便地构建单页面应用程序,提供无缝的用户体验和高效的前后端交互。用户在访问不同“页面”时,实际上只是在同一个HTML文档内进行部分内容的更新和显示,无需重新加载整个页面资源。
2023-05-14 15:02:10
109
程序媛
Sqoop
...: bash java.lang.ClassNotFoundException: com.mysql.jdbc.MySQLBlobInputStream 这是因为Sqoop在默认配置下可能并不支持所有数据库特定的内置类型,尤其是那些非标准的或者用户自定义的类型。 3. 解决方案详述 3.1 自定义jdbc驱动类映射 为了解决上述问题,我们需要帮助Sqoop识别并正确处理这些特定的列类型。Sqoop这个工具超级贴心,它让用户能够自由定制JDBC驱动的类映射。你只需要在命令行耍个“小魔法”,也就是加上--map-column-java这个参数,就能轻松指定源表中特定列在Java环境下的对应类型啦,就像给不同数据类型找到各自合适的“变身衣裳”一样。 例如,对于上述的MEDIUMBLOB类型,我们可以将其映射为Java的BytesWritable类型: bash sqoop import \ --connect jdbc:mysql://localhost/mydatabase \ --table my_table \ --columns 'id, medium_blob_column' \ --map-column-java medium_blob_column=BytesWritable \ --target-dir /user/hadoop/my_table_data 3.2 扩展Sqoop的JDBC驱动 另一种更为复杂但更为彻底的方法是扩展Sqoop的JDBC驱动,实现对特定类型的支持。通常来说,这意味着你需要亲自操刀,写一个定制版的JDBC驱动程序。这个驱动要能“接班” Sqoop自带的那个驱动,专门对付那些原生驱动搞不定的数据类型转换问题。 java // 这是一个简化的示例,实际操作中需要对接具体的数据库API public class CustomMySQLDriver extends com.mysql.jdbc.Driver { // 重写方法以支持对MEDIUMBLOB类型的处理 @Override public java.sql.ResultSetMetaData getMetaData(java.sql.Connection connection, java.sql.Statement statement, String sql) throws SQLException { ResultSetMetaData metadata = super.getMetaData(connection, statement, sql); // 对于MEDIUMBLOB类型的列,返回对应的Java类型 for (int i = 1; i <= metadata.getColumnCount(); i++) { if ("MEDIUMBLOB".equals(metadata.getColumnTypeName(i))) { metadata.getColumnClassName(i); // 返回"java.sql.Blob" } } return metadata; } } 然后在Sqoop命令行中引用这个自定义的驱动: bash sqoop import \ --driver com.example.CustomMySQLDriver \ ... 4. 思考与讨论 尽管Sqoop在大多数情况下可以很好地处理数据迁移任务,但在面对一些特殊的数据库表列类型时,我们仍需灵活应对。无论是对JDBC驱动进行小幅度的类映射微调,还是大刀阔斧地深度定制,最重要的一点,就是要摸透Sqoop的工作机制,搞清楚它背后是怎么通过底层的JDBC接口,把那些Java对象两者之间巧妙地对应和映射起来的。想要真正玩转那个功能强大的Sqoop数据迁移神器,就得在实际操作中不断摸爬滚打、学习积累。这样,才能避免被“ClassNotFoundException”这类让人头疼的小插曲绊住手脚,顺利推进工作进程。
2023-04-02 14:43:37
83
风轻云淡
PHP
...竟然无法获取到传入的参数,当时真是让我抓耳挠腮了一阵子。 二、什么是__callStatic()? 首先,我们需要了解一下__callStatic()这个魔术方法。在PHP中,魔术方法是一种特殊的方法名,它们会在特定的情况下被自动调用。__callStatic()是其中的一种,它会在尝试调用不存在的静态方法时被调用。 三、为什么会出现这种问题? 对于这个问题,我认为可能是因为我们在使用__callStatic()时,没有正确地传入参数导致的。让我们来看一个具体的例子: php class Test { public static function __callStatic($name, $arguments) { var_dump($arguments); } } Test::test(); 这段代码会输出空数组,这就是因为我们没有传入任何参数给__callStatic()。 四、如何解决这个问题? 为了解决这个问题,我们需要确保我们在调用__callStatic()时,传入了正确的参数。我们可以这样做: php class Test { public static function __callStatic($name, $arguments) { var_dump($arguments); } } Test::__callStatic('test', array()); 这段代码会输出一个空数组,这就意味着我们的参数传递是正确的。 五、深入理解__callStatic() 通过上面的例子,我们可以看到,当我们试图调用一个不存在的静态方法时,PHP会自动调用__callStatic(),并传入该方法的名字和参数。这就是为啥我们能在__callStatic()这个方法里头捞到这些信息的原因啦! 六、总结 总的来说,我们在使用__callStatic()时,需要注意正确地传入参数。否则,我们就无法获取到正确的参数信息。同时呢,我们也要好好琢磨一下__callStatic()这个小家伙的工作机制,这样一来才能把它用得溜溜的。 七、结尾语 希望这篇文章能对你有所帮助。如果你在使用PHP过程中遇到了其他问题,欢迎随时与我交流。让我们一起学习,一起进步!
2023-07-09 15:08:34
161
断桥残雪_t
Java
泛型方法 , 在Java编程中,泛型方法是一种允许程序员定义能够处理多种数据类型的方法。具体表现为,在方法签名中包含一个或多个类型参数(如<T>),这些参数在调用方法时由实际的数据类型替换。这样,同一个方法可以应用于不同类型的数据对象,同时保证编译期的类型检查和运行时的类型安全。 类型参数 , 类型参数是Java泛型中的概念,它是一个占位符,代表某种未知的具体类型。在定义泛型类或泛型方法时使用,如<T>、<E>、<K>等,它们可以在整个类或方法的作用域内被当作已知类型来使用。在实例化泛型类或调用泛型方法时,类型参数会被实际的类类型所替换。 静态类型检查 , 静态类型检查是编程语言的一种特性,它在编译阶段就能对代码进行类型一致性验证。在Java泛型中,通过引入类型参数,编译器能在编译时确保传递给泛型方法或存储在泛型类中的对象与指定的类型参数匹配,如果类型不匹配,编译器将报错,从而提高了程序的健壮性和安全性。 类型安全 , 类型安全是指编程语言能够在编译期间或者运行期间确保变量、表达式以及方法调用具有正确的数据类型,并且不会发生非法类型转换导致的错误。在Java泛型中,通过类型擦除和类型参数机制,使得程序员在编写代码时必须明确指定类型,这有助于避免因为类型混淆引发的潜在问题,增强了程序的稳定性和可靠性。例如,通过泛型,集合类如ArrayList<T>只能存储T类型的元素,从而确保了集合内数据的一致性,提高了类型安全性。
2023-01-06 19:10:18
357
码农
站内搜索
用于搜索本网站内部文章,支持栏目切换。
知识学习
实践的时候请根据实际情况谨慎操作。
随机学习一条linux命令:
id -u username
- 获取用户的UID(用户ID)。
推荐内容
推荐本栏目内的其它文章,看看还有哪些文章让你感兴趣。
2023-04-28
2023-08-09
2023-06-18
2023-04-14
2023-02-18
2023-04-17
2024-01-11
2023-10-03
2023-09-09
2023-06-13
2023-08-07
2023-03-11
历史内容
快速导航到对应月份的历史文章列表。
随便看看
拉到页底了吧,随便看看还有哪些文章你可能感兴趣。
时光飞逝
"流光容易把人抛,红了樱桃,绿了芭蕉。"