
在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下: 公平锁/非公平锁可重入锁独享锁/共享锁互斥锁/读写锁乐观锁/悲观锁分段锁偏向锁/轻量级锁/重量级锁自旋锁上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。 可重入锁 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。 对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。 synchronized void setA() throws Exception{ Thread.sleep(1000); setB(); } synchronized void setB() throws Exception{ Thread.sleep(1000); } 上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。 独享锁/共享锁 独享锁是指该锁一次只能被一个线程所持有。 共享锁是指该锁可被多个线程所持有。 对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。 对于Synchronized而言,当然是独享锁。 互斥锁/读写锁 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是ReentrantLock 读写锁在Java中的具体实现就是ReadWriteLock 乐观锁/悲观锁 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1] 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题 乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。 从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。 悲观锁在Java中的使用,就是利用各种锁。 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。 分段锁 分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。 我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。 当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。 但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。 分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。 偏向锁/轻量级锁/重量级锁 这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 自旋锁 在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
OQL (Object Query Language)类似于 SQL 的查询语言,可以方便在堆中进行对象的查找和 筛选。 语法 select [from[instanceof] ][where ] select * from com.XX.CacheManager 例子(MAT 版本)1.查询stringSELECT * FROM java.lang.String 2.正则查询 java.langSELECT FROM "java.lang.." 3.查询类地址0x79571fb40SELECT * FROM 0x79571fb40 4.属性访问器SELECT * FROM ${snapshot}.getClasses() 5.条件查询SELECT FROM java.lang.String s WHERE toString(s) LIKE ".day" 6.查询char数组长度大于 10 的select * from char[] s where s.@length > 10 7.查询长度大于 15 且深堆大于 1000 字节的所有 Vector 对象select * from java.util.Vector v where v.elementData.@length > 15 AND v.@retainedHeapSize > 1000 8.显示Vector内部数组长度SELECT v.elementData.@length FROM java.util.Vector v 9.显示int数组索引下表为 2 的数据内容SELECT s.getValueAt(2) FROM int[] s WHERE (s.@length > 2) 10.显示堆中所有类型select * from ${snapshot}.getClasses() 11.显示长度为 4 且值不为空的string对象内容SELECT toString(s) FROM java.lang.String s WHERE ((s.value.@length = 4) and (s.value != null)) 12.查看保留集SELECT AS RETAINED SET * from com.mousycoder.mycode.thinking_in_jvm.Student 13.查询 string 实例select distinct objects classof(s) from java.lang.String s 14.查询包下的实例select from "com.mousycoder.mycode.thinking_in_jvm.." 15.查找子类SELECT * FROM INSTANCEOF java.util.AbstractCollection 16.查看类的属性SELECT toString(f.path.value) FROM java.io.File f 17.查看对象地址SELECT s.toString(), s.@objectId, s.@objectAddress FROM java.lang.String s 例子(Visual VM版本)筛选长度大于 100 的字符串select s from java.lang.String s where s.value.length >=1002.查找 classloader的子类select cl from instanceof java.lang.ClassLoader cl3.查找父类select heap.findClass("java.util.Vector").superclasses()4.输出引用链select heap.livepaths(s) from java.lang.String s where s.toString() == '56'5.访问对象属性select heap.findClass("com.mousycoder.mycode.thinking_in_jvm.TraceStudent").webpages6.查看可达对象select {r:toHtml(reachables(s)),url:s.url.toString()} from com.mousycoder.mycode.thinking_in_jvm.WebPage s7.长度为 2 至少被 2 个对象引用的字符串select s.toString() from java.lang.String s where (s.value != null && s.value.length == 2 && count(referrers(s)) >= 2)8.查找直接引用对象select referees(s) from com.mousycoder.mycode.thinking_in_jvm.Student s9.Vector的浅堆select {size:sizeof(o),Object:0} from java.util.Vector o10.Vector的深堆select {size:sizeof(o),rsize:rsizeof(o)} from java.util.Vector o11.通过File 类打开的文件select {size:sizeof(o),rsize:rsizeof(o)} from java.util.Vector o12.查找长度最长的 5 个字符串并显示对象和长度select map (top(filter(heap.objects('java.lang.String'),'it.value !=null'),'rhs.value.length -lhs.value.length',5),'{length: it.value.length,obj:it}')13.查看string字符串一共多少个不重复的元素select count(unique(map(heap.objects('java.lang.String'),'it.value')))14.统计脚本var sessions = toArray(heap.objects("http://java.lang.xxx")) var count = sessions.length; var createtimes = new Array(); for(var i=0;i
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙。当遇到需要占用8位字节以上的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。 ClassFile 结构说明magic:魔数。唯一作用是确定这个文件是否为一个能被虚拟机所接收的class文件。固定值:0xCAFEBABE。minor_version:class文件的副版本号。major_version:class文件的主版本号。constant_pool_count:常量池计数器。constant_pool[constant_pool_count]:常量池。包含字符串常量、类或接口名、字段名和其他常量。常量池每一项都具备相同特征,即第一个字节作为类型标记,用来确定该项格式。access_flags:访问标志。用于表示某个类或接口的访问权限及属性。this_class:当前的类索引。super_class:父类索引。interfaces_count:接口计数器。当前类或接口的直接超的接口数量。interfaces[interfaces_count]:接口表。fields_count:字段计数器。当前class文件的字段成员个数。fields[fields_count]:字段表。methods_count:方法计数器。方法的个数。methods[methods_count]:方法表。attributes_count:属性计数器。属性个数。attributes[attributes_count]:属性表。一个例子对下面的类进行编译生成 Test2.class 。 public class Test2 { public static int i = 1; public static void main() { System.out.println(i); } }通过十六进制查看工具打开 Test2.class 。 根据 ClassFile 结构说明,前面4个字节是魔数oxcafebabe。接下来是次版本号和主版本号ox0000和ox0034,十进制为0和52,52表示JDK1.8,所以JDK版本是1.8.0。 还可以使用Java自带的反编译工具来解析字节码文件。命令 javap -v Test2.class 。 Classfile /E:/CODE/JVM/Test2.class Last modified 2019-7-19; size 219 bytes MD5 checksum 841c66674d71005bc6a97fe6c3b0fb1d Compiled from "Test2.java"public class Test2 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #4.#13 // java/lang/Object."":()V #2 = Fieldref #3.#14 // Test2.i:I #3 = Class #15 // Test2 #4 = Class #16 // java/lang/Object #5 = Utf8 i #6 = Utf8 I #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 SourceFile #12 = Utf8 Test2.java #13 = NameAndType #7:#8 // "":()V #14 = NameAndType #5:#6 // i:I #15 = Utf8 Test2 #16 = Utf8 java/lang/Object{ public int i; descriptor: I flags: ACC_PUBLIC public Test2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field i:I 9: return LineNumberTable: line 1: 0 line 2: 4 }SourceFile: "Test2.java"Java虚拟机限制每个类或接口的常量池最多65535个。类或接口中可声明的字段数最多65535个。类或接口中可声明的方法数最多65535个。类或接口的直接父类接口最多65535个。方法调用时创建的栈帧,其局部变量表中的最大局部变量数位65535个。方法帧中操作数栈的最大深度为65535。方法的参数最多255个。字段和方法名称、字段和方法描述符以及其他常量字符串值的最大长度为65535个字符。数组的维度最大为255维。喜欢这篇文章的可以给笔者点个赞同,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~
一、注解(annotations)列表@SpringBootApplication:包含了@ComponentScan、@Configuration和@EnableAutoConfiguration注解。其中@ComponentScan让Spring Boot扫描到Configuration类并把它加入到程序上下文。 @Configuration 等同于spring的XML配置文件;使用java代码可以检查类型安全。 @EnableAutoConfiguration 自动配置。 @ComponentScan 组件扫描,可自动发现和装配一些Bean。 @Component可配合CommandLineRunner使用,在程序启动后执行一些基础任务。 @RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器bean,并且是将函数的返回值直 接填入HTTP响应体中,是REST风格的控制器。 @Autowired自动导入。 @PathVariable获取参数。 @JsonBackReference解决嵌套外链问题。 @RepositoryRestResourcepublic配合spring-boot-starter-data-rest使用。 二、注解(annotations)详解@SpringBootApplication:申明让spring boot自动给程序进行必要的配置,这个配置等同于:@Configuration ,@EnableAutoConfiguration 和 @ComponentScan 三个配置。 package com.example.myproject; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } @ResponseBody:表示该方法的返回结果直接写入HTTP response body中,一般在异步获取数据时使用,用于构建RESTful的api。在使用@RequestMapping后,返回值通常解析为跳转路径,加上@responsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP response body中。比如异步获取json数据,加上@responsebody后,会直接返回json数据。该注解一般会配合@RequestMapping一起使用。示例代码: @RequestMapping(“/test”) @ResponseBody public String test(){ return”ok”; } @Controller:用于定义控制器类,在spring 项目中由控制器负责将用户发来的URL请求转发到对应的服务接口(service层),一般这个注解在类中,通常方法需要配合注解@RequestMapping。示例代码: @Controller @RequestMapping(“/demoInfo”) publicclass DemoController { @Autowired private DemoInfoService demoInfoService; @RequestMapping("/hello") public String hello(Map map){ System.out.println("DemoController.hello()"); map.put("hello","from TemplateController.helloHtml"); //会使用hello.html或者hello.ftl模板进行渲染显示. return"/hello"; } @RestController:用于标注控制层组件(如struts中的action),@ResponseBody和@Controller的合集。示例代码: package com.kfit.demo.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Angel(QQ交流群:193341332,QQ:412887952) @version v.0.1 @date 2016年7月29日下午7:26:04 */ @RestController @RequestMapping(“/demoInfo2”) publicclass DemoController2 { @RequestMapping("/test") public String test(){ return"ok"; } @RequestMapping:提供路信息,负责URL到Controller中的具体函数的映射。 @EnableAutoConfiguration:Spring Boot自动配置(auto-configuration):尝试根据你添加的jar依赖自动配置你的Spring应用。例如,如果你的classpath下存在HSQLDB,并且你没有手动配置任何数据库连接beans,那么我们将自动配置一个内存型(in-memory)数据库”。你可以将@EnableAutoConfiguration或者@SpringBootApplication注解添加到一个@Configuration类上来选择自动配置。如果发现应用了你不想要的特定自动配置类,你可以使用@EnableAutoConfiguration注解的排除属性来禁用它们。 @ComponentScan:表示将该类自动发现扫描组件。个人理解相当于,如果扫描到有@Component、@Controller、@Service等这些注解的类,并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。我们经常使用@ComponentScan注解搜索beans,并结合@Autowired注解导入。可以自动收集所有的Spring组件,包括@Configuration类。我们经常使用@ComponentScan注解搜索beans,并结合@Autowired注解导入。如果没有配置的话,Spring Boot会扫描启动类所在包下以及子包下的使用了@Service,@Repository等注解的类。 @Configuration:相当于传统的xml配置文件,如果有些第三方库需要用到xml文件,建议仍然通过@Configuration类作为项目的配置主类——可以使用@ImportResource注解加载xml配置文件。 @Import:用来导入其他配置类。 @ImportResource:用来加载xml配置文件。 @Autowired:自动导入依赖的bean @Service:一般用于修饰service层的组件 @Repository:使用@Repository注解可以确保DAO或者repositories提供异常转译,这个注解修饰的DAO或者repositories类会被ComponetScan发现并配置,同时也不需要为它们提供XML配置项。 @Bean:用@Bean标注方法等价于XML中配置的bean。 @Value:注入Spring boot application.properties配置的属性的值。示例代码: @Value(value = “#{message}”) private String message; @Inject:等价于默认的@Autowired,只是没有required属性; @Component:泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 @Bean:相当于XML中的,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。 @AutoWired:自动导入依赖的bean。byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。当加上(required=false)时,就算找不到bean也不报错。 @Qualifier:当有多个同一类型的Bean时,可以用@Qualifier(“name”)来指定。与@Autowired配合使用。@Qualifier限定描述符除了能根据名字进行注入,但能进行更细粒度的控制如何选择候选者,具体使用方式如下: @Autowired @Qualifier(value = “demoInfoService”) private DemoInfoService demoInfoService; @Resource(name=”name”,type=”type”):没有括号内内容的话,默认byName。与@Autowired干类似的事。 三、JPA注解@Entity:@Table(name=”“):表明这是一个实体类。一般用于jpa这两个注解一般一块使用,但是如果表名和实体类名相同的话,@Table可以省略 @MappedSuperClass:用在确定是父类的entity上。父类的属性子类可以继承。 @NoRepositoryBean:一般用作父类的repository,有这个注解,spring不会去实例化该repository。 @Column:如果字段名与列名相同,则可以省略。 @Id:表示该属性为主键。 @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = “repair_seq”):表示主键生成策略是sequence(可以为Auto、IDENTITY、native等,Auto表示可在多个数据库间切换),指定sequence的名字是repair_seq。 @SequenceGeneretor(name = “repair_seq”, sequenceName = “seq_repair”, allocationSize = 1):name为sequence的名称,以便使用,sequenceName为数据库的sequence名称,两个名称可以一致。 @Transient:表示该属性并非一个到数据库表的字段的映射,ORM框架将忽略该属性。如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient,否则,ORM框架默认其注解为@Basic。@Basic(fetch=FetchType.LAZY):标记可以指定实体属性的加载方式 @JsonIgnore:作用是json序列化时将java bean中的一些属性忽略掉,序列化和反序列化都受影响。 @JoinColumn(name=”loginId”):一对一:本表中指向另一个表的外键。一对多:另一个表指向本表的外键。 @OneToOne、@OneToMany、@ManyToOne:对应Hibernate配置文件中的一对一,一对多,多对一。 四、springMVC相关注解@RequestMapping:@RequestMapping(“/path”)表示该控制器处理所有“/path”的UR L请求。RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。 用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。该注解有六个属性: params:指定request中必须包含某些参数值是,才让该方法处理。 headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。 value:指定请求的实际地址,指定的地址可以是URI Template 模式 method:指定请求的method类型, GET、POST、PUT、DELETE等 consumes:指定处理请求的提交内容类型(Content-Type),如application/json,text/html; produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 @RequestParam:用在方法的参数前面。 @RequestParam String a =request.getParameter(“a”)。 @PathVariable:路径变量。如 RequestMapping(“user/get/mac/{macAddress}”) public String getByMacAddress(@PathVariable String macAddress){ //do something; } 参数与大括号里的名字一样要相同。 五、全局异常处理@ControllerAdvice:包含@Component。可以被扫描到。统一处理异常。 @ExceptionHandler(Exception.class):用在方法上面表示遇到这个异常就执行以下方法。 喜欢这篇文章的可以给笔者点个赞同,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~
一.事务特性原子性:强调事务的不可分割一致性:强调的是事务的执行的前后,数据的完整性要保持一致隔离性:一个事务的执行不应该受到其他事务的干扰持久性:事务一旦结束(提交/回滚)数据就持久保持到了数据库 二.如果不考虑隔离性,会引发一些安全性问题读问题脏读:一个事务读到另一个事务还没有提交的数据不可重复读:一个事务读到了另一个事务已经提交的update数据,导致在当前的事务中多次查询数据不一致虚读/幻读:一个事务读到另一个事务已经insert数据,导致当前事务中多次查询结果不一致 写问题引发两类丢失更新 三.解决引发的读问题设置事务的隔离级别read uncommitted :未提交读。脏读,不可重复读,虚读都可能发生read committed :已提交读。避免脏读,不可重复读和虚度有可能发生repeatable read :可重复读。避免脏读和不可重复读,虚读可能发生serializable :串行化的。避免脏读,不可重复读,虚读的发生 select @@tx_isolation; 查看隔离级别set session transaction isolation level 级别; 设置隔离级别 四.演示读问题演示脏读分别开启两个dos窗口 A.B先查看两个窗口的隔离级别 select @@tx_isolation;设置A窗口的隔离级别为未提交读 set session transaction isolation level read uncommitted;分别在两个窗口开启事务 start transaction;分别开启两个dos窗口 A.Bupdate account set money = money - 1000 where name = ‘张森’;update account set money = money + 1000 where name = ‘凤姐’;在A窗口查询数据 select * from account; – A事务读到了B事务还没有提交的数据; 演示避免脏读,演示不可重复读发送分别开两个窗口,A.B设置A窗口的隔离级别:read committed set session transaction isolation level read committed;分别在两个窗口开启事务 start transaction;在B窗口完成转账update account set money = money - 1000 where name = ‘张森’;update account set money = money + 1000 where name = ‘凤姐’;在A窗口进行查询 select * from account; – 避免脏读在B窗口提交事务 commit;在A窗口中再次查询 select * from account; – 转账成功.(不可重复读:一个事务读到另一个事务中已经提交的update的数据,导致多次查询结果不一致.) 避免脏读和不可重复读,演示虚读分别开启两个窗口,A.B设置A窗口的隔离级别:repeatable read set session transaction isolation level repeatable read;分别在两个窗口中开启事务 start transaction;在B窗口完成转账的操作update account set money = money - 1000 where name = ‘张森’;update account set money = money + 1000 where name = ‘凤姐’;在A窗口查询 select * from account; – 转账没有成功:避免脏读.在B窗口提交事务 commit;在A窗口再次查询 select * from account; – 转账没有成功:避免不可重复读. 避免虚读分别开启两个窗口,A.B设置A窗口的隔离级别:repeatable read set session transaction isolation level repeatable read;分别在两个窗口中开启事务 start transaction;在B窗口完成插入操作 insert into account values (null,‘王老师’,10000);在A中进行查询操作 select * from account; – 没有查询到任何结果在B窗口提交事务 commit; – A窗口马上就会显示数据 喜欢这篇文章的可以给笔者点个赞同,关注一下,每天都会分享Java相关文章!还有不定时的福利赠送,包括整理的学习资料,面试题,源码等~~