代码拉取完成,页面将自动刷新
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Java知识随记]]></title>
<url>%2F2020%2F11%2F08%2FJava%E5%9F%BA%E7%A1%80%2FJava%E7%9F%A5%E8%AF%86%E9%9A%8F%E8%AE%B0%2F</url>
<content type="text"><![CDATA[使用set存储对象为什么需要重写equals和hascode方法equals和hascode方法是Object的两个方法,作用是: 1、区分两个Object 2、给子类重写,让子类去实现自己的区分逻辑。 Object的源码是如何区分两个Object的 123public boolean equals(Object obj) { return (this == obj);} 对于hashCode,源码注释中这样解释的: 1、它是一个代表Object的整数int值,它有利于哈希表的存储使用。(重要) 2、无论何时调用同一对象此方法,返回值都应是相同的。(equals用到的比较信息没有被修改的情况下) 3、如果equals()方法确定了两个对象相等,则这两个对象的hashCode必须返回相同的值(这点就可以决定重写equals()方法就必须重写hashCode方法了) 4、如果equals()方法确定了两个对象不相等,这个两个对象的hashCode还是有可能相等的。但是我们强烈建议不同的对象应该有着不同的hashCode,这样可以提高hash tables的使用效率。 为什么要重写equals()和hashCode()方法的原因有两个: 1、重写equals()是为了实现自己的区分逻辑。 2、重写hashCode()是为了提高hash tables的使用效率。 所以当set存储对象,set集合只能存储不同的元素,对象如果不重写equals方法,则会以==比较两个对象的内存地址,这个是必然不相同的,所以重写equals方法让两个对象的比较是对象中其中一个或几个属性的内容比较如id、name 重写equals为什么需要重写hascode方法呢?原因是当需要使用到一些集合类如hashmap、hashset等存储对象时候,是通过equals方法去判断是否相等,如果不重写hascode方法,但equals判断相同时候,hascode值可能不同,因为equals判断相同的因素和hascode不同(举例:equals判断对象的属性,hascode判断对象的地址),所以当重写equals方法时候,必须重写hascode方法,避免在使用该对象存储的集合类出现判断相不相同的情况出现不一致。 java和mysql时间处理 MySql的时间类型 Java中与之对应的时间 date java.sql.Date Datetime java.sql.Timestamp Timestamp java.sql.Timestamp Time java.sql.Time Year java.sql.Date java流式操作它抽象出一种叫做流的东西让你以声明的方式处理数据,更重要的是,它还实现了多线程:帮你处理底层诸如线程、锁、条件变量、易变变量等等。 假定你需要过滤出一沓发票找出哪些跟特定消费者相关的,以金额大小排列,再取出这些发票的ID。如果用Stream API,你很容易写出下面这种优雅的查询: 1234List ids = invoices.stream().filter( inv ->inv.getCustomer() == Customer.ORACLE) .sorted(comparingDouble(Invoice::getAmount)) .map(Invoice::getId).collect(Collectors.toList()); 什么是流:通俗地讲,你可以认为是支持类似数据库操作的“花哨的迭代器”。技术上讲,它是从某个数据源获得的支持聚合操作的元素序列。下面着重介绍一下正式的定义:元素序列针对特定元素类型的有序集合流提供了一个接口。但是流不会存储元素,只会根据要求对其做计算。数据源流所用到的数据源来自集合、数组或者I/O。聚合操作流支持类似数据库的操作以及函数式语言的基本操作,比如filter,map,reduce,findFirst,allMatch,sorted等待。 此外,流操作还有两种额外的基础属性根据不同的集合区分:管道连接许多流操作返回流本身,这种操作可以串联成很长的管道,这种方式更加有利于像延迟加载,短路,循环合并等操作。内部迭代器 不像集合依赖外部迭代器,流操作在内部帮你实现了迭代器。 流操作流接口在java.util.stream.Stream定义了许多操作,这些可以分为以下两类: 像filter,sorted和map一样的可以被连接起来形成一个管道的操作。 像collect,findFirst和allMatch一样的终止管道并返回数据的操作。 可以被连接起来的操作被称为中间操作,它们能被连接起来是因为都返回流。中间操作都“很懒”并且可以被优化。终止一个流管道的操作被叫做结束操作,它们从流管道返回像List,Integer或者甚至是void等非流类型的数据。下面我们介绍一下流里面的一些方法,完整的方法列表可以在java.util.stream.Stream找到。 Filter有好几个方法可以用来从流里面过滤出元素:filter通过传递一个预期匹配的对象作为参数并返回一个包含所有匹配到的对象的流。distinct返回包含唯一元素的流(唯一性取决于元素相等的实现方式)。limit返回一个特定上限的流。skip返回一个丢弃前n个元素的流。List expensiveInvoices= invoices.stream().filter(inv -> inv.getAmount() > 10_000).limit(5).collect(Collectors.toList()); Matching匹配是一个判断是否匹配到给定属性的普遍的数据处理模式。你可以用anyMatch,allMatch和noneMatch来匹配数据,它们都需要一个预期匹配的对象作为参数并返回一个boolen型的数据。例如,你可以用allMatch来检查是否所有的发票流里面的元素的值都大于1000:boolean expensive =invoices.stream().allMatch(inv -> inv.getAmount() > 1_000); Finding此外,流接口还提供了像findFirst和findAny等从流中取出任意的元素。它们能与像filter方法相连接。findFirst和findAny都返回一个可选对象(我们已经在第一章中讨论过)。Optional =invoices.stream().filter(inv ->inv.getCustomer() == Customer.ORACLE).findAny(); Mapping流支持映射方法,传递一个函数对象作为方法,把流中的元素转换成另一种类型。这种方法应用于单个元素,将其映射成新元素。例如,你有可能想用它来提取流中每个元素的信息。下面这段代码从一列发票中返回一列ID:List ids= invoices.stream().map(Invoice::getId).collect(Collectors.toList()); Reducing另一个常用的模式是把数据源中的所有元素结合起来提供单一的值。例如,“计算最高金额的发票” 或者 “计算所有发票的总额”。 这可以应用流中的reduce方法反复应用于每个元素直到返回最后数据。下面是reduce模式的例子,能帮你了解如何用for循环来计算一列数据的和:int sum = 0;for (int x : numbers) {sum += x;}对一列数据的每一个元素的值反复应用加法运算符获得结果,最终将一列值减少到一个值。这段代码用到两个参数:初始化总和变量,这里是0;用来结合所有列表里面元素的操作方法,这里是加法操作。在流上应用reduce方法,可以把流里面的所有元素相加,如下:int sum = numbers.stream().reduce(0, (a, b) -> a + b);reduce方法需要两个参数: 初始值,这里是0 一个BinaryOperator方法连接两个元素产生一个新元素。reduce方法本质上是抽象了重复方法模式。其他查询像“计算总和” 或者“计算最大值” 都是reduce方法的特殊用例,比如: 1int product = numbers.stream().reduce(1, (a, b) -> a * b);int max = numbers.stream().reduce(Integer.MIN_VALUE,Integer::max); Collectors目前为止你所了解的方法都是返回另一个流或者一个像boolean,int类型的值,或者返回一个可选对象。相比之下,collect方法是一个结束操作,它可以使流里面的所有元素聚集到汇总结果。传递给collect方法参数是一个java.util.stream.Collector类型的对象。Collector对象实际上定义了一个如何把流中的元素聚集到最终结果的方法。最开始,工厂方法Collectors.toList()被用来返回一个描述了如何把流转变成一个List的Collector对象。后来Collectors类又内建了很多相似的collectors变量。例如,你可以用Collectors.groupingBy方法按消费者把发票分组,如下:Map<Customer, List> customerToInvoices= invoices.stream().collect(Collectors.groupingBy(Invoice::getCustomer)); Putting It All Together下面是一个手把手的例子你可以练习如何把老式代码用Stream API重构。下面代码的用途是按照特定消费者过滤出的与训练有关的发票,以金额高低排序,最后提取出最高的前5张发票的ID:List oracleAndTrainingInvoices = new ArrayList();List ids = new ArrayList();List firstFiveIds = new ArrayList();for(Invoice inv: invoices) {if(inv.getCustomer() == Customer.ORACLE) {if(inv.getTitle().contains("Training")) {oracleAndTrainingInvoices.add(inv);}}}Collections.sort(oracleAndTrainingInvoices,new Comparator() {@Overridepublic int compare(Invoice inv1, Invoice inv2) {return Double.compare(inv1.getAmount(), inv2.getAmount());}});for(Invoice inv: oracleAndTrainingInvoices) {ids.add(inv.getId());}for(int i = 0; i < 5; i++) {firstFiveIds.add(ids.get(i));}接下来,你将用Stream API一步一步地重构这些代码。首先,你或者注意到代码中用到了一个中间容器来存储那些消费者是Customer.ORACLE并且title中含有“Training”字段的发票。这正是应用filter方法的地方:Stream oracleAndTrainingInvoices= invoices.stream().filter(inv ->inv.getCustomer() == Customer.ORACLE).filter(inv ->inv.getTitle().contains("Training"));接下来,你需要按照数量来把这些发票排序,你可以用新的工具方法Comparator.comparing结合sorted方法来实现:Stream sortedInvoices= oracleAndTrainingInvoices.sorted(comparingDouble(Invoice::getAmount));下面,你需要提取ID,这是map方法的用途:Stream ids= sortedInvoices.map(Invoice::getId);最后,你只对前5张发票感兴趣。你可以用limit方法截取这5张发票。当你整理一下代码,再用collect方法,最终的代码如下:List firstFiveIds= invoices.stream().filter(inv ->inv.getCustomer() == Customer.ORACLE).filter(inv ->inv.getTitle().contains("Training")).sorted(comparingDouble(Invoice::getAmount)).map(Invoice::getId).limit(5).collect(Collectors.toList());当你观察一下老式的代码你会发现每一个本地变量只被存储了一次,被下一段代码用了一次。当用Stream API之后,就完全消除了这个本地变量。 Parallel StreamsStream API 支持方便的数据并行。换句话说,你可以明确地让流管道以并行的方式运行而不用关心底层的具体实现。在这背后,Stream API使用了Fork/Join框架充分利用了你机器的多核架构。你所需要做的无非是用parallelStream()方法替换stream()方法。例如,下面代码显示如何并行地过滤金额高的发票:List expensiveInvoices= invoices.parallelStream().filter(inv -> inv.getAmount() > 10_000).collect(Collectors.toList());此外,你可以用并行方法将现有的Stream转换成parallel Stream:Stream expensiveInvoices= invoices.stream().filter(inv -> inv.getAmount() > 10_000);List result= expensiveInvoices.parallel().collect(Collectors.toList());然而,并不是所有的地方都可以用parallel Stream,从性能角度考虑,有几点你需要注意:Splittabilityparallel streams的内部实现依赖于将数据结构划分成可以让不同线程使用的难易程度。像数组这种数据结构很容易划分,而像链表或者文件这种数据结构很难划分。Cost per element越是计算流中单个元素花费的资源最高,应用并行越有意义。Boxing如果可能的话尽量用原始数据类型,这样可以占用更少的内存,也更缓存命中率也更高。Size流中元素的数据量越大越好,因为并行的成本会分摊到所有元素,并行节省的时间相对会更多。当然,这也跟单个元素计算的成本相关。Number of cores一般来说,核越多越好。在实践中,如果你想提高代码的性能,你应该检测你代码的指标。Java Microbenchmark Harness (JMH) 是一个Oracle维护的流行的框架,你可以用它来帮你完成代码分析检测。如果不检测的话,简单的应用并行,代码的性能或许更差。 Summary下面是本章的重点内容: 流是一列支持聚合操作的来自于不同数据源的元素列表 流有两种类型的操作方法:中间方法和终结方法 中间方法可以被连接起来形成管道 中间方法包括filter,map,distinct和sorted 终结方法处理流管道并返回一个结果 终结方法包括allMatch,collect和forEach Collectors是一个第应以了如何将流中的元素聚集到最终结果的方法,包括像List和Map一样的容器 流管道可以被并行地计算 当应用parallel stream 来提高性能时有很多个方面需要考虑,包括数据结构划分的难易程度,计算每个元素花费的高低,装箱的难易,数据量的多少和可用核的数量。]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title></title>
<url>%2F2020%2F11%2F05%2FIDEA%E5%BF%AB%E6%8D%B7%E9%94%AE%2F</url>
<content type="text"><![CDATA[IDEA快捷键搜索快捷键1.当前类搜索1.1 ctrl+F12查看当前类中的所有成员:方法、属性、内部类 搜索类中某个成员位置,直接输入关键字就会匹配出当前类中所有符合的成员。选择查询结果直接定位到跳转到代码位置。 2.项目中搜索2.1 ctrl+shift+F功能:在整个项目中搜索匹配符合关键词的位置 2.2 Shift+Shift功能:在整个项目中搜索匹配符合关键词的位置亮点:Symbols支持模糊查询只要记住几个字母就能查询出位置 3.查看最近修改的文件快捷键:Ctrl+e 查看最近操作的文件 IDEA断点debug调试step over:程序向下执行一行(如果当前行有方法调用,这个方法将被执行完毕返回,然后到下一行) step into:程序向下执行一行。如果该行有自定义方法,则运行进入自定义方法(不会进入官方类库的方法) Force step into:该按钮在调试的时候能进入任何方法。 step out:如果在调试的时候,step into / force step into进入了一个方法,并觉得该方法没有问题,你就可以使用step out跳出该方法,返回该方法被调用处的下一行语句,注意:该方法已执行完毕。 Drop frame:点击该按钮后,将返回到当前方法的调用处重新执行,并且所有上下文变量都回到那个时候,只要调用链中还有上级方法,可以跳到其中的任何一个方法。 跨断点调试:设置多个断点开始调试,想移动到下一个断点,点击的|>按钮。 设置变量值:这个功能可以更加快速的跳出循环语句,图形像个计算器的 跳到断点处:step over的前一个 跳到光标所在行:drop frame的后一个]]></content>
</entry>
<entry>
<title><![CDATA[mybatis知识点]]></title>
<url>%2F2020%2F10%2F28%2Fmybatis%2Fmybatis%E7%9F%A5%E8%AF%86%E7%82%B9%2F</url>
<content type="text"></content>
<categories>
<category>mybatis</category>
</categories>
<tags>
<tag>mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[mybatis使用常见错误]]></title>
<url>%2F2020%2F10%2F18%2Fmybatis%2Fmybatis%E4%BD%BF%E7%94%A8%E5%B8%B8%E8%A7%81%E9%94%99%E8%AF%AF%2F</url>
<content type="text"><![CDATA[1、使用@Param绑定对象1234567891011121314151617当定义接口int insert(HourRoomEntity hourRoomEntity);<insert id="insert" parameterType="com.qunar.scm.ct.model.hourRoom.HourRoomEntity" useGeneratedKeys="true" keyProperty="id"> insert into hour_room( version, pkg_serial, attribute, create_time ) values( #{version}, #{pkgSerial}, #{attribute}, now() ) </insert>如上去使用没有问题 12345当接口加入@param修饰对象时int insert(@Param("hourRoomEntity") HourRoomEntity hourRoomEntity);此时就会报错Error rg.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘version’ not found. Available parameters are param1, hourRoomEntity 是因为当使用@param时,会将@param的所有参数封装到map中,必须写下如下格式 123#{hourRoomEntity.version},#{hourRoomEntity.pkgSerial},#{hourRoomEntity.attribute}, 对于使用@Param修饰一个对象类型,需要使用如下格式 1#{对象.成员} 当不使用@Param时,就当成一个对象来处理,只需要写成 123#{version},#{pkgSerial},#{attribute}, 建议@param只是在多个参数(多个简单类型或者多个实体类对象类型)中使用 @Param和parameterType的关系 12345678二者的关系如下如果使用了@Param,就不需要在mapper.xml中设置parameterType属性了如果不使用@Param,就需要在mapper.xml中设置parameterType属性了。所以进行如下总结如果在dao接口代码中,函数中参数只有一个,那么此时就在mapper.xml中使用parameterType,不使用@param了;如果在dao接口中代码中,函数中参数有多个参数时,此时就使用@param,在mapper.xml中不再需要parameterType了。 2、Mybatis插入语句在mysql设置的默认值不生效问题Mybatis插入语句默认值不生效,但直接在mysql命令行是生效的 Mybatis语句:insert into UserInfo (userName,age,sex) values (#{userName},#{age},#{sex}) 其中 只给userName和age传入值,sex没有传入值,期望用默认值(建数据表时规定default)填入 Mysql命令行语句:INSERT INTO UserInfo (userName,age) VALUE (“阿三”,18) 原因: 对比MyBatis和Mysql两条sql语句,mysql没有写sex,而MyBatis写了sex,但又没有给sex赋值因为mybatis中的sql语句是这样的,如果sex在mysql有默认值,这里使用这么一个通用的语句,如果此时不给sex传入值最后导致sex为空,也就是默认值不生效, mybatis xml代码如下: 1insert into UserInfo (userName,age,sex) values (#{userName},#{age},#{sex}) 用动态sql insert语句解决。代码判断有某个字段的值时sql语句才包含这个字段,例如:如果有sex值传入时 insert语句里才有sex字段否者没有sex字段,具体代码如下: 1234567891011121314151617181920212223<!--一种动态的insert语句--><sql id="userInfoColumns"> <trim suffixOverrides=","> <if test="userName != null">userName,</if> <if test="age != null">age,</if> <if test="sex != null">sex</if> </trim></sql> <sql id="userInfoValues"> <trim suffixOverrides=","> <if test="userName != null">#{userName},</if> <if test="age != null">#{age},</if> <if test="sex != null">#{sex}</if> </trim></sql> <insert id="insert" parameterType="com.example.mybatisdemo1.domin.UserInfo" keyColumn="userInfoId" keyProperty="userInfoId" useGeneratedKeys="true"> insert into UserInfo(<include refid="userInfoColumns"/>) values (<include refid="userInfoValues"/>)</insert><trim>标签去除片段首尾可能出现的多余的“,”字符 3、mybatis查询没有数据返回list时是返回空List(size=0)而不是null查询出返回的List是为空即isEmpty(),size=0,而不是null 所以在foreach循环list时候,不用判断list是否为null,遍历size为空则会直接跳出,不会报错]]></content>
<categories>
<category>mybatis</category>
</categories>
<tags>
<tag>mybatis</tag>
</tags>
</entry>
<entry>
<title></title>
<url>%2F2020%2F09%2F23%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2Fgit%2F</url>
<content type="text"><![CDATA[在dev分支上 123git add .git commit -m '提交的备注信息'git push -u origin dev 想将dev分支合并到master分支,操作如下: 1、首先切换到master分支上 1git checkout master 2、如果是多人开发的话 需要把远程master上的代码pull下来 1234git pull origin master//如果是自己一个开发就没有必要了,为了保险期间还是pull git pull 是将master先拉下来和本地代码合并 3、然后我们把dev分支的代码合并到master上 1git merge dev 4、然后查看状态及执行提交命令 12345678910git statusOn branch masterYour branch is ahead of 'origin/master' by 12 commits. (use "git push" to publish your local commits)nothing to commit, working tree clean//上面的意思就是你有12个commit,需要push到远程master上 > 最后执行下面提交命令git push origin master git fetch不会进行合并执行后需要手动执行git merge合并分支,而git pull拉取远程分之后直接与本地分支进行合并。更准确地说,git pull使用给定的参数运行git fetch,并调用git merge将检索到的分支头合并到当前分支中。 例如执行下面语句: 1git pull origin master:brantest 将远程主机origin的master分支拉取过来,与本地的brantest分支合并。 后面的冒号可以省略: 1git pull origin master 表示将远程origin主机的master分支拉取过来和本地的当前分支进行合并。 上面的pull操作用fetch表示为: 12git fetch origin master:brantestgit merge brantest git 的本地版本管理有三个部分 名称 说明 工作区(Working Directory) 我们直接编辑的文件部分 暂存区(Staged Snapshot) 文件执行 git add .后存的地方 版本库区 (Commit History) 文件执行 git commit .后存的地方]]></content>
</entry>
<entry>
<title></title>
<url>%2F2020%2F08%2F19%2F%E4%BC%81%E4%B8%9A%E7%BA%A7%E7%94%B5%E5%95%86%E9%A1%B9%E7%9B%AE%2F%E7%AC%AC%E4%BA%8C%E7%AB%A0%2F</url>
<content type="text"></content>
</entry>
<entry>
<title></title>
<url>%2F2020%2F08%2F19%2F%E4%BC%81%E4%B8%9A%E7%BA%A7%E7%94%B5%E5%95%86%E9%A1%B9%E7%9B%AE%2F%E7%AC%AC%E4%B8%80%E7%AB%A0%2F</url>
<content type="text"><![CDATA[]]></content>
</entry>
<entry>
<title><![CDATA[docker基础]]></title>
<url>%2F2020%2F08%2F08%2Fdocker%2Fdocker%E5%9F%BA%E7%A1%80%2F</url>
<content type="text"><![CDATA[docker学习概述 docker安装 docker命令 镜像命令 容器命令 操作命令 docker镜像 容器数据卷 docker file docker 网络原理 IDEA整合docker docker compose docker swarm CI/CD Jenkins]]></content>
<categories>
<category>docker</category>
</categories>
<tags>
<tag>docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java基础面试题]]></title>
<url>%2F2020%2F08%2F03%2F%E9%9D%A2%E8%AF%95%E9%A2%98%2FJava%E5%9F%BA%E7%A1%80%2F</url>
<content type="text"><![CDATA[1、重写和重载的区别:重载:在一个类中,方法名必须相同,参数列表(参数类型,参数个数、参数顺序)可以不同、返回类型和访问修饰符可以不同,发生在编译时。 重写:在父子类两类中,方法名必须相同,参数列表必须相同,返回类型必须相同;返回值范围、异常抛出范围子类要比父类小或等,访问修饰符访问子类比父类大或等;如果父类访问修饰符是private,则子类不能重写父类该方法 2、String、StringBuilder、StringBuffer的区别可变性: String类使用final关键字字符数组保存字符串,private final char value[],String对象是不可变的 StringBuilder和StringBuffer继承于AbstractStringBuilder类,AbstractStringBuilder类是使用char value[],没有使用到final关键字修饰,所以对象可变 12345678910AbstractStringBuilder.javaabstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; }} 线程安全性: String对象是不可变,即常量,线程安全 StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,线程安全 StringBuilder没有对方法加锁,线程不安全 性能: String:每次对String对象进行改变,都会生成一个新的String对象,然后指针指向新的String对象 StringBuffer:每次对StringBuffer对象进行改变,不会生成新的对象,而是改变对象引用 相同情况下,StringBuilder比StringBuffer提高15%的性能 操作少量的数据 = String 单线程操作字符串缓冲区下操作大量数据 = StringBuilder 多线程操作字符串缓冲区下操作大量数据 = StringBuffffer 3、自动装箱和自动拆箱装箱:将基本类型用所对应的引用类型包装起来 拆箱:将包装类型转换为基本数据类型 4、==和equals的区别==:判断两个对象的地址是否相等,即判断两个对象是否同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址) equals():判断两个对象是否相等,分两种情况使用: 类没有重写equals()方法,则通过equals()方法比较该类的两个对象时,等价于通过==比较两个对象 类重写了equals()方法,重写之后则比较两个对象的内容是否相等 String、Integer、Double等引用数据类型被重写,Object的equals方法是比较两个对象的地址,而String则是比较对象内容 当创建String类型的对象时,虚拟机在常量池中查找有没有已经存在的值和要创建的值相同的对象,有则赋给当前引用,无则创建对象。 Integer、Double等则是有IntegerCache会缓存-128~127之间的对象。 如:Integer x = 100,会调用Integer的valueOf()方法,这个方法就是返回一个Integer对象,但是在返回前,做一个判断,判断要赋给对象的值是否在[-128,127]区间中,且IntegerCache(是Integer类的内部类,里面有一个Integer对象数组,用于存放已经存在的且范围在[-128,127]中的对象)中是否存在此对象,如果存在,则直接返回引用,否则,创建一个新对象返回。 5、final关键字的一些总结final关键字用在三个地方:变量、方法和类 final修饰变量:基本数据类型的变量,一旦初始化则不可以改变;引用类型的变量,则在对其初始化之后便不能让其指向另一个对象 final修饰类:当final修饰一个类时,该类不能被继承;对于 final 类中的成员,可以定义其为 final,也可以不是 final。而对于方法,由于所属类为 final 的关系,自然也就成了 final 型。 使用fifinal方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将fifinal方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用fifinal方法进行这些优化了)。类中所有的private方法都隐式地指定为fianl。]]></content>
<categories>
<category>面试题</category>
</categories>
<tags>
<tag>面试题</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ES6]]></title>
<url>%2F2020%2F05%2F22%2FJS%2FES6%2F</url>
<content type="text"><![CDATA[构造函数原型:prototype 构造函数通过原型分配的函数是所有对象所共享 每一个构造函数都有一个prototype属性,指向另一个对象,这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有,可以把那些不变的方法,直接定义在prototype对象上,这样所有的对象实例就可以共享这些方法了 对象原型_ proto :对象都会有一个属性 _ proto _指向构造函数的prototype原型对象,我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有原型proto的存在,proto对象原型和原型对象prototype是等价的 原型链: 原型对象this指向的是实例对象。 es6之前没有提供继承extends,可以通过构造函数和原型对象模拟继承,称为组合继承 call():调用这个函数,并且修改函数运行时的this指向。 call方法的两个参数:thisArg(),当前调用这个函数的this指向对象|arg:传递其他参数 1234567891011121314function(x, y){ console.log("ssdfsd"); console.log(this);//此时this指向o对象 console.log(x,y);}var o= { }//1.调用函数fn.call();//2.改变函数的this指向fn.call(o);//3.其他参数传递fn.call(o,1,2);//输出结果3 函数this指向总结 改变函数内部this指向: 常用方法有:bind()、call()、apply() apply()方法:fun.apply(thisArg, [argsArray]) thisArg:在fun函数运行时指定的this值 argsArray:传递的值,必须包含在数组里面 返回值就是函数的返回值,因为它就是调用函数 1234//利用apply用数组进行筛选最大值function getMaxOfArray(numArray) { return Math.max.apply(null, numArray);} bind()方法:fun.bind(thisArg,arg1,arg2,…) bind不会调用函数,但是能改变this的指向 thisArg:在fun函数运行时指定的this值 args1,args2:传递的其他参数 返回由指定的this值和初始化参数改造的原函数拷贝 闭包: 闭包指有权访问另一个函数作用域中变量的函数,即一个作用域可以访问另一个函数内部的局部变量。 浅拷贝和深拷贝: 浅拷贝:浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用,Object.assign(target,…source) 深拷贝:拷贝多层,每一级别的数据都会拷贝 正则表达式:是用于匹配字符串中字符组合的模式,正则表达式也是一个对象 边界符: 字符类: [],[^]方括号内部 取反的意思 量词符: 预定义类:]]></content>
<categories>
<category>ES6</category>
</categories>
<tags>
<tag>ES6</tag>
</tags>
</entry>
<entry>
<title><![CDATA[WebApi基础知识]]></title>
<url>%2F2020%2F04%2F25%2FJS%2FWebApi%2F</url>
<content type="text"><![CDATA[DOM 获取页面元素方式: 根据ID获取document.getElementById(); 根据标签名获取document.getElementByTagName();获取到元素对象的集合 通过HTML5新增方法获取document.getElementByClassName();通过类名获取 document.querySelector();返回指定选择器的第一个元素对象,class加.、id加#、标签不加 document.querySelectorAll(); 特殊元素获取,获取body和html,document.body、document.documentElement 事件基础事件三要素:事件源、事件类型、事件处理程序 节点操作 节点至少拥有nodeType(节点类型)、nodeName(节点名字)、nodeValue(节点值)三个基本属性 元素节点 nodeType=1 属性节点 nodeType=2 文本节点 nodeType=3(文本节点包括文本、换行、空格) 创建元素的效率对比: dom的重点核心: 增删改查 事件高级注册事件: 给元素添加事件,称为注册事件或者绑定事件 注册事件两种方式:传统方式和方法监听注册方式 删除事件: DOM事件流: 事件流描述的是从页面中接收事件的顺序 事件发生时会在元素节点之间按照特定的顺序传播,这个传播过程称为DOM事件流 事件对象: e.target返回的是触发事件对象,this返回是绑定事件对象 事件委托: 原理:不是每个子节点单独设置事件监听器,而是事件监听器设置在父节点,然后利用冒泡原理影响设置每个子节点 BOM浏览器模型BOM是浏览器对象模型,提供了独立于内容而与浏览器窗口进行交互的对象,核心对象是window BOM构成: this: this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this指向谁,一般情况下this的最终指向的是那个调用他的对象。 js执行机制js是单线程,同一个时间只能做一件事,所以有了同步和异步的任务处理 同步和异步的区别:在一条流水线上各个流程的执行顺序不同 同步任务:同步任务在主线程上执行,形成一个执行栈 异步任务:JS的异步是通过回调函数实现的,异步任务的三种类型 普通事件:如click、resize等 资源加载:如load、error等 定时器:包括setInterval、setTimeout等 异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列) 事件循环: 由于主线程不断的重复获得任务、执行任务、再获取任务、再执行的过程,这种机制叫事件循环 location对象: window对象提供location属性用于获取或者设置窗体的URL,并且可以解析URL 本地存储: sessionStorage: 存储:sessionStorage.setItem(key,value); 获取:sessionStorage.getItem(key); 删除:sessionStorage.removeItem(key); 清除所有:sessionStorage.clear(); localStorage: 存储获取删除等方法与sessionStorage一致]]></content>
<categories>
<category>JS-WebApi</category>
</categories>
<tags>
<tag>JS-WebApi</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JS基础知识]]></title>
<url>%2F2020%2F03%2F25%2FJS%2Fjs%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%2F</url>
<content type="text"><![CDATA[js的组成:ECMAScipt(JavaScript的语法)、DOM(页面文档对象模型)、BOM(浏览器对象模型) js输入输出方式:alert()浏览器弹出警示框、console.log()控制台输出、prompt()浏览器弹出输入框。 js数据类型: 简单数据类型(Number、String、Boolean、Undefined、Null) 复杂数据类型(object) 字符串转义符: \n:换行符 \\:斜杠\ \‘:’单引号 \t:tab 缩进 \b:空格 获取变量数据类型:typeof 数据转换类型: 转换为字符串:toString()、String()强制转换、加号拼接字符串 转换为数字型:parseInt(string)、parseFloat(string)、Number()、js隐式转换(- * /) 例子:’12‘-0 加法需要显示转换、减号有隐式转换 转换为布尔型:‘’、0、NaN、null、undefined都是false,其余都是true Js创建数组两种方式: new创建数组 利用数组字面量创建数组 数组新增:修改数组长度arr.length = i; 函数argument作用:接受无定义实参的值 js作用域js作用域:全局作用域和局部作用域 在全局作用域下的变量,在全局都可以使用,如果在函数内部没用声明,也属于全局变量 从执行效率来看,全局变量在浏览器关闭之后才销毁,局部变量在程序运行完之后销毁 js没用块级作用域(块级作用域{}),在es6之后才有 if(3<5){var num = 10;}console.log(num);正常使用 作用域链:根据内部函数可以访问外部函数的机制,用链式查找那些数据能被内部函数访问 js预解析js解析器运行js代码分为两步:预解析、代码执行 (1)预解析:js引擎会把js里面所有的var和function提升到当前作用域的最前面 (2)代码执行:根据代码顺序从上往下执行 预解析分为变量预解析(变量提升)和函数预解析(函数提升) 变量提升:把所有的变量声明放在当前作用域的最前面,是变量声明不是变量赋值 函数提升:把所有的函数声明放在当前作用域的最前面,不调用函数 js对象在js中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象,包括字符串、数值、数组、函数等 创建对象的三种方式: (1)利用字面量创建对象var obj = {} (2)利用new object创建对象var obj = new Object(); (3)利用构造函数创建对象,构造函数名需要大写、不需要return就可以返回结果、调用构造函数必须new function 构造函数名(){ this.属性名 = 值; this.方法名 = function{} } new 构造函数名(); 对象的调用:obj.属性名或obj[‘属性名’]、obj.方法名 new关键字执行过程: new在内存中创建一个空的对象 this指向创建的空对象 执行构造函数中的代码,给空对象进行添加属性和方法 返回这个对象 遍历对象for…in… for(变量 in 对象){对象[变量]} js的内置对象js对象分为三种:自定义对象、内置对象、浏览器对象 常用的内置对象:math、Date、数组 简单类型和复杂类型简单类型又叫基本数据类型或值类型,复杂类型又叫引用类型 五大数据类型是值类型:string、number、boolean、undefined、null 通过new创建的对象都是复杂类型,如Object、Array、Date 简单数据类型放在栈里面、复杂数据类型放在堆里面 简单数据类型传参 复杂数据类型传参]]></content>
<categories>
<category>JS</category>
</categories>
<tags>
<tag>JS</tag>
</tags>
</entry>
<entry>
<title></title>
<url>%2F2020%2F03%2F24%2F%E5%BC%80%E9%A2%98%E6%8A%A5%E5%91%8A%2F</url>
<content type="text"><![CDATA[选题的意义和目的随着我国移动互联网和智能手机相机的高速发展,专注于社交的视频应用正在崛起,尤其是短视频app。如今4G移动网络普及下,使图像的传输速度更快,让传输图像的质量越来越高。首先,4G通信在图片、视频传输上能够实现原图、原视频高清传输,其传输质量与电脑画质不相上下;其次,利用4G通信技术,在软件、文件、图片、音视频下载上其速度最高可达到最高每秒几十兆,这是3G通信技术无法实现的,同时这也是4G通信技术一个显著优势;这种快捷的下载模式能够为我们带来更佳的通信体验,也便于我们日常学习中学习资料的下载,随着社交 app 的发展,文字、图片在信息承载力方面表现出明显的不足,因此,将文字、声音与视频结合在一起的短视频成为人们阅读和使用的首选方式,短视频依靠其“短平快”的特点受到众多用户的喜爱,社交应用的内容也渐渐从传统的纯文字变成了信息量更加丰富的图文,内容也更加丰富多彩, 4G 网络开始普及,在更加快速的网络这个基础上,让短视频社交的方式越来越盛行,通过十几秒甚至几秒的短视频更能给大众传递更多的更好的信息,无论在生活、娱乐、学习方面,短视频能够承载着更多的信息和方式来丰富人们的生活和满足互联网普及的需求。 中国的短视频社交app市场规模越来越大,用户数量从2016年开始一直以指数增长,从抖音到现在的微视、快手、西瓜短视频等app的推广,短视频类app的竞争越来越激烈。在激烈的竞争中需要打造自身特色以及针对性满足用户需求才可以赢得用户的依赖和喜爱,而微信小程序与现在手机上安装的app有着背靠微信大量的用户基数,引流效果好,并且在现在中国互联网用户随手都是微信社交的潮流下,小程序可以满足用户随手用随手关的便捷性、不需要安装下载即可使用的应用以及无需占用内存的优势。 短视频的发展是与人们生活的多样化紧密相联系的,作为短视频应用,对于用户有东西分享,敢分享生活的丰富多彩,方式的个性化才能转化为视频show。新媒体背景下短视频的快速发展不仅为移动网络用户提供了以视频为载体的社交新模式,也可为新闻、广告、营销等领域提供了新的传播方式,是颇有潜力的传播途径。 文献综述短视频新媒体传播信息方式是一种融合文字、声音和动态画面及后期编辑于一体、以短视频形式进行互动的社交网络,也可以称作短视频社区。它可以让用户用手机或者其他智能终端,拍摄一段时间的视频,用户可以将自己的生活、娱乐、思想等更准确更快捷的传递分享给其他用户,从而吸引定位某些领域的用户关注并同时进行交互,该社交方式以更方便更快捷更好玩的优势替代了以前使用文本、图片等方式的社交方式,使得网络社交更加有趣。用户连同文本解释分享到社交平台,根据用户自身的喜爱一键分享到给其他用户,以视频传播的方式进行交友的社交网络。 短视频小程序平台更是可以依靠微信社交平台大流量用户的支持,实现快捷有趣的传递信息新媒体方式,实现随用随关的便捷性,让用户在不知觉地分享自己生活,体验他人生活。评论及分享功能吸引用户通过网络社交,记录彼此的快乐生活。 研究现状如今短视频app盛行,但基于小程序方式却可以让用户无需下载安装app便可体验应用,在现在app泛滥的时候,开发app的成本比小程序大且app的推广需依靠自身的营销去吸引用户下载,小程序则依靠了微信社交平台方便驱使用户体验应用。 小程序开发的便捷给开发者带来诸多好处,新框架kbone的出现提供web端代码编译小程序端的能力,解决了长久以来小程序端和web端的代码问题,极大减轻了开发和维护的工作量。小程序对于web端和app来说,是更完善的生态和更多的变现方式,小程序具有不可限量的潜力,它能直达服务,相信小程序会开创它的移动互联网收割时代。 创新思路小程序端采取springboot后台取代SpringMVC,一个完整的小程序包括前端与后端,前端是页面的展示,使用了微信开发工具内置的语言,更加快速地搭建起前端的手机页面,许多函数和样式针对手机来调整,后端是处理业务逻辑和数据库对接的部分,使用SpringBoot框架替代配置繁琐的SpringMVC框架,迅速搭建起后台环境,通过前后端的交互实现小程序端的功能。 论文提纲参考文献]]></content>
</entry>
<entry>
<title></title>
<url>%2F2020%2F03%2F11%2F%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B9%E7%9B%AE%2F</url>
<content type="text"><![CDATA[java短视频小程序开发第一章:微信小程序是一种全新的连接用户与服务的方式,可以在微信内被便捷地获取和传播,同时具有出色的使用体验,无需下载过占用手机内存的app,小程序直接打开使用。 1.微信小程序项目构造 2.基础语法 3.小程序生命周期 介绍小程序一些基本标签和基础语法 第二章:1.flex布局 2.小程序的基础组件 第三章:1.小程序的表单组件 2.小程序与后台api通信,内网穿透 3.ngrok内网转发 第四章:1.数据库表设计 第五章:1.前端小程序页面开发 2.springboot开发常用技术整合(前期慕课视频基础) 3.短视频后台SpringBoot搭建,使用maven搭建分层的聚合工程 4.使用swagger2构建restful接口测试 第六章:1.开发bgm接口 2.开发视频上传接口 3.利用ffmpeg结合视频和bgm 第七章:1.开发视频列表接口 2.微信小程序搜索框视频组件mindawei 3.小程序视频主页的完善 4.登录等页面拦截 数据库bgm表: /表: bgm/———— /列信息/———– Field Type Collation Null Key Default Extra Privileges Comment id varchar(64) utf8mb4_general_ci NO PRI (NULL) select,insert,update,referencesauthor varchar(255) utf8mb4_general_ci NO (NULL) select,insert,update,referencesname varchar(255) utf8mb4_general_ci NO (NULL) select,insert,update,referencespath varchar(255) utf8mb4_general_ci NO (NULL) select,insert,update,references 播放地址 comments表: /表: comments/—————– /列信息/———– Field Type Collation Null Key Default Extra Privileges Comment id varchar(20) utf8mb4_general_ci NO PRI (NULL) select,insert,update,referencesfather_comment_id varchar(20) utf8mb4_general_ci YES (NULL) select,insert,update,references to_user_id varchar(20) utf8mb4_general_ci YES (NULL) select,insert,update,references video_id varchar(20) utf8mb4_general_ci NO (NULL) select,insert,update,references 视频id from_user_id varchar(20) utf8mb4_general_ci NO (NULL) select,insert,update,references 留言者,评论的用户id comment text utf8mb4_general_ci NO (NULL) select,insert,update,references 评论内容 create_time datetime (NULL) NO (NULL) select,insert,update,references 答辩内容]]></content>
</entry>
<entry>
<title></title>
<url>%2F2020%2F03%2F07%2F%E8%B0%B7%E7%B2%92%E5%AD%A6%E9%99%A2%E9%A1%B9%E7%9B%AE%E7%9F%A5%E8%AF%86%E6%B8%85%E5%8D%95%2F</url>
<content type="text"><![CDATA[谷粒学院项目知识清单Lombok 分布式系统唯一id生成方案 sql执行效率插件 条件构造器Wrapper mybatis-plus逆向工程(代码生成器) swagger2 康威定律(微服务)]]></content>
</entry>
<entry>
<title><![CDATA[创建型模式]]></title>
<url>%2F2019%2F10%2F30%2F%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%2F%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[创建型模式概述 创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。 创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。 单例模式单例模式简介1.1 定义保证一个类仅有一个实例,并提供一个访问它的全局访问点。 1.2 为什么要用单例模式呢?在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。 简单来说使用单例模式的好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销; 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 1.3 为什么不使用全局变量确保一个类只有一个实例呢?我们知道全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。 但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。 单例的模式的实现通常单例模式在Java语言中,有两种构建方式: 饿汉方式。指全局的单例实例在类装载时构建 懒汉方式。指全局的单例实例在第一次被使用时构建。 不管是那种创建方式,它们通常都存在下面几点相似处: 单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化; instance 成员变量和 uniqueInstance 方法必须是 static 的。 2.1 饿汉方式(线程安全)123456789public class Singleton { //在静态初始化器中创建单例实例,这段代码保证了线程安全 private static Singleton uniqueInstance = new Singleton(); //Singleton类只有一个构造方法并且是被private修饰的,所以用户无法通过new方法创建该对象实例 private Singleton(){} public static Singleton getInstance(){ return uniqueInstance; }} 所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。 2.2 懒汉式(非线程安全和synchronized关键字线程安全版本 )12345678910111213public class Singleton { private static Singleton uniqueInstance; private Singleton (){ } //没有加入synchronized关键字的版本是线程不安全的 public static Singleton getInstance() { //判断当前单例是否已经存在,若存在则返回,不存在则再建立单例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } } 所谓 “ 懒汉式” 就是说单例实例在第一次被使用时构建,而不是在JVM在加载这个类时就马上创建此唯一的单例实例。 但是上面这种方式很明显是线程不安全的,如果多个线程同时访问getInstance()方法时就会出现问题。如果想要保证线程安全,一种比较常见的方式就是在getInstance() 方法前加上synchronized关键字,如下: 123456public static synchronized Singleton getInstance() { if (instance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } 我们知道synchronized关键字偏重量级锁。虽然在JavaSE1.6之后synchronized关键字进行了主要包括:为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升。 但是在程序中每次使用getInstance() 都要经过synchronized加锁这一层,这难免会增加getInstance()的方法的时间消费,而且还可能会发生阻塞。我们下面介绍到的 双重检查加锁版本 就是为了解决这个问题而存在的。 2.3 懒汉式(双重检查加锁版本)利用双重检查加锁(double-checked locking),首先检查是否实例已经创建,如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的效果。 1234567891011121314151617181920public class Singleton { //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getInstance() { //检查实例,如果不存在,就进入同步代码块 if (uniqueInstance == null) { //只有第一次才彻底执行这里的代码 synchronized(Singleton.class) { //进入同步代码块后,再检查一次,如果仍是null,才创建实例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }} 很明显,这种方式相比于使用synchronized关键字的方法,可以大大减少getInstance() 的时间消费。 注意: 双重检查加锁版本不适用于1.4及更早版本的Java。1.4及更早版本的Java中,许多JVM对于volatile关键字的实现会导致双重检查加锁的失效。 2.4 懒汉式(登记式/静态内部类方式)静态内部实现的单例是懒加载的且线程安全。 只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。 123456789public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } 2.5 饿汉式(枚举方式)这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。 它更简洁,自动支持序列化机制,绝对防止多次实例化 (如果单例类实现了Serializable接口,默认情况下每次反序列化总会创建一个新的实例对象, 12345678public enum Singleton { //定义一个枚举的元素,它就是 Singleton 的一个实例 INSTANCE; public void doSomeThing() { System.out.println("枚举方法实现单例"); } } 使用方法: 1234567public class ESTest { public static void main(String[] args) { Singleton singleton = Singleton.INSTANCE; singleton.doSomeThing();//output:枚举方法实现单例 }} 工厂模式工厂模式介绍1.1 工厂模式的定义 在基类中定义创建对象的一个接口,让子类决定实例化哪个类。工厂方法让一个类的实例化延迟到子类中进行。 1.2 工厂模式的分类:(1)简单工厂(Simple Factory)模式,又称静态工厂方法模式(Static Factory Method Pattern)。 (2)工厂方法(Factory Method)模式,又称多态性工厂(Polymorphic Factory)模式或虚拟构造(Virtual Constructor)模式; (3)抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式。 1.3 在开源框架中的使用举两个比较常见的例子 (1)Spring中通过getBean(“xxx”)获取Bean; (2) Java消息服务JMS中(下面以消息队列ActiveMQ为例子) 123// 1、创建一个连接工厂对象,需要指定服务的ip及端口。 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.155:61616"); // 2、使用工厂对象创建一个Connection对象。 1.4 为什么要用工厂模式(1) 解耦 :把对象的创建和使用的过程分开 (2)降低代码重复: 如果创建某个对象的过程都很复杂,需要一定的代码量,而且很多地方都要用到,那么就会有很多的重复代码。 (3) 降低维护成本 :由于创建过程都由工厂统一管理,所以发生业务逻辑变化,不需要找到所有需要创建对象B的地方去逐个修正,只需要在工厂里修改即可,降低维护成本。 简单工厂模式2.1 介绍严格的说,简单工厂模式并不是23种常用的设计模式之一,它只算工厂模式的一个特殊实现。简单工厂模式在实际中的应用相对于其他2个工厂模式用的还是相对少得多,因为它只适应很多简单的情况。 最重要的是它违背了我们在概述中说的 开放-封闭原则 (虽然可以通过反射的机制来避免) 。因为每次你要新添加一个功能,都需要在生switch-case 语句(或者if-else 语句)中去修改代码,添加分支条件。 2.2 适用场景(1)需要创建的对象较少。 (2)客户端不关心对象的创建过程。 2.3 简单工厂模式角色分配: 工厂(Factory)角色 :简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。 抽象产品(Product)角色 :简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 具体产品(Concrete Product)角色:简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例。 2.4 简单工厂实例创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw()方法用于绘图. (1)创建Shape接口 123public interface Shape { void draw();} (2)创建实现该接口的具体图形类 123456789101112131415161718192021222324252627282930313233//圆形public class Circle implements Shape { public Circle() { System.out.println("Circle"); } @Override public void draw() { System.out.println("Draw Circle"); }}//长方形public class Rectangle implements Shape { public Rectangle() { System.out.println("Rectangle"); } @Override public void draw() { System.out.println("Draw Rectangle"); }}//正方形public class Square implements Shape { public Square() { System.out.println("Square"); } @Override public void draw() { System.out.println("Draw Square"); }} (3)创建工厂类: 123456789101112131415161718public class ShapeFactory { // 使用 getShape 方法获取形状类型的对象 public static Shape getShape(String shapeType) { if (shapeType == null) { return null; } if (shapeType.equalsIgnoreCase("CIRCLE")) { return new Circle(); } else if (shapeType.equalsIgnoreCase("RECTANGLE")) { return new Rectangle(); } else if (shapeType.equalsIgnoreCase("SQUARE")) { return new Square(); } return null; }} (4)测试方法: 12345678910111213141516public class Test { public static void main(String[] args) { // 获取 Circle 的对象,并调用它的 draw 方法 Shape circle = ShapeFactory.getShape("CIRCLE"); circle.draw(); // 获取 Rectangle 的对象,并调用它的 draw 方法 Shape rectangle = ShapeFactory.getShape("RECTANGLE"); rectangle.draw(); // 获取 Square 的对象,并调用它的 draw 方法 Shape square = ShapeFactory.getShape("SQUARE"); square.draw(); }} 输出结果: 123456CircleDraw CircleRectangleDraw RectangleSquareDraw Square 这样的实现有个问题,如果我们新增产品类的话,就需要修改工厂类中的getShape()方法,这很明显不符合 开放-封闭原则 2.5 使用反射机制改善简单工厂将工厂类改为下面的形式: 1234567891011121314151617181920212223package factory_pattern;/** * 利用反射解决简单工厂每次增加新了产品类都要修改产品工厂的弊端 * * @author Administrator * */public class ShapeFactory2 { public static Object getClass(Class<? extends Shape> clazz) { Object obj = null; try { obj = Class.forName(clazz.getName()).newInstance(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return obj; }} 测试方法: 1234567891011121314package factory_pattern;public class Test2 { public static void main(String[] args) { Circle circle = (Circle) ShapeFactory2.getClass(factory_pattern.Circle.class); circle.draw(); Rectangle rectangle = (Rectangle) ShapeFactory2.getClass(factory_pattern.Rectangle.class); rectangle.draw(); Square square = (Square) ShapeFactory2.getClass(factory_pattern.Square.class); square.draw(); }} 这种方式的虽然符合了 开放-关闭原则 ,但是每一次传入的都是产品类的全部路径,这样比较麻烦。如果需要改善的话可以通过 反射+配置文件 的形式来改善,这种方式使用的也是比较多的。 工厂方法模式3.1 介绍工厂方法模式应该是在工厂模式家族中是用的最多模式,一般项目中存在最多的就是这个模式。 工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说 每个对象都有一个与之对应的工厂 。 3.2 适用场景 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无需关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。 3.3 工厂方法模式角色分配: 抽象工厂(Abstract Factory)角色:是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。 具体工厂(Concrete Factory)角色 :这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。 抽象产品(AbstractProduct)角色 :工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。 具体产品(Concrete Product)角色 :这个角色实现了抽象产品角色所定义的接口。某具体产品有专门的具体工厂创建,它们之间往往一一对应 3.4 工厂方法模式实例上面简单工厂例子中的图形接口以及相关图像实现类不变。我们只需要增加一个工厂接口以及实现这个接口的工厂类即可。 (1)增加一个工厂接口: 123public interface Factory { public Shape getShape();} (2)增加相关工厂类: 1234567891011121314151617181920212223242526272829//圆形工厂类public class CircleFactory implements Factory { @Override public Shape getShape() { // TODO Auto-generated method stub return new Circle(); }}//长方形工厂类public class RectangleFactory implements Factory{ @Override public Shape getShape() { // TODO Auto-generated method stub return new Rectangle(); }}//圆形工厂类public class SquareFactory implements Factory{ @Override public Shape getShape() { // TODO Auto-generated method stub return new Square(); }} (3)测试: 123456789public class Test { public static void main(String[] args) { Factory circlefactory = new CircleFactory(); Shape circle = circlefactory.getShape(); circle.draw(); }} 输出结果: 12CircleDraw Circle 抽象工厂模式4.1 介绍在工厂方法模式中,其实我们有一个潜在意识的意识。那就是我们生产的都是同一类产品。抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一种产品,而是可以创建一组产品。 抽象工厂应该是比较最难理解的一个工厂模式了。 4.2 适用场景 和工厂方法一样客户端不需要知道它所创建的对象的类。 需要一组对象共同完成某种功能时,并且可能存在多组对象完成不同功能的情况。(同属于同一个产品族的产品) 系统结构稳定,不会频繁的增加对象。(因为一旦增加就需要修改原有代码,不符合开闭原则) 4.3 抽象工厂方法模式角色分配: 抽象工厂(AbstractFactory)角色 :是工厂方法模式的核心,与应用程序无关。任何在模式中创建的对象的工厂类必须实现这个接口。 具体工厂类(ConreteFactory)角色 :这是实现抽象工厂接口的具体工厂类,包含与应用程序密切相关的逻辑,并且受到应用程序调用以创建某一种产品对象。 抽象产品(Abstract Product)角色 :工厂方法模式所创建的对象的超类型,也就是产品对象的共同父类或共同拥有的接口。 具体产品(Concrete Product)角色 :抽象工厂模式所创建的任何产品对象都是某一个具体产品类的实例。在抽象工厂中创建的产品属于同一产品族,这不同于工厂模式中的工厂只创建单一产品,我后面也会详解介绍到。 4.4 抽象工厂的工厂和工厂方法中的工厂有什么区别呢?抽象工厂是生产一整套有产品的(至少要生产两个产品),这些产品必须相互是有关系或有依赖的,而工厂方法中的工厂是生产单一产品的工厂。 下面就是抽象工厂图示: 4.5 抽象工厂模式实例我们假设现在存在AK、M4A1两类枪,每一种枪对应一种子弹。我们现在这样考虑生产AK的工厂可以顺便生产AK使用的子弹,生产M4A1的工厂可以顺便生产M4A1使用的子弹。(AK工厂生产AK系列产品包括子弹啊,AK枪的类型啊这些,M4A1工厂同理) (1)创建相关接口: 123456789//枪public interface Gun { public void shooting();}//子弹public interface Bullet { public void load();} (2)创建接口对应实现类: 123456789101112131415161718192021222324252627282930313233343536//AK类public class AK implements Gun{ @Override public void shooting() { System.out.println("shooting with AK"); }}//M4A1类public class M4A1 implements Gun { @Override public void shooting() { System.out.println("shooting with M4A1"); }}//AK子弹类public class AK_Bullet implements Bullet { @Override public void load() { System.out.println("Load bullets with AK"); }}//M4A1子弹类public class M4A1_Bullet implements Bullet { @Override public void load() { System.out.println("Load bullets with M4A1"); }} (3)创建工厂接口 1234public interface Factory { public Gun produceGun(); public Bullet produceBullet();} (4)创建具体工厂 123456789101112131415161718192021222324252627//生产AK和AK子弹的工厂public class AK_Factory implements Factory{ @Override public Gun produceGun() { return new AK(); } @Override public Bullet produceBullet() { return new AK_Bullet(); }}//生产M4A1和M4A1子弹的工厂public class M4A1_Factory implements Factory{ @Override public Gun produceGun() { return new M4A1(); } @Override public Bullet produceBullet() { return new M4A1_Bullet(); }} (5)测试 123456789101112131415public class Test { public static void main(String[] args) { Factory factory; Gun gun; Bullet bullet; factory =new AK_Factory(); bullet=factory.produceBullet(); bullet.load(); gun=factory.produceGun(); gun.shooting(); }} 输出结果: 12Load bullets with AKshooting with AK]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[并发工具]]></title>
<url>%2F2019%2F10%2F29%2F%E5%B9%B6%E5%8F%91%2F%E5%B9%B6%E5%8F%91%E5%B7%A5%E5%85%B7%2F</url>
<content type="text"><![CDATA[1. 倒计时器CountDownLatch在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join方法,让主线程等待被join的线程执行完之后,主线程才能继续往下执行。 当然,使用线程间消息通信机制也可以完成。其实,java并发工具类中为我们提供了类似“倒计时”这样的工具类,可以十分方便的完成所说的这种业务场景。 为了能够理解CountDownLatch,举一个很通俗的例子,运动员进行跑步比赛时,假设有6个运动员参与比赛,裁判员在终点会为这6个运动员分别计时,可以想象没当一个运动员到达终点的时候,对于裁判员来说就少了一个计时任务。直到所有运动员都到达终点了,裁判员的任务也才完成。这6个运动员可以类比成6个线程,当线程调用CountDownLatch.countDown方法时就会对计数器的值减一,直到计数器的值为0的时候,裁判员(调用await方法的线程)才能继续往下执行。 从CountDownLatch的构造方法看起: 1public CountDownLatch(int count) 构造方法会传入一个整型数N,之后调用CountDownLatch的countDown方法会对N减一,知道N减到0的时候,当前调用await方法的线程继续执行。 CountDownLatch的方法: await() throws InterruptedException:调用该方法的线程等到构造方法传入的N减到0的时候,才能继续往下执行; await(long timeout, TimeUnit unit):与上面的await方法功能一致,只不过这里有了时间限制,调用该方法的线程等到指定的timeout时间后,不管N是否减至为0,都会继续往下执行; countDown():使CountDownLatch初始值N减1; long getCount():获取当前CountDownLatch维护的值; 下面用一个具体的例子来说明CountDownLatch的具体用法: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public class CountDownLatchDemo {private static CountDownLatch startSignal = new CountDownLatch(1);//用来表示裁判员需要维护的是6个运动员private static CountDownLatch endSignal = new CountDownLatch(6);public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(6); for (int i = 0; i < 6; i++) { executorService.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 运动员等待裁判员响哨!!!"); startSignal.await(); System.out.println(Thread.currentThread().getName() + "正在全力冲刺"); endSignal.countDown(); System.out.println(Thread.currentThread().getName() + " 到达终点"); } catch (InterruptedException e) { e.printStackTrace(); } }); } System.out.println("裁判员发号施令啦!!!"); startSignal.countDown(); endSignal.await(); System.out.println("所有运动员到达终点,比赛结束!"); executorService.shutdown(); }}输出结果:pool-1-thread-2 运动员等待裁判员响哨!!!pool-1-thread-3 运动员等待裁判员响哨!!!pool-1-thread-1 运动员等待裁判员响哨!!!pool-1-thread-4 运动员等待裁判员响哨!!!pool-1-thread-5 运动员等待裁判员响哨!!!pool-1-thread-6 运动员等待裁判员响哨!!!裁判员发号施令啦!!!pool-1-thread-2正在全力冲刺pool-1-thread-2 到达终点pool-1-thread-3正在全力冲刺pool-1-thread-3 到达终点pool-1-thread-1正在全力冲刺pool-1-thread-1 到达终点pool-1-thread-4正在全力冲刺pool-1-thread-4 到达终点pool-1-thread-5正在全力冲刺pool-1-thread-5 到达终点pool-1-thread-6正在全力冲刺pool-1-thread-6 到达终点所有运动员到达终点,比赛结束! 该示例代码中设置了两个CountDownLatch,第一个endSignal用于控制让main线程(裁判员)必须等到其他线程(运动员)让CountDownLatch维护的数值N减到0为止。另一个startSignal用于让main线程对其他线程进行“发号施令”,startSignal引用的CountDownLatch初始值为1,而其他线程执行的run方法中都会先通过 startSignal.await()让这些线程都被阻塞,直到main线程通过调用startSignal.countDown();,将值N减1,CountDownLatch维护的数值N为0后,其他线程才能往下执行,并且,每个线程执行的run方法中都会通过endSignal.countDown();对endSignal维护的数值进行减一,由于往线程池提交了6个任务,会被减6次,所以endSignal维护的值最终会变为0,因此main线程在latch.await();阻塞结束,才能继续往下执行。 另外,需要注意的是,当调用CountDownLatch的countDown方法时,当前线程是不会被阻塞,会继续往下执行,比如在该例中会继续输出pool-1-thread-4 到达终点。 2. 循环栅栏:CyclicBarrierCyclicBarrier也是一种多线程并发控制的实用工具,和CountDownLatch一样具有等待计数的功能,但是相比于CountDownLatch功能更加强大。 为了理解CyclicBarrier,这里举一个通俗的例子。开运动会时,会有跑步这一项运动,我们来模拟下运动员入场时的情况,假设有6条跑道,在比赛开始时,就需要6个运动员在比赛开始的时候都站在起点了,裁判员吹哨后才能开始跑步。跑道起点就相当于“barrier”,是临界点,而这6个运动员就类比成线程的话,就是这6个线程都必须到达指定点了,意味着凑齐了一波,然后才能继续执行,否则每个线程都得阻塞等待,直至凑齐一波即可。 CyclicBarrier当多个线程凑齐了一波之后,仍然有效,可以继续凑齐下一波。CyclicBarrier的执行示意图如下: 当多个线程都达到了指定点后,才能继续往下继续执行。这就有点像报数的感觉,假设6个线程就相当于6个运动员,到赛道起点时会报数进行统计,如果刚好是6的话,这一波就凑齐了,才能往下执行。 CyclicBarrier在使用一次后,下面依然有效,可以继续当做计数器使用,这是与CountDownLatch的区别之一。这里的6个线程,也就是计数器的初始值6,是通过CyclicBarrier的构造方法传入的。 下面来看下CyclicBarrier的主要方法: 123456789101112131415//等到所有的线程都到达指定的临界点await() throws InterruptedException, BrokenBarrierException //与上面的await方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException //获取当前有多少个线程阻塞等待在临界点上int getNumberWaiting()//用于查询阻塞等待的线程是否被中断boolean isBroken()//将屏障重置为初始状态。如果当前有线程正在临界点等待的话,将抛出BrokenBarrierException。void reset() 另外需要注意的是,CyclicBarrier提供了这样的构造方法: 1public CyclicBarrier(int parties, Runnable barrierAction) 可以用来,当指定的线程都到达了指定的临界点的时,接下来执行的操作可以由barrierAction传入即可。 一个例子 用一个简单的例子,来看下CyclicBarrier的用法,我们来模拟下上面的运动员的例子。 12345678910111213141516171819202122232425262728293031323334353637383940public class CyclicBarrierDemo { //指定必须有6个运动员到达才行 private static CyclicBarrier barrier = new CyclicBarrier(6, () -> { System.out.println("所有运动员入场,裁判员一声令下!!!!!"); }); public static void main(String[] args) { System.out.println("运动员准备进场,全场欢呼............"); ExecutorService service = Executors.newFixedThreadPool(6); for (int i = 0; i < 6; i++) { service.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 运动员,进场"); barrier.await(); System.out.println(Thread.currentThread().getName() + " 运动员出发"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }); } }}输出结果:运动员准备进场,全场欢呼............pool-1-thread-2 运动员,进场pool-1-thread-1 运动员,进场pool-1-thread-3 运动员,进场pool-1-thread-4 运动员,进场pool-1-thread-5 运动员,进场pool-1-thread-6 运动员,进场所有运动员入场,裁判员一声令下!!!!!pool-1-thread-6 运动员出发pool-1-thread-1 运动员出发pool-1-thread-5 运动员出发pool-1-thread-4 运动员出发pool-1-thread-3 运动员出发pool-1-thread-2 运动员出发 从输出结果可以看出,当6个运动员(线程)都到达了指定的临界点(barrier)时候,才能继续往下执行,否则,则会阻塞等待在调用await()处 3. CountDownLatch与CyclicBarrier的比较CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的: CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行; CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能; CountDownLatch是不能复用的,而CyclicLatch是可以复用的。 4. 控制资源并发访问–SemaphoreSemaphore可以理解为信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待。当执行完业务功能后,需要通过release()方法将许可证归还,以便其他线程能够获得许可证继续执行。 Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有多个线程读取数据后,需要将数据保存在数据库中,而可用的最大数据库连接只有10个,这时候就需要使用Semaphore来控制能够并发访问到数据库连接资源的线程个数最多只有10个。在限制资源使用的应用场景下,Semaphore是特别合适的。 下面来看下Semaphore的主要方法: 1234567891011121314151617181920212223242526272829303132333435//获取许可,如果无法获取到,则阻塞等待直至能够获取为止void acquire() throws InterruptedException //同acquire方法功能基本一样,只不过该方法可以一次获取多个许可void acquire(int permits) throws InterruptedException//释放许可void release()//释放指定个数的许可void release(int permits)//尝试获取许可,如果能够获取成功则立即返回true,否则,则返回falseboolean tryAcquire()//与tryAcquire方法一致,只不过这里可以指定获取多个许可boolean tryAcquire(int permits)//尝试获取许可,如果能够立即获取到或者在指定时间内能够获取到,则返回true,否则返回falseboolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException//与上一个方法一致,只不过这里能够获取多个许可boolean tryAcquire(int permits, long timeout, TimeUnit unit)//返回当前可用的许可证个数int availablePermits()//返回正在等待获取许可证的线程数int getQueueLength()//是否有线程正在等待获取许可证boolean hasQueuedThreads()//获取所有正在等待许可的线程集合Collection<Thread> getQueuedThreads() 另外,在Semaphore的构造方法中还支持指定是够具有公平性,默认的是非公平性,这样也是为了保证吞吐量。 一个例子 下面用一个简单的例子来说明Semaphore的具体使用。我们来模拟这样一样场景。有一天,班主任需要班上10个同学到讲台上来填写一个表格,但是老师只准备了5支笔,因此,只能保证同时只有5个同学能够拿到笔并填写表格,没有获取到笔的同学只能够等前面的同学用完之后,才能拿到笔去填写表格。该示例代码如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374public class SemaphoreDemo { //表示老师只有10支笔 private static Semaphore semaphore = new Semaphore(5); public static void main(String[] args) { //表示10个学生 ExecutorService service = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { service.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 同学准备获取笔......"); semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 同学获取到笔"); System.out.println(Thread.currentThread().getName() + " 填写表格ing....."); TimeUnit.SECONDS.sleep(3); semaphore.release(); System.out.println(Thread.currentThread().getName() + " 填写完表格,归还了笔!!!!!!"); } catch (InterruptedException e) { e.printStackTrace(); } }); } service.shutdown(); }}输出结果:pool-1-thread-1 同学准备获取笔......pool-1-thread-1 同学获取到笔pool-1-thread-1 填写表格ing.....pool-1-thread-2 同学准备获取笔......pool-1-thread-2 同学获取到笔pool-1-thread-2 填写表格ing.....pool-1-thread-3 同学准备获取笔......pool-1-thread-4 同学准备获取笔......pool-1-thread-3 同学获取到笔pool-1-thread-4 同学获取到笔pool-1-thread-4 填写表格ing.....pool-1-thread-3 填写表格ing.....pool-1-thread-5 同学准备获取笔......pool-1-thread-5 同学获取到笔pool-1-thread-5 填写表格ing.....pool-1-thread-6 同学准备获取笔......pool-1-thread-7 同学准备获取笔......pool-1-thread-8 同学准备获取笔......pool-1-thread-9 同学准备获取笔......pool-1-thread-10 同学准备获取笔......pool-1-thread-4 填写完表格,归还了笔!!!!!!pool-1-thread-9 同学获取到笔pool-1-thread-9 填写表格ing.....pool-1-thread-5 填写完表格,归还了笔!!!!!!pool-1-thread-7 同学获取到笔pool-1-thread-7 填写表格ing.....pool-1-thread-8 同学获取到笔pool-1-thread-8 填写表格ing.....pool-1-thread-1 填写完表格,归还了笔!!!!!!pool-1-thread-6 同学获取到笔pool-1-thread-6 填写表格ing.....pool-1-thread-3 填写完表格,归还了笔!!!!!!pool-1-thread-2 填写完表格,归还了笔!!!!!!pool-1-thread-10 同学获取到笔pool-1-thread-10 填写表格ing.....pool-1-thread-7 填写完表格,归还了笔!!!!!!pool-1-thread-9 填写完表格,归还了笔!!!!!!pool-1-thread-8 填写完表格,归还了笔!!!!!!pool-1-thread-6 填写完表格,归还了笔!!!!!!pool-1-thread-10 填写完表格,归还了笔!!!!!! 根据输出结果进行分析,Semaphore允许的最大许可数为5,也就是允许的最大并发执行的线程个数为5,可以看出,前5个线程(前5个学生)先获取到笔,然后填写表格,而6-10这5个线程,由于获取不到许可,只能阻塞等待。当线程pool-1-thread-4释放了许可之后,pool-1-thread-9就可以获取到许可,继续往下执行。对其他线程的执行过程,也是同样的道理。从这个例子就可以看出,Semaphore用来做特殊资源的并发访问控制是相当合适的,如果有业务场景需要进行流量控制,可以优先考虑Semaphore。 5.线程间交换数据的工具–ExchangerExchanger是一个用于线程间协作的工具类,用于两个线程间能够交换。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。 具体交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。 Exchanger除了一个无参的构造方法外,主要方法也很简单: 1234567//当一个线程执行该方法的时候,会等待另一个线程也执行该方法,因此两个线程就都达到了同步点//将数据交换给另一个线程,同时返回获取的数据V exchange(V x) throws InterruptedException//同上一个方法功能基本一样,只不过这个方法同步等待的时候,增加了超时时间V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException 一个例子 Exchanger理解起来很容易,这里用一个简单的例子来看下它的具体使用。我们来模拟这样一个情景,在青春洋溢的中学时代,下课期间,男生经常会给走廊里为自己喜欢的女孩子送情书,男孩会先到女孩教室门口,然后等女孩出来,教室那里就是一个同步点,然后彼此交换信物,也就是彼此交换了数据。现在,就来模拟这个情景。 12345678910111213141516171819202122232425262728293031323334353637public class ExchangerDemo { private static Exchanger<String> exchanger = new Exchanger(); public static void main(String[] args) { //代表男生和女生 ExecutorService service = Executors.newFixedThreadPool(2); service.execute(() -> { try { //男生对女生说的话 String girl = exchanger.exchange("我其实暗恋你很久了......"); System.out.println("女孩儿说:" + girl); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(() -> { try { System.out.println("女生慢慢的从教室你走出来......"); TimeUnit.SECONDS.sleep(3); //男生对女生说的话 String boy = exchanger.exchange("我也很喜欢你......"); System.out.println("男孩儿说:" + boy); } catch (InterruptedException e) { e.printStackTrace(); } }); }}输出结果:女生慢慢的从教室你走出来......男孩儿说:我其实暗恋你很久了......女孩儿说:我也很喜欢你...... 这个例子很简单,也很能说明Exchanger的基本使用。当两个线程都到达调用exchange方法的同步点的时候,两个线程就能交换彼此的数据。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[生产者消费者问题]]></title>
<url>%2F2019%2F10%2F29%2F%E5%B9%B6%E5%8F%91%2F%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%2F</url>
<content type="text"><![CDATA[生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。 所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能: 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内; 如果共享数据区为空的话,阻塞消费者继续消费数据; 在实现生产者消费者问题时,可以采用三种方式: 1.使用Object的wait/notify的消息通知机制; 2.使用Lock的Condition的await/signal的消息通知机制; 3.使用BlockingQueue实现。 1. wait/notify的消息通知机制1.1 预备知识Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从wait()方法出返回,继续执行下面的操作。 wait 该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用wait()方法之后,当前线程会释放锁。如果调用wait()方法时,线程并未获取到锁的话,则会抛出IllegalMonitorStateException异常,这是以个RuntimeException。如果再次获取到锁的话,当前线程才能从wait()方法处成功返回。 notify 该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。 该方法任意从WAITTING状态的线程中挑选一个进行通知,使得调用wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用wait()方法的线程能够从wait()方法处退出。调用notify后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。 notifyAll 该方法与 notify ()方法的工作方式相同,重要的一点差异是: notifyAll 使所有原来在该对象上 wait 的线程统统退出WAITTING状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。 1.2 wait/notify消息通知潜在的一些问题1.notify早期通知 notify 通知的遗漏很容易理解,即 threadA 还没开始 wait 的时候,threadB 已经 notify 了,threadB 通知是没有任何响应的,当 threadB 退出 synchronized 代码块后,threadA 再开始 wait,便会一直阻塞等待,直到被别的线程打断。比如在下面的示例代码中,就模拟出notify早期通知带来的问题: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556public class EarlyNotify { private static String lockObject = ""; public static void main(String[] args) { WaitThread waitThread = new WaitThread(lockObject); NotifyThread notifyThread = new NotifyThread(lockObject); notifyThread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } waitThread.start(); } static class WaitThread extends Thread { private String lock; public WaitThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始wait"); lock.wait(); System.out.println(Thread.currentThread().getName() + " 结束wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class NotifyThread extends Thread { private String lock; public NotifyThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始notify"); lock.notify(); System.out.println(Thread.currentThread().getName() + " 结束开始notify"); } } }} 示例中开启了两个线程,一个是WaitThread,另一个是NotifyThread。NotifyThread会先启动,先调用notify方法。然后WaitThread线程才启动,调用wait方法,但是由于通知过了,wait方法就无法再获取到相应的通知,因此WaitThread会一直在wait方法出阻塞,这种现象就是通知过早的现象。针对这种现象,解决方法,一般是添加一个状态标志,让waitThread调用wait方法前先判断状态是否已经改变了没,如果通知早已发出的话,WaitThread就不再去wait。对上面的代码进行更正: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960public class EarlyNotify { private static String lockObject = ""; private static boolean isWait = true; public static void main(String[] args) { WaitThread waitThread = new WaitThread(lockObject); NotifyThread notifyThread = new NotifyThread(lockObject); notifyThread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } waitThread.start(); } static class WaitThread extends Thread { private String lock; public WaitThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { while (isWait) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始wait"); lock.wait(); System.out.println(Thread.currentThread().getName() + " 结束wait"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } static class NotifyThread extends Thread { private String lock; public NotifyThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始notify"); lock.notifyAll(); isWait = false; System.out.println(Thread.currentThread().getName() + " 结束开始notify"); } } }} 这段代码只是增加了一个isWait状态变量,NotifyThread调用notify方法后会对状态变量进行更新,在WaitThread中调用wait方法之前会先对状态变量进行判断,在该示例中,调用notify后将状态变量isWait改变为false,因此,在WaitThread中while对isWait判断后就不会执行wait方法,从而避免了Notify过早通知造成遗漏的情况。 总结:在使用线程的等待/通知机制时,一般都要配合一个 boolean 变量值(或者其他能够判断真假的条件),在 notify 之前改变该 boolean 变量的值,让 wait 返回后能够退出 while 循环(一般都要在 wait 方法外围加一层 while 循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在 wait 方法处。这样便保证了程序的正确性。 2.等待wait的条件发生变化 如果线程在等待时接受到了通知,但是之后等待的条件发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。 下面用一个例子来说明这种情况 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071public class ConditionChange {private static List<String> lockObject = new ArrayList();public static void main(String[] args) { Consumer consumer1 = new Consumer(lockObject); Consumer consumer2 = new Consumer(lockObject); Productor productor = new Productor(lockObject); consumer1.start(); consumer2.start(); productor.start();}static class Consumer extends Thread { private List<String> lock; public Consumer(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { //这里使用if的话,就会存在wait条件变化造成程序错误的问题 if (lock.isEmpty()) { System.out.println(Thread.currentThread().getName() + " list为空"); System.out.println(Thread.currentThread().getName() + " 调用wait方法"); lock.wait(); System.out.println(Thread.currentThread().getName() + " wait方法结束"); } String element = lock.remove(0); System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); } catch (InterruptedException e) { e.printStackTrace(); } } }}static class Productor extends Thread { private List<String> lock; public Productor(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 开始添加元素"); lock.add(Thread.currentThread().getName()); lock.notifyAll(); } }}}会报异常:Exception in thread "Thread-1" Thread-0 list为空Thread-0 调用wait方法Thread-1 list为空Thread-1 调用wait方法Thread-2 开始添加元素Thread-1 wait方法结束java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 异常原因分析:在这个例子中一共开启了3个线程,Consumer1,Consumer2以及Productor。首先Consumer1调用了wait方法后,线程处于了WAITTING状态,并且将对象锁释放出来。因此,Consumer2能够获取对象锁,从而进入到同步代块中,当执行到wait方法时,同样的也会释放对象锁。因此,productor能够获取到对象锁,进入到同步代码块中,向list中插入数据后,通过notifyAll方法通知处于WAITING状态的Consumer1和Consumer2线程。consumer1得到对象锁后,从wait方法出退出,删除了一个元素让List为空,方法执行结束,退出同步块,释放掉对象锁。这个时候Consumer2获取到对象锁后,从wait方法退出,继续往下执行,这个时候Consumer2再执行lock.remove(0);就会出错,因为List由于Consumer1删除一个元素之后已经为空了。 解决方案:通过上面的分析,可以看出Consumer2报异常是因为线程从wait方法退出之后没有再次对wait条件进行判断,因此,此时的wait条件已经发生了变化。解决办法就是,在wait退出之后再对条件进行判断即可。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061public class ConditionChange {private static List<String> lockObject = new ArrayList();public static void main(String[] args) { Consumer consumer1 = new Consumer(lockObject); Consumer consumer2 = new Consumer(lockObject); Productor productor = new Productor(lockObject); consumer1.start(); consumer2.start(); productor.start();}static class Consumer extends Thread { private List<String> lock; public Consumer(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { //这里使用if的话,就会存在wait条件变化造成程序错误的问题 while (lock.isEmpty()) { System.out.println(Thread.currentThread().getName() + " list为空"); System.out.println(Thread.currentThread().getName() + " 调用wait方法"); lock.wait(); System.out.println(Thread.currentThread().getName() + " wait方法结束"); } String element = lock.remove(0); System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); } catch (InterruptedException e) { e.printStackTrace(); } } }}static class Productor extends Thread { private List<String> lock; public Productor(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 开始添加元素"); lock.add(Thread.currentThread().getName()); lock.notifyAll(); } }}} 上面的代码与之前的代码仅仅只是将 wait 外围的 if 语句改为 while 循环即可,这样当 list 为空时,线程便会继续等待,而不会继续去执行删除 list 中元素的代码。 总结:在使用线程的等待/通知机制时,一般都要在 while 循环中调用 wait()方法,因此xuy配合使用一个 boolean 变量(或其他能判断真假的条件,如本文中的 list.isEmpty()),满足 while 循环的条件时,进入 while 循环,执行 wait()方法,不满足 while 循环的条件时,跳出循环,执行后面的代码。 3. “假死”状态 现象:如果是多消费者和多生产者情况,如果使用notify方法可能会出现“假死”的情况,即唤醒的是同类线程。 原因分析:假设当前多个生产者线程会调用wait方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify通知处于WAITTING状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。 解决办法:将notify方法替换成notifyAll方法,如果使用的是lock的话,就将signal方法替换成signalAll方法。 总结 在Object提供的消息通知机制应该遵循如下这些条件: 永远在while循环中对条件进行判断而不是if语句中进行wait条件的判断; 使用NotifyAll而不是使用notify。 基本的使用范式如下: 12345678// The standard idiom for calling the wait method in Java synchronized (sharedObject) { while (condition) { sharedObject.wait(); // (Releases lock, and reacquires on wakeup) } // do action based upon condition e.g. take or put into queue } 1.3 wait/notifyAll实现生产者-消费者利用wait/notifyAll实现生产者和消费者代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112public class ProductorConsumer {public static void main(String[] args) { LinkedList linkedList = new LinkedList(); ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(linkedList, 8)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(linkedList)); }}static class Productor implements Runnable { private List<Integer> list; private int maxLength; public Productor(List list, int maxLength) { this.list = list; this.maxLength = maxLength; } @Override public void run() { while (true) { synchronized (list) { try { while (list.size() == maxLength) { System.out.println("生产者" + Thread.currentThread().getName() + " list以达到最大容量,进行wait"); list.wait(); System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait"); } Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i); list.add(i); list.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } } }}static class Consumer implements Runnable { private List<Integer> list; public Consumer(List list) { this.list = list; } @Override public void run() { while (true) { synchronized (list) { try { while (list.isEmpty()) { System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait"); list.wait(); System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait"); } Integer element = list.remove(0); System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element); list.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } } }}}输出结果:生产者pool-1-thread-1 生产数据-232820990生产者pool-1-thread-1 生产数据1432164130生产者pool-1-thread-1 生产数据1057090222生产者pool-1-thread-1 生产数据1201395916生产者pool-1-thread-1 生产数据482766516生产者pool-1-thread-1 list以达到最大容量,进行wait消费者pool-1-thread-15 退出wait消费者pool-1-thread-15 消费数据:1237535349消费者pool-1-thread-15 消费数据:-1617438932消费者pool-1-thread-15 消费数据:-535396055消费者pool-1-thread-15 消费数据:-232820990消费者pool-1-thread-15 消费数据:1432164130消费者pool-1-thread-15 消费数据:1057090222消费者pool-1-thread-15 消费数据:1201395916消费者pool-1-thread-15 消费数据:482766516消费者pool-1-thread-15 list为空,进行wait生产者pool-1-thread-5 退出wait生产者pool-1-thread-5 生产数据1442969724生产者pool-1-thread-5 生产数据1177554422生产者pool-1-thread-5 生产数据-133137235生产者pool-1-thread-5 生产数据324882560生产者pool-1-thread-5 生产数据2065211573生产者pool-1-thread-5 生产数据253569900生产者pool-1-thread-5 生产数据571277922生产者pool-1-thread-5 生产数据1622323863生产者pool-1-thread-5 list以达到最大容量,进行wait消费者pool-1-thread-10 退出wait 2. 使用Lock中Condition的await/signalAll实现生产者-消费者参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法: 针对wait方法 void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常; long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时; boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位 boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间 针对notify方法 void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。 void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程 如果采用lock中Conditon的消息通知原理来实现生产者-消费者问题,原理同使用wait/notifyAll一样。直接上代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122public class ProductorConsumer {private static ReentrantLock lock = new ReentrantLock();private static Condition full = lock.newCondition();private static Condition empty = lock.newCondition();public static void main(String[] args) { LinkedList linkedList = new LinkedList(); ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(linkedList, 8, lock)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(linkedList, lock)); }}static class Productor implements Runnable { private List<Integer> list; private int maxLength; private Lock lock; public Productor(List list, int maxLength, Lock lock) { this.list = list; this.maxLength = maxLength; this.lock = lock; } @Override public void run() { while (true) { lock.lock(); try { while (list.size() == maxLength) { System.out.println("生产者" + Thread.currentThread().getName() + " list以达到最大容量,进行wait"); full.await(); System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait"); } Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i); list.add(i); empty.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }}static class Consumer implements Runnable { private List<Integer> list; private Lock lock; public Consumer(List list, Lock lock) { this.list = list; this.lock = lock; } @Override public void run() { while (true) { lock.lock(); try { while (list.isEmpty()) { System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait"); empty.await(); System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait"); } Integer element = list.remove(0); System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element); full.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }}}输出结果:消费者pool-1-thread-9 消费数据:1146627506消费者pool-1-thread-9 消费数据:1508001019消费者pool-1-thread-9 消费数据:-600080565消费者pool-1-thread-9 消费数据:-1000305429消费者pool-1-thread-9 消费数据:-1270658620消费者pool-1-thread-9 消费数据:1961046169消费者pool-1-thread-9 消费数据:-307680655消费者pool-1-thread-9 list为空,进行wait消费者pool-1-thread-13 退出wait消费者pool-1-thread-13 list为空,进行wait消费者pool-1-thread-10 退出wait生产者pool-1-thread-5 退出wait生产者pool-1-thread-5 生产数据-892558288生产者pool-1-thread-5 生产数据-1917220008生产者pool-1-thread-5 生产数据2146351766生产者pool-1-thread-5 生产数据452445380生产者pool-1-thread-5 生产数据1695168334生产者pool-1-thread-5 生产数据1979746693生产者pool-1-thread-5 生产数据-1905436249生产者pool-1-thread-5 生产数据-101410137生产者pool-1-thread-5 list以达到最大容量,进行wait生产者pool-1-thread-1 退出wait生产者pool-1-thread-1 list以达到最大容量,进行wait生产者pool-1-thread-4 退出wait生产者pool-1-thread-4 list以达到最大容量,进行wait生产者pool-1-thread-2 退出wait生产者pool-1-thread-2 list以达到最大容量,进行wait生产者pool-1-thread-3 退出wait生产者pool-1-thread-3 list以达到最大容量,进行wait消费者pool-1-thread-9 退出wait消费者pool-1-thread-9 消费数据:-892558288 3. 使用BlockingQueue实现生产者-消费者由于BlockingQueue内部实现就附加了两个阻塞操作。即当队列已满时,阻塞向队列中插入数据的线程,直至队列中未满;当队列为空时,阻塞从队列中获取数据的线程,直至队列非空时为止。可以利用BlockingQueue实现生产者-消费者为题,阻塞队列完全可以充当共享数据区域,就可以很好的完成生产者和消费者线程之间的协作。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899public class ProductorConsumer { private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(queue)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(queue)); } } static class Productor implements Runnable { private BlockingQueue queue; public Productor(BlockingQueue queue) { this.queue = queue; } @Override public void run() { try { while (true) { Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + "生产数据" + i); queue.put(i); } } catch (InterruptedException e) { e.printStackTrace(); } } } static class Consumer implements Runnable { private BlockingQueue queue; public Consumer(BlockingQueue queue) { this.queue = queue; } @Override public void run() { try { while (true) { Integer element = (Integer) queue.take(); System.out.println("消费者" + Thread.currentThread().getName() + "正在消费数据" + element); } } catch (InterruptedException e) { e.printStackTrace(); } } }}输出结果:消费者pool-1-thread-7正在消费数据1520577501生产者pool-1-thread-4生产数据-127809610消费者pool-1-thread-8正在消费数据504316513生产者pool-1-thread-2生产数据1994678907消费者pool-1-thread-11正在消费数据1967302829生产者pool-1-thread-1生产数据369331507消费者pool-1-thread-9正在消费数据1994678907生产者pool-1-thread-2生产数据-919544017消费者pool-1-thread-12正在消费数据-127809610生产者pool-1-thread-4生产数据1475197572消费者pool-1-thread-14正在消费数据-893487914生产者pool-1-thread-3生产数据906921688消费者pool-1-thread-6正在消费数据-1292015016生产者pool-1-thread-5生产数据-652105379生产者pool-1-thread-5生产数据-1622505717生产者pool-1-thread-3生产数据-1350268764消费者pool-1-thread-7正在消费数据906921688生产者pool-1-thread-4生产数据2091628867消费者pool-1-thread-13正在消费数据1475197572消费者pool-1-thread-15正在消费数据-919544017生产者pool-1-thread-2生产数据564860122生产者pool-1-thread-2生产数据822954707消费者pool-1-thread-14正在消费数据564860122消费者pool-1-thread-10正在消费数据369331507生产者pool-1-thread-1生产数据-245820912消费者pool-1-thread-6正在消费数据822954707生产者pool-1-thread-2生产数据1724595968生产者pool-1-thread-2生产数据-1151855115消费者pool-1-thread-12正在消费数据2091628867生产者pool-1-thread-4生产数据-1774364499生产者pool-1-thread-4生产数据2006106757消费者pool-1-thread-14正在消费数据-1774364499生产者pool-1-thread-3生产数据-1070853639消费者pool-1-thread-9正在消费数据-1350268764消费者pool-1-thread-11正在消费数据-1622505717生产者pool-1-thread-5生产数据355412953 可以看出,使用BlockingQueue来实现生产者-消费者很简洁,这正是利用了BlockingQueue插入和获取数据附加阻塞操作的特性。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[原子操作类]]></title>
<url>%2F2019%2F10%2F28%2F%E5%B9%B6%E5%8F%91%2F%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C%E7%B1%BB%2F</url>
<content type="text"><![CDATA[1. 原子操作类介绍在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的,但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。 实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。 2. 预备知识–CAS操作 什么是CAS? 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。 而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。 如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 CAS的操作过程 CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。 当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。 V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。 Synchronized VS CAS 元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。 CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 CAS的问题 ABA问题 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。 解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。 自旋时间过长 使用CAS时非阻塞同步,不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。 3. 原子更新基本类型atomic包提高原子更新基本类型的工具类,主要有这些: AtomicBoolean:以原子更新的方式更新boolean; AtomicInteger:以原子更新的方式更新Integer; AtomicLong:以原子更新的方式更新Long; 这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法 addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果; incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果; getAndSet(int newValue):将实例中的值更新为新值,并返回旧值; getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值; 为了能够弄懂AtomicInteger的实现原理,以getAndIncrement方法为例,来看下源码: 123public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);} 该方法实际上是调用了unsafe实例的getAndAddInt方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe获取: 1private static final Unsafe unsafe = Unsafe.getUnsafe(); Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法来进行实现。 下面用一个简单的例子来说明AtomicInteger的用法: 1234567891011public class AtomicDemo { private static AtomicInteger atomicInteger = new AtomicInteger(1); public static void main(String[] args) { System.out.println(atomicInteger.getAndIncrement()); System.out.println(atomicInteger.get()); }}输出结果:12 新建了一个atomicInteger对象,而atomicInteger的构造方法也就是传入一个基本类型数据即可,对其进行了封装。对基本变量的操作比如自增,自减,相加,更新等操作,atomicInteger也提供了相应的方法进行这些操作。 因为atomicInteger借助了UnSafe提供的CAS操作能够保证数据更新的时候是线程安全的,并且由于CAS是采用乐观锁策略,因此,这种数据更新的方法也具有高效性。 AtomicLong的实现原理和AtomicInteger一致,只不过一个针对的是long变量,一个针对的是int变量。 而boolean变量的更新类AtomicBoolean类的核心方法是compareAndSett方法,其源码如下: 12345public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u);} compareAndSet方法的实际上也是先转换成0,1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt来实现的。 可以看出atomic包中只提供了对boolean,int ,long这三种基本类型的原子更新的方法,参考对boolean更新的方式,原子更新char,doule,float也可以采用类似的思路进行实现。 4. 原子更新数组类型atomic包下提供能原子更新数组中元素的类有: AtomicIntegerArray:原子更新整型数组中的元素; AtomicLongArray:原子更新长整型数组中的元素; AtomicReferenceArray:原子更新引用类型数组中的元素; 这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法: addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加; getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1; compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新; AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在AtomicIntegerArray的方法中会多一个指定数组索引位i。下面举一个简单的例子: 123456789101112131415public class AtomicDemo { //private static AtomicInteger atomicInteger = new AtomicInteger(1); private static int[] value = new int[]{1, 2, 3}; private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value); public static void main(String[] args) { //对数组中索引为1的位置的元素加5 int result = integerArray.getAndAdd(1, 5); System.out.println(integerArray.get(1)); System.out.println(result); }}输出结果:72 通过getAndAdd方法将位置为1的元素加5,从结果可以看出索引为1的元素变成了7,该方法返回的也是相加之前的数为2。 5. 原子更新引用类型如果需要原子更新引用类型变量的话,为了保证线程安全,atomic也提供了相关的类: AtomicReference:原子更新引用类型; AtomicReferenceFieldUpdater:原子更新引用类型里的字段; AtomicMarkableReference:原子更新带有标记位的引用类型; 这几个类的使用方法也是基本一样的,以AtomicReference为例,来说明这些类的基本用法。下面是一个demo 1234567891011121314151617181920212223242526272829303132333435public class AtomicDemo { private static AtomicReference<User> reference = new AtomicReference<>(); public static void main(String[] args) { User user1 = new User("a", 1); reference.set(user1); User user2 = new User("b",2); User user = reference.getAndSet(user2); System.out.println(user); System.out.println(reference.get()); } static class User { private String userName; private int age; public User(String userName, int age) { this.userName = userName; this.age = age; } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}'; } }}输出结果:User{userName='a', age=1}User{userName='b', age=2} 首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为User{userName='b', age=2},返回的是原来的user对象User{userName='a', age=1}。 6. 原子更新字段类型如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类: AtomicIntegeFieldUpdater:原子更新整型字段类; AtomicLongFieldUpdater:原子更新长整型字段类; AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。而为什么在更新的时候会带有版本号,是为了解决CAS的ABA问题; 要想使用原子更新字段需要两步操作: 原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性; 更新类的属性必须使用public volatile进行修饰; 这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例来看看具体的使用: 1234567891011121314151617181920212223242526272829303132public class AtomicDemo { private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); public static void main(String[] args) { User user = new User("a", 1); int oldValue = updater.getAndAdd(user, 5); System.out.println(oldValue); System.out.println(updater.get(user)); } static class User { private String userName; public volatile int age; public User(String userName, int age) { this.userName = userName; this.age = age; } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}'; } }} 输出结果:16 创建AtomicIntegerFieldUpdater是通过它提供的静态方法进行创建,getAndAdd方法会将指定的字段加上输入的值,并且返回相加之前的值。user对象中age字段原值为1,加5之后,可以看出user对象中的age字段的值已经变成了6。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[线程池]]></title>
<url>%2F2019%2F10%2F27%2F%E5%B9%B6%E5%8F%91%2F%E7%BA%BF%E7%A8%8B%E6%B1%A0%2F</url>
<content type="text"><![CDATA[线程池ThreadPoolExecutor实现原理1. 为什么要使用线程池在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处: 降低资源消耗。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗; 提升系统响应速度。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度; 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。 2. 线程池的工作原理当一个并发任务提交给线程池,线程池分配线程去执行任务的过程如下图所示: 线程池执行所提交的任务过程主要有这样几个阶段: 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步; 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步; 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理 3. 线程池的创建创建线程池主要是ThreadPoolExecutor类来完成,ThreadPoolExecutor的有许多重载的构造方法,通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。ThreadPoolExecutor的构造方法为: 1234567ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 下面对参数进行说明: corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动。 maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。 keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。 unit:时间单位。为keepAliveTime指定时间单位。 workQueue:阻塞队列。用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。 threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。 handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种: AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常; CallerRunsPolicy:只用调用者所在的线程来执行任务; DiscardPolicy:不处理直接丢弃掉任务; DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务 线程池执行逻辑 通过ThreadPoolExecutor创建线程池后,提交任务后执行过程是怎样的。execute方法源码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142public 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(); //如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } //如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务 else if (!addWorker(command, false)) reject(command);} 下图为ThreadPoolExecutor的execute方法的执行示意图: execute方法执行逻辑有这样几种情况: 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务; 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中; 如果当前workQueue队列已满的话,则会创建新的线程来执行任务; 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理。 需要注意的是,线程池的设计思想就是使用了核心线程池corePoolSize,阻塞队列workQueue和线程池maximumPoolSize,这样的缓存策略来处理任务,实际上这样的设计思想在实际框架中都会使用。 4. 线程池的关闭关闭线程池,可以通过shutdown和shutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown和shutdownNow还是有不一样的地方: shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表; shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程 可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回true。 5. 如何合理配置线程池参数?要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析: 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。 任务的优先级:高,中和低。 任务的执行时间:长,中和短。 任务的依赖性:是否依赖其他系统资源,如数据库连接。 任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。 并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。 ScheduledThreadPoolExecutor1. ScheduledThreadPoolExecutor简介ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务 而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。ScheduledThreadPoolExecutor类的UML图如下: 从UML图可以看出,ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,也就是说ScheduledThreadPoolExecutor拥有execute()和submit()提交异步任务的基础功能,但是,ScheduledThreadPoolExecutor类实现了ScheduledExecutorService,该接口定义了ScheduledThreadPoolExecutor能够延时执行任务和周期执行任务的功能; ScheduledThreadPoolExecutor两个重要的内部类:DelayedWorkQueue和ScheduledFutureTask。可以看出DelayedWorkQueue实现了BlockingQueue接口,也就是一个阻塞队列,ScheduledFutureTask则是继承了FutureTask类,也表示该类用于返回异步任务的结果。 1.1 构造方法ScheduledThreadPoolExecutor有如下几个构造方法: 1234567891011121314151617181920212223public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());};public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);};public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler);};public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler);} ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor。 可以看出,ScheduledThreadPoolExecutor的核心线程池的线程个数为指定的corePoolSize,当核心线程池的线程个数达到corePoolSize后,就会将任务提交给有界阻塞队列DelayedWorkQueue,线程池允许最大的线程个数为Integer.MAX_VALUE,也就是说理论上这是一个大小无界的线程池。 1.2 特有方法ScheduledThreadPoolExecutor实现了ScheduledExecutorService接口,该接口定义了可延时执行异步任务和可周期执行异步任务的特有功能,相应的方法分别为: 123456789101112131415161718192021222324//达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务,//因此通过ScheduledFuture.get()获取结果为nullpublic ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);//达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务,//因此,返回的是任务的最终计算结果public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);//是以上一个任务开始的时间计时,period时间过去后,//检测上一个任务是否执行完毕,如果上一个任务执行完毕,//则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);//当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次//任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); 2. 可周期性执行的任务—ScheduledFutureTaskScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务,当调用schedule,scheduleAtFixedRate和scheduleWithFixedDelay方法时,实际上是将提交的任务转换成的ScheduledFutureTask类。以schedule方法为例: 123456789public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit))); delayedExecute(t); return t;} 通过decorateTask会将传入的Runnable转换成ScheduledFutureTask类。线程池最大作用是将任务和线程进行解耦,线程主要是任务的执行者,而任务也就是现在所说的ScheduledFutureTask。紧接着,会想到任何线程执行任务,总会调用run()方法。 为了保证ScheduledThreadPoolExecutor能够延时执行任务以及能够周期性执行任务,ScheduledFutureTask重写了run方法: 12345678910111213public void run() { boolean periodic = isPeriodic(); if (!canRunInCurrentRunState(periodic)) cancel(false); else if (!periodic) //如果不是周期性执行任务,则直接调用run方法 ScheduledFutureTask.super.run(); //如果是周期性执行任务的话,需要重设下一次执行任务的时间 else if (ScheduledFutureTask.super.runAndReset()) { setNextRunTime(); reExecutePeriodic(outerTask); }} 在重写的run方法中会先if (!periodic)判断当前任务是否是周期性任务,如果不是的话就直接调用run()方法;否则的话执行setNextRunTime()方法重设下一次任务执行的时间,并通过reExecutePeriodic(outerTask)方法将下一次待执行的任务放置到DelayedWorkQueue中。 可以得出结论:ScheduledFutureTask最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。 如果不是周期性任务(调用schedule方法)则直接通过run()执行,若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到阻塞队列中。 3. DelayedWorkQueue在ScheduledThreadPoolExecutor中还有另外的一个重要的类就是DelayedWorkQueue。为了实现其ScheduledThreadPoolExecutor能够延时执行异步任务以及能够周期执行任务,DelayedWorkQueue进行相应的封装。 DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。 为什么要使用DelayedWorkQueue呢? 定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。 DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。 DelayedWorkQueue的数据结构 12345678//初始大小private static final int INITIAL_CAPACITY = 16;//DelayedWorkQueue是由一个大小为16的数组组成,数组元素为实现RunnableScheduleFuture接口的类//实际上为ScheduledFutureTaskprivate RunnableScheduledFuture<?>[] queue = new RunnableScheduledFuture<?>[INITIAL_CAPACITY];private final ReentrantLock lock = new ReentrantLock();private int size = 0; 可以看出DelayedWorkQueue底层是采用数组构成的 关于DelayedWorkQueue我们可以得出这样的结论:DelayedWorkQueue是基于堆的数据结构,按照时间顺序将每个任务进行排序,将待执行时间越近的任务放在在队列的队头位置,以便于最先进行执行。 4.ScheduledThreadPoolExecutor执行过程ScheduledThreadPoolExecutor的两个内部类ScheduledFutueTask和DelayedWorkQueue实际上也是线程池工作流程中最重要的两个关键因素:任务以及阻塞队列。现在我们来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程。以ScheduledThreadPoolExecutor的schedule方法为例,具体源码为: 12345678910111213public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); //将提交的任务转换成ScheduledFutureTask RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit))); //延时执行任务ScheduledFutureTask delayedExecute(t); return t;} 为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。然后会调用delayedExecute方法进行执行任务,这个方法也是关键方法,来看下源码: 12345678910111213141516private void delayedExecute(RunnableScheduledFuture<?> task) { if (isShutdown()) //如果当前线程池已经关闭,则拒绝任务 reject(task); else { //将任务放入阻塞队列中 super.getQueue().add(task); if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else //保证至少有一个线程启动,即使corePoolSize=0 ensurePrestart(); }} delayedExecute方法可以看出该方法的重要逻辑会是在ensurePrestart()方法中,它的源码为: 1234567void ensurePrestart() { int wc = workerCountOf(ctl.get()); if (wc < corePoolSize) addWorker(null, true); else if (wc == 0) addWorker(null, false);} 可以看出该方法逻辑很简单,关键在于它所调用的addWorker方法,该方法主要功能:新建Worker类,当执行任务时,就会调用被Worker所重写的run方法,进而会继续执行runWorker方法。在runWorker方法中会调用getTask方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止。addWorker方法是ThreadPoolExecutor类中的方法 5.总结 ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此,整体上功能一致,线程池主要负责创建线程(Worker类),线程从阻塞队列中不断获取新的异步任务,直到阻塞队列中已经没有了异步任务为止。但是相较于ThreadPoolExecutor来说,ScheduledThreadPoolExecutor具有延时执行任务和可周期性执行任务的特性,ScheduledThreadPoolExecutor重新设计了任务类 ScheduleFutureTask:ScheduleFutureTask重写了run方法使其具有可延时执行和可周期性执行任务的特性。另外,阻塞队列DelayedWorkQueue是可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越靠近的任务放置队头,以便线程能够获取到任务进行执行; 线程池无论是ThreadPoolExecutor还是ScheduledThreadPoolExecutor,在设计时的三个关键要素是:任务,执行者以及任务结果。它们的设计思想也是完全将这三个关键要素进行了解耦。 执行者 任务的执行机制,完全交由Worker类,也就是进一步了封装了Thread。向线程池提交任务,无论为ThreadPoolExecutor的execute方法和submit方法,还是ScheduledThreadPoolExecutor的schedule方法,都是先将任务移入到阻塞队列中,然后通过addWork方法新建了Work类,并通过runWorker方法启动线程,并不断的从阻塞对列中获取异步任务执行交给Worker执行,直至阻塞队列中无法取到任务为止。 任务 在ThreadPoolExecutor和ScheduledThreadPoolExecutor中任务是指实现了Runnable接口和Callable接口的实现类。ThreadPoolExecutor中会将任务转换成FutureTask类,而在ScheduledThreadPoolExecutor中为了实现可延时执行任务和周期性执行任务的特性,任务会被转换成ScheduledFutureTask类,该类继承了FutureTask,并重写了run方法。 任务结果 在ThreadPoolExecutor中提交任务后,获取任务结果可以通过Future接口的类,在ThreadPoolExecutor中实际上为FutureTask类,而在ScheduledThreadPoolExecutor中则是ScheduledFutureTask类 FutureTask基本操作总结1.FutureTask简介在Executors框架体系中,FutureTask用来表示可获取结果的异步任务。FutureTask实现了Future接口,FutureTask提供了启动和取消异步任务,查询异步任务是否计算结束以及获取最终的异步任务的结果的一些常用的方法。 通过get()方法来获取异步任务的结果,但是会阻塞当前线程直至异步任务执行结束。一旦任务执行结束,任务不能重新启动或取消,除非调用runAndReset()方法。在FutureTask的源码中为其定义了这些状态: 1234567private static final int NEW = 0;private static final int COMPLETING = 1;private static final int NORMAL = 2;private static final int EXCEPTIONAL = 3;private static final int CANCELLED = 4;private static final int INTERRUPTING = 5;private static final int INTERRUPTED = 6; FutureTask分为了3种状态: 未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,还没有执行FutureTask.run()方法之前,FutureTask处于未启动状态。 已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。 已完成。FutureTask.run()方法执行结束,或者调用FutureTask.cancel(…)方法取消任务,或者在执行任务期间抛出异常,这些情况都称之为FutureTask的已完成状态。 下图总结了FutureTask的状态变化的过程: 由于FutureTask具有这三种状态,因此执行FutureTask的get方法和cancel方法,当前处于不同的状态对应的结果也是大不相同。这里对get方法和cancel方法做个总结: get方法 当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞。 如果FutureTask处于已完成状态,调用FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常 cancel方法 当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将此任务永远不会执行; 当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断线程的方式来阻止任务继续进行,如果执行FutureTask.cancel(false)将不会对正在执行任务的线程有任何影响; 当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。 对Future的get()方法和cancel()方法用下图进行总结: 2. FutureTask的基本使用FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用的线程直接执行(FutureTask.run())。 另外,FutureTask的获取也可以通过ExecutorService.submit()方法返回一个FutureTask对象,然后在通过FutureTask.get()或者FutureTask.cancel方法。 应用场景:当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[bean的作用域]]></title>
<url>%2F2019%2F10%2F26%2F%E6%A1%86%E6%9E%B6%2Fbean%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%2F</url>
<content type="text"><![CDATA[作用域scope配置项作用域限定了Spring Bean的作用范围,在Spring配置文件定义Bean时,通过声明scope配置项,可以灵活定义Bean的作用范围。 例如,当你希望每次IOC容器返回的Bean是同一个实例时,可以设置scope为singleton;当你希望每次IOC容器返回的Bean实例是一个新的实例时,可以设置scope为prototype。 scope配置项有5个属性,用于描述不同的作用域。 ① singleton 使用该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。 ② prototype 使用该属性定义Bean时,IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。 ③ request 该属性仅对HTTP请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。 ④ session 该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。 ⑤ global-session 该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[bean的配置方式]]></title>
<url>%2F2019%2F10%2F26%2F%E6%A1%86%E6%9E%B6%2Fbean%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%B9%E5%BC%8F%2F</url>
<content type="text"><![CDATA[什么是beanSpring Bean是被实例的,组装的及被Spring 容器管理的Java对象。 Spring 容器会自动完成@bean对象的实例化。 创建应用对象之间的协作关系的行为称为:装配(wiring),这就是依赖注入的本质。 三种配置方案1.在XML中进行显示配置 2.使用Java代码进行显示配置 3.隐式的bean发现机制和自动装配 推荐方式: 3>2>1 1.自动化装配bean1.组件扫描(component scanning): Spring会自动发现应用上下文中所创建的bean。 2.自动装配(autowiring):Spring自动满足bean之间的依赖。 123456789package com.stalkers;/** * CD唱片接口 * Created by stalkers on 2016/11/17. */public interface ICompactDisc { void play();} 123456789101112131415161718package com.stalkers.impl; import com.stalkers.ICompactDisc;import org.springframework.stereotype.Component; /** * Jay同名专辑 * Created by stalkers on 2016/11/17. */@Componentpublic class JayDisc implements ICompactDisc { private String title = "星晴"; public void play() { System.out.println(title + ":一步两步三步四步,望着天上星星..."); }} Component注解作用:表明该类会作为组件类。 不过,组件扫描默认是不开启用的,我们还需要显示配置下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。 1.java code开启组件扫描:其中,如果CompoentScan后面没有参数的话,默认会扫描与配置类相同的包 12345678@Configuration@ComponentScanpublic class CDPlayerConfig { @Bean public ICompactDisc disc() { return new JayDisc(); }} 2.xml启动组件扫描 12345678<?xml version="1.0" encoding="utf-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.stalkers.impl"/></beans> 测试代码 1234567891011121314151617181920212223package com.stalkers; import com.stalkers.config.CDPlayerConfig;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/** * Created by stalkers on 2016/11/18. */@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = CDPlayerConfig.class)public class TestPlay { @Autowired private ICompactDisc jayDisc; @Test public void play() { jayDisc.play(); }} 在ComponentScan扫描的包中,所有带有@Component注解的类都会创建为bean 为组件扫描的bean命名Spring应用上下文种所有的bean都会给定一个ID。在前面的例子中,尽管我们没有明确地为JayDisc bean设置ID,但是Spring会默认为JayDisc设置ID为jayDisc,也就是将类名的第一个字母变成小写。 如果想为这个bean设置不同的ID,那就将期望的值传递给@Component注解 1234@Component("zhoujielun")public class JayDisc implements ICompactDisc { ...} 如果不使用@Component注解的话,则使用Java依赖注入规范(Java Dependency Injection)中所提供的@Named注解bean的ID。 需要引入: 12345<dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version></dependency> 1234@Named("zhoujielun")public class JayDisc implements ICompactDisc { ....} 设置组件扫描的基础包前面再给CDPlayerConfig类设置@ComponentScan,我们并没有设置任何属性,这个时候默认扫描默认包是:CDPlayerConfig类所在包及其包的子包。 如果是下图这种情况,DisConfig与其这时候就需要设置@ComponentScan的扫描的包。 1234@Configuration@ComponentScan(basePackages = {"com.stalkers.soundsystem"})public class DiscConfig {} basePackages使用的是复数,则意味着可以设置多个基础包。 但是basePackages后面跟的是String类型,这种类型并不安全。可以使用basePackageClasses有下面这种写法: 1234@Configuration@ComponentScan(basePackageClasses = {com.stalkers.soundsystem.JayCompactDisc.class})public class DiscConfig {} 通过为bean添加注解实现自动装配如果所有的对象都是独立的,彼此之间没有任何依赖,那么使用组件扫描就能自动化装配bean。 但是实际工作中,很多对象会依赖其他对象完成任务。这时候就需要能够将组件扫描得到的bean和他们依赖装配在一起。这就是自动装配(autowiring) 使用Spring的Autowired 123public interface IMediaPlayer { void play();} 123456789101112131415@Componentpublic class CDPlayer implements IMediaPlayer { private ICompactDisc cd; @Autowired public CDPlayer(ICompactDisc cd) { this.cd = cd; } public void play() { System.out.println("cd Play:"); cd.play(); }} CDPlayer类的构造器上添加了@Autowired注解,表明当Spring创建CDPlayerbean的时候,会通过这个构造器来进行实例化 Autowired的多种方式 1.构造器注解(constructor) 2.属性setter注解 3.field注解 不管使用上面3中的哪个方法,Spring都会满足声明的依赖。假如有且只有一个bean匹配依赖的话,那么这个bean将会被装配进来。 如果使用2,3方式注解,有多个bean的话,则用Qualifier指定。 如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,可以使用 12@Autowired(required = false)private IMediaPlayer CDPlayer; required=false表示如果没有匹配的话,Spring会让bean处于未装配的样子。使用未装配的属性,会出现NullPointerException 总结:所以在使用开发的时候一般建议使用Resource(package javax.annotation)进行注解。但是Resource不支持构造器注解 2.通过Java代码装配Bean尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化更为推荐,但是有时候行不通。比如引用第三方组件,没办法在它的类上添加@Component及@Autowired。所以就需要JavaConfig或者XML配置 在进行显示配置的时候,JavaConfig是更好的解决方案。 JavaConfig与其他的Java代码又有所区别,在概念上它与应用程序中的业务逻辑和领域代码又有所不同。JavaConfig是配置相关代码,不含任何逻辑代码。通常会将JavaConfig放到单独的包中。 创建JavaConfig类 123@Configurationpublic class CDPlayerConfig {} 使用@Configuration表明CDPlayerConfig是一个配置类 声明简单的bean 1234@Beanpublic IMediaPlayer cdplayer() { return new VCDPlayer(new JayCompactDisc());} @Bean注解会告诉Spring将返回一个对象。 默认情况下,@Bean的Id与带有@Bean的方法名一样。当然也可以通过@Bean的name属性指定额外的方法名。 借助JavaConfig注入 在上面的例子中,初始化个VCDPlayer都需要new一个JayCompactDisc对象。如果其他的对象的也需要JayCompactDisc,所以优化如下: 123456789@Beanpublic IMediaPlayer cdplayer() { return new VCDPlayer(disc());}@Beanpublic ICompactDisc disc() { return new JayCompactDisc();} 单独抽出disc()方法,在其方法上加上Bean注解,Spring上加@Bean注解的都是默认单例模式,不管disc()被多个方法调用,其disc()都是同一个实例。 当然上面的初始化可以优化如下: 1234@Beanpublic IMediaPlayer cdplayer(ICompactDisc disc) { return new VCDPlayer(disc);} 3.通过XML装配Bean在xml配置中,创建一个xml文件,并且要以元素为根。 123456<?xml version="1.0" encoding="utf-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"></beans> 在使用xml的时候,需要在配置文件顶部声明多个xml模式(XML Schema Definition xsd)文件 对于我们需要配置bean的则在spring-beans模式中。 1234567<?xml version="1.0" encoding="utf-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="jayCompactDisc" class="com.stalkers.soundsystem.JayCompactDisc"></bean></beans> 1.借助构造器注入初始化bean 构造器注入的方案: 1.元素 12345678910<?xml version="1.0" encoding="utf-8" ?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="jayCompactDisc" class="com.stalkers.soundsystem.JayCompactDisc"></bean> <bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer"> <constructor-arg ref="jayCompactDisc"/> </bean></beans> 2.使用Spring3.0所引入的c-命名空间 使用c-命名空间,需要引入: 1xmlns:c="http://www.springframework.org/schema/c" 解析:c-命名空间的语法: c:cd-ref=”jayCompactDisc” 1.c 代表命名空间前缀 2.cd 代表VCDPlayer类的构造器参数名。当然我们也可以使用参数在整个参数列表的位置 c:_0-ref 1<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer" c:_0-ref="jayCompactDisc"> 使用下划线因为参数不能以数字开头,所以加下划线。 3.-ref 代表注入bean引用 4.jayCompactDisc 要注入的bean的id 注意: c-命名需要写在标签内,与constructor-arg写法差别很大 将字面量注入到构造器中 上面我们所做的DI通常指的是类型的装配,也就是将对象的引用装配到依赖他们的其他对象中,但是有时候我们传的只是一个字面量值 1234567891011public class VaeCompactDisc implements ICompactDisc { private String title; public VaeCompactDisc(String title) { this.title = title; } public void play() { System.out.println("大家好,我是Vae,下面这首:" + title + "献给大家的"); }} 12345<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer" c:_0-ref="vaeCompactDisc"></bean><bean id="vaeCompactDisc" class="com.stalkers.soundsystem.VaeCompactDisc"> <constructor-arg value="浅唱"></constructor-arg></bean> c-命名空间的写法 12345<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer" c:_0-ref="vaeCompactDisc"></bean><bean id="vaeCompactDisc" class="com.stalkers.soundsystem.VaeCompactDisc" c:title="城府"> <!--<constructor-arg value="浅唱"></constructor-arg>--></bean> 装配集合 1234567891011121314151617public class VaeCompactDisc implements ICompactDisc { private String title; private List<String> tracks; public VaeCompactDisc(String title, List<String> tracks) { this.title = title; this.tracks = tracks; } public void play() { System.out.println("大家好,我是Vae,下面这专辑:" + title + "献给大家的"); for (String s : tracks) { System.out.println(s); } }} Spring配置使用constructor-arg。而c-命名的是无法使用装配集合的功能 1234567891011<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer" c:_0-ref="vaeCompactDisc"></bean><bean id="vaeCompactDisc" class="com.stalkers.soundsystem.VaeCompactDisc"> <constructor-arg name="title" value="自定义"></constructor-arg> <constructor-arg name="tracks"> <list> <value>有何不可</value> <value>多余的解释</value> </list> </constructor-arg></bean> 2.使用属性Setter方法注入 123456789101112131415161718public class CDPlayer implements IMediaPlayer { private ICompactDisc cd; @Autowired public void setCd(ICompactDisc cd) { this.cd = cd; } public CDPlayer(ICompactDisc cd) { this.cd = cd; } public void play() { System.out.println("cd Play:"); cd.play(); }} Spring.xml配置里面 123<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer"> <property name="cd" ref="jayCompactDisc"></property></bean> 元素为属性的Setter方法所提供的功能与元素为构造器所提供的功能是一样的。 与c-命名空间的类似的作为property的替代方案:p-命名空间。使用p-命名空间需要引入: 1xmlns:p="http://www.springframework.org/schema/p" Spring.xml配置如下 1<bean id="cdPlayer" class="com.stalkers.soundsystem.VCDPlayer" p:cd-ref="vaeCompactDisc"> 语法解析: p:cd-ref=”vaeCompactDisc” 1.p-:命名空间的前缀 2.cd:属性名称 3.-ref:注入bean引用 4.vaeCompactDisc:所注入的bean的id 将字面量注入到属性中 字面量注入到属性与上面将字面量注入到构造方法中方式一样。只不过标签名改成了property。 装配list也是与上面的构造器的装配list一样。 虽然我们无法使用c-及p-命名空间装配list,但是我们可以使用 123456<bean id="vaeCompactDisc" class="com.stalkers.soundsystem.VaeCompactDisc" c:title="自定义" c:tracks-ref="songs"></bean><util:list id="songs"> <value>有何不可</value> <value>多余的解释</value></util:list>]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HashMap遍历与删除]]></title>
<url>%2F2019%2F10%2F22%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2FHashMap%E9%81%8D%E5%8E%86%E4%B8%8E%E5%88%A0%E9%99%A4%2F</url>
<content type="text"><![CDATA[HashMap遍历123456789101112Map<String,String> map=new HashMap<String,String>(); map.put("1", "value1"); map.put("2", "value2"); map.put("3", "value3"); map.put("4", "value4"); //第一种:普通使用,二次取值 System.out.println("\n通过Map.keySet遍历key和value:"); for(String key:map.keySet()) { System.out.println("Key: "+key+" Value: "+map.get(key)); } 12345678//第二种 System.out.println("\n通过Map.entrySet使用iterator遍历key和value: "); Iterator map1ist=map.entrySet().iterator(); while(map1ist.hasNext()) { Map.Entry<String, String> entry=(Entry<String, String>) map1ist.next(); System.out.println("Key: "+entry.getKey()+" Value: "+entry.getValue()); } 123456//第三种:推荐,尤其是容量大时 System.out.println("\n通过Map.entrySet遍历key和value"); for(Map.Entry<String, String> entry: map.entrySet()){ System.out.println("Key: "+ entry.getKey()+ " Value: "+entry.getValue());} 123456//第四种 System.out.println("\n通过Map.values()遍历所有的value,但不能遍历key"); for(String v:map.values()){ System.out.println("The value is "+v);} HashMap遍历删除1、第一种遍历删除: 12345678for(Map.Entry<Integer, String> entry : map.entrySet()){ Integer key = entry.getKey(); if(key % 2 == 0){ System.out.println("To delete key " + key); map.remove(key); System.out.println("The key " + + key + " was deleted"); }} 这种遍历删除依旧会报ConcurrentModificationException异常 2、第二种遍历删除: 123456789Set<Integer> keySet = map.keySet();for(Integer key : keySet){ if(key % 2 == 0){ System.out.println("To delete key " + key); keySet.remove(key); System.out.println("The key " + + key + " was deleted"); } }} 这种遍历删除依旧会报ConcurrentModificationException异常 3、第三种遍历删除: 1234567891011 Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator(); while(it.hasNext()){ Map.Entry<Integer, String> entry = it.next(); Integer key = entry.getKey(); if(key % 2 == 0){ System.out.println("To delete key " + key); it.remove(); System.out.println("The key " + + key + " was deleted"); } }} 这种遍历是OK的 分析上述原因,如果大家理解了List的遍历删除,HashMap的遍历删除有类似之处。下面就分析一下原因: 如果查询源代码以上的三种的删除方式都是通过调用HashMap.removeEntryForKey方法来实现删除key的操作。在removeEntryForKey方法内执行一次 modCount就会执行一次自增操作,此时modCount就与expectedModCount不一致了,上面三种remove实现中,只有第三种iterator的remove方法在调用完removeEntryForKey方法后同步了expectedModCount值与modCount相同,所以iterator方式不会抛出异常。]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux命令行下识别文件类型]]></title>
<url>%2F2019%2F10%2F21%2FLinux%2FLinux%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%8B%E8%AF%86%E5%88%AB%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B%2F</url>
<content type="text"><![CDATA[Linux 中可以用不同的颜色来区分不同种类的文件,例如绿色代表可执行文件、红色代表压缩文件、浅绿色代表链接文件、白色代表其他文件、黄色代表设备文件等。 如果想详细了解不同文件类型所对应的颜色,可以使用 man 命令,例如: 1[root@localhost ~]# man dir_colors 每行代表一个文件或目录,其中第一个字符表示的就是文件的类型,其可能的取值以及表示的文件类型 第一个字符 文件类型 - 普通文件,包括纯文本文件、二进制文件、各种压缩文件等。 d 目录,类似 Windows 系统中的文件夹。 b 块设备文件,就是保存大块数据的设备,比如最常见的硬盘。 c 字符设备文件,例如键盘、鼠标等。 s 套接字文件,通常用在网络数据连接,可以启动一个程序开监听用户的要求,用户可以通过套接字进行数据通信。 p 管道文件,其主要作用是解决多个程序同时存取一个文件所造成的错误。 l 链接文件,类似 Windows 系统中的快捷方式。 Linux命令基本格式命令提示符[root@localhost ~]# 这就是 Linux 系统的命令提示符。那么,这个提示符的含义是什么呢? []:这是提示符的分隔符号,没有特殊含义。 root:显示的是当前的登录用户,笔者现在使用的是 root 用户登录。 @:分隔符号,没有特殊含义。 localhost:当前系统的简写主机名(完整主机名是 localhost.localdomain)。 ~:代表用户当前所在的目录,此例中用户当前所在的目录是家目录。 #:命令提示符,Linux 用这个符号标识登录的用户权限等级。如果是超级用户,提示符就是 #;如果是普通用户,提示符就是 $。 家目录 (又称主目录)是什么? Linux 系统是纯字符界面,用户登录后,要有一个初始登录的位置,这个初始登录位置就称为用户的家: 超级用户的家目录:/root。 普通用户的家目录:/home/用户名。 用户在自己的家目录中拥有完整权限,所以我们也建议操作实验可以放在家目录中进行 12[root@localhost ~]# cd /usr/local[root@localhost local]# 如果切换用户所在目录,那么命令提示符中的会变成用户当前所在目录的最后一个目录(不显示完整的所在目录 /usr/ local,只显示最后一个目录 local)。 命令的基本格式1[root@localhost ~]# 命令[选项][参数] 1) 选项的作用12345[root@localhost ~]# Is -l总用量44-rw-------.1 root root 1207 1 月 14 18:18 anaconda-ks.cfg-rw-r--r--.1 root root 24772 1 月 14 18:17 install.log-rw-r--r--.1 root root 7690 1 月 14 18:17 install.log.syslog 如果加一个”-l”选项,则可以看到显示的内容明显增多了。”-l”是长格式(long list)的意思,也就是显示文件的详细信息。至于 “-l” 选项的具体含义,我们稍后再详细讲解。可以看到选项的作用是调整命令功能。如果没有选项,那么命令只能执行最基本的功能;而一旦有选项,则可以显示更加丰富的数据。 Linux 的选项又分为短格式选项(-l)和长格式选项(–all)。短格式选项是英文的简写,用一个减号调用,例如: 1[root@localhost ~]# ls -l 而长格式选项是英文完整单词,一般用两个减号调用,例如: 1[root@localhost ~]# ls --all 一般情况下,短格式选项是长格式选项的缩写,也就是一个短格式选项会有对应的长格式选项。 2) 参数的作用参数是命令的操作对象,一般文件、目录、用户和进程等可以作为参数被命令操作。例如: 12[root@localhost ~]# ls -l anaconda-ks.cfg-rw-------.1 root root 1207 1 月 14 18:18 anaconda-ks.cfg]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux文件目录结构]]></title>
<url>%2F2019%2F10%2F21%2FLinux%2FLinux%E6%96%87%E4%BB%B6%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84%2F</url>
<content type="text"><![CDATA[Linux 根目录(/)Linux 系统的根目录(/)最为重要,其原因有以下 2 点: 所有目录都是由根目录衍生出来的; 根目录与系统的开机、修复、还原密切相关; 因此,根目录必须包含开机软件、核心文件、开机所需程序、函数库、修复系统程序等文件 一级目录 功能(作用) /bin/ 存放系统命令,普通用户和 root 都可以执行。放在 /bin 下的命令在单用户模式下也可以执行 /boot/ 系统启动目录,保存与系统启动相关的文件,如内核文件和启动引导程序(grub)文件等 /dev/ 设备文件保存位置 /etc/ 配置文件保存位置。系统内所有采用默认安装方式(rpm 安装)的服务配置文件全部保存在此目录中,如用户信息、服务的启动脚本、常用服务的配置文件等 /home/ 普通用户的主目录(也称为家目录)。在创建用户时,每个用户要有一个默认登录和保存自己数据的位置,就是用户的主目录,所有普通用户的主目录是在 /home/ 下建立一个和用户名相同的目录。如用户 liming 的主目录就是 /home/liming /lib/ 系统调用的函数库保存位置 /media/ 挂载目录。系统建议用来挂载媒体设备,如软盘和光盘 /mnt/ 挂载目录。早期 Linux 中只有这一个挂载目录,并没有细分。系统建议这个目录用来挂载额外的设备,如 U 盘、移动硬盘和其他操作系统的分区 /misc/ 挂载目录。系统建议用来挂载 NFS 服务的共享目录。虽然系统准备了三个默认挂载目录 /media/、/mnt/、/misc/,但是到底在哪个目录中挂载什么设备可以由管理员自己决定。例如,笔者在接触 Linux 的时候,默认挂载目录只有 /mnt/,所以养成了在 /mnt/ 下建立不同目录挂载不同设备的习惯,如 /mnt/cdrom/ 挂载光盘、/mnt/usb/ 挂载 U 盘,都是可以的 /opt/ 第三方安装的软件保存位置。这个目录是放置和安装其他软件的位置,手工安装的源码包软件都可以安装到这个目录中。不过笔者还是习惯把软件放到 /usr/local/ 目录中,也就是说,/usr/local/ 目录也可以用来安装软件 /root/ root 的主目录。普通用户主目录在 /home/ 下,root 主目录直接在“/”下 /sbin/ 保存与系统环境设置相关的命令,只有 root 可以使用这些命令进行系统环境设置,但也有些命令可以允许普通用户查看 /srv/ 服务数据目录。一些系统服务启动之后,可以在这个目录中保存所需要的数据 /tmp/ 临时目录。系统存放临时文件的目录,在该目录下,所有用户都可以访问和写入。建议此目录中不能保存重要数据,最好每次开机都把该目录清空 FHS 针对根目录中包含的子目录仅限于表 1,但除此之外,Linux 系统根目录下通常还包含表 2 中的几个一级目录。 一级目录 功能(作用) /lost+found/ 当系统意外崩溃或意外关机时,产生的一些文件碎片会存放在这里。在系统启动的过程中,fsck 工具会检查这里,并修复已经损坏的文件系统。这个目录只在每个分区中出现,例如,/lost+found 就是根分区的备份恢复目录,/boot/lost+found 就是 /boot 分区的备份恢复目录 /proc/ 虚拟文件系统。该目录中的数据并不保存在硬盘上,而是保存到内存中。主要保存系统的内核、进程、外部设备状态和网络状态等。如 /proc/cpuinfo 是保存 CPU 信息的,/proc/devices 是保存设备驱动的列表的,/proc/filesystems 是保存文件系统列表的,/proc/net 是保存网络协议信息的…… /sys/ 虚拟文件系统。和 /proc/ 目录相似,该目录中的数据都保存在内存中,主要保存与内核相关的信息 Linux /usr目录usr(注意不是 user),全称为 Unix Software Resource,此目录用于存储系统软件资源。FHS 建议所有开发者,应把软件产品的数据合理的放置在 /usr 目录下的各子目录中,而不是为他们的产品创建单独的目录。 Linux 系统中,所有系统默认的软件都存储在 /usr 目录下,/usr 目录类似 Windows 系统中 C:\Windows\ + C:\Program files\ 两个目录的综合体。 子目录 功能(作用) /usr/bin/ 存放系统命令,普通用户和超级用户都可以执行。这些命令和系统启动无关,在单用户模式下不能执行 /usr/sbin/ 存放根文件系统不必要的系统管理命令,如多数服务程序,只有 root 可以使用。 /usr/lib/ 应用程序调用的函数库保存位置 /usr/XllR6/ 图形界面系统保存位置 /usr/local/ 手工安装的软件保存位置。我们一般建议源码包软件安装在这个位置 /usr/share/ 应用程序的资源文件保存位置,如帮助文档、说明文档和字体目录 /usr/src/ 源码包保存位置。我们手工下载的源码包和内核源码包都可以保存到这里。不过笔者更习惯把手工下载的源码包保存到 /usr/local/src/ 目录中,把内核源码保存到 /usr/src/linux/ 目录中 /usr/include C/C++等编程语言头文件的放置目录 Linux /var 目录/var 目录用于存储动态数据,例如缓存、日志文件、软件运行过程中产生的文件等。通常,此目录下建议包含如下这些子目录。 /var子目录 功能(作用) /var/lib/ 程序运行中需要调用或改变的数据保存位置。如 MySQL 的数据库保存在 /var/lib/mysql/ 目录中 /var/log/ 登陆文件放置的目录,其中所包含比较重要的文件如 /var/log/messages, /var/log/wtmp 等。 /var/run/ 一些服务和程序运行后,它们的 PID(进程 ID)保存位置 /var/spool/ 里面主要都是一些临时存放,随时会被用户所调用的数据,例如 /var/spool/mail/ 存放新收到的邮件,/var/spool/cron/ 存放系统定时任务。 /var/www/ RPM 包安装的 Apache 的网页主目录 /var/nis和/var/yp NIS 服务机制所使用的目录,nis 主要记录所有网络中每一个 client 的连接信息;yp 是 linux 的 nis 服务的日志文件存放的目录 /var/tmp 一些应用程序在安装或执行时,需要在重启后使用的某些文件,此目录能将该类文件暂时存放起来,完成后再行删除 Linux挂载Linux系统中“一切皆文件”,所有文件都放置在以根目录为树根的树形目录结构中。在 Linux 看来,任何硬件设备也都是文件,它们各有自己的一套文件系统(文件目录结构)。 如果不挂载,通过Linux系统中的图形界面系统可以查看找到硬件设备,但命令行方式无法找到。 挂载,指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。 纠正一个误区,并不是根目录下任何一个目录都可以作为挂载点,由于挂载操作会使得原有目录中文件被隐藏,因此根目录以及系统原有目录都不要作为挂载点,会造成系统异常甚至崩溃,挂载点最好是新建的空目录。 举个例子,我们想通过命令行访问某个 U 盘中的数据,图 1 所示为 U 盘文件目录结构和 Linux 系统中的文件目录结构。 图 1 中可以看到,目前 U 盘和 Linux 系统文件分属两个文件系统,还无法使用命令行找到 U 盘文件,需要将两个文件系统进行挂载。 接下来,我们在根目录下新建一个目录 /sdb-u,通过挂载命令将 U 盘文件系统挂载到此目录,挂载效果如图 2 所示。 可以看到,U 盘文件系统已经成为 Linux 文件系统目录的一部分,此时访问 /sdb-u/ 就等同于访问 U 盘。 前面讲过,根目录下的 /dev/ 目录文件负责所有的硬件设备文件,事实上,当 U 盘插入 Linux 后,系统也确实会给 U 盘分配一个目录文件(比如 sdb1),就位于 /dev/ 目录下(/dev/sdb1),但无法通过 /dev/sdb1/ 直接访问 U 盘数据,访问此目录只会提供给你此设备的一些基本信息(比如容量)。 总之,Linux 系统使用任何硬件设备,都必须将设备文件与已有目录文件进行挂载。]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[redis主从架构]]></title>
<url>%2F2019%2F10%2F21%2F%E6%9E%B6%E6%9E%84%2Fredis%E4%B8%BB%E4%BB%8E%E6%9E%B6%E6%9E%84%2F</url>
<content type="text"></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[redis持久化]]></title>
<url>%2F2019%2F10%2F21%2F%E6%9E%B6%E6%9E%84%2Fredis%E6%8C%81%E4%B9%85%E5%8C%96%2F</url>
<content type="text"><![CDATA[RDB和AOF介绍redis的持久化,RDB,AOF,区别,各自的特点是什么,适合什么场景 比如你部署了一个redis,作为cache缓存,当然也可以保存一些较为重要的数据 如果没有持久化的话,redis遇到灾难性故障的时候,就会丢失所有的数据 如果通过持久化将数据搞一份儿在磁盘上去,然后定期比如说同步和备份到一些云存储服务上去,那么就可以保证数据不丢失全部,还是可以恢复一部分数据回来的 持久化主要是做灾难恢复,数据恢复,也可以归类到高可用的一个环节里面去 比如你redis整个挂了,然后redis就不可用了,你要做的事情是让redis变得可用,尽快变得可用 重启redis,尽快让它对外提供服务,但是就像上一讲说,如果你没做数据备份,这个时候redis启动了,也不可用啊,数据都没了 很可能说,大量的请求过来,缓存全部无法命中,在redis里根本找不到数据,这个时候就死定了,缓存雪崩问题,所有请求,没有在redis命中,就会去mysql数据库这种数据源头中去找,一下子mysql承接高并发,然后就挂了,mysql挂掉,你都没法去找数据恢复到redis里面去,redis的数据从哪儿来?从mysql来。。。 1、RDB和AOF两种持久化机制的介绍 RDB持久化机制,对redis中的数据执行周期性的持久化 AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集 如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制 通过RDB或AOF,都可以将redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云,云服务 如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务 如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整 2、RDB持久化机制的优点 (1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据 (2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可 (3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速 3、RDB持久化机制的缺点 (1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据 (2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒 4、AOF持久化机制的优点 (1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据 (2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复 (3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。可以使用bgrewrite aof (4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据 5、AOF持久化机制的缺点 (1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大 (2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的 (3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。 6、RDB和AOF到底该如何选择 (1)不要仅仅使用RDB,因为那样会导致你丢失很多数据 (2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug (3)综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复 AOF rewrite原理 RDB和AOF的介绍 RDB丢失数据的问题 RDB持久化机制配置 1、如何配置RDB持久化机制 redis.conf文件,也就是/etc/redis/6379.conf,去配置持久化 save 60 1000 每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting,快照 也可以手动调用save或者bgsave命令,同步或异步执行rdb快照生成 save可以设置多个,就是多个snapshotting检查点,每到一个检查点,就会去check一下,是否有指定的key数量发生了变更,如果有,就生成一个新的dump.rdb文件 2、RDB持久化机制的工作流程 (1)redis根据配置自己尝试去生成rdb快照文件(2)fork一个子进程出来(3)子进程尝试将数据dump到临时的rdb快照文件中(4)完成rdb快照文件的生成之后,就替换之前的旧的快照文件 dump.rdb,每次生成一个新的快照,都会覆盖之前的老快照 3、基于RDB持久化机制的数据恢复实验 (1)在redis中保存几条数据,立即停掉redis进程,然后重启redis,看看刚才插入的数据还在不在 数据还在,为什么? 带出来一个知识点,通过redis-cli SHUTDOWN这种方式去停掉redis,其实是一种安全退出的模式,redis在退出的时候会将内存中的数据立即生成一份完整的rdb快照 /var/redis/6379/dump.rdb (2)在redis中再保存几条新的数据,用kill -9粗暴杀死redis进程,模拟redis故障异常退出,导致内存数据丢失的场景 这次就发现,redis进程异常被杀掉,数据没有进dump文件,几条最新的数据就丢失了 (2)手动设置一个save检查点,save 5 1(3)写入几条数据,等待5秒钟,会发现自动进行了一次dump rdb快照,在dump.rdb中发现了数据(4)异常停掉redis进程,再重新启动redis,看刚才插入的数据还在 AOF持久化的配置1、AOF持久化的配置 AOF持久化,默认是关闭的,默认是打开RDB持久化 appendonly yes,可以打开AOF持久化机制,在生产环境里面,一般来说AOF都是要打开的,除非你说随便丢个几分钟的数据也无所谓 打开AOF持久化机制之后,redis每次接收到一条写命令,就会写入日志文件中,当然是先写入os cache的,然后每隔一定时间再fsync一下 而且即使AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为aof数据比较完整 可以配置AOF的fsync策略,有三种策略可以选择,一种是每次写入一条数据就执行一次fsync; 一种是每隔一秒执行一次fsync; 一种是不主动执行fsync always: 每次写入一条数据,立即将这个数据对应的写日志fsync到磁盘上去,性能非常非常差,吞吐量很低; 确保说redis里的数据一条都不丢,那就只能这样了 mysql -> 内存策略,大量磁盘,QPS到多少,一两k。QPS,每秒钟的请求数量redis -> 内存,磁盘持久化,QPS到多少,单机,一般来说,上万QPS没问题 everysec: 每秒将os cache中的数据fsync到磁盘,这个最常用的,生产环境一般都这么配置,性能很高,QPS还是可以上万的 no: 仅仅redis负责将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控了 2、AOF持久化的数据恢复实验 (1)先仅仅打开RDB,写入一些数据,然后kill -9杀掉redis进程,接着重启redis,发现数据没了,因为RDB快照还没生成(2)打开AOF的开关,启用AOF持久化(3)写入一些数据,观察AOF文件中的日志内容 其实你在appendonly.aof文件中,可以看到刚写的日志,它们其实就是先写入os cache的,然后1秒后才fsync到磁盘中,只有fsync到磁盘中了,才是安全的,要不然光是在os cache中,机器只要重启,就什么都没了 (4)kill -9杀掉redis进程,重新启动redis进程,发现数据被恢复回来了,就是从AOF文件中恢复回来的 redis进程启动的时候,直接就会从appendonly.aof中加载所有的日志,把内存中的数据恢复回来 3、AOF rewrite redis中的数据其实有限的,很多数据可能会自动过期,可能会被用户删除,可能会被redis用缓存清除的算法清理掉 redis中的数据会不断淘汰掉旧的,就一部分常用的数据会被自动保留在redis内存中 所以可能很多之前的已经被清理掉的数据,对应的写日志还停留在AOF中,AOF日志文件就一个,会不断的膨胀,到很大很大 所以AOF会自动在后台每隔一定时间做rewrite操作,比如日志里已经存放了针对100w数据的写日志了; redis内存只剩下10万; 基于内存中当前的10万数据构建一套最新的日志,到AOF中; 覆盖之前的老日志; 确保AOF日志文件不会过大,保持跟redis内存数据量一致 redis 2.4之前,还需要手动,开发一些脚本,crontab,通过BGREWRITEAOF命令去执行AOF rewrite,但是redis 2.4之后,会自动进行rewrite操作 在redis.conf中,可以配置rewrite策略 auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb 比如说上一次AOF rewrite之后,是128mb 然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite 但是此时还要去跟min-size,64mb去比较,256mb > 64mb,才会去触发rewrite (1)redis fork一个子进程(2)子进程基于当前内存中的数据,构建日志,开始往一个新的临时的AOF文件中写入日志(3)redis主进程,接收到client新的写操作之后,在内存中写入日志,同时新的日志也继续写入旧的AOF文件(4)子进程写完新的日志文件之后,redis主进程将内存中的新日志再次追加到新的AOF文件中(5)用新的日志文件替换掉旧的日志文件 4、AOF破损文件的修复 如果redis在append数据到AOF文件时,机器宕机了,可能会导致AOF文件破损 用redis-check-aof –fix命令来修复破损的AOF文件 5、AOF和RDB同时工作 (1)如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite; 如果redis再执行AOF rewrite,那么就不会执行RDB snapshotting(2)如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成之后,才会去执行AOF rewrite(3)同时有RDB snapshot文件和AOF日志文件,那么redis重启的时候,会优先使用AOF进行数据恢复,因为其中的日志更完整 6、最后一个小实验,让大家对redis的数据恢复有更加深刻的体会 (1)在有rdb的dump和aof的appendonly的同时,rdb里也有部分数据,aof里也有部分数据,这个时候其实会发现,rdb的数据不会恢复到内存中(2)我们模拟让aof破损,然后fix,有一条数据会被fix删除(3)再次用fix得aof文件去重启redis,发现数据只剩下一条了 数据恢复完全是依赖于底层的磁盘的持久化的,主要rdb和aof上都没有数据,那就没了]]></content>
<categories>
<category>架构</category>
</categories>
<tags>
<tag>架构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Mybatis如何防止SQL注入]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FMybatis%E5%A6%82%E4%BD%95%E9%98%B2%E6%AD%A2SQL%E6%B3%A8%E5%85%A5%2F</url>
<content type="text"><![CDATA[两个sql语句的区别: 123456<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">select id, username, password, rolefrom userwhere username = #{username,jdbcType=VARCHAR}and password = #{password,jdbcType=VARCHAR}</select> 123456<select id="selectByNameAndPassword" parameterType="java.util.Map" resultMap="BaseResultMap">select id, username, password, rolefrom userwhere username = ${username,jdbcType=VARCHAR}and password = ${password,jdbcType=VARCHAR}</select> mybatis中的#和$的区别: 1、#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。 如:where username=#{username},如果传入的值是111,那么解析成sql时的值为where username=”111”, 如 果传入的值是id,则解析成的sql为where username=”id”. 2、$将传入的数据直接显示生成在sql中。 如:where username=${username},如果传入的值是111,那么解析成sql时的值为where username=111; 如果传入的值是;drop table user;,则解析成的sql为:select id, username, password, role from user where username=;drop table user; 3、#方式能够很大程度防止sql注入,$方式无法防止Sql注入。 4、$方式一般用于传入数据库对象,例如传入表名. 5、一般能用#的就别用$,若不得不使用“${xxx}”这样的参数,要手工地做好过滤工作,来防止sql注入攻击。 6、在MyBatis中,“${xxx}”这样格式的参数会直接参与SQL编译,从而不能避免注入攻击。但涉及到动态表名和列 名时,只能使用“${xxx}”这样的参数格式。所以,这样的参数需要我们在代码中手工进行处理来防止注入。 【结论】在编写MyBatis的映射语句时,尽量采用“#{xxx}”这样的格式。若不得不使用“${xxx}”这样的参数,要手工 地做好过滤工作,来防止SQL注入攻击。 什么是sql注入是一种代码注入技术,用于攻击数据驱动的应用,恶意的SQL语句被插入到执行的实体字段中(例如,为了转储数据库内容给攻击者) SQL注入,大家都不陌生,是一种常见的攻击方式。攻击者在界面的表单信息或URL上输入一些奇怪的SQL片段(例如“or ‘1’=’1’”这样的语句),有可能入侵参数检验不足的应用程序。所以,在我们的应用中需要做一些工作,来防备这样的攻击方式。在一些安全性要求很高的应用中(比如银行软件),经常使用将SQL语句全部替换为存储过程这样的方式,来防止SQL注入。这当然是一种很安全的方式,但我们平时开发中,可能不需要这种死板的方式。 mybatis是如何做到防止sql注入的 MyBatis框架作为一款半自动化的持久层框架,其SQL语句都要我们自己手动编写,这个时候当然需要防止SQL注入。其实,MyBatis的SQL是一个具有“输入+输出”的功能,类似于函数的结构,参考上面的两个例子。其中,parameterType表示了输入的参数类型,resultType表示了输出的参数类型。回应上文,如果我们想防止SQL注入,理所当然地要在输入参数上下功夫。上面代码中使用#的即输入参数在SQL中拼接的部分,传入参数后,打印出执行的SQL语句,会看到SQL是这样的: 1select id, username, password, role from user where username=? and password=? 不管输入什么参数,打印出的SQL都是这样的。这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。 【底层实现原理】MyBatis是如何做到SQL预编译的呢?其实在框架底层,是JDBC中的PreparedStatement类在起作用,PreparedStatement是我们很熟悉的Statement的子类,它的对象包含了编译好的SQL语句。这种“准备好”的方式不仅能提高安全性,而且在多次执行同一个SQL时,能够提高效率。原因是SQL已编译好,再次执行时无需再编译。 1234567//安全的,预编译了的Connection conn = getConn();//获得连接String sql = "select id, username, password, role from user where id=?"; //执行sql前会预编译号该条语句PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, id); ResultSet rs=pstmt.executeUpdate(); ...... 12345678910//不安全的,没进行预编译private String getNameByUserId(String userId) { Connection conn = getConn();//获得连接 String sql = "select id,username,password,role from user where id=" + id; //当id参数为"3;drop table user;"时,执行的sql语句如下: //select id,username,password,role from user where id=3; drop table user; PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs=pstmt.executeUpdate(); ......} 【 结论:】 #{}:相当于JDBC中的PreparedStatement ${}:是输出变量的值 简单说,#{}是经过预编译的,是安全的;${}是未经过预编译的,仅仅是取变量的值,是非安全的,存在SQL注入。如果我们order by语句后用了${},那么不做任何处理的时候是存在SQL注入危险的。你说怎么防止,那我只能悲惨的告诉你,你得手动处理过滤一下输入的内容。如判断一下输入的参数的长度是否正常(注入语句一般很长),更精确的过滤则可以查询一下输入的参数是否在预期的参数集合中。]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Resource和Autowired的区别]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FResource%E5%92%8CAutowired%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[@Resource和@Autowired都是做bean的注入时使用,其实@Resource不是Spring的注解,是javaEE的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。 1、共同点 两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。 2、不同点 (1)@Autowired @Autowired为Spring提供的注解,需要导入包org.springframework.beans.factory.annotation.Autowired;只按照byType注入。 12345678910public class TestServiceImpl { // 下面两种@Autowired只要使用一种即可 @Autowired private UserDao userDao; // 用于字段上 @Autowired public void setUserDao(UserDao userDao) { // 用于属性的方法上 this.userDao = userDao; }} @Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值, 可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起 使用。如下: 12345public class TestServiceImpl { @Autowired @Qualifier("userDao") private UserDao userDao; } (2)@Resource @Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。默认按照名称进 行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行 安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进 行装配。 但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。 如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。 12345678910public class TestServiceImpl { // 下面两种@Resource只要使用一种即可 @Resource(name="userDao") private UserDao userDao; // 用于字段上 @Resource(name="userDao") public void setUserDao(UserDao userDao) { // 用于属性的setter方法上 this.userDao = userDao; }} 注:最好是将@Resource放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。 @Resource装配顺序: ①如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。 ②如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。 ③如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。 ④如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。 @Resource的作用相当于@Autowired,只不过@Autowired按照byType自动注入。 byName和byType看什么 byName会搜索整个配置文件中的bean,如果有相同名称的bean则自动装配,否则显示异常。 例如,在装配com.tutorialspoint.TextEditor的spellChecker时,spring会搜索整个配置文件的bean查找是否有名称为spellChecker的bean,有则自动装配,没有就抛出异常。 byType会搜索整个配置文件中的bean,如果有相同类型的bean则自动装配,否则显示异常。 例如,在装配com.tutorialspoint.TextEditor的spellChecker时,spring会搜索整个配置文件的bean查找是否有跟spellChecker属于相同类的bean,有则自动装配,没有就抛出异常。 ####]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ResponseBody的作用]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FResponseBody%E7%9A%84%E4%BD%9C%E7%94%A8%2F</url>
<content type="text"><![CDATA[@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML 数据,需要注意的呢,在使用此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。 12345 @RequestMapping("/login") @ResponseBody public User login(User user){ return user; } User字段:userName pwd 那么在前台接收到的数据为:’{“userName”:”xxx”,”pwd”:”xxx”}’ 效果等同于如下代码: 1234@RequestMapping("/login") public void login(User user, HttpServletResponse response){ response.getWriter.write(JSONObject.fromObject(user).toString()); }]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[RestController和Controller的区别]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FRestController%E5%92%8CController%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[@RestController注解相当于@ResponseBody + @Controller合在一起的作用。 1、如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,配置的视图解析器InternalResourceViewResolver则不起作用,返回的内容就是Return 里的内容(String/JSON)。例如:本来应该到success.jsp页面的,则其显示success. 1234@RequestMapping(value = "/test")public String test(HttpServletRequest request, HttpServletResponse response){ return "success";} 2、如果使用@RestController注解Controller,需要返回到指定页面,则需要配置视图解析器InternalResourceViewResolver,可以利用ModelAndView返回视图。 1234@RequestMapping(value = "/test")public String test(HttpServletRequest request, HttpServletResponse response){ return newModelAndView("success");} 3、如果使用@Controller注解Controller,在对应的方法上,视图解析器可以解析return 的jsp,html页面,并且跳转到相应页面 若需要返回JSON,XML或自定义mediaType内容到页面,则需要在对应的方法上加上@ResponseBody注解。 12345@ResponseBody@RequestMapping(value = "/test")public String test(HttpServletRequest request, HttpServletResponse response){ return "success";}]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring IOC和AOP]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FSpring%20IOC%E5%92%8CAOP%2F</url>
<content type="text"><![CDATA[Spring IOCIoC 全称为 Inversion of Control,翻译为 “控制反转”,它还有一个别名为 DI(Dependency Injection),即依赖注入。 ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好Ioc呢?理解好Ioc的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下: ●谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。 ●为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。 DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下: ●谁依赖于谁:当然是应用程序依赖于IoC容器; ●为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源; ●谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象; ●注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 Spring AOPAOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。 例如,在一个业务系统中,用户登录是基础功能,凡是涉及到用户的业务流程都要求用户进行系统登录。如果把用户登录功能代码写入到每个业务流程中,会造成代码冗余,维护也非常麻烦,当需要修改用户登录功能时,就需要修改每个业务流程的用户登录代码,这种处理方式显然是不可取的。比较好的做法是把用户登录功能抽取出来,形成独立的模块,当业务流程需要用户登录时,系统自动把登录功能切入到业务流程中。下图是用户登录功能切入到业务流程示意图。 ● Aspect 表示切面。切入业务流程的一个独立模块。 ● Join point 表示连接点。也就是业务流程在运行过程中需要插入切面的具体位置。 ● Advice 表示通知。是切面的具体实现方法。可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)五种。实现方法具体属于哪类通知,是在配置文件和注解中指定的。 ● Pointcut 表示切入点。用于定义通知应该切入到哪些连接点上,不同的通知通常需要切入到不同的连接点上。 ● Target 表示目标对象。被一个或者多个切面所通知的对象。 ● Proxy 表示代理对象。将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。 ● Weaving 表示切入,也称为织入。将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期。]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringMVC的工作流程]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FSpringMVC%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%2F</url>
<content type="text"><![CDATA[SpringMVC架构 框架流程 用户发送请求至前端控制器DispatcherServlet。 DispatcherServlet收到请求调用HandlerMapping处理器映射器。 处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。 DispatcherServlet调用HandlerAdapter处理器适配器。 HandlerAdapter经过适配调用具体的处理器(controller,也叫后端控制器)。 Controller执行完成返回ModelAndView。 HandlerAdapter将Controller返回的执行结果ModelAndView返回给DispatcherServlet。 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。 ViewReslover解析之后返回具体的view。 DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图jsp/freemaker..中)。 DispatcherServlet响应用户。 组件说明以下组件通常使用框架提供实现: DispatcherServlet:前端控制器 用户请求到达前端控制器,它就相当于mvc模式中的c,dispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,dispatcherServlet的存在降低了组件之间的耦合性。 HandlerMapping:处理器映射器 HandlerMapping负责根据用户请求找到Handler即处理器,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 Handler:处理器 Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。 HandlAdapter:处理器适配器 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。 View Resolver:视图解析器 View Resolver负责将处理结果生成View视图(JSP,freemarker),View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 View:视图 springmvc框架提供了很多的View视图类型的支持,包括:jstlView、freemarkerView、pdfView等。我们最常用的视图就是jsp。 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由程序员根据业务需求开发具体的页面。 需要用户开发的组件有handler(controller类)、view(jsp/freemarker velocity,thyeleaf) 常用注解<mvc:annotation-driven /> 说明: 是一种简写形式,可以让初学者快速成应用默认的配置方案,会默认注册 DefaultAnnotationHandleMapping以及AnnotionMethodHandleAdapter 这两个 Bean, 这两个 Bean ,前者对应类级别, 后者对应到方法级别; 上面的 DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter 是 Spring 为 @Controller 分发请求所必需的。 annotation-driven 扫描指定包中类上的注解,常用的注解有: @Controller 声明Action组件 @Service 声明Service组件 @Service(“myMovieLister”) @Repository 声明Dao组件 @Component 泛指组件, 当不好归类时. @RequestMapping(“/menu”) 请求映射 @Resource 用于注入,( j2ee提供的 ) 默认按名称装配,@Resource(name=”beanName”) @Autowired 用于注入,(srping提供的) 默认按类型装配 @Transactional( rollbackFor={Exception.class}) 事务管理 @ResponseBody @Scope(“prototype”) 设定bean的作用域 适配器作用SpringMVC涉及的映射器,视图解析器的作用不难理解,映射器负责将前端请求的url映射到配置的处理器,视图解析器将最终的结果进行解析,但中间为什么要经过一层适配器呢,为什么不经映射器找到controller后直接执行返回呢? 那是因为SpringMVC为业务处理器提供了多种接口实现(例如实现了Controller接口),而适配器就是用来根据处理器实现了什么接口,最终选择与已经注册好的不同类型的Handler Adapter进行匹配,并最终执行,例如,SimpleControllerHandlerAdapter是支持实现了controller接口的控制器,如果自己写的控制器实现了controller接口,那么SimpleControllerHandlerAdapter就会去执行自己写的控制器中的具体方法来完成请求。]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring事务]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2FSpring%E4%BA%8B%E5%8A%A1%2F</url>
<content type="text"><![CDATA[Spring事务机制Spring事务机制主要包括 声明式事务和编程式事务,此处侧重讲解声明式事务,编程式事务在实际开发中得不到广泛使用 Spring声明式事务让我们从复杂的事务处理中得到解脱。使得我们 再也无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。再也无需要我们在与事务相关的方法中处理大量的try…catch…finally代码。我们在使用Spring声明式事务时,有一个非常重要的概念就是事务属性。 事务属性通常由事务的传播行为,事务的隔离级别,事务的超时值和事务只读标志组成。我们在进行事务划分时,需要进行事务定义,也就是配置事务的属性。 Spring在TransactionDefinition接口中定义这些属性,以供PlatfromTransactionManager使用, PlatfromTransactionManager是Spring事务管理的核心接口。 123456public interface TransactionDefinition { int getPropagationBehavior(); //返回事务的传播行为。 int getIsolationLevel(); //返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据。 int getTimeout(); //返回事务必须在多少秒内完成。 boolean isReadOnly(); //事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的。} 1、TransactionDefinition接口中定义五个隔离级别: 123456789ISOLATION_DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应;ISOLATION_READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。ISOLATION_READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。ISOLATION_REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。 2、在TransactionDefinition接口中定义了七个事务传播行为: (1)PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 12345678910// 事务属性 PROPAGATION_REQUIREDmethodA { …… methodB(); …… }// 事务属性 PROPAGATION_REQUIREDmethodB { ……} 使用Spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。 单独调用methodB方法: 123456789101112131415161718192021main { metodB();}相当于Main { Connection con=null; try{ con = getConnection(); con.setAutoCommit(false); //方法调用 methodB(); //提交事务 con.commit(); } Catch(RuntimeException ex) { //回滚事务 con.rollback(); } finally { //释放资源 closeCon(); }} Spring保证在methodB方法中所有的调用都获得到一个相同的连接。在调用methodB时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。 单独调用MethodA时,在MethodA内又会调用MethodB。执行效果相当于: 123456789101112Main { Connection con = null; try { con = getConnection(); methodA(); con.commit(); } catch(RuntimeException ex) { con.rollback(); } finally { closeCon(); }} 调用MethodA时,环境中没有事务,所以开启一个新的事务.当在MethodA中调用MethodB时,环境中已经有了一个事务,所以methodB就加入当前事务。 (2)PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。 12345678// 事务属性 PROPAGATION_REQUIREDmethodA() { methodB();}// 事务属性 PROPAGATION_SUPPORTSmethodB() { ……} 单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中执行。 (3)PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 12345678// 事务属性 PROPAGATION_REQUIREDmethodA() { methodB();}//事务属性 PROPAGATION_MANDATORY methodB() { ……} 当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”); 当调用methodA时,methodB则加入到methodA的事务中执行。 (4)PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。 1234567891011121314//事务属性 PROPAGATION_REQUIREDmethodA() { doSomeThingA(); methodB(); doSomeThingB();}//事务属性 PROPAGATION_REQUIRES_NEWmethodB() { ……}main() { methodA();} 相当于: 123456789101112131415161718192021222324252627282930main() { TransactionManager tm = null; try { // 获得一个JTA事务管理器 tm = getTransactionManager(); tm.begin(); // 开启一个新的事务 Transaction ts1 = tm.getTransaction(); doSomeThing(); tm.suspend(); // 挂起当前事务 try { tm.begin();// 重新开启第二个事务 Transaction ts2 = tm.getTransaction(); methodB(); ts2.commit();// 提交第二个事务 } catch(RunTimeException ex) { ts2.rollback(); // 回滚第二个事务 } finally { // 释放资源 } // methodB执行完后,恢复第一个事务 tm.resume(ts1); doSomeThingB(); ts1.commit();// 提交第一个事务 } catch(RunTimeException ex) { ts1.rollback();// 回滚第一个事务 } finally { //释放资源 }} 在这里,我把 ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了methodB之外的其它代码导致的结果却被回滚了。使用PROPAGATION_REQUIRES_NEW,需要使用JtaTransactionManager作为事务管理器。 (5)PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。 (6)PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常; (7)PROPAGATION_NESTED如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。需要JDBC 驱动的java.sql.Savepoint类。 有一些JTA的事务管理器实现可能也提供了同样的功能。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true; 而nestedTransactionAllowed属性值默认为false; 12345678910// 事务属性 PROPAGATION_REQUIREDmethodA() { doSomeThingA(); methodB(); doSomeThingB();}//事务属性 PROPAGATION_NESTEDmethodB() { ……} 如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果: 1234567891011121314151617181920212223main() { Connection con = null; Savepoint savepoint = null; try{ con = getConnection(); con.setAutoCommit(false); doSomeThingA(); savepoint = con.setSavepoint(); try{ methodB(); } catch(RuntimeException ex) { con.rollback(savepoint); } finally { //释放资源 } doSomeThingB(); con.commit(); } catch(RuntimeException ex) { con.rollback(); } finally { //释放资源 }} 当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。 嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。 使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTA TrasactionManager实现可能有不同的支持方式。 PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务。这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。 另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。 由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 潜套事务也会被 commit, 这个规则同样适用于 roll back。PROPAGATION_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。 作者:猿码道 链接:https://juejin.im/post/5a3b1dc4f265da43333e9049 来源:掘金]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Bean的生命周期]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2Fbean%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%2F</url>
<content type="text"><![CDATA[可以简述为以下九步 实例化bean对象(通过构造方法或者工厂方法) 设置对象属性(setter等)(依赖注入) 如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口) 如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身 将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法 调用Bean的初始化方法 将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法 使用Bean 容器关闭之前,调用Bean的销毁方法]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[servlet生命周期]]></title>
<url>%2F2019%2F10%2F21%2F%E6%A1%86%E6%9E%B6%2Fservlet%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%2F</url>
<content type="text"><![CDATA[Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程: Servlet 通过调用 init () 方法进行初始化。 Servlet 调用 service() 方法来处理客户端的请求。 Servlet 通过调用 destroy() 方法终止(结束)。 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 加载和实例化当Servlet容器启动或客户端发送一个请求时,Servlet容器会查找内存中是否存在该Servlet实例,若存在,则直接读取该实例响应请求;如果不存在,就创建一个Servlet实例。 init() 方法init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。 Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。 当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。 123public void init() throws ServletException { // 初始化代码...} service() 方法service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。 每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。 下面是该方法的特征: 1234public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{} service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。 doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。 doGet() 方法GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。 12345public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Servlet 代码} doPost() 方法POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。 12345public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Servlet 代码} destroy() 方法当Servlet容器关闭时,Servlet实例也随时销毁。其间,Servlet容器会调用Servlet 的destroy()方法去判断该Servlet是否应当被释放(或回收资源) destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。 在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示: 123public void destroy() { // 终止化代码...} 下图显示了一个典型的 Servlet 生命周期方案。 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。 Servlet 容器在调用 service() 方法之前加载 Servlet。 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。 * 1、从第一次调用到服务器关闭。 * 2、如果Servlet在web.xml中配置了load-on-startup,生命周期为从服务器启动到服务器关闭。]]></content>
<categories>
<category>框架</category>
</categories>
<tags>
<tag>框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux基础命令]]></title>
<url>%2F2019%2F10%2F21%2FLinux%2FLinux%E5%9F%BA%E7%A1%80%E5%91%BD%E4%BB%A4%2F</url>
<content type="text"><![CDATA[切换目录 cdcd 命令,是 Change Directory 的缩写,用来切换工作目录。 cd 命令的基本格式如下: 1[root@localhost ~]# cd [相对路径或绝对路径] 特殊符号 作 用 ~ 代表当前登录用户的主目录 ~用户名 表示切换至指定用户的主目录 - 代表上次所在目录 . 代表当前目录 .. 代表上级目录 123456[root@localhost vbird]# cd ~#表示回到自己的主目录,对于 root 用户,其主目录为 /root[root@localhost ~]# cd#没有加上任何路径,也代表回到当前登录用户的主目录[root@localhost ~]# cd ~vbird#代表切换到 vbird 这个用户的主目录,亦即 /home/vbird 12[root@localhost ~]# cd ..#表示切换到目前的上一级目录,亦即是 /root 的上一级目录的意思; 12[root@localhost /]# cd -#表示回到刚刚的那个目录 显示当前路径 pwdpwd 命令,是 Print Working Directory (打印工作目录)的缩写,功能是显示用户当前所处的工作目录。该命令的基本格式为: 1[root@localhost ~]# pwd 1234[root@localhost ~]# whoamiroot[root@localhost ~]# pwd/root whoami 命令用于确定当前登陆的用户 查看目录下文件 lsls 命令,list 的缩写 此命令的基本格式为: 1[root@localhost ~]# ls [选项] 目录名称 选项 功能 -a 显示全部的文件,包括隐藏文件(开头为 . 的文件)也一起罗列出来,这是最常用的选项之一。 -A 显示全部的文件,连同隐藏文件,但不包括 . 与 .. 这两个目录。 -d 仅列出目录本身,而不是列出目录内的文件数据。 -f ls 默认会以文件名排序,使用 -f 选项会直接列出结果,而不进行排序。 -F 在文件或目录名后加上文件类型的指示符号,例如,* 代表可运行文件,/ 代表目录,= 代表 socket 文件,\ 代表 FIFO 文件。 -h 以人们易读的方式显示文件或目录大小,如 1KB、234MB、2GB 等。 -i 显示 inode 节点信息。 -l 使用长格式列出文件和目录信息。 -n 以 UID 和 GID 分别代替文件用户名和群组名显示出来。 -r 将排序结果反向输出,比如,若原本文件名由小到大,反向则为由大到小。 -R 连同子目录内容一起列出来,等於将该目录下的所有文件都显示出来。 -S 以文件容量大小排序,而不是以文件名排序。 -t 以时间排序,而不是以文件名排序。 –color=never –color=always –color=auto never 表示不依据文件特性给予颜色显示。 always 表示显示颜色,ls 默认采用这种方式。 auto 表示让系统自行依据配置来判断是否给予颜色。 –full-time 以完整时间模式 (包含年、月、日、时、分)输出 –time={atime,ctime} 输出 access 时间或改变权限属性时间(ctime),而不是内容变更时间。 当 ls 命令不使用任何选项时,默认只会显示非隐藏文件的名称,并以文件名进行排序,同时会根据文件的具体类型给文件名配色(蓝色显示目录,白色显示一般文件) 123456789101112[root@www ~]# ls -al ~total 156drwxr-x--- 4 root root 4096 Sep 24 00:07 .drwxr-xr-x 23 root root 4096 Sep 22 12:09 ..-rw------- 1 root root 1474 Sep 4 18:27 anaconda-ks.cfg-rw------- 1 root root 955 Sep 24 00:08 .bash_history-rw-r--r-- 1 root root 24 Jan 6 2007 .bash_logout-rw-r--r-- 1 root root 191 Jan 6 2007 .bash_profile-rw-r--r-- 1 root root 176 Jan 6 2007 .bashrcdrwx------ 3 root root 4096 Sep 5 10:37 .gconf-rw-r--r-- 1 root root 42304 Sep 4 18:26 install.log-rw-r--r-- 1 root root 5661 Sep 4 18:25 install.log.syslog ls 命令还使用了 -l 选项,因此才显示出了文件的详细信息,此选项显示的这 7 列的含义分别是: 第一列:规定了不同的用户对文件所拥有的权限,具体权限的含义将在后续章节中讲解。 第二列:引用计数,文件的引用计数代表该文件的硬链接个数,而目录的引用计数代表该目录有多少个一级子目录。 第三列:所有者,也就是这个文件属于哪个用户。默认所有者是文件的建立用户。 第四列:所属组,默认所属组是文件建立用户的有效组,一般情况下就是建立用户的所在组。 第五列:大小,默认单位是字节。 第六列:文件修改时间,文件状态修改时间或文件数据修改时间都会更改这个时间,注意这个时间不是文件的创建时间。 第七列:文件名或目录名。 创建目录(文件夹)mkdirmkdir 命令,是 make directories 的缩写,用于创建新目录 mkdir 命令的基本格式为: 1[root@localhost ~]# mkdir [-mp] 目录名 -m 选项用于手动配置所创建目录的权限,而不再使用默认权限。 -p 选项递归创建所有目录,以创建 /home/test/demo 为例,在默认情况下,你需要一层一层的创建各个目录,而使用 -p 选项,则系统会自动帮你创建 /home、/home/test 以及 /home/test/demo。 删除空目录 rmdirrmdir(remove empty directories 的缩写)命令用于删除空目录,此命令的基本格式为: 1[root@localhost ~]# rmdir [-p] 目录名 -p 选项用于递归删除空目录。 rmdir 命令的作用十分有限,因为只能刪除空目录,所以一旦目录中有内容,就会报错。例如: 1234567[root@localhost # mkdir test#建立测试目录[root@localhost ~]# touch test/boduo[root@localhost ~]# touch test/longze#在测试目录中建立两个文件[root@localhost ~]# rmdir testrmdir:删除"test"失败:目录非空 创建文件及修改文件时间戳touch 命令不止可以用来创建文件(当指定操作文件不存在时,该命令会在当前位置建立一个空文件),此命令更重要的功能是修改文件的时间参数(但当文件存在时,会修改此文件的时间参数)。 Linux 系统中,每个文件主要拥有 3 个时间参数(通过 stat 命令进行查看),分别是文件的访问时间、数据修改时间以及状态修改时间: 访问时间(Access Time,简称 atime):只要文件的内容被读取,访问时间就会更新。例如,使用 cat 命令可以查看文件的内容,此时文件的访问时间就会发生改变。 数据修改时间(Modify Time,简称 mtime):当文件的内容数据发生改变,此文件的数据修改时间就会跟着相应改变。 状态修改时间(Change Time,简称 ctime):当文件的状态发生变化,就会相应改变这个时间。比如说,如果文件的权限或者属性发生改变,此时间就会相应改变。 touch 命令的基本格式如下: 1[root@localhost ~]# touch [选项] 文件名 选项: -a:只修改文件的访问时间; -c:仅修改文件的时间参数(3 个时间参数都改变),如果文件不存在,则不建立新文件。 -d:后面可以跟欲修订的日期,而不用当前的日期,即把文件的 atime 和 mtime 时间改为指定的时间。 -m:只修改文件的数据修改时间。 -t:命令后面可以跟欲修订的时间,而不用目前的时间,时间书写格式为 YYMMDDhhmm。 touch 命令可以只修改文件的访问时间,也可以只修改文件的数据修改时间,但是不能只修改文件的状态修改时间。因为,不论是修改访问时间,还是修改文件的数据时间,对文件来讲,状态都会发生改变,即状态修改时间会随之改变(更新为操作当前文件的真正时间)。 touch 命令创建文件。 12[root@localhost ~]#touch bols#建立名为 bols 的空文件 在上面的基础上修改文件的访问时间。 12345678[root@localhost ~]#ll --time=atime bols#查看文件的访问时间-rw-r--r-- 1 root root 0 Sep 25 21:23 bols#文件上次的访问时间为 9 月 25 号 21:23[root@localhost ~]#touch bols[root@localhost ~]#ll --time=atime bols-rw-r--r-- 1 root root 0 May 15 16:36 bols#而如果文件已经存在,则也不会报错,只是会修改文件的访问时间。 修改 bols 文件的 atime 和 mtime。 123456[root@localhost ~]# touch -d "2017-05-04 15:44" bols[root@localhost ~]# ll bols; ll --time=atime bols; ll --time=ctime bols-rw-r--r-- 1 root root 0 May 4 2017 bols-rw-r--r-- 1 root root 0 May 4 2017 bols-rw-r--r-- 1 root root 0 Sep 25 21:40 bols#ctime不会变为设定时间,但更新为当前服务器的时间 在文件之间建立链接(硬链接和软链接)lnLinux 目前使用的是 ext4 文件系统。如果用一张示意图来描述 ext4 文件系统 ext4 文件系统会把分区主要分为两大部分(暂时不提超级块):小部分用于保存文件的 inode (i 节点)信息;剩余的大部分用于保存 block 信息。 inode 的默认大小为 128 Byte,用来记录文件的权限(r、w、x)、文件的所有者和属组、文件的大小、文件的状态改变时间(ctime)、文件的最近一次读取时间(atime)、文件的最近一次修改时间(mtime)、文件的数据真正保存的 block 编号。每个文件需要占用一个 inode。仔细查看,就会发现 inode 中是不记录文件名的,那是因为文件名记录在文件所在目录的 block 中。 block 的大小可以是 1KB、2KB、4KB,默认为 4KB。block 用于实际的数据存储,如果一个 block 放不下数据,则可以占用多个 block。例如,有一个 10KB 的文件需要存储,则会占用 3 个 block,虽然最后一个 block 不能占满,但也不能再放入其他文件的数据。这 3 个 block 有可能是连续的,也有可能是分散的。 由此,可以知道以下 2 个重要的信息: 每个文件都独自占用一个 inode,文件内容由 inode 的记录来指向; 如果想要读取文件内容,就必须借助目录中记录的文件名找到该文件的 inode,才能成功找到文件内容所在的 block 块; ln 命令用于给文件创建链接,根据 Linux 系统存储文件的特点,链接的方式分为以下 2 种: 软链接:类似于 Windows 系统中给文件创建快捷方式,即产生一个特殊的文件,该文件用来指向另一个文件,此链接方式同样适用于目录。 硬链接:我们知道,文件的基本信息都存储在 inode 中,而硬链接指的就是给一个文件的 inode 分配多个文件名,通过任何一个文件名,都可以找到此文件的 inode,从而读取该文件的数据信息。 ln 命令的基本格式如下: 1[root@localhost ~]# ln [选项] 源文件 目标文件 选项: -s:建立软链接文件。如果不加 “-s” 选项,则建立硬链接文件; -f:强制。如果目标文件已经存在,则删除目标文件后再建立链接文件; 创建硬链接: 1234[root@localhost ~]# touch cangls[root@localhost ~]# ln /root/cangls /tmp#建立硬链接文件,目标文件没有写文件名,会和原名一致#也就是/tmp/cangls 是硬链接文件 创建软链接: 123[root@localhost ~]# touch bols[root@localhost ~]# ln -s /root/bols /tmp#建立软链接文件 这里需要注意,软链接文件的源文件必须写成绝对路径,而不能写成相对路径(硬链接没有这样的要求);否则软链接文件会报错。 复制文件和目录 cpcp 命令,主要用来复制文件和目录,同时借助某些选项,还可以实现复制整个目录,以及比对两文件的新旧而予以升级等功能。 cp 命令的基本格式如下: 1[root@localhost ~]# cp [选项] 源文件 目标文件 选项: -a:相当于 -d、-p、-r 选项的集合; -d:如果源文件为软链接(对硬链接无效),则复制出的目标文件也为软链接; -i:询问,如果目标文件已经存在,则会询问是否覆盖; -l:把目标文件建立为源文件的硬链接文件,而不是复制源文件; -s:把目标文件建立为源文件的软链接文件,而不是复制源文件; -p:复制后目标文件保留源文件的属性(包括所有者、所属组、权限和时间); -r:递归复制,用于复制目录; -u:若目标文件比源文件有差异,则使用该选项可以更新目标文件,此选项可用于对文件的升级和备用。 需要注意的是,源文件可以有多个,但这种情况下,目标文件必须是目录才可以。 删除文件或目录 rmrm 是强大的删除命令,它可以永久性地删除文件系统中指定的文件或目录。在使用 rm 命令删除文件或目录时,系统不会产生任何提示信息。此命令的基本格式为: 1[root@localhost ~]# rm[选项] 文件或目录 选项: -f:强制删除(force),和 -i 选项相反,使用 -f,系统将不再询问,而是直接删除目标文件或目录。 -i:和 -f 正好相反,在删除文件或目录之前,系统会给出提示信息,使用 -i 可以有效防止不小心删除有用的文件或目录。 -r:递归删除,主要用于删除目录,可删除指定目录及包含的所有内容,包括所有的子目录和文件。 移动文件或改名 mvmv 命令(move 的缩写),既可以在不同的目录之间移动文件或目录,也可以对文件和目录进行重命名。该命令的基本格式如下: 1[root@localhost ~]# mv 【选项】 源文件 目标文件 选项: -f:强制覆盖,如果目标文件已经存在,则不询问,直接强制覆盖; -i:交互移动,如果目标文件已经存在,则询问用户是否覆盖(默认选项); -n:如果目标文件已经存在,则不会覆盖移动,而且不询问用户; -v:显示文件或目录的移动过程; -u:若目标文件已经存在,但两者相比,源文件更新,则会对目标文件进行升级; 需要注意的是,同 rm 命令类似,mv 命令也是一个具有破坏性的命令,如果使用不当,很可能给系统带来灾难性的后果。 移动文件或目录。 12345[root@localhost ~]# mv cangls /tmp#移动之后,源文件会被删除,类似剪切[root@localhost ~]# mkdir movie[root@localhost ~]# mv movie/ /tmp#也可以移动目录。和 rm、cp 不同的是,mv 移动目录不需要加入 "-r" 选项 如果移动的目标位置已经存在同名的文件,则同样会提示是否覆盖,因为 mv 命令默认执行的也是 “mv -i” 的别名,例如: 12345[root@localhost ~]# touch cangls#重新建立文件[root@localhost ~]# mv cangls /tmpmv:县否覆盖"tmp/cangls"?y#由于 /tmp 目录下已经存在 cangls 文件,所以会提示是否覆盖,需要手工输入 y 覆盖移动 强制移动。 如果我们确认需要覆盖已经存在的同名文件,则可以使用 “-f” 选项进行强制移动,这就不再需要用户手工确认了 1234[root@localhost ~]# touch cangls#重新建立文件[root@localhost ~]# mv -f cangls /tmp#就算 /tmp/ 目录下已经存在同名的文件,由于"-f"选项的作用,所以会强制覆盖 不覆盖移动。 既然可以强制覆盖移动,那也有可能需要不覆盖的移动。如果需要移动几百个同名文件,但是不想覆盖,这时就需要 “-n” 选项的帮助了。例如: 123456[root@localhost ~]# ls /tmp/tmp/bols /tmp/cangls#在/tmp/目录下已经存在bols、cangls文件了[root@localhost ~]# mv -vn bols cangls lmls /tmp/、"lmls"->"/tmp/lmls"#再向 /tmp/ 目录中移动同名文件,如果使用了 "-n" 选项,则可以看到只移动了 lmls,而同名的 bols 和 cangls 并没有移动("-v" 选项用于显示移动过程) 改名。如果源文件和目标文件在同一目录中,那就是改名。例如: 12[root@localhost ~]# mv bols lmls#把 bols 改名为 lmls 目录也可以按照同样的方法改名。 显示移动过程。如果我们想要知道在移动过程中到底有哪些文件进行了移动,则可以使用 “-v” 选项来查看详细的移动信息。例如: 1234567[root@localhost ~]# touch test1.txt test2.txt test3.txt#建立三个测试文件[root@localhost ~]# mv -v *.txt /tmp"test1.txt" -> "/tmp/test1.txt""test2.txt" -> "/tmp/test2.txt""test3.txt" -> "/tmp/test3.txt"#加入"-v"选项,可以看到有哪些文件进行了移动 自动补全功能和通配符按一次 Tab 键补全 连续按两次 Tab 键出对应列表 符号 作用 * 匹配任意数量的字符。 ? 匹配任意一个字符。 [] 匹配括号内的任意一个字符,甚至 [] 中还可以包含用 -(短横线)连接的字符或数字,表示一定范围内的字符或数字。 打包操作 tar当 tar 命令用于打包操作时,该命令的基本格式为: 1[root@localhost ~]#tar [选项] 源文件或目录 选项 含义 -c 将多个文件或目录进行打包。 -A 追加 tar 文件到归档文件。 -f 包名 指定包的文件名。包的扩展名是用来给管理员识别格式的,所以一定要正确指定扩展名; -v 显示打包文件过程; 打包文件和目录: 12[root@localhost ~]# tar -cvf anaconda-ks.cfg.tar anaconda-ks.cfg#把anacondehks.cfg打包为 anacondehks.cfg.tar文件 选项 “-cvf” 一般是习惯用法,打包时需要指定打包之后的文件名,而且要用 “.tar” 作为扩展名。打包目录也是如此: 123456789101112[root@localhost ~]# ll -d test/drwxr-xr-x 2 root root 4096 6月 17 21:09 test/#test是我们之前的测试目录[root@localhost ~]# tar -cvf test.tar test/test/test/test3test/test2test/test1#把目录打包为test.tar文件tar命令也可以打包多个文件或目录,只要用空格分开即可。例如:[root@localhost ~]# tar -cvf ana.tar anaconda-ks.cfg /tmp/#把anaconda-ks.cfg文件和/tmp目录打包成ana.tar文件包 打包并压缩目录: 压缩命令不能直接压缩目录,必须先用 tar 命令将目录打包,然后才能用 gzip 命令或 bzip2 命令对打包文件进行压缩。 1234567drwxr-xr-x 2 root root 4096 6月 17 21:09 test-rw-r--r-- 1 root root 10240 6月 18 01:06 test.tar#我们之前已经把test目录打包成test.tar文件[root@localhost ~]# gzip test.tar[root@localhost ~]# ll test.tar.gz-rw-r--r-- 1 root root 176 6月 18 01:06 test.tar.gz#gzip命令会把test.tar压缩成test.tar.gz 解打包操作 tar当 tar 命令用于对 tar 包做解打包操作时,该命令的基本格式如下: 1[root@localhost ~]#tar [选项] 压缩包 选项 含义 -x 对 tar 包做解打包操作。 -f 指定要解压的 tar 包的包名。 -t 只查看 tar 包中有哪些文件或目录,不对 tar 包做解打包操作。 -C 目录 指定解打包位置。 -v 显示解打包的具体过程。 解打包和打包相比,只是把打包选项 “-cvf” 更换为 “-xvf”: 12[root@localhost ~]# tar -xvf anaconda-ks.cfg. tar#解打包到当前目录下 如果使用 “-xvf” 选项,则会把包中的文件解压到当前目录下。如果想要指定解压位置,则需要使用 “-C(大写)” 选项。例如: 12[root@localhost ~]# tar -xvf test.tar -C /tmp#把文件包test.tar解打包到/tmp/目录下 如果只想查看文件包中有哪些文件,则可以把解打包选项 “-x” 更换为测试选项 “-t”: 123456[root@localhost ~]# tar -tvf test.tardrwxr-xr-x root/root 0 2016-06-17 21:09 test/-rw-r-r- root/root 0 2016-06-17 17:51 test/test3-rw-r-r- root/root 0 2016-06-17 17:51 test/test2-rw-r-r- root/root 0 2016-06-17 17:51 test/test1#会用长格式显示test.tar文件包中文件的详细信息 打包压缩(解压缩解打包)tartar 命令是可以同时打包压缩的,前面的打包和压缩分开,是为了了解在 Linux 中打包和压缩的不同。 当 tar 命令同时做打包压缩的操作时,其基本格式如下: 1[root@localhost ~]#tar [选项] 压缩包 源文件或目录 常用的选项有以下 2 个,分别是: -z:压缩和解压缩 “.tar.gz” 格式; -j:压缩和解压缩 “.tar.bz2”格式。 压缩与解压缩 “.tar.gz”格式: 12[root@localhost ~]# tar -zcvf tmp.tar.gz /tmp/#把/temp/目录直接打包压缩为".tar.gz"格式,通过"-z"来识别格式,"-cvf"和打包选项一致 解压缩也只是在解打包选项 “-xvf” 前面加了一个 “-z” 选项: 12[root@localhost ~]# tar -zxvf tmp.tar.gz#解压缩与解打包".tar.gz"格式 前面讲的选项 “-C” 用于指定解压位置、”-t” 用于查看压缩包内容,在这里同样适用。 压缩与解压缩 “.tar.bz2” 格式:和”.tar.gz”格式唯一的不同就是”-zcvf”选项换成了 “-jcvf”,如下所示: 1234[root@localhost ~]# tar -jcvf tmp.tar.bz2 /tmp/#打包压缩为".tar.bz2"格式,注意压缩包文件名[root@localhost ~]# tar -jxvf tmp.tar.bz2#解压缩与解打包".tar.bz2"格式 压缩文件或目录 zip zip 命令,类似于 Windows 系统中的 winzip 压缩程序,其基本格式如下: 1[root@localhost ~]#zip [选项] 压缩包名 源文件或源目录列表 注意,zip 压缩命令需要手工指定压缩之后的压缩包名,注意写清楚扩展名,以便解压缩时使用。 选项 含义 -r 递归压缩目录,及将制定目录下的所有文件以及子目录全部压缩。 -m 将文件压缩之后,删除原始文件,相当于把文件移到压缩文件中。 -v 显示详细的压缩过程信息。 -q 在压缩的时候不显示命令的执行过程。 -压缩级别 压缩级别是从 1~9 的数字,-1 代表压缩速度更快,-9 代表压缩效果更好。 -u 更新压缩文件,即往压缩文件中添加新文件。 zip 命令的基本使用: 123456[root@localhost ~]# zip ana.zip anaconda-ks.cfgadding: anaconda-ks.cfg (deflated 37%)#压缩[root@localhost ~]# ll ana.zip-rw-r--r-- 1 root root 935 6月 1716:00 ana.zip#压缩文件生成 所有的压缩命令都可以同时压缩多个文件,例如: 1234567[root@localhost ~]# zip test.zip install.log install.log.syslogadding: install.log (deflated 72%)adding: install.log.syslog (deflated 85%)#同时压缩多个文件到test.zip压缩包中[root@localhost ~]#ll test.zip-rw-r--r-- 1 root root 8368 6月 1716:03 test.zip#压缩文件生成 使用 zip 命令压缩目录,需要使用“-r”选项,例如: 12345678[root@localhost ~]# mkdir dir1#建立测试目录[root@localhost ~]# zip -r dir1.zip dir1adding: dir1/(stored 0%)#压缩目录[root@localhost ~]# ls -dl dir1.zip-rw-r--r-- 1 root root 160 6月 1716:22 dir1.zip#压缩文件生成 解压zip文件 unzipunzip 命令可以查看和解压缩 zip 文件。该命令的基本格式如下: 1[root@localhost ~]# unzip [选项] 压缩包名 选项 含义 -d 目录名 将压缩文件解压到指定目录下。 -n 解压时并不覆盖已经存在的文件。 -o 解压时覆盖已经存在的文件,并且无需用户确认。 -v 查看压缩文件的详细信息,包括压缩文件中包含的文件大小、文件名以及压缩比等,但并不做解压操作。 -t 测试压缩文件有无损坏,但并不解压。 -x 文件列表 解压文件,但不包含文件列表中指定的文件。 不论是文件压缩包,还是目录压缩包,都可以直接解压缩,例如: 1234[root@localhost ~]# unzip dir1.zipArchive: dir1.zipcreating: dirl/#解压缩 使用 -d 选项手动指定解压缩位置,例如: 1234[root@localhost ~]# unzip -d /tmp/ ana.zipArchive: ana.zipinflating: /tmp/anaconda-ks.cfg#把压缩包解压到指定位置 压缩文件或目录 gzipgzip 是 Linux系统中经常用来对文件进行压缩和解压缩的命令,通过此命令压缩得到的新文件,其扩展名通常标记为“.gz”。 gzip 命令只能用来压缩文件,不能压缩目录,即便指定了目录,也只能压缩目录内的所有文件。 gzip 命令的基本格式如下: 1[root@localhost ~]# gzip [选项] 源文件 选项 含义 -c 将压缩数据输出到标准输出中,并保留源文件。 -d 对压缩文件进行解压缩。 -r 递归压缩指定目录下以及子目录下的所有文件。 -v 对于每个压缩和解压缩的文件,显示相应的文件名和压缩比。 -l 对每一个压缩文件,显示以下字段:压缩文件的大小;未压缩文件的大小;压缩比;未压缩文件的名称。 -数字 用于指定压缩等级,-1 压缩等级最低,压缩比最差;-9 压缩比最高。默认压缩比是 -6。 基本压缩:gzip 压缩命令非常简单,甚至不需要指定压缩之后的压缩包名,只需指定源文件名即可: 12345[root@localhost ~]# gzip install.log#压缩instal.log 文件[root@localhost ~]# lsanaconda-ks.cfg install.log.gz install.log.syslog#压缩文件生成,但是源文件也消失了 保留源文件压缩:在使用 gzip 命令压缩文件时,源文件会消失,从而生成压缩文件。可以在压缩文件的时候,不让源文件消失: 12345[root@localhost ~]# gzip -c anaconda-ks.cfg >anaconda-ks.cfg.gz#使用-c选项,但是不让压缩数据输出到屏幕上,而是重定向到压缩文件中,这样可以缩文件的同时不删除源文件[root@localhost ~]# lsanaconda-ks.cfg anaconda-ks.cfg.gz install.log.gz install.log.syslog#可以看到压缩文件和源文件都存在 压缩目录: 123456789101112[root@localhost ~]# mkdir test[root@localhost ~]# touch test/test1[root@localhost ~]# touch test/test2[root@localhost ~]# touch test/test3 #建立测试目录,并在里面建立几个测试文件[root@localhost ~]# gzip -r test/#压缩目录,并没有报错[root@localhost ~]# lsanaconda-ks.cfg anaconda-ks.cfg.gz install.log.gz install.log.syslog test#但是查看发现test目录依然存在,并没有变为压缩文件[root@localhost ~]# ls test/testl.gz test2.gz test3.gz#原来gzip命令不会打包目录,而是把目录下所有的子文件分别压缩 在 Linux 中,打包和压缩是分开处理的。而 gzip 命令只会压缩,不能打包,所以才会出现没有打包目录,而只把目录下的文件进行压缩的情况。 解压缩文件或目录 gunzipgunzip 是一个使用广泛的解压缩命令,它用于解压被 gzip 压缩过的文件(扩展名为 .gz)。 对于解压被 gzip 压缩过的文件,还可以使用 gzip 自己,即 gzip -d 解压缩包。 gunzip 命令的基本格式为: 1[root@localhost ~]# gunzip [选项] 文件 选项 含义 -r 递归处理,解压缩指定目录下以及子目录下的所有文件。 -c 把解压缩后的文件输出到标准输出设备。 -f 强制解压缩文件,不理会文件是否已存在等情况。 -l 列出压缩文件内容。 -v 显示命令执行过程。 -t 测试压缩文件是否正常,但不对其做解压缩操作。 直接解压缩文件: 1[root@localhost ~]# gunzip install.log.gz 当然,”gunzip -r”依然只会解压缩目录下的文件,而不会解打包。要想解压缩”.gz”格式,还可以使用 “gzip -d”命令,例如: 1[root@localhost ~]# gzip -d anaconda-ks.cfg.gz 要解压缩目录下的内容,则需使用 “-r” 选项,例如: 1[root@localhost ~]# gunzip -r test/ 如果我们压缩的是一个纯文本文件,则可以直接使用 zcat 命令在不解压缩的情况下查看这个文本文件中的内容。例如: 1[root@localhost ~]# zcat anaconda-ks.cfg.gz 压缩文件(.bz2格式)bzip2bzip2 命令同 gzip 命令类似,只能对文件进行压缩(或解压缩),对于目录只能压缩(或解压缩)该目录及子目录下的所有文件。当执行压缩任务完成后,会生成一个以“.bz2”为后缀的压缩包。 bzip2 命令的基本格式如下: 1[root@localhost ~]# bzip2 [选项] 源文件 选项 含义 -d 执行解压缩,此时该选项后的源文件应为标记有 .bz2 后缀的压缩包文件。 -k bzip2 在压缩或解压缩任务完成后,会删除原始文件,若要保留原始文件,可使用此选项。 -f bzip2 在压缩或解压缩时,若输出文件与现有文件同名,默认不会覆盖现有文件,若使用此选项,则会强制覆盖现有文件。 -t 测试压缩包文件的完整性。 -v 压缩或解压缩文件时,显示详细信息。 -数字 这个参数和 gzip 命令的作用一样,用于指定压缩等级,-1 压缩等级最低,压缩比最差;-9 压缩比最高 gzip 只是不会打包目录,但是如果使用“-r”选项,则可以分别压缩目录下的每个文件;而 bzip2 命令则根本不支持压缩目录,也没有“-r”选项。 直接压缩文件: 12[root@localhost ~]# bzip2 anaconda-ks.cfg#压缩成".bz2"格式 此压缩命令会在压缩的同时删除源文件。 压缩的同时保留源文件: 12345[root@localhost ~]# bzip2 -k install.log.syslog#压缩[root@localhost ~]# lsanaconda-ks.cfg.bz2 install.loginstalLlogsyslog install.logsyslogbz2#压缩文件和源文件都存在 bz2格式的解压缩 bunzip2要解压“.bz2”格式的压缩包文件,除了使用“bzip2 -d 压缩包名”命令外,还可以使用 bunzip2 命令。 bunzip2 命令的使用和 gunzip 命令大致相同,bunzip2 命令只能用于解压文件,即便解压目录,也是解压该目录以及所含子目录下的所有文件。 bunzip2 命令的基本格式为: 1[root@localhost ~]# bunzip2 [选项] 源文件 选项 含义 -k 解压缩后,默认会删除原来的压缩文件。若要保留压缩文件,需使用此参数。 -f 解压缩时,若输出的文件与现有文件同名时,默认不会覆盖现有的文件。若要覆盖,可使用此选项。 -v 显示命令执行过程。 -L 列出压缩文件内容。 使用 gunzip2 命令来进行解压缩: 1[root@localhost ~]# bunzip2 anaconda-ks.cfg.bz2 “.bz2” 格式也可以使用 “bzip2 -d 压缩包” 命令来进行解压缩,例如: 1[root@localhost ~]# bzip2 -d install.log.syslog.bz2 和 “.gz” 格式一样,”.bz2” 格式压缩的纯文本文件也可以不解压缩直接查看,使用的命令是 bzcat。例如: 1[root@localhost ~]# bzcat install.log.syslog.bz2 vim基本操作使用 Vim 编辑文件时,存在 3 种工作模式,分别是命令模式、输入模式和编辑模式,这 3 种工作模式可随意切换 Vim的命令模式 使用 Vim 编辑文件时,默认处于命令模式。此模式下,可使用方向键(上、下、左、右键)或 k、j、h、i 移动光标的位置,还可以对文件内容进行复制、粘贴、替换、删除等操作。 Vim的输入模式 在输入模式下,Vim 可以对文件执行写操作,类似于在 Windows 系统的文档中输入内容。 使 Vim 进行输入模式的方式是在命令模式状态下输入 i、I、a、A、o、O 等插入命令(各指令的具体功能如表 3 所示),当编辑文件完成后按 Esc 键即可返回命令模式。 快捷键 功能描述 i 在当前光标所在位置插入随后输入的文本,光标后的文本相应向右移动 I 在光标所在行的行首插入随后输入的文本,行首是该行的第一个非空白字符,相当于光标移动到行首执行 i 命令 o 在光标所在行的下面插入新的一行。光标停在空行首,等待输入文本 O 在光标所在行的上面插入新的一行。光标停在空行的行首,等待输入文本 a 在当前光标所在位置之后插入随后输入的文本 A 在光标所在行的行尾插入随后输入的文本,相当于光标移动到行尾再执行a命令 Vim 的编辑模式 编辑模式用于对文件中的指定内容执行保存、查找或替换等操作。 使 Vim 切换到编辑模式的方法是在命令模式状态下按“:”键,此时 Vim 窗口的左下方出现一个“:”符号,这是就可以输入相关指令进行操作了。 指令执行后 Vim 会自动返回命令模式。如想直接返回命令模式,按 Esc 即可。 Vim 打开文件 打开方法如下: 1[root@itxdl ~]# vim /test/vi.test Vi 使用的选项 说 明 vim filename 打开或新建一个文件,并将光标置于第一行的首部 vim -r filename 恢复上次 vim 打开时崩溃的文件 vim -R filename 把指定的文件以只读方式放入 Vim 编辑器中 vim + filename 打开文件,并将光标置于最后一行的首部 vi +n filename 打开文件,并将光标置于第 n 行的首部 vi +/pattern filename 打幵文件,并将光标置于第一个与 pattern 匹配的位置 vi -c command filename 在对文件进行编辑前,先执行指定的命令 使用 Vim 进行编辑 Vim 插入文本 从命令模式进入输入模式进行编辑,可以按下 I、i、O、o、A、a 等键来完成,使用不同的键,光标所处的位置不同 快捷键 功能描述 i 在当前光标所在位置插入随后输入的文本,光标后的文本相应向右移动 I 在光标所在行的行首插入随后输入的文本,行首是该行的第一个非空白字符,相当于光标移动到行首执行 i 命令 o 在光标所在行的下面插入新的一行。光标停在空行首,等待输入文本 O(大写) 在光标所在行的上面插入新的一行。光标停在空行的行首,等待输入文本 a 在当前光标所在位置之后插入随后输入的文本 A 在光标所在行的行尾插入随后输入的文本,相当于光标移动到行尾再执行 a 命令 Vim 查找文本 快捷键 功能描述 /abc 从光标所在位置向前查找字符串 abc /^abc 查找以 abc 为行首的行 /abc$ 查找以 abc 为行尾的行 ?abc 从光标所在为主向后查找字符串 abc n 向同一方向重复上次的查找指令 N 向相反方向重复上次的查找指定 例如,在 /etc/passwd.vi 文件中查找字符串 “root”,则运行命令如下图 在查找过程中需要注意的是,要查找的字符串是严格区分大小写的,如查找 “shenchao” 和 “ShenChao” 会得到不同的结果。如果想忽略大小写,则输入命令 “:set ic”;调整回来输入”:set noic”。 如果在字符串中出现特殊符号,则需要加上转义字符 “\”。常见的特殊符号有 \、*、?、$ 等。如果出现这些字符,例如,要查找字符串 “10$”,则需要在命令模式中输入 “/10\$”。 Vim 替换文本 快捷键 功能描述 r 替换光标所在位置的字符 R 从光标所在位置开始替换字符,其输入内容会覆盖掉后面等长的文本内容,按“Esc”可以结束 :s/a1/a2/g 将当前光标所在行中的所有 a1 用 a2 替换 :n1,n2s/a1/a2/g 将文件中 n1 到 n2 行中所有 a1 都用 a2 替换 :g/a1/a2/g 将文件中所有的 a1 都用 a2 替换 例如,要将某文件中所有的 “root” 替换为 “liudehua”,则有两种输入命令,分别为: 123:1, $s/root/liudehua/g或:%s/root/liudehua/g 上述命令是在编辑模式下操作的,表示的是从第一行到最后一行,即全文查找 “root”,然后替换成 “liudehua”。 如果刚才的命令变成 :10,20 s/root/liudehua/g,则只替换从第 10 行到第 20 行的 “root”。 Vim删除文本 快捷键 功能描述 x 删除光标所在位置的字符 dd 删除光标所在行 ndd 删除当前行(包括此行)后 n 行文本 dG 删除光标所在行一直到文件末尾的所有内容 D 删除光标位置到行尾的内容 :a1,a2d 函数从 a1 行到 a2 行的文本内容 注意,被删除的内容并没有真正删除,都放在了剪贴板中。将光标移动到指定位置处,按下 “p” 键,就可以将刚才删除的内容又粘贴到此处。 Vim复制和粘贴文本 快捷键 功能描述 p 将剪贴板中的内容粘贴到光标后 P(大写) 将剪贴板中的内容粘贴到光标前 y 复制已选中的文本到剪贴板 yy 将光标所在行复制到剪贴板,此命令前可以加数字 n,可复制多行 yw 将光标位置的单词复制到剪贴板 Vim其他常用快捷键 某些情况下,可能需要把两行进行连接。比如说,下面的文件中有两行文本,现在需要将其合并成一行(实际上就是将两行间的换行符去掉)。可以直接在命令模式中按下 “J” 键 如果不小心误删除了文件内容,则可以通过 “u” 键来撤销刚才执行的命令。如果要撤销刚才的多次操作,可以多按几次 “u” 键。 Vim 保存退出文本 Vim 的保存和退出是在编辑模式中进行的,其常用命令如下表所示。 命令 功能描述 :wq! 保存并强制退出 Vim 编辑器 :q 不保存就退出 Vim 编辑器 :q! 不保存,且强制退出 Vim 编辑器 :w 保存但是不退出 Vim 编辑器 :w! 强制保存文本 :w filename 另存到 filename 文件 :wq 保存并退出 Vim 编辑器 x! 保存文本,并退出 Vim 编辑器,更通用的一个 vim 命令 ZZ 直接退出 Vim 编辑器 Vim快捷方向键 快捷键 功能描述 h 光标向左移动一位 j 光标向下移动一行(以回车为换行符),也就是光标向下移动 k 光标向上移动一行(也就是向上移动) l 光标向右移动一位 Vim光标以单词为单位移动 某些情形下,可能需要光标迅速移动至一行中的某个位置,将光标以单词为单位进行移动就会很方便。 快捷键 功能描述 w 或 W 光标移动至下一个单词的单词首 b 或 B 光标移动至上一个单词的单词首 e 或 E 光标移动至下一个单词的单词尾 nw 或 nW n 为数字,表示光标向右移动 n 个单词 nb 或 nB n 为数字,表示光标向左移动 n 个单词 Vim光标移动至行首或行尾 快捷键 功能描述 0 或 ^ 光标移动至当前行的行首 $ 光标移动至当前行的行尾 n$ 光标移动至当前行只有 n 行的行尾,n为数字 Vim光标移动至指定字符 快捷键 功能描述 fx 光标移动至当前行中下一个 x 字符处 Fx 光标移动至当前行中下一个 x 字符处 Vim光标移动到指定行 快捷键 功能描述 gg 光标移动到文件开头 G 光标移动至文件末尾 nG 光标移动到第 n 行,n 为数字 :n 编辑模式下使用的快捷键,可以将光标快速定义到指定行的行首 Vim光标移动到匹配的括号处 在编辑程序时,经常会为将光标移动到与一个 “(“ 匹配的 “)” (对于 [] 和 {} 也是一样的)处而感到头疼。Vim 里面提供了一个非常方便地査找匹配括号的命令,这就是 “%”。 比如,在 /etc/init.d/sshd 脚本文件中,想迅速地将光标定位到与之 “{“ 相对应的 “}” 处,则可以将光标先定位在 “{“ 处,然后再使用 “%” 命令,使之定位在 “}” 处 Vim批量注释和自定义注释快捷键使用 Vim 编辑 Shell 脚本,在进行调试时,需要进行多行的注释,每次都要先切换到输入模式,在行首输入注释符”#”再退回命令模式,非常麻烦。 连续行的注释其实可以用替换命令来完成。换句话说,在指定范围行加”#”注释,可以使用 “:起始行,终止行 s/^/#/g”,例如: 1:1,10s/^/#/g 表示在第 1~10 行行首加”#”注释。”^”意为行首;”g”表示执行替换时不询问确认。如果希望每行交互询问是否执行,则可将 “g” 改为 “c”。 取消连续行注释,则可以使用 “:起始行,终止行s/^#//g”,例如: 1:1,10s/^#//g 意为将行首的”#”替换为空,即删除。 添加”//“注释要稍微麻烦一些,命令格式为 “:起始行,终止行 s/^/\/\//g”。例如: 1:1,5s/^/\/\//g 表示在第 1~5 行行首加”//“注释,因为 “/“ 前面需要加转义字符 “\“ 以上方法可以解决连续行的注释问题,如果是非连续的多行就不灵了,这时我们可以定义快捷键简化操作。格式如下: 1:map 快捷键 执行命令 如定义快捷键 “Ctrl+P” 为在行首添加 “#” 注释,可以执行 “:map^P l#“。其中 “^P” 为定义快捷键 “Ctrl+P”。注意:必须同时按 “Ctrl+V+P” 快捷键生成 “^P” 方可有效,或先按 “Ctrl+V” 再按 “Ctrl+P” 也可以,直接输入 “^P” 是无效的。 “l#“ 就是此快捷键要触发的动作,”l” 为在光标所在行行首插入,”#” 为要输入的字符,”“ 表示退回命令模式。”“ 要逐个字符输入,不可直接按键盘上的 Esc 键。 设置成功后,直接在任意需要注释的行上按 “Ctrl+P” 快捷键,就会自动在行首加上 “#” 注释。取消此快捷键定义,输入 “:unmap^P” 即可。 我们可以延伸一下,如果想取消文件行首的快捷键,则可以设置 “:map^B 0x”,快捷键为 “Ctrl+B”, “0” 表示跳到行首,”x” 表示删除光标所在处字符。 再如,有时我们写完脚本等文件,需要在末尾注释中加入自己的邮箱,则可以直接定义每次按快捷键 “Ctrl+E” 实现插入邮箱,定义方法为 “:map^E asamlee@itxdl.net“。其中 “a” 表示在当前字符后插入,”samlee@itxdl.net“ 为插入的邮箱,”“ 表示插入后返回命令模式。 所以,通过定义快捷键,可以把前面讲到的命令组合起来使用。 将快捷键对应的命令保存在 .vimrc 文件中,即可在每次使用 Vim 时自动调用,非常方便。 连接文件并打印输出到标准输出设备 catcat 命令可以用来显示文本文件的内容(类似于 DOS 下的 type 命令),也可以把几个文件内容附加到另一个文件中,即连接合并文件。cat 是 concatenate(连接、连续)的简写。 cat 命令的基本格式如下: 123[root@localhost ~]# cat [选项] 文件名或者[root@localhost ~]# cat 文件1 文件2 > 文件3 这两种格式中,前者用于显示文件的内容,常用选项及各自的含义如下表所示;而后者用于连接合并文件。 选项 含义 -A 相当于 -vET 选项的整合,用于列出所有隐藏符号; -E 列出每行结尾的回车符 $; -n 对输出的所有行进行编号; -b 同 -n 不同,此选项表示只对非空行进行编号。 -T 把 Tab 键 ^I 显示出来; -V 列出特殊字符; -s 当遇到有连续 2 行以上的空白行时,就替换为 1 行的空白行。 注意,cat 命令用于查看文件内容时,不论文件内容有多少,都会一次性显示。如果文件非常大,那么文件开头的内容就看不到了。不过可以使用PgUp+上箭头组合键向上翻页,但是这种翻页是有极限的,如果文件足够长,那么还是无法看全文件的内容。 cat 命令本身非常简单,我们可以直接查看文件的内容。例如: 123456789[root@localhost ~]# cat anaconda-ks.cfg# Kickstart file automatically generated by anaconda.#version=DEVELinstallcdromlang zh一CN.UTF-8…省略部分内容... 而如果使用 “-n” 选项,则会显示行号。例如: 12345678[root@localhost ~]# cat -n anaconda-ks.cfg1 # Kickstart file automatically generated by anaconda.234 #version=DEVEL5 install6 cdrom…省略部分内容... 如果使用 “-A” 选项,则相当于使用了 “-vET” 选项,可以查看文本中的所有隐藏符号,包括回车符($)、Tab 键(^I)等。例如: 12345678[root@localhost ~]# cat -A anaconda-ks.cfg# Kickstart file automatically generated by anaconda.$$$#version=DEVEL$install$cdrom$…省略部分内容… 将文件 file1.txt 和 file2.txt 的内容合并后输出到文件 file3.txt 中: 12345678910111213[root@localhost base]# lsfile1.txt file2.txt[root@localhost base]# cat file1.txthttp://c.biancheng.net(file1.txt)[root@localhost base]# cat file2.txtis great(file2.txt)[root@localhost base]# cat file1.txt file2.txt > file3.txt[root@localhost base]# more file3.txt#more 命令可查看文件中的内容http://c.biancheng.net(file1.txt)is great(file2.txt)[root@localhost base]# lsfile1.txt file2.txt file3.txt 分屏显示文件内容 moremore 命令可以分页显示文本文件的内容,使用者可以逐页阅读文件中内容,此命令的基本格式如下: 1[root@localhost ~]# more [选项] 文件名 选项 含义 -f 计算行数时,以实际的行数,而不是自动换行过后的行数。 -p 不以卷动的方式显示每一页,而是先清除屏幕后再显示内容。 -c 跟 -p 选项相似,不同的是先显示内容再清除其他旧资料。 -s 当遇到有连续两行以上的空白行时,就替换为一行的空白行。 -u 不显示下引号(根据环境变量 TERM 指定的终端而有所不同)。 +n 从第 n 行开始显示文件内容,n 代表数字。 -n 一次显示的行数,n 代表数字。 more 命令的执行会打开一个交互界面,常用的交互命令: 交互指令 功能 h 或 ? 显示 more 命令交互命令帮助。 q 或 Q 退出 more。 v 在当前行启动一个编辑器。 :f 显示当前文件的文件名和行号。 !<命令> 或 :!<命令> 在子Shell中执行指定命令。 回车键 向下移动一行。 空格键 向下移动一页。 Ctrl+l 刷新屏幕。 = 显示当前行的行号。 ‘ 转到上一次搜索开始的地方。 Ctrf+f 向下滚动一页。 . 重复上次输入的命令。 / 字符串 搜索指定的字符串。 d 向下移动半页。 b 向上移动一页。 用分页的方式显示 anaconda-ks.cfg 文件的内容: 12345678[root@localhost ~]# more anaconda-ks.cfg# Kickstart file automatically generated by anaconda.#version=DEVELinstallcdrom…省略部分内容…--More--(69%)#在这里执行交互命令即可 显示文件 anaconda-ks.cfg 的内容,每 10 行显示一屏,同时清楚屏幕,使用以下命令: 12[root@localhost ~]# more -c -10 anaconda-ks.cfg#省略输出内容。 显示文件开头的内容 headhead 命令可以显示指定文件前若干行的文件内容,其基本格式如下: 1[root@localhost ~]# head [选项] 文件名 选项 含义 -n K 这里的 K 表示行数,该选项用来显示文件前 K 行的内容;如果使用 “-K” 作为参数,则表示除了文件最后 K 行外,显示剩余的全部内容。 -c K 这里的 K 表示字节数,该选项用来显示文件前 K 个字节的内容;如果使用 “-K”,则表示除了文件最后 K 字节的内容,显示剩余全部内容。 -v 显示文件名; 基本用法: 1[root@localhost ~]# head anaconda-ks.cfg head 命令默认显示文件的开头 10 行内容。如果想显示指定的行数,则只需使用 “-n” 选项即可,例如: 1[root@localhost ~]# head -n 20 anaconda-ks.cfg 这是显示文件的开头 20 行内容,也可以直接写 “-行数”,例如: 1[root@localhost ~]# head -20 anaconda-ks.cfg 查看文件内容 lessless 命令的作用和 more 十分类似,都用来浏览文本文件中的内容,不同之处在于,使用 more 命令浏览文件内容时,只能不断向后翻看,而使用 less 命令浏览,既可以向后翻看,也可以向前翻看。 不仅如此,为了方面用户浏览文本内容,less 命令还提供了以下几个功能: 使用光标键可以在文本文件中前后(左后)滚屏; 用行号或百分比作为书签浏览文件; 提供更加友好的检索、高亮显示等操作; 兼容常用的字处理程序(如 Vim、Emacs)的键盘操作; 阅读到文件结束时,less 命令不会退出; 屏幕底部的信息提示更容易控制使用,而且提供了更多的信息。 less 命令的基本格式如下: 1[root@localhost ~]# less [选项] 文件名 选项 选项含义 -N 显示每行的行号。 -S 行过长时将超出部分舍弃。 -e 当文件显示结束后,自动离开。 -g 只标志最后搜索到的关键同。 -Q 不使用警告音。 -i 忽略搜索时的大小写。 -m 显示类似 more 命令的百分比。 -f 强迫打开特殊文件,比如外围设备代号、目录和二进制文件。 -s 显示连续空行为一行。 -b <缓冲区大小> 设置缓冲区的大小。 -o <文件名> 将 less 输出的内容保存到指定文件中。 -x <数字> 将【Tab】键显示为规定的数字空格。 在使用 less 命令查看文件内容的过程中,和 more 命令一样,也会进入交互界面 交互指令 功能 /字符串 向下搜索“字符串”的功能。 ?字符串 向上搜索“字符串”的功能。 n 重复*前一个搜索(与 / 成 ? 有关)。 N 反向重复前一个搜索(与 / 或 ? 有关)。 b 向上移动一页。 d 向下移动半页。 h 或 H 显示帮助界面。 q 或 Q 退出 less 命令。 y 向上移动一行。 空格键 向下移动一页。 回车键 向下移动一行。 【PgDn】键 向下移动一页。 【PgUp】键 向上移动一页。 Ctrl+f 向下移动一页。 Ctrl+b 向上移动一页。 Ctrl+d 向下移动一页。 Ctrl+u 向上移动半页。 j 向下移动一行。 k 向上移动一行。 G 移动至最后一行。 g 移动到第一行。 ZZ 退出 less 命令。 v 使用配置的编辑器编辑当前文件。 [ 移动到本文档的上一个节点。 ] 移动到本文档的下一个节点。 p 移动到同级的上一个节点。 u 向上移动半页。 显示文件结尾的内容 tailtail 命令和 head 命令正好相反,它用来查看文件末尾的数据,其基本格式如下: 1[root@localhost ~]# tail [选项] 文件名 选项 含义 -n K 这里的 K 指的是行数,该选项表示输出最后 K 行,在此基础上,如果使用 -n +K,则表示从文件的第 K 行开始输出。 -c K 这里的 K 指的是字节数,该选项表示输出文件最后 K 个字节的内容,在此基础上,使用 -c +K 则表示从文件第 K 个字节开始输出。 -f 输出文件变化后新增加的数据。 查看 /etc/passwd 文件最后 3 行的数据内容: 1234[root@localhost ~]# tail -n 3 /etc/passwdsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologinoprofile:x:16:16:Special user account to be used by OProfile:/var/lib/oprofile:/sbin/nologintcpdump:x:72:72::/:/sbin/nologin 1234[root@localhost ~]# tail -3 /etc/passwdsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologinoprofile:x:16:16:Special user account to be used by OProfile:/var/lib/oprofile:/sbin/nologintcpdump:x:72:72::/:/sbin/nologin 使用 tail -n 3 /etc/passwd 命令和 tail -3 /etc/passwd 的效果是一样的。 查看 /etc/passwd 文件末尾 100 个字节的数据内容: 12[root@localhost ~]# tail -c 100 /etc/passwdcpdump:x:72:72::/:/sbin/nologin Linux重定向Linux中标准的输入设备默认指的是键盘,标准的输出设备默认指的是显示器。而本节所要介绍的输入、输出重定向,也就是: 输入重定向:指的是重新指定设备来代替键盘作为新的输入设备; 输出重定向:指的是重新指定设备来代替显示器作为新的输出设备。 通常是用文件或命令的执行结果来代替键盘作为新的输入设备,而新的输出设备通常指的就是文件。 Linux输入重定向 命令符号格式 作用 命令 < 文件 将指定文件作为命令的输入设备 命令 << 分界符 表示从标准输入设备(键盘)中读入,直到遇到分界符才停止(读入的数据不包括分界符),这里的分界符其实就是自定义的字符串 命令 < 文件 1 > 文件 2 将文件 1 作为命令的输入设备,该命令的执行结果输出到文件 2 中。 【例 1】默认情况下,cat 命令会接受标准输入设备(键盘)的输入,并显示到控制台,但如果用文件代替键盘作为输入设备,那么该命令会以指定的文件作为输入设备,并将文件中的内容读取并显示到控制台。 以 /etc/passwd 文件(存储了系统中所有用户的基本信息)为例,执行如下命令: 1234[root@localhost ~]# cat /etc/passwd#这里省略输出信息,读者可自行查看[root@localhost ~]# cat < /etc/passwd#输出结果同上面命令相同 虽然执行结果相同,但第一行代表是以键盘作为输入设备,而第二行代码是以 /etc/passwd 文件作为输入设备。 【例 2】 123456[root@localhost ~]# cat << 0>c.biancheng.net>Linux>0c.biancheng.netLinux 当指定了 0 作为分界符之后,只要不输入 0,就可以一直输入数据。 【例 3】首先,新建文本文件 a.tx,然后执行如下命令: 1234[root@localhost ~]# cat a.txt[root@localhost ~]# cat < /etc/passwd > a.txt[root@localhost ~]# cat a.txt#输出了和 /etc/passwd 文件内容相同的数据 通过重定向 /etc/passwd 作为输入设备,并输出重定向到 a.txt,最终实现了将 /etc/passwd 文件中内容复制到 a.txt 中。 Linux输出重定向 相较于输入重定向,使用输出重定向的频率更高。并且,和输入重定向不同的是,输出重定向还可以细分为标准输出重定向和错误输出重定向两种技术。 例如,使用 ls 命令分别查看两个文件的属性信息,但其中一个文件是不存在的,如下所示: 12345[root@localhost ~]# touch demo1.txt[root@localhost ~]# ls -l demo1.txt-rw-rw-r--. 1 root root 0 Oct 12 15:02 demo1.txt[root@localhost ~]# ls -l demo2.txt <-- 不存在的文件ls: cannot access demo2.txt: No such file or directory 上述命令中,demo1.txt 是存在的,因此正确输出了该文件的一些属性信息,这也是该命令执行的标准输出信息;而 demo2.txt 是不存在的,因此执行 ls 命令之后显示的报错信息,是该命令的错误输出信息。 想把原本输出到屏幕上的数据转而写入到文件中,这两种输出信息就要区别对待。 在此基础上,标准输出重定向和错误输出重定向又分别包含清空写入和追加写入两种模式 命令符号格式 作用 命令 > 文件 将命令执行的标准输出结果重定向输出到指定的文件中,如果该文件已包含数据,会清空原有数据,再写入新数据。 命令 2> 文件 将命令执行的错误输出结果重定向到指定的文件中,如果该文件中已包含数据,会清空原有数据,再写入新数据。 命令 >> 文件 将命令执行的标准输出结果重定向输出到指定的文件中,如果该文件已包含数据,新数据将写入到原有内容的后面。 命令 2>> 文件 将命令执行的错误输出结果重定向到指定的文件中,如果该文件中已包含数据,新数据将写入到原有内容的后面。 命令 >> 文件 2>&1 或者 命令 &>> 文件 将标准输出或者错误输出写入到指定文件,如果该文件中已包含数据,新数据将写入到原有内容的后面。注意,第一种格式中,最后的 2>&1 是一体的,可以认为是固定写法。 新建一个包含有 “Linux” 字符串的文本文件 Linux.txt,以及空文本文件 demo.txt,然后执行如下命令: 12345678910111213141516171819[root@localhost ~]# cat Linux.txt > demo.txt[root@localhost ~]# cat demo.txtLinux[root@localhost ~]# cat Linux.txt > demo.txt[root@localhost ~]# cat demo.txtLinux <--这里的 Linux 是清空原有的 Linux 之后,写入的新的 Linux[root@localhost ~]# cat Linux.txt >> demo.txt[root@localhost ~]# cat demo.txtLinuxLinux <--以追加的方式,新数据写入到原有数据之后[root@localhost ~]# cat b.txt > demo.txtcat: b.txt: No such file or directory <-- 错误输出信息依然输出到了显示器中[root@localhost ~]# cat b.txt 2> demo.txt[root@localhost ~]# cat demo.txtcat: b.txt: No such file or directory <--清空文件,再将错误输出信息写入到该文件中[root@localhost ~]# cat b.txt 2>> demo.txt[root@localhost ~]# cat demo.txtcat: b.txt: No such file or directorycat: b.txt: No such file or directory <--追加写入错误输出信息 查找文件内容 grep用户在要搜索的字符串前加上前缀 global(全面的),一旦找到相匹配的内容,用户就像将其输出(print)到屏幕上,而将这一系列的操作整合到一起就是 global regular expressions print,而这也就是 grep 命令的全称。 grep命令能够在一个或多个文件中,搜索某一特定的字符模式(也就是正则表达式),此模式可以是单一的字符、字符串、单词或句子。 正则表达式是描述一组字符串的一个模式,正则表达式的构成模仿了数学表达式,通过使用操作符将较小的表达式组合成一个新的表达式。正则表达式可以是一些纯文本文字,也可以是用来产生模式的一些特殊字符。为了进一步定义一个搜索模式,grep 命令支持如表所示的这几种正则表达式的元字符(也就是通配符)。 通配符 功能 c* 将匹配 0 个(即空白)或多个字符 c(c 为任一字符)。 . 将匹配任何一个字符,且只能是一个字符。 [xyz] 匹配方括号中的任意一个字符。 [^xyz] 匹配除方括号中字符外的所有字符。 ^ 锁定行的开头。 $ 锁定行的结尾。 需要注意的是,在基本正则表达式中,如通配符 、+、{、|、( 和 )等,已经失去了它们原本的含义,而若要恢复它们原本的含义,则要在之前添加反斜杠 \,如 \、+、{、|、( 和 )。 grep 命令是用来在每一个文件或中(或特定输出上)搜索特定的模式,当使用 grep 时,包含指定字符模式的每一行内容,都会被打印(显示)到屏幕上,但是使用 grep 命令并不改变文件中的内容。 grep 命令的基本格式如下: 1[root@localhost ~]# grep [选项] 模式 文件名 这里的模式,要么是字符(串),要么是正则表达式。而此命令常用的选项以及各自的含义如表所示。 选项 含义 -c 仅列出文件中包含模式的行数。 -i 忽略模式中的字母大小写。 -l 列出带有匹配行的文件名。 -n 在每一行的最前面列出行号。 -v 列出没有匹配模式的行。 -w 把表达式当做一个完整的单字符来搜寻,忽略那些部分匹配的行。 注意,如果是搜索多个文件,grep 命令的搜索结果只显示文件中发现匹配模式的文件名;而如果搜索单个文件,grep 命令的结果将显示每一个包含匹配模式的行。 假设有一份 emp.data 员工清单,现在要搜索此文件,找出职位为 CLERK 的所有员工,则执行命令如下: 12[root@localhost ~]# grep CLERK emp.data#忽略输出内容 而在此基础上,如果只想知道职位为 CLERK 的员工的人数,可以使用“-c”选项,执行命令如下: 12[root@localhost ~]# grep -c CLERK emp.data#忽略输出内容 搜索 emp.data 文件,使用正则表达式找出以 78 开头的数据行,执行命令如下: 12[root@localhost ~]# grep ^78 emp.data#忽略输出内容 脚本命令 sedVim采用的是交互式文本编辑模式,可以用键盘命令来交互性地插入、删除或替换数据中的文本。但 sed 命令不同,它采用的是流编辑模式,最明显的特点是,在 sed 处理数据之前,需要预先提供一组规则,sed 会按照此规则来编辑数据。 sed 会根据脚本命令来处理文本文件中的数据,这些命令要么从命令行中输入,要么存储在一个文本文件中,此命令执行数据的顺序如下: 每次仅读取一行内容; 根据提供的规则命令匹配并修改数据。注意,sed 默认不会直接修改源文件数据,而是会将数据复制到缓冲区中,修改也仅限于缓冲区中的数据; 将执行结果输出。 当一行数据匹配完成后,它会继续读取下一行数据,并重复这个过程,直到将文件中所有数据处理完毕。 sed 命令的基本格式如下: 1[root@localhost ~]# sed [选项] [脚本命令] 文件名 选项 含义 -e 脚本命令 该选项会将其后跟的脚本命令添加到已有的命令中。 -f 脚本命令文件 该选项会将其后文件中的脚本命令添加到已有的命令中。 -n 默认情况下,sed 会在所有的脚本指定执行完毕后,会自动输出处理后的内容,而该选项会屏蔽启动输出,需使用 print 命令来完成输出。 -i 此选项会直接修改源文件,要慎用。 sed脚本命令 1、sed s 替换脚本命令 此命令的基本格式为: 1[address]s/pattern/replacement/flags 其中,address 表示指定要操作的具体行,pattern 指的是需要替换的内容,replacement 指的是要替换的新内容。 flags 标记 功能 n 1~512 之间的数字,表示指定要替换的字符串出现第几次时才进行替换,例如,一行中有 3 个 A,但用户只想替换第二个 A,这是就用到这个标记; g 对数据中所有匹配到的内容进行替换,如果没有 g,则只会在第一次匹配成功时做替换操作。例如,一行数据中有 3 个 A,则只会替换第一个 A; p 会打印与替换命令中指定的模式匹配的行。此标记通常与 -n 选项一起使用。 w file 将缓冲区中的内容写到指定的 file 文件中; & 用正则表达式匹配的内容进行替换; \n 匹配第 n 个子串,该子串之前在 pattern 中用 () 指定。 \ 转义(转义替换部分包含:&、\ 等)。 可以指定 sed 用新文本替换第几处模式匹配的地方: 123[root@localhost ~]# sed 's/test/trial/2' data4.txtThis is a test of the trial script.This is the second test of the trial script. 使用数字 2 作为标记的结果就是,sed 编辑器只替换每行中第 2 次出现的匹配模式。 如果要用新文件替换所有匹配的字符串,可以使用 g 标记: 123[root@localhost ~]# sed 's/test/trial/g' data4.txtThis is a trial of the trial script.This is the second trial of the trial script. -n 选项会禁止 sed 输出,但 p 标记会输出修改过的行,将二者匹配使用的效果就是只输出被替换命令修改过的行,例如: 12345[root@localhost ~]# cat data5.txtThis is a test line.This is a different line.[root@localhost ~]# sed -n 's/test/trial/p' data5.txtThis is a trial line. w 标记会将匹配后的结果保存到指定文件中,比如: 12345[root@localhost ~]# sed 's/test/trial/w test.txt' data5.txtThis is a trial line.This is a different line.[root@localhost ~]#cat test.txtThis is a trial line. 在使用 s 脚本命令时,替换类似文件路径的字符串会比较麻烦,需要将路径中的正斜线进行转义,例如: 1[root@localhost ~]# sed 's/\/bin\/bash/\/bin\/csh/' /etc/passwd 2、sed d 替换脚本命令 此命令的基本格式为: 1[address]d 如果需要删除文本中的特定行,可以用 d 脚本命令,它会删除指定行中的所有内容。但使用该命令时要特别小心,如果忘记指定具体行的话,文件中的所有内容都会被删除,举个例子: 1234567[root@localhost ~]# cat data1.txtThe quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dogThe quick brown fox jumps over the lazy dog[root@localhost ~]# sed 'd' data1.txt#什么也不输出,证明成了空文件 当和指定地址一起使用时,删除命令显然能发挥出大的功用。可以从数据流中删除特定的文本行。 address 举几个简单的例子: 通过行号指定,比如删除 data6.txt 文件内容中的第 3 行: 123456789[root@localhost ~]# cat data6.txtThis is line number 1.This is line number 2.This is line number 3.This is line number 4.[root@localhost ~]# sed '3d' data6.txtThis is line number 1.This is line number 2.This is line number 4. 或者通过特定行区间指定,比如删除 data6.txt 文件内容中的第 2、3行: 123[root@localhost ~]# sed '2,3d' data6.txtThis is line number 1.This is line number 4. 也可以使用两个文本模式来删除某个区间内的行,但这么做时要小心,指定的第一个模式会“打开”行删除功能,第二个模式会“关闭”行删除功能,因此,sed 会删除两个指定行之间的所有行(包括指定的行),例如: 123[root@localhost ~]#sed '/1/,/3/d' data6.txt#删除第 1~3 行的文本数据This is line number 4. 或者通过特殊的文件结尾字符,比如删除 data6.txt 文件内容中第 3 行开始的所有的内容: 123[root@localhost ~]# sed '3,$d' data6.txtThis is line number 1.This is line number 2. 在默认情况下 sed 并不会修改原始文件,这里被删除的行只是从 sed 的输出中消失了,原始文件没做任何改变。 3、sed a 和 i 脚本命令 a 命令表示在指定行的后面附加一行,i 命令表示在指定行的前面插入一行,这 2 个脚本命令,它们的基本格式完全相同,如下所示: 1[address]a(或 i)\新文本内容 将一个新行插入到数据流第三行前,执行命令如下: 1234567[root@localhost ~]# sed '3i\> This is an inserted line.' data6.txtThis is line number 1.This is line number 2.This is an inserted line.This is line number 3.This is line number 4. 将一个新行附加到数据流中第三行后,执行命令如下: 1234567[root@localhost ~]# sed '3a\> This is an appended line.' data6.txtThis is line number 1.This is line number 2.This is line number 3.This is an appended line.This is line number 4. 如果想将一个多行数据添加到数据流中,只需对要插入或附加的文本中的每一行末尾(除最后一行)添加反斜线即可,例如: 123456789[root@localhost ~]# sed '1i\> This is one line of new text.\> This is another line of new text.' data6.txtThis is one line of new text.This is another line of new text.This is line number 1.This is line number 2.This is line number 3.This is line number 4. 4、sed c 替换脚本命令 c 命令表示将指定行中的所有内容,替换成该选项后面的字符串。该命令的基本格式为: 1[address]c\用于替换的新文本 举个例子: 12345678910111213[root@localhost ~]# sed '3c\> This is a changed line of text.' data6.txtThis is line number 1.This is line number 2.This is a changed line of text.This is line number 4.在这个例子中,sed 编辑器会修改第三行中的文本,其实,下面的写法也可以实现此目的:[root@localhost ~]# sed '/number 3/c\> This is a changed line of text.' data6.txtThis is line number 1.This is line number 2.This is a changed line of text.This is line number 4. 5、sed y 转换脚本命令 y 转换命令是唯一可以处理单个字符的 sed 脚本命令,其基本格式如下: 1[address]y/inchars/outchars/ 转换命令会对 inchars 和 outchars 值进行一对一的映射,即 inchars 中的第一个字符会被转换为 outchars 中的第一个字符,第二个字符会被转换成 outchars 中的第二个字符…这个映射过程会一直持续到处理完指定字符。如果 inchars 和 outchars 的长度不同,则 sed 会产生一条错误消息。 举个简单例子: 12345678[root@localhost ~]# sed 'y/123/789/' data8.txtThis is line number 7.This is line number 8.This is line number 9.This is line number 4.This is line number 7 again.This is yet another line.This is the last line in the file. 可以看到,inchars 模式中指定字符的每个实例都会被替换成 outchars 模式中相同位置的那个字符。 转换命令是一个全局命令,也就是说,它会文本行中找到的所有指定字符自动进行转换,而不会考虑它们出现的位置: 12[root@localhost ~]# echo "This 1 is a test of 1 try." | sed 'y/123/456/'This 4 is a test of 4 try. sed 转换了在文本行中匹配到的字符 1 的两个实例,无法限定只转换在特定地方出现的字符。 6、sed p 打印脚本命令 p 命令表示搜索符号条件的行,并输出该行的内容,此命令的基本格式为: 1[address]p p 命令常见的用法是打印包含匹配文本模式的行,例如: 1234567[root@localhost ~]# cat data6.txtThis is line number 1.This is line number 2.This is line number 3.This is line number 4.[root@localhost ~]# sed -n '/number 3/p' data6.txtThis is line number 3. 可以看到,用 -n 选项和 p 命令配合使用,我们可以禁止输出其他行,只打印包含匹配文本模式的行。 如果需要在修改之前查看行,也可以使用打印命令,比如与替换或修改命令一起使用。可以创建一个脚本在修改行之前显示该行,如下所示: 123456[root@localhost ~]# sed -n '/3/{> p> s/line/test/p> }' data6.txtThis is line number 3.This is test number 3. sed 命令会查找包含数字 3 的行,然后执行两条命令。首先,脚本用 p 命令来打印出原始行;然后它用 s 命令替换文本,并用 p 标记打印出替换结果。输出同时显示了原来的行文本和新的行文本。 7、sed w 脚本命令 w 命令用来将文本中指定行的内容写入文件中,此命令的基本格式如下: 1[address]w filename 这里的 filename 表示文件名,可以使用相对路径或绝对路径,但不管是哪种,运行 sed 命令的用户都必须有文件的写权限。 下面的例子是将数据流中的前两行打印到一个文本文件中: 12345678[root@localhost ~]# sed '1,2w test.txt' data6.txtThis is line number 1.This is line number 2.This is line number 3.This is line number 4.[root@localhost ~]# cat test.txtThis is line number 1.This is line number 2. 如果不想让行直接输出,可以用 -n 选项,再举个例子: 123456789[root@localhost ~]# cat data11.txtBlum, R BrowncoatMcGuiness, A AllianceBresnahan, C BrowncoatHarken, C Alliance[root@localhost ~]# sed -n '/Browncoat/w Browncoats.txt' data11.txtcat Browncoats.txtBlum, R BrowncoatBresnahan, C Browncoat 通过使用 w 脚本命令,sed 可以实现将包含文本模式的数据行写入目标文件。 8、sed r 脚本命令 r 命令用于将一个独立文件的数据插入到当前数据流的指定位置,该命令的基本格式为: 1[address]r filename sed 命令会将 filename 文件中的内容插入到 address 指定行的后面,比如说: 12345678910[root@localhost ~]# cat data12.txtThis is an added line.This is the second added line.[root@localhost ~]# sed '3r data12.txt' data6.txtThis is line number 1.This is line number 2.This is line number 3.This is an added line.This is the second added line.This is line number 4. 如果想将指定文件中的数据插入到数据流的末尾,可以使用 $ 地址符,例如: 1234567[root@localhost ~]# sed '$r data12.txt' data6.txtThis is line number 1.This is line number 2.This is line number 3.This is line number 4.This is an added line.This is the second added line. 9、sed q 退出脚本命令 q 命令的作用是使 sed 命令在第一次匹配任务结束后,退出 sed 程序,不再进行对后续数据的处理。 比如: 123[root@localhost ~]# sed '2q' test.txtThis is line number 1.This is line number 2. 可以看到,sed 命令在打印输出第 2 行之后,就停止了,是 q 命令造成的,再比如: 12[root@localhost ~]# sed '/number 1/{ s/number 1/number 0/;q; }' test.txtThis is line number 0. 使用 q 命令之后,sed 命令会在匹配到 number 1 时,将其替换成 number 0,然后直接退出。 文本数据处理工具 awkawk 命令是逐行扫描文件(从第 1 行到最后一行),寻找含有目标文本的行,如果匹配成功,则会在该行上执行用户想要的操作;反之,则不对行做任何处理。 awk 命令的基本格式为: 1[root@localhost ~]# awk [选项] '脚本命令' 文件名 选项 含义 -F fs 指定以 fs 作为输入行的分隔符,awk 命令默认分隔符为空格或制表符。 -f file 从脚本文件中读取 awk 脚本指令,以取代直接在命令行中输入指令。 -v var=val 在执行处理过程之前,设置一个变量 var,并给其设备初始值为 val。 awk 的强大之处在于脚本命令,它由 2 部分组成,分别为匹配规则和执行命令,如下所示: 1'匹配规则{执行命令}' 这里的匹配规则,和 sed 命令中的 address 部分作用相同,用来指定脚本命令可以作用到文本内容中的具体行,可以使用字符串(比如 /demo/,表示查看含有 demo 字符串的行)或者正则表达式指定。 另外需要注意的是,整个脚本命令是用单引号(’’)括起,而其中的执行命令部分需要用大括号({})括起来。 1在 awk 程序执行时,如果没有指定执行命令,则默认会把匹配的行输出;如果不指定匹配规则,则默认匹配文本中所有的行。 举个简单的例子: 1[root@localhost ~]# awk '/^$/ {print "Blank line"}' test.txt 在此命令中,/^$/ 是一个正则表达式,功能是匹配文本中的空白行,同时可以看到,执行命令使用的是 print 命令,此命令经常会使用,它的作用很简单,就是将指定的文本进行输出。因此,整个命令的功能是,如果 test.txt 有 N 个空白行,那么执行此命令会输出 N 个 Blank line。 awk 使用数据字段变量 awk 的主要特性之一是其处理文本文件中数据的能力,它会自动给一行中的每个数据元素分配一个变量。 默认情况下,awk 会将如下变量分配给它在文本行中发现的数据字段: $0 代表整个文本行; $1 代表文本行中的第 1 个数据字段; $2 代表文本行中的第 2 个数据字段; $n 代表文本行中的第 n 个数据字段。 前面说过,在 awk 中,默认的字段分隔符是任意的空白字符(例如空格或制表符)。 在文本行中,每个数据字段都是通过字段分隔符划分的。awk 在读取一行文本时,会用预定义的字段分隔符划分每个数据字段。 所以在下面的例子中,awk 程序读取文本文件,只显示第 1 个数据字段的值: 12345678[root@localhost ~]# cat data2.txtOne line of test text.Two lines of test text.Three lines of test text.[root@localhost ~]# awk '{print $1}' data2.txtOneTwoThree 该程序用 $1 字段变量来表示“仅显示每行文本的第 1 个数据字段”。当然,如果要读取采用了其他字段分隔符的文件,可以用 -F 选项手动指定。 awk 脚本命令使用多个命令 awk 允许将多条命令组合成一个正常的程序。要在命令行上的程序脚本中使用多条命令,只要在命令之间放个分号即可,例如: 12[root@localhost ~]# echo "My name is Rich" | awk '{$4="Christine"; print $0}'My name is Christine 第一条命令会给字段变量 $4 赋值。第二条命令会打印整个数据字段。可以看到,awk 程序在输出中已经将原文本中的第四个数据字段替换成了新值。 除此之外,也可以一次一行地输入程序脚本命令,比如说: 12345[root@localhost ~]# awk '{> $4="Christine"> print $0}'My name is RichMy name is Christine 在用了表示起始的单引号后,bash shell 会使用 > 来提示输入更多数据,我们可以每次在每行加一条命令,直到输入了结尾的单引号。 1注意,此例中因为没有在命令行中指定文件名,awk 程序需要用户输入获得数据,因此当运行这个程序的时候,它会一直等着用户输入文本,此时如果要退出程序,只需按下 Ctrl+D 组合键即可。 awk从文件中读取程序 跟 sed 一样,awk 允许将脚本命令存储到文件中,然后再在命令行中引用,比如: 123456789101112[root@localhost ~]# cat awk.sh{print $1 "'s home directory is " $6}[root@localhost ~]# awk -F: -f awk.sh /etc/passwdroot's home directory is /rootbin's home directory is /bindaemon's home directory is /sbinadm's home directory is /var/admlp's home directory is /var/spool/lpd...Christine's home directory is /home/ChristineSamantha's home directory is /home/SamanthaTimothy's home directory is /home/Timothy awk.sh 脚本文件会使用 print 命令打印 /etc/passwd 文件的主目录数据字段(字段变量 $6),以及 userid 数据字段(字段变量 $1)。注意,在程序文件中,也可以指定多条命令,只要一条命令放一行即可,之间不需要用分号。 awk BEGIN关键字 awk 中还可以指定脚本命令的运行时机。默认情况下,awk 会从输入中读取一行文本,然后针对该行的数据执行程序脚本,但有时可能需要在处理数据前运行一些脚本命令,这就需要使用 BEGIN 关键字。 BEGIN 会强制 awk 在读取数据前执行该关键字后指定的脚本命令,例如: 12345678910[root@localhost ~]# cat data3.txtLine 1Line 2Line 3[root@localhost ~]# awk 'BEGIN {print "The data3 File Contents:"}> {print $0}' data3.txtThe data3 File Contents:Line 1Line 2Line 3 可以看到,这里的脚本命令中分为 2 部分,BEGIN 部分的脚本指令会在 awk 命令处理数据前运行,而真正用来处理数据的是第二段脚本命令。 awk END关键字 和 BEGIN 关键字相对应,END 关键字允许我们指定一些脚本命令,awk 会在读完数据后执行它们,例如: 12345678[root@localhost ~]# awk 'BEGIN {print "The data3 File Contents:"}> {print $0}> END {print "End of File"}' data3.txtThe data3 File Contents:Line 1Line 2Line 3End of File 可以看到,当 awk 程序打印完文件内容后,才会执行 END 中的脚本命令。 RPM包安装、卸载和升级RPM包默认安装路径 通常情况下,RPM 包采用系统默认的安装路径,所有安装文件会按照类别分散安装到表 1 所示的目录中。 安装路径 含 义 /etc/ 配置文件安装目录 /usr/bin/ 可执行的命令安装目录 /usr/lib/ 程序所使用的函数库保存位置 /usr/share/doc/ 基本的软件使用手册保存位置 /usr/share/man/ 帮助文件保存位置 RPM 包的默认安装路径是可以通过命令查询的。 除此之外,RPM 包也支持手动指定安装路径,但此方式并不推荐。因为一旦手动指定安装路径,所有的安装文件会集中安装到指定位置,且系统中用来查询安装路径的命令也无法使用(需要进行手工配置才能被系统识别),得不偿失。 与 RPM 包不同,源码包的安装通常采用手动指定安装路径(习惯安装到 /usr/local/ 中)的方式。既然安装路径不同,同一 apache 程序的源码包和 RPM 包就可以安装到一台 Linux服务器上(但同一时间只能开启一个,因为它们需要占用同一个 80 端口)。 实际情况中,一台服务器几乎不会同时包含两个 apache 程序,管理员不好管理,还会占用过多的服务器磁盘空间。 RPM 包的安装 安装 RPM 的命令格式为: 1[root@localhost ~]# rpm -ivh 包全名 注意一定是包全名。涉及到包全名的命令,一定要注意路径,可能软件包在光盘中,因此需提前做好设备的挂载工作。 此命令中各选项参数的含义为: -i:安装(install); -v:显示更详细的信息(verbose); -h:打印 #,显示安装进度(hash); 例如,使用此命令安装 apache 软件包,如下所示: 12345678[root@localhost ~]# rpm -ivh \/mnt/cdrom/Packages/httpd-2.2.15-15.el6.centos.1.i686.rpmPreparing...####################[100%]1:httpd####################[100%] 注意,直到出现两个 100% 才是真正的安装成功,第一个 100% 仅表示完成了安装准备工作。 此命令还可以一次性安装多个软件包,仅需将包全名用空格分开即可,如下所示: 1[root@localhost ~]# rpm -ivh a.rpm b.rpm c.rpm apache 服务安装完成后,可以尝试启动: 1[root@localhost ~]# service 服务名 start|stop|restart|status 各参数含义: start:启动服务; stop:停止服务; restart:重启服务; status: 查看服务状态; 例如: 1[root@localhost ~]# service httpd start #启动apache服务 服务启动后,可以查看端口号 80 是否出现。命令如下: 12[root@localhost ~]# netstat -tlun | grep 80tcp 0 0 :::80:::* LISTEN RPM包的升级 使用如下命令即可实现 RPM 包的升级: 1[root@localhost ~]# rpm -Uvh 包全名 -U(大写)选项的含义是:如果该软件没安装过则直接安装;若没安装则升级至最新版本。 1[root@localhost ~]# rpm -Fvh 包全名 -F(大写)选项的含义是:如果该软件没有安装,则不会安装,必须安装有较低版本才能升级。 RPM包的卸载 RPM 软件包的卸载要考虑包之间的依赖性。例如,我们先安装的 httpd 软件包,后安装 httpd 的功能模块 mod_ssl 包,那么在卸载时,就必须先卸载 mod_ssl,然后卸载 httpd,否则会报错。 如果卸载 RPM 软件不考虑依赖性,执行卸载命令会包依赖性错误,例如: 123456789101112[root@localhost ~]# rpm -e httpderror: Failed dependencies:httpd-mmn = 20051115 is needed by (installed) mod_wsgi-3.2-1.el6.i686httpd-mmn = 20051115 is needed by (installed) php-5.3.3-3.el6_2.8.i686httpd-mmn = 20051115 is needed by (installed) mod_ssl-1:2.2.15-15.el6.centos.1.i686httpd-mmn = 20051115 is needed by (installed) mod_perl-2.0.4-10.el6.i686httpd = 2.2.15-15.el6.centos.1 is needed by (installed) httpd-manual-2.2.15-15.el6.centos.1 .noarchhttpd is needed by (installed) webalizer-2.21_02-3.3.el6.i686httpd is needed by (installed) mod_ssl-1:2.2.15-15.el6.centos.1.i686httpd=0:2.2.15-15.el6.centos.1 is needed by(installed)mod_ssl-1:2.2.15-15.el6.centos.1.i686 RPM 软件包的卸载很简单,使用如下命令即可: 1[root@localhost ~]# rpm -e 包名 -e 选项表示卸载,也就是 erase 的首字母。 rpm命令查询软件包(-q、-qa、-i、-p、-l、-f、-R)rpm 命令还可用来对 RPM 软件包做查询操作,具体包括: 查询软件包是否已安装; 查询系统中所有已安装的软件包; 查看软件包的详细信息; 查询软件包的文件列表; 查询某系统文件具体属于哪个 RPM 包。 使用 rpm 做查询命令的格式如下: 1[root@localhost ~]# rpm 选项 查询对象 rpm -q:查询软件包是否安装 用 rpm 查询软件包是否安装的命令格式为: 1[root@localhost ~]# rpm -q 包名 -q 表示查询,是 query 的首字母。 例如,查看 Linux 系统中是否安装 apache,rpm 查询命令应写成: 12[root@localhost ~]# rpm -q httpdhttpd-2.2.15-15.el6.centos.1.i686 注意这里使用的是包名,而不是包全名。因为已安装的软件包只需给出包名,系统就可以成功识别(使用包全名反而无法识别)。 rpm -qa:查询系统中所有安装的软件包 使用 rpm 查询 Linux 系统中所有已安装软件包的命令为: 123456789[root@localhost ~]# rpm -qalibsamplerate-0.1.7-2.1.el6.i686startup-notification-0.10-2.1.el6.i686gnome-themes-2.28.1-6.el6.noarchfontpackages-filesystem-1.41-1.1.el6.noarchgdm-libs-2.30.4-33.el6_2.i686gstreamer-0.10.29-1.el6.i686redhat-lsb-graphics-4.0-3.el6.centos.i686…省略部分输出… 此外,这里还可以使用管道符查找出需要的内容,比如: 12345[root@localhost ~]# rpm -qa | grep httpdhttpd-devel-2.2.15-15.el6.centos.1.i686httpd-tools-2.2.15-15.el6.centos.1.i686httpd-manual-2.2.15-15.el6.centos.1.noarchhttpd-2.2.15-15.el6.centos.1.i686 相比rpm -q 包名命令,采用这种方式可以找到含有包名的所有软件包。 rpm -qi:查询软件包的详细信息 通过 rpm 命令可以查询软件包的详细信息,命令格式如下: 1[root@localhost ~]# rpm -qi 包名 -i 选项表示查询软件信息,是 information 的首字母。 例如,想查看 apache 包的详细信息,可以使用如下命令: 123456789101112131415161718192021222324252627[root@localhost ~]# rpm -qi httpdName : httpd Relocations:(not relocatable)#包名Version : 2.2.15 Vendor:CentOS#版本和厂商Release : 15.el6.centos.1 Build Date: 2012年02月14日星期二 06时27分1秒#发行版本和建立时间Install Date: 2013年01月07日星期一19时22分43秒Build Host:c6b18n2.bsys.dev.centos.org#安装时间Group : System Environment/Daemons Source RPM:httpd-2.2.15-15.el6.centos.1.src.rpm#组和源RPM包文件名Size : 2896132 License: ASL 2.0#软件包大小和许可协议Signature :RSA/SHA1,2012年02月14日星期二 19时11分00秒,Key ID0946fca2c105b9de#数字签名Packager:CentOS BuildSystem <http://bugs.centos.org>URL : http://httpd.apache.org/#厂商网址Summary : Apache HTTP Server#软件包说明Description:The Apache HTTP Server is a powerful, efficient, and extensible web server.#描述 除此之外,还可以查询未安装软件包的详细信息,命令格式为: 1[root@localhost ~]# rpm -qip 包全名 -p 选项表示查询未安装的软件包,是 package 的首字母。 注意,这里用的是包全名,且未安装的软件包需使用“绝对路径+包全名”的方式才能确定包。 rpm -ql:命令查询软件包的文件列表 rpm 软件包通常采用默认路径安装,各安装文件会分门别类安放在适当的目录文件下。使用 rpm 命令可以查询到已安装软件包中包含的所有文件及各自安装路径,命令格式为: 1[root@localhost ~]# rpm -ql 包名 -l 选项表示列出软件包所有文件的安装目录。 例如,查看 apache 软件包中所有文件以及各自的安装位置,可使用如下命令: 123456789[root@localhost ~]# rpm -ql httpd/etc/httpd/etc/httpd/conf/etc/httpd/conf.d/etc/httpd/conf.d/README/etc/httpd/conf.d/welcome.conf/etc/httpd/conf/httpd.conf/etc/httpd/conf/magic…省略部分输出… 同时,rpm 命令还可以查询未安装软件包中包含的所有文件以及打算安装的路径,命令格式如下: 1[root@localhost ~]# rpm -qlp 包全名 -p 选项表示查询未安装的软件包信息,是 package 的首字母。 注意,由于软件包还未安装,因此需要使用“绝对路径+包全名”的方式才能确定包。 比如,我们想查看 bing 软件包(未安装,绝对路径为:/mnt/cdrom/Packages/bind-9.8.2-0.10.rc1.el6.i686.rpm)中的所有文件及各自打算安装的位置,可以执行如下命令: 12345678[root@localhost ~]# rpm -qlp /mnt/cdrom/Packages/bind-9.8.2-0.10.rc1.el6.i686.rpm/etc/NetworkManager/dispatcher.d/13-named/etc/logrotate.d/named/etc/named/etc/named.conf/etc/named.iscdlv.key/etc/named.rfc1912.zones…省略部分输出… rpm -qf:命令查询系统文件属于哪个RPM包 rpm -ql 命令是通过软件包查询所含文件的安装路径,rpm 还支持反向查询,即查询某系统文件所属哪个 RPM 软件包。其命令格式如下: 1[root@localhost ~]# rpm -qf 系统文件名 -f 选项的含义是查询系统文件所属哪个软件包,是 file 的首字母。 注意,只有使用 RPM 包安装的文件才能使用该命令,手动方式建立的文件无法使用此命令。 例如,查询 ls 命令所属的软件包,可以执行如下命令: 12[root@localhost ~]# rpm -qf /bin/lscoreutils-8.4-19.el6.i686 rpm -qR:查询软件包的依赖关系 使用 rpm 命令安装 RPM 包,需考虑与其他 RPM 包的依赖关系。rpm -qR 命令就用来查询某已安装软件包依赖的其他包,该命令的格式为: 1[root@localhost ~]# rpm -qR 包名 -R(大写)选项的含义是查询软件包的依赖性,是 requires 的首字母。 例如,查询 apache 软件包的依赖性,可执行以下命令: 1234567891011[root@localhost ~]# rpm -qR httpd/bin/bash/bin/sh/etc/mime.types/usr/sbin/useraddapr-util-ldapchkconfigconfig(httpd) = 2.2.15-15.el6.centos.1httpd-tods = 2.2.15-15.el6.centos.1initscripts >= 8.36…省略部分输出… yum命令yum,全称“Yellow dog Updater, Modified”,是一个专门为了解决包的依赖关系而存在的软件包管理器。就好像 Windows 系统上可以通过 360 软件管家实现软件的一键安装、升级和卸载。 可以这么说,yum 是改进型的 RPM 软件管理器,它很好的解决了 RPM 所面临的软件包依赖问题。yum 在服务器端存有所有的 RPM 包,并将各个包之间的依赖关系记录在文件中,当管理员使用 yum 安装 RPM 包时,yum 会先从服务器端下载包的依赖性文件,通过分析此文件从服务器端一次性下载所有相关的 RPM 包并进行安装。 yum 软件可以用 rpm 命令安装,安装之前可以通过如下命令查看 yum 是否已安装: 123456[root@localhost ~]# rpm -qa | grep yumyum-metadata-parser-1.1.2-16.el6.i686yum-3.2.29-30.el6.centos.noarchyum-utils-1.1.30-14.el6.noarchyum-plugin-fastestmirror-1.1.30-14.el6.noarchyum-plugin-security-1.1.30-14.el6.noarch]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HashMap按键排序和按值排序]]></title>
<url>%2F2019%2F10%2F20%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2FHashMap%E6%8C%89%E9%94%AE%E6%8E%92%E5%BA%8F%E5%92%8C%E6%8C%89%E5%80%BC%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[HashMap按键排序和按值排序HashMap:我们最常用的Map,它根据key的HashCode 值来存储数据,根据key可以直接获取它的Value,同时它具有很快的访问速度。HashMap最多只允许一条记录的key值为Null(多条会覆盖);允许多条记录的Value为 Null。非同步的。 TreeMap: 能够把它保存的记录根据key排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。 Hashtable: 与 HashMap类似,不同的是:key和value的值均不允许为null;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。 LinkedHashMap: 保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历的时候会比HashMap慢。key和value均允许为空,非同步的。 TreeMapTreeMap默认是升序的,如果我们需要改变排序方式,则需要使用比较器:Comparator。 Comparator可以对集合对象或者数组进行排序的比较器接口,实现该接口的public compare(T o1,To2)方法即可实现排序,该方法主要是根据第一个参数o1,小于、等于或者大于o2分别返回负整数、0或者正整数。如下: 12345678910111213141516171819202122public class TreeMapTest { public static void main(String[] args) { Map<String, String> map = new TreeMap<String, String>( new Comparator<String>() { public int compare(String obj1, String obj2) { // 降序排序 return obj2.compareTo(obj1); } }); map.put("c", "ccccc"); map.put("a", "aaaaa"); map.put("b", "bbbbb"); map.put("d", "ddddd"); Set<String> keySet = map.keySet(); Iterator<String> iter = keySet.iterator(); while (iter.hasNext()) { String key = iter.next(); System.out.println(key + ":" + map.get(key)); } }} 上面例子是对根据TreeMap的key值来进行排序的,但是有时我们需要根据TreeMap的value来进行排序。对value排序我们就需要借助于Collections的sort(List list, Comparator<? super T> c)方法,该方法根据指定比较器产生的顺序对指定列表进行排序。但是有一个前提条件,那就是所有的元素都必须能够根据所提供的比较器来进行比较。如下: 123456789101112131415161718192021222324public class TreeMapTest { public static void main(String[] args) { Map<String, String> map = new TreeMap<String, String>(); map.put("d", "ddddd"); map.put("b", "bbbbb"); map.put("a", "aaaaa"); map.put("c", "ccccc"); //这里将map.entrySet()转换成list List<Map.Entry<String,String>> list = new ArrayList<Map.Entry<String,String>>(map.entrySet()); //然后通过比较器来实现排序 Collections.sort(list,new Comparator<Map.Entry<String,String>>() { //升序排序 public int compare(Entry<String, String> o1, Entry<String, String> o2) { return o1.getValue().compareTo(o2.getValue()); } }); for(Map.Entry<String,String> mapping:list){ System.out.println(mapping.getKey()+":"+mapping.getValue()); } }} HashMap我们都是HashMap的值是没有顺序的,他是按照key的HashCode来实现的。对于这个无序的HashMap我们要怎么来实现排序呢?参照TreeMap的value排序,我们一样的也可以实现HashMap的排序。 1234567891011121314151617181920212223public class HashMapTest { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("c", "ccccc"); map.put("a", "aaaaa"); map.put("b", "bbbbb"); map.put("d", "ddddd"); List<Map.Entry<String,String>> list = new ArrayList<Map.Entry<String,String>>(map.entrySet()); Collections.sort(list,new Comparator<Map.Entry<String,String>>() { //升序排序 public int compare(Entry<String, String> o1, Entry<String, String> o2) { return o1.getValue().compareTo(o2.getValue()); } }); for(Map.Entry<String,String> mapping:list){ System.out.println(mapping.getKey()+":"+mapping.getValue()); } }}]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[redis为什么可以承担高并发]]></title>
<url>%2F2019%2F10%2F20%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2Fredis%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8F%AF%E4%BB%A5%E6%89%BF%E6%8B%85%E9%AB%98%E5%B9%B6%E5%8F%91%2F</url>
<content type="text"><![CDATA[绝大部分请求是纯粹的内存操作(非常快速) 采用单线程,避免了不必要的上下文切换和竞争条件 非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间 epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。 对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[GET和POST的区别]]></title>
<url>%2F2019%2F10%2F19%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2FGET%E5%92%8CPOST%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[数据传输方式不同:GET请求通过URL传输数据,而POST的数据通过请求体传输。 安全性不同:POST的数据因为在请求主体内,所以有一定的安全性保证,而GET的数据在URL中,通过历史记录,缓存很容易查到数据信息。 数据类型不同:GET只允许 ASCII 字符,而POST无限制 后退页面的反应:get请求页面后退时,不产生影响;post请求页面后退时,会重新提交请求 特性不同:GET是安全(这里的安全是指只读特性,就是使用这个方法不会引起服务器状态变化)且幂等(幂等的概念是指同一个请求方法执行多次和仅执行一次的效果完全相同),而POST是非安全非幂等 缓存性:get请求是可以缓存的,post请求不可以缓存 传输数据的大小: get一般传输数据大小不超过2k-4k(根据浏览器不同,限制不一样,但相差不大);post请求传输数据的大小根据php.ini 配置文件设定,也可以无限大。]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[网络概念]]></title>
<url>%2F2019%2F10%2F19%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2F%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E6%A6%82%E5%BF%B5%2F</url>
<content type="text"><![CDATA[]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTP]]></title>
<url>%2F2019%2F10%2F15%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2FHTTP%2F</url>
<content type="text"><![CDATA[http请求信息格式请求行、请求头、空行、请求数据 请求行描述客户端的请求方式、请求资源的名称、http协议的版本号。 例如: GET/BOOK/JAVA.HTML HTTP/1.1 请求头客户机请求的服务器主机名,客户机的环境信息等 Accept:用于告诉服务器,客户机支持的数据类型 (例如:Accept:text/html,image/*)Accept-Charset:用于告诉服务器,客户机采用的编码格式Accept-Encoding:用于告诉服务器,客户机支持的数据压缩格式Accept-Language:客户机语言环境Host:客户机通过这个服务器,想访问的主机名If-Modified-Since:客户机通过这个头告诉服务器,资源的缓存时间Referer:客户机通过这个头告诉服务器,它(客户端)是从哪个资源来访问服务器的(防盗链)User-Agent:客户机通过这个头告诉服务器,客户机的软件环境(操作系统,浏览器版本等)Cookie:客户机通过这个头,将Coockie信息带给服务器Connection:告诉服务器,请求完成后,是否保持连接Date:告诉服务器,当前请求的时间 空行请求数据就是指浏览器端通过http协议发送给服务器的实体数据。例如:name=dylan&id=110(get请求时,通过url传给服务器的值。post请求时,通过表单发送给服务器的值) http响应信息格式状态行、消息报头、空行、响应正文 状态行例如: HTTP/1.1 200 OK (协议的版本号是1.1 响应状态码为200 响应结果为 OK) 消息报头Location:这个头配合302状态吗,用于告诉客户端找谁Server:服务器通过这个头,告诉浏览器服务器的类型Content-Encoding:告诉浏览器,服务器的数据压缩格式Content-Length:告诉浏览器,回送数据的长度Content-Type:告诉浏览器,回送数据的类型Last-Modified:告诉浏览器当前资源缓存时间Refresh:告诉浏览器,隔多长时间刷新Content-Disposition:告诉浏览器以下载的方式打开数据。例如: context.Response.AddHeader(“Content-Disposition”,”attachment:filename=aa.jpg”); context.Response.WriteFile(“aa.jpg”);Transfer-Encoding:告诉浏览器,传送数据的编码格式ETag:缓存相关的头(可以做到实时更新)Expries:告诉浏览器回送的资源缓存多长时间。如果是-1或者0,表示不缓存Cache-Control:控制浏览器不要缓存数据 no-cachePragma:控制浏览器不要缓存数据 no-cache Connection:响应完成后,是否断开连接。 close/Keep-AliveDate:告诉浏览器,服务器响应时间 响应正文响应包含浏览器能够解析的静态内容,例如:html,纯文本,图片等等信息 http状态码 分类 分类描述 1** 信息,服务器收到请求,需要请求者继续执行操作 2** 成功,操作被成功接收并处理 3** 重定向,需要进一步的操作以完成请求 4** 客户端错误,请求包含语法错误或无法完成请求 5** 服务器错误,服务器在处理请求的过程中发生了错误 1** 100 Continue 继续。客户端应继续其请求 101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 2** 200* OK 请求成功。一般用于GET与POST请求 201 Created 已创建。成功请求并创建了新的资源 202 Accepted 已接受。已经接受请求,但未处理完成 203 Non-Authoritative Information 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 204* No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 206* Partial Content 部分内容。服务器成功处理了部分GET请求 3** 300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 301* Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 302* Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI 303* See Other 查看其它地址。与301类似。使用GET和POST请求查看 304* Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 305 Use Proxy 使用代理。所请求的资源必须通过代理访问 306 Unused 已经被废弃的HTTP状态码 307* Temporary Redirect 临时重定向。与302类似。使用GET请求重定向 4** 400* Bad Request 客户端请求的语法错误,服务器无法理解 401* Unauthorized 请求要求用户的身份认证 402 Payment Required 保留,将来使用 403* Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求 404* Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面 405 Method Not Allowed 客户端请求中的方法被禁止 406 Not Acceptable 服务器无法根据客户端请求的内容特性完成请求 407 Proxy Authentication Required 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 408 Request Time-out 服务器等待客户端发送的请求时间过长,超时 409 Conflict 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 410 Gone 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 411 Length Required 服务器无法处理客户端发送的不带Content-Length的请求信息 412 Precondition Failed 客户端请求信息的先决条件错误 413 Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 414 Request-URI Too Large 请求的URI过长(URI通常为网址),服务器无法处理 415 Unsupported Media Type 服务器无法处理请求附带的媒体格式 416 Requested range not satisfiable 客户端请求的范围无效 417 Expectation Failed 服务器无法满足Expect的请求头信息 5** 500* Internal Server Error 服务器内部错误,无法完成请求 501 Not Implemented 服务器不支持请求的功能,无法完成请求 502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 503* Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求 505 HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理 http请求方法HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。 HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。 GET 请求指定的页面信息,并返回实体主体。 HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 PUT 从客户端向服务器传送的数据取代指定的文档的内容。 DELETE 请求服务器删除指定的页面。 CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 OPTIONS 允许客户端查看服务器的性能。 TRACE 回显服务器收到的请求,主要用于测试或诊断。 PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 HTTP的keep-alive是干什么的?在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上Connection: keep-alive。 keep-alive的优点: 较少的CPU和内存的使用(由于同时打开的连接的减少了) 允许请求和应答的HTTP管线化 降低拥塞控制 (TCP连接减少了) 减少了后续请求的延迟(无需再进行握手) 报告错误无需关闭TCP连接 什么是长连接、短连接?在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。 而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:Connection:keep-alive 在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。 HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。 HTTP2相对于HTTP1.x有什么优势和特点?二进制分帧帧:HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。 流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 头部压缩HTTP/1.x会在请求和响应中中重复地携带不常改变的、冗长的头部数据,给网络带来额外的负担。 HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新; 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。 你可以理解为只发送差异数据,而不是全部发送,从而减少头部的信息量 服务器推送服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。 服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。 多路复用HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制。 HTTP2中: 同域名下所有通信都在单个连接上完成。 单个连接可以承载任意数量的双向数据流。 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装 HTTP的缓存的过程是怎样的?通常情况下的步骤是: 客户端向服务器发出请求,请求资源 服务器返回资源,并通过响应头决定缓存策略 客户端根据响应头的策略决定是否缓存资源(这里假设是),并将响应头与资源缓存下来 在客户端再次请求且命中资源的时候,此时客户端去检查上次缓存的缓存策略,根据策略的不同、是否过期等判断是直接读取本地缓存还是与服务器协商缓存]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTPS和HTTP的区别]]></title>
<url>%2F2019%2F10%2F15%2F%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%2FHTTPS%E5%92%8CHTTP%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[HTTPS和HTTP的区别 HTTPS协议需要到CA申请证书,一般免费证书很少,需要交费。 HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。 HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。 HTTP的连接很简单,是无状态的。Https协议是由SSL+Http协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。 (无状态的意思是其数据包的发送、传输和接收都是相互独立的。 无连接的意思是指通信双方都不长久的维持对方的任何信息。) HTTPS的优点:1、使用Https协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。 2、Https协议是由SSL+Http协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、修改,确保数据的完整性。 3、Https是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。 HTTPS的缺点:1、Https协议握手阶段比较费时,会使页面的加载时间延长近。 2、Https连接缓存不如Http高效,会增加数据开销,甚至已有的安全措施也会因此而受到影响; 3、SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。 4、Https协议的加密范围也比较有限。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。 HTTPS连接过程 ①客户端的浏览器向服务器发送请求,并传送客户端SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。 ②服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。 ③客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。 ④用户端随机产生一个用于通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。 ⑤如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。 ⑥如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥能否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。 ⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通讯的加解密通讯。同时在SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。 ⑧客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。 ⑨服务器向客户端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。 ⑩SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。 对称加密和非对称加密的区别对称加密只有一把公钥,加密解密都是这个密钥进行 非对称加密有公钥和私钥,用公钥加密,用私钥解密 HTTPS是对称加密和非对称加密两者结合的,在传输对称加密的公钥的时候用到非对称加密进行加密传输]]></content>
<categories>
<category>计算机网络</category>
</categories>
<tags>
<tag>计算机网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[栈与队列]]></title>
<url>%2F2019%2F10%2F11%2F%E7%AE%97%E6%B3%95%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F%E6%A0%88%E4%B8%8E%E9%98%9F%E5%88%97%2F</url>
<content type="text"><![CDATA[广度优先搜索 - 模板使用 BFS 的两个主要方案:遍历或找出最短路径。通常,这发生在树或图中。 在特定问题中执行 BFS 之前确定结点和边缘非常重要。通常,结点将是实际结点或是状态,而边缘将是实际边缘或可能的转换。 模板 I 123456789101112131415161718192021222324/** * 返回根节点到目标节点之间的最短路径长度。 */int BFS(Node root, Node target) { Queue<Node> queue; // 存储所有等待处理的节点 int step = 0; // 从根到当前节点所需的步骤数 // 初始化 add root to queue; // BFS while (queue is not empty) { step = step + 1; // 迭代队列中已有的节点 int size = queue.size(); for (int i = 0; i < size; ++i) { Node cur = the first node in queue; return step if cur is target; for (Node next : the neighbors of cur) { add next to queue; } remove the first node from queue; } } return -1; // 没有从根到目标的路径} 如代码所示,在每一轮中,队列中的结点是等待处理的结点。 在每个更外一层的 while 循环之后,我们距离根结点更远一步。变量 step 指示从根结点到我们正在访问的当前结点的距离。 模板 II 有时,确保我们永远不会访问一个结点两次很重要。否则,我们可能陷入无限循环。如果是这样,我们可以在上面的代码中添加一个哈希集来解决这个问题。这是修改后的伪代码: 1234567891011121314151617181920212223242526272829/** * 返回根节点到目标节点之间的最短路径长度。 */int BFS(Node root, Node target) { Queue<Node> queue; // 存储所有等待处理的节点 Set<Node> used; // 存储所有使用的节点 int step = 0; // 从根到当前节点所需的步骤数 // 初始化 add root to queue; add root to used; // BFS while (queue is not empty) { step = step + 1; // 迭代队列中已有的节点 int size = queue.size(); for (int i = 0; i < size; ++i) { Node cur = the first node in queue; return step if cur is target; for (Node next : the neighbors of cur) { if (next is not in used) { add next to queue; add next to used; } } remove the first node from queue; } } return -1; // 没有从根到目标的路径} 有两种情况不需要使用哈希集: 完全确定没有循环,例如,在树遍历中; 确实希望多次将结点添加到队列中。 岛屿数量给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。 示例 1: 123456输入:11110110101100000000输出: 1 示例 2: 123456输入:11000110000010000011输出: 3 123456789101112131415161718192021222324252627282930313233343536373839404142434445class Solution { public int numIslands(char[][] grid) { if(grid==null || grid.length==0){ return 0; } int nr = grid.length;//数组行数 int nc = grid[0].length;//数组列数 int num_islands = 0; for(int r=0 ; r < nr ; r++){ for(int c=0 ; c < nc ; c++){ if(grid[r][c]=='1'){ num_islands++; grid[r][c] = '0'; Queue<Integer> queue = new LinkedList<Integer>(); queue.add(r*nc+c);//用这种方式存下数组下标 while(!queue.isEmpty()){ int id = queue.remove(); int row = id/nc;//整除得到行数 int col = id%nc;//取余得到列数 //上下左右进行判断,若是'1',则添加到队列,但num_islands不加一,是同一个岛屿 if(row-1 >= 0 && grid[row-1][col]=='1'){ grid[row-1][col] = '0'; queue.add((row-1)*nc + col); } if(row+1 < nr && grid[row+1][col]=='1'){ grid[row+1][col] = '0'; queue.add((row+1)*nc + col); } if(col-1 >=0 && grid[row][col-1]=='1'){ grid[row][col-1] = '0'; queue.add(row*nc + col-1); } if(col+1 < nc && grid[row][col+1]=='1'){ grid[row][col+1] = '0'; queue.add(row*nc + col+1); } } } } } return num_islands; }} 设计循坏队列设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。 你的实现应该支持如下操作: MyCircularQueue(k): 构造器,设置队列长度为 k 。 Front: 从队首获取元素。如果队列为空,返回 -1 。 Rear: 获取队尾元素。如果队列为空,返回 -1 。 enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。 deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。 isEmpty(): 检查循环队列是否为空。 isFull(): 检查循环队列是否已满。 12345678910111213141516171819MyCircularQueue circularQueue = new MycircularQueue(3); // 设置长度为 3circularQueue.enQueue(1); // 返回 truecircularQueue.enQueue(2); // 返回 truecircularQueue.enQueue(3); // 返回 truecircularQueue.enQueue(4); // 返回 false,队列已满circularQueue.Rear(); // 返回 3circularQueue.isFull(); // 返回 truecircularQueue.deQueue(); // 返回 truecircularQueue.enQueue(4); // 返回 truecircularQueue.Rear(); // 返回 4 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071class MyCircularQueue { private int[] content; private int head; private int tail; private int length; /** Initialize your data structure here. Set the size of the queue to be k. */ public MyCircularQueue(int k) { length = k+1; content = new int[k+1]; head = 0; tail = 0; } /** Insert an element into the circular queue. Return true if the operation is successful. */ public boolean enQueue(int value) { if((tail+1)%length==head){ return false; } content[tail] = value; tail = (tail+1)%length; return true; } /** Delete an element from the circular queue. Return true if the operation is successful. */ public boolean deQueue() { if(tail==head){ return false; } head = (head+1)%length; return true; } /** Get the front item from the queue. */ public int Front() { if(isEmpty()){ return -1; } return content[head]; } /** Get the last item from the queue. */ public int Rear() { if(isEmpty()){ return -1; } return content[(tail-1+length)%length]; } /** Checks whether the circular queue is empty or not. */ public boolean isEmpty() { return head==tail; } /** Checks whether the circular queue is full or not. */ public boolean isFull() { return (tail+1)%length==head; }}/** * Your MyCircularQueue object will be instantiated and called as such: * MyCircularQueue obj = new MyCircularQueue(k); * boolean param_1 = obj.enQueue(value); * boolean param_2 = obj.deQueue(); * int param_3 = obj.Front(); * int param_4 = obj.Rear(); * boolean param_5 = obj.isEmpty(); * boolean param_6 = obj.isFull(); */ 打开转盘锁你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。 锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。 列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。 字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。 示例 1: 123456输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"输出:6解释:可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,因为当拨动到 "0102" 时这个锁就会被锁定。 示例 2: 1234输入: deadends = ["8888"], target = "0009"输出:1解释:把最后一位反向旋转一次即可 "0000" -> "0009"。 示例 3: 1234输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"输出:-1解释:无法旋转到目标数字且不被锁定。 示例 4: 12输入: deadends = ["0000"], target = "8888"输出:-1 提示: 死亡列表 deadends 的长度范围为 [1, 500]。 目标数字 target 不会在 deadends 之中。 每个 deadends 和 target 中的字符串的数字会在 10,000 个可能的情况 '0000' 到 '9999' 中产生。 普通做法: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354class Solution { public int openLock(String[] deadends, String target) { int step=0;//旋转次数 //将deadends转为list,方便判断字符串是否是禁忌字符串 List<String> list=Arrays.asList(deadends); Queue<String> queue=new LinkedList<>();//生成队列 Set<String> used=new HashSet<>();//存放已经尝试过的密码值 queue.add("0000");//初始密码,也就是根节点 used.add("0000");//“0000”已经尝试,不再允许使用 while (queue.isEmpty()==false) { int size=queue.size();//队列长度 for (int i = 0; i < size; i++) { String cur=queue.peek();//当前尝试的密码 if (cur.equals(target)) { return step;//如果当前密码与target相同,则成功,直接返回旋转次数 } String[] neib=neighbour(cur);//生成邻居,也就是旋转一次之后可能的字符串 for (String str1 : neib) { //对邻居检查,如果没有使用过并且不是禁忌字符串,则放入队列 if (used.contains(str1)==false && list.contains(cur)==false) { queue.add(str1); used.add(str1); } } queue.poll();//已经尝试过的上层节点释放 } step=step+1; //尝试结束之后部署+1;因为进入队列的数需要下一次循环才能判断是否是正确密码 } return -1;//如果没有返回值说明打开失败 } public String[] neighbour(String a) { //生成邻居,一个字符串有4位数字,每位数字可以增加1,或减少1,所以总共有4*2=8个邻居 String ans[]=new String[8]; for (int i = 0; i < 4; i++) { char[] aa=a.toCharArray(); int a1=aa[i]-'0'; if (a1!=0) { aa[i]=Character.forDigit(a1-1, 10); }else { aa[i]='9'; } ans[i*2]=String.valueOf(aa); if(a1!=9) { aa[i]=Character.forDigit(a1+1, 10); }else { aa[i]='0'; } ans[i*2+1]=String.valueOf(aa); } return ans; }} 完全平方数给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4. 示例 2: 输入: n = 13 输出: 2 解释: 13 = 4 + 9.*/ 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263package queue;import java.util.Arrays;import java.util.LinkedList;import java.util.Queue;import java.util.Scanner;/** * @author beny * @Create 2020-08-04-20:47 */public class PerfectSquare { /* 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 示例 1: 输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4. 示例 2: 输入: n = 13 输出: 2 解释: 13 = 4 + 9.*/ public static int numSquares(int n){ Boolean[] visited = new Boolean[n+1];//记录广度搜索时,过滤重复的数值 Arrays.fill(visited,false); Queue<Integer> queue = new LinkedList<>(); int count = 0; queue.offer(n); //1、队列不为空时,证明n没有为0,还可以继续往下 while(!queue.isEmpty()){ int size = queue.size(); count++;//2、每一层就是一个平方数 for(int i=0 ; i<size ; i++){ int tmp = queue.poll(); visited[tmp] = true; //3、遍历每一层的队列里面的数值,直到n为0 for(int j=1 ; j*j<=n ; j++){ if(tmp-j*j==0){ return count; }else if(tmp-j*j<0){ break; }else{ if(!visited[tmp-j*j]){ queue.offer(tmp-j*j); } } } } } return count; } public static void main(String[] args) { Scanner s = new Scanner(System.in); System.out.println("请输入n:"); int n = s.nextInt(); System.out.println(numSquares(n)); }} 最小栈设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 push(x) —— 将元素 x 推入栈中。pop() —— 删除栈顶的元素。top() —— 获取栈顶元素。getMin() —— 检索栈中的最小元素。 123456789101112131415161718示例:输入:["MinStack","push","push","push","getMin","pop","top","getMin"][[],[-2],[0],[-3],[],[],[],[]]输出:[null,null,null,null,-3,null,0,-2]解释:MinStack minStack = new MinStack();minStack.push(-2);minStack.push(0);minStack.push(-3);minStack.getMin(); --> 返回 -3.minStack.pop();minStack.top(); --> 返回 0.minStack.getMin(); --> 返回 -2. 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283package stack;import java.util.Stack;/** * @author beny * @Create 2020-08-06-22:24 */public class MinStack { /** * 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 * * push(x) —— 将元素 x 推入栈中。 * pop() —— 删除栈顶的元素。 * top() —— 获取栈顶元素。 * getMin() —— 检索栈中的最小元素。 * * * 示例: * * 输入: * ["MinStack","push","push","push","getMin","pop","top","getMin"] * [[],[-2],[0],[-3],[],[],[],[]] * * 输出: * [null,null,null,null,-3,null,0,-2] * * 解释: * MinStack minStack = new MinStack(); * minStack.push(-2); * minStack.push(0); * minStack.push(-3); * minStack.getMin(); --> 返回 -3. * minStack.pop(); * minStack.top(); --> 返回 0. * minStack.getMin(); --> 返回 -2. */ Stack<Integer> s1 = new Stack<>(); Stack<Integer> s2 = new Stack<>(); /** initialize your data structure here. */ public MinStack() { } public void push(int x) { s1.push(x); if(s2.isEmpty() || x <= s2.peek()){ s2.push(x); } } public void pop() { if(s1.isEmpty()) return; //Integer的==和equals注意 if(s1.peek().equals(s2.peek())){ s2.pop(); } s1.pop(); } public int top() { return s1.peek(); } public int getMin() { return s2.peek(); } public static void main(String[] args) { MinStack stack = new MinStack(); stack.push(512); stack.push(-1024); stack.push(-1024); stack.push(512); stack.pop(); System.out.println(stack.getMin()); stack.pop(); System.out.println(stack.getMin()); stack.pop(); System.out.println(stack.getMin()); }} 有效的括号给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。注意空字符串可被认为是有效字符串。 示例 1: 输入: “()”输出: true示例 2: 输入: “()[]{}”输出: true示例 3: 输入: “(]”输出: false示例 4: 输入: “([)]”输出: false示例 5: 输入: “{[]}”输出: true 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566package stack;import java.util.Stack;/** * @author beny * @Create 2020-08-07-21:59 */public class ValidParentheses { /* * 有效的括号 * 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。 注意空字符串可被认为是有效字符串。 示例 1: 输入: "()" 输出: true 示例 2: 输入: "()[]{}" 输出: true 示例 3: 输入: "(]" 输出: false 示例 4: 输入: "([)]" 输出: false 示例 5: 输入: "{[]}" 输出: true */ public static boolean isValid(String s){ Stack<Character> stack = new Stack<>(); int length = s.length(); for(int i=0; i<length; i++){ if(stack.empty() || s.charAt(i) == '(' || s.charAt(i) == '{' || s.charAt(i) == '['){ stack.push(s.charAt(i)); }else{ if(s.charAt(i) == ')' && stack.peek() == '(' || s.charAt(i) == '}' && stack.peek() == '{' || s.charAt(i) == ']' && stack.peek() == '[' ){ stack.pop(); }else return false; } } if(stack.empty()) return true; else return false; } public static void main(String[] args) { String s = "({)[]}"; System.out.println(isValid(s)); }} 每日温度 请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 123456789101112131415161718192021222324252627282930313233343536373839404142434445package stack;import java.util.Stack;/** * @author beny * @Create 2020-08-19-22:37 */public class DailyTemperature { /** * 每日温度 * 请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 * * 例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 * * 提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 */ public static int[] dailyTemperatures(int[] T){ int ans[] = new int[T.length]; Stack<Integer> s = new Stack<>(); //循环坐标,如果栈顶元素小于当前坐标,则用当前坐标减去栈顶元素为坐标之间的差,否则将当前坐标放入栈中 //思路:主要是考虑栈的回溯和坐标之间距离即是当前ans坐标的值,前面比后面小则进行回溯,否则把当前坐标放入栈中 for(int i=0; i<T.length; i++){ while (!s.isEmpty() && T[s.peek()]<T[i]){ ans[s.peek()] = i - s.peek(); s.pop(); } s.add(i); } while (!s.isEmpty()){ ans[s.pop()] = 0; } return ans; } public static void main(String[] args) { int[] T = new int[]{73,74,75,71,69,72,76,73}; int ans[] = dailyTemperatures(T); for(int a:ans){ System.out.print(a+","); } }} 逆波兰表达式求值根据 逆波兰表示法,求表达式的值。 有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 说明: 整数除法只保留整数部分。给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 示例 1: 输入: [“2”, “1”, “+”, “3”, ““]输出: 9解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) 3) = 9示例 2: 输入: [“4”, “13”, “5”, “/“, “+”]输出: 6解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6示例 3: 输入: [“10”, “6”, “9”, “3”, “+”, “-11”, ““, “/“, ““, “17”, “+”, “5”, “+”]输出: 22解释:该算式转化为常见的中缀算术表达式为: ((10 (6 / ((9 + 3) -11))) + 17) + 5= ((10 (6 / (12 -11))) + 17) + 5= ((10 (6 / -132)) + 17) + 5= ((10 0) + 17) + 5= (0 + 17) + 5= 17 + 5= 22 逆波兰表达式: 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) ( 3 + 4 ) 。该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) ) 。逆波兰表达式主要有以下两个优点: 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106package stack;import java.util.Stack;/** * @author beny * @Create 2020-08-20-21:21 */public class AgainstPoland { /** * 逆波兰表达式求值 * 根据 逆波兰表示法,求表达式的值。 * * 有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 * * 说明: * * 整数除法只保留整数部分。 * 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 * * 示例 1: * * 输入: ["2", "1", "+", "3", "*"] * 输出: 9 * 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 * 示例 2: * * 输入: ["4", "13", "5", "/", "+"] * 输出: 6 * 解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 * 示例 3: * * 输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] * 输出: 22 * 解释: * 该算式转化为常见的中缀算术表达式为: * ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 * = ((10 * (6 / (12 * -11))) + 17) + 5 * = ((10 * (6 / -132)) + 17) + 5 * = ((10 * 0) + 17) + 5 * = (0 + 17) + 5 * = 17 + 5 * = 22 * * * 逆波兰表达式: * * 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。 * * 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 * 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。 * 逆波兰表达式主要有以下两个优点: * * 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 * 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 */ public static int evalRPN(String[] tokens){ Stack<Integer> stack = new Stack<>(); int length = tokens.length; int a = 0; int b = 0; for(int i=0 ; i<length ; i++) { if (!tokens[i].equals("+") && !tokens[i].equals("-") && !tokens[i].equals("*") && !tokens[i].equals("/")) { stack.add(Integer.valueOf(tokens[i])); } else { a = stack.pop(); b = stack.pop(); if (tokens[i].equals("+")) { stack.add(b + a); } else if (tokens[i].equals("-")) { stack.add(b - a); } else if (tokens[i].equals("*")) { stack.add(b * a); } else if (tokens[i].equals("/")) { stack.add(b / a); } } } return stack.peek(); } public static int evalRPN2(String[] tokens){ Stack<Integer> stack = new Stack<>(); int a=0,b=0; for(String val : tokens){ if("+-*/".contains(val)){ a = stack.pop(); b = stack.pop(); } switch (val){ case "+" :stack.push(b + a); break; case "-" :stack.push(b - a); break; case "*" :stack.push(b * a); break; case "/" :stack.push(b / a); break; default: stack.push(Integer.valueOf(val)); } } return stack.pop(); } public static void main(String[] args) { String[] tokens = new String[]{"10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"}; System.out.println(evalRPN(tokens)); }} DFS模板I12345678910111213/* * Return true if there is a path from cur to target. */boolean DFS(Node cur, Node target, Set<Node> visited) { return true if cur is target; for (next : each neighbor of cur) { if (next is not in visited) { add next to visted; return true if DFS(next, target, visited) == true; } } return false;} 克隆图1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071package stack;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;/** * @author beny * @Create 2020-08-24-22:07 */class Node { public int val; public List<Node> neighbors; public Node() { val = 0; neighbors = new ArrayList<Node>(); } public Node(int _val) { val = _val; neighbors = new ArrayList<Node>(); } public Node(int _val, ArrayList<Node> _neighbors) { val = _val; neighbors = _neighbors; }}public class ClonalGraph { /** * 提示: * * 节点数不超过 100 。 * 每个节点值 Node.val 都是唯一的,1 <= Node.val <= 100。 * 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。 * 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。 * 图是连通图,你可以从给定节点访问到所有节点。 * * 输入:adjList = [[2,4],[1,3],[2,4],[1,3]] * 输出:[[2,4],[1,3],[2,4],[1,3]] * 解释: * 图中有 4 个节点。 * 节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 * 节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 * 节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 * 节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 * */ public Node cloneGraph(Node node){ Map<Node , Node> map = new HashMap<>(); return dfs(node,map); } public Node dfs(Node node, Map<Node,Node> map){ if(node == null){ return null; } if(map.containsKey(node)){ return map.get(node); } Node clone = new Node(node.val,new ArrayList<>()); map.put(node,clone); for(Node n : node.neighbors){ clone.neighbors.add(dfs(n,map)); } return clone; }}]]></content>
<categories>
<category>算法</category>
</categories>
<tags>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[面经]]></title>
<url>%2F2019%2F10%2F10%2F%E9%9D%A2%E7%BB%8F%2F%E9%9D%A2%E7%BB%8F%2F</url>
<content type="text"><![CDATA[远光oracle和mysql的区别 Exception和Error有什么区别,使用异常需要注意什么 java优化代码 重构的定义 土巴兔一面 Java数据类型(char啊) hashmap底层原理 如果是put对象怎么办 string stringbuffer stringbuilder区别 string 中文字符占几个字节 java的反射机制 二面 项目加入mq怎么做,加在哪里 mq放在下订单不行,会阻塞 mq几个特性:限流削峰,异步调用,怎么把这些功能加进项目里 注册怎么注册,手机登录,过程可以怎么做 邮件注册登录怎么实现 md5加密是对称加密还是非对称加密 金证bean的生命周期,过程 https和http的区别 servlet创建销毁过程 mybatis连接MySQL Spring AOP和Spring IOC 多线程创建方式 对业务的了解 纷享一面: ==和equals的区别(Integer i=1;int j=1;i==j,i.equals(j)) http和https的区别 @resource和@autowired的区别 ArrayList手写遍历,手写删除的几种方式 list1和list2相等,list1包含list2,怎么判断 HashMap是存key还是value,HashMap扩容机制 写一道Group By的SQL 联合索引的最左匹配原则 双亲委派模型,同一个类用不同的加载器加载,jvm会认为是同一个类吗? 二面: Spring的底层原理,IOC和AOP底层原理 为什么使用Redis Redis为什么可以承担高并发,为什么可以这么快 zookeeper分布式锁用来干嘛的,为什么用它来解决双写数据不一致的问题 可以用java的锁去解决这个问题吗 招商金科一面: Redis集群脑裂怎么做 spring的事务 SimpleDateFormat那个转换日期类是否线程安全,怎么领它线程安全 阻塞队列blockingquenen了解吗 object方法了解哪个?equals方法重写注意什么 hashmap遍历几种方法 Restcontroller和controller的区别 Java数组怎么输出控制台,怎么在控制台输入 mybatis里面#和$符号区别,mybatis如何防止SQL注入 labada表达式 二面(综合面,没问什么) 简单介绍一下zookeeper,我说了项目的zookeeper分布式锁 Spring的核心机制(Spring IOC和Spring AOP) 数据结构学了什么内容,说说排序有哪几种,快排的时间复杂度,冒泡的时间复杂度 java 里面有Synchronized和reentranlock,两者的区别 java里面有个是等待所有线程一起执行的工具类循环栅栏说一下 棒谷慢查询 索引优化 zookeeper分布式锁解决什么问题 项目架构图,项目包 网络攻击,跨域攻击 烧绳子问题 海量数据数据库隔离级别 实习内容 zookeeper分布式锁怎么用 jdbc使用,statement和praprestatement的区别 MySQL索引 在java开发当中需要注意什么问题 两个数据集,内存只有4G,怎么获取两个相同的数据,两个数据集都>4G java开发时有没有遇过性能问题 赞同tcp三次握手 servlet生命周期 springmvc过程 线程池说一下,几种创建线程池方式 synchronized和retreenlock区别 MySQL索引底层原理,联合索引最左匹配原则 项目Redis集群说一下 中间件开发了解吗? object方法了解哪些]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Exception和Error有什么区别]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FException%E5%92%8CError%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。 Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。 Exception又分为两类 CheckedException:(编译时异常) 需要用try——catch显示的捕获,对于可恢复的异常使用CheckedException。 UnCheckedException(RuntimeException):(运行时异常)不需要捕获,对于程序错误(不可恢复)的异常使用RuntimeException。 常见的RuntimeException异常: illegalArgumentException:此异常表明向方法传递了一个不合法或不正确的参数。 illegalStateException:在不合理或不正确时间内唤醒一方法时出现的异常信息。换句话说,即 Java 环境或 Java 应用不满足请求操作。 NullpointerException:空指针异常(我目前遇见的最多的) IndexOutOfBoundsException:索引超出边界异常 常见的CheckedException异常 我们在编写程序过程中try——catch捕获到的一场都是CheckedException。 io包中的IOExecption及其子类,都是CheckedException。 使用异常注意事项: 子类重写父类方法时,子类的方法必须抛出相同的异常或父类异常的子类。 如果父类抛出了多个异常,子类重写父类时,只能抛出相同的异常或者是他的子集,子类不能抛出父类没有的异常 如果被重写的方法没有异常抛出,那么子类的方法绝对不可以抛出异常,如果子类方法内有异常发生,那么子类只能try,不能throws 尽量不要捕获类似Exception这样的通用异常,而应捕获特定异常。例如进行文件流操作时应捕获IOException,而不能笼统地捕获Exception。 4.捕获多个异常时必须先捕获小的异常再捕获大的异常。]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java反射机制]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FJava%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。 反射机制常用的类Java.lang.Class; Java.lang.reflect.Constructor; Java.lang.reflect.Field; Java.lang.reflect.Method; Java.lang.reflect.Modifier; 反射的原理下图是类的正常加载过程,反射原理与class对象: Class对象的由来是将class文件读入内存,并为之创建一个Class对象。 反射的优缺点:1、优点:使用反射,我们就可以在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。 2、缺点: (1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射; (2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。 反射的用途:1、反编译:.class–>.java 2、通过反射机制访问java对象的属性,方法,构造方法等 3、当我们在使用IDE,比如Ecplise时,当我们输入一个对象或者类,并想调用他的属性和方法是,一按点号,编译器就会自动列出他的属性或者方法,这里就是用到反射。 4、反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。 反射的基本使用:1、获得Class:主要有三种方法: (1)Object–>getClass (2)任何数据类型(包括基本的数据类型)都有一个“静态”的class属性 (3)通过class类的静态方法:forName(String className)(最常用) 12345678910111213141516171819202122package fanshe; public class Fanshe { public static void main(String[] args) { //第一种方式获取Class对象 Student stu1 = new Student();//这一new 产生一个Student对象,一个Class对象。 Class stuClass = stu1.getClass();//获取Class对象 System.out.println(stuClass.getName()); //第二种方式获取Class对象 Class stuClass2 = Student.class; System.out.println(stuClass == stuClass2);//判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 //第三种方式获取Class对象 try { Class stuClass3 = Class.forName("fanshe.Student");//注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 System.out.println(stuClass3 == stuClass2);//判断三种方式是否获取的是同一个Class对象 } catch (ClassNotFoundException e) { e.printStackTrace(); } }} 在运行期间,一个类,只有一个Class对象产生,所以打印结果都是true; 三种方式中,常用第三种,第一种对象都有了还要反射干什么,第二种需要导入类包,依赖太强,不导包就抛编译错误。一般都使用第三种,一个字符串可以传入也可以写在配置文件中等多种方法。 2、判断是否为某个类的示例: 一般的,我们使用instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断是否为某个类的实例,他是一个native方法。 1public native boolean isInstance(Object obj); 3、创建实例:通过反射来生成对象主要有两种方法: (1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。 12Class<?> c = String.class;Object str = c.newInstance(); (2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建对象,这种方法可以用指定的构造器构造类的实例。 123456//获取String的Class对象Class<?> c = String.class;//通过Class对象获取指定的Constructor构造器对象Constructor constructor=c.getConstructor(String.class);//根据构造器创建实例:Object obj = constructor.newInstance(“hello reflection”); 4、通过反射获取构造方法并使用: (1)批量获取的方法:public Constructor[] getConstructors():所有”公有的”构造方法public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有) (2)单个获取的方法,并调用:public Constructor getConstructor(Class… parameterTypes):获取单个的”公有的”构造方法:public Constructor getDeclaredConstructor(Class… parameterTypes):获取”某个构造方法”可以是私有的,或受保护、默认、公有; (3) 调用构造方法: Constructor–>newInstance(Object… initargs) newInstance是 Constructor类的方法(管理构造函数的类) api的解释为:newInstance(Object… initargs) ,使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。 它的返回值是T类型,所以newInstance是创建了一个构造方法的声明类的新实例对象,并为之调用。 例子: Student类:共六个构造方法。 12345678910111213141516171819202122232425262728package fanshe;public class Student { //---------------构造方法------------------- //(默认的构造方法) Student(String str){ System.out.println("(默认)的构造方法 s = " + str); } //无参构造方法 public Student(){ System.out.println("调用了公有、无参构造方法执行了。。。"); } //有一个参数的构造方法 public Student(char name){ System.out.println("姓名:" + name); } //有多个参数的构造方法 public Student(String name ,int age){ System.out.println("姓名:"+name+"年龄:"+ age);//这的执行效率有问题,以后解决。 } //受保护的构造方法 protected Student(boolean n){ System.out.println("受保护的构造方法 n = " + n); } //私有构造方法 private Student(int age){ System.out.println("私有的构造方法 年龄:"+ age); }} 测试类: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354package fanshe;import java.lang.reflect.Constructor; /* * 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员; * * 1.获取构造方法: * 1).批量的方法: * public Constructor[] getConstructors():所有"公有的"构造方法 public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有) * 2).获取单个的方法,并调用: * public Constructor getConstructor(Class... parameterTypes):获取单个的"公有的"构造方法: * public Constructor getDeclaredConstructor(Class... parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有; * 3).调用构造方法: * Constructor-->newInstance(Object... initargs) */public class Constructors { public static void main(String[] args) throws Exception { //1.加载Class对象 Class clazz = Class.forName("fanshe.Student"); //2.获取所有公有构造方法 System.out.println("**********************所有公有构造方法*********************************"); Constructor[] conArray = clazz.getConstructors(); for(Constructor c : conArray){ System.out.println(c); } System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); conArray = clazz.getDeclaredConstructors(); for(Constructor c : conArray){ System.out.println(c); } System.out.println("*****************获取公有、无参的构造方法*******************************"); Constructor con = clazz.getConstructor(null); //1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型 //2>、返回的是描述这个无参构造函数的类对象。 System.out.println("con = " + con); //调用构造方法 Object obj = con.newInstance(); // System.out.println("obj = " + obj); // Student stu = (Student)obj; System.out.println("******************获取私有构造方法,并调用*******************************"); con = clazz.getDeclaredConstructor(char.class); System.out.println(con); //调用构造方法 con.setAccessible(true);//暴力访问(忽略掉访问修饰符) obj = con.newInstance('男'); }} 控制台输出: 1234567891011121314151617**********************所有公有构造方法*********************************public fanshe.Student(java.lang.String,int)public fanshe.Student(char)public fanshe.Student()************所有的构造方法(包括:私有、受保护、默认、公有)***************private fanshe.Student(int)protected fanshe.Student(boolean)public fanshe.Student(java.lang.String,int)public fanshe.Student(char)public fanshe.Student()fanshe.Student(java.lang.String)*****************获取公有、无参的构造方法*******************************con = public fanshe.Student()调用了公有、无参构造方法执行了。。。******************获取私有构造方法,并调用*******************************public fanshe.Student(char)姓名:男 5、获取成员变量并调用: Student类: 123456789101112131415161718package fanshe.field; public class Student { public Student(){ } //**********字段*************// public String name; protected int age; char sex; private String phoneNum; @Override public String toString() { return "Student [name=" + name + ", age=" + age + ", sex=" + sex + ", phoneNum=" + phoneNum + "]"; }} 测试类: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354package fanshe.field;import java.lang.reflect.Field;/* * 获取成员变量并调用: * * 1.批量的 * 1).Field[] getFields():获取所有的"公有字段" * 2).Field[] getDeclaredFields():获取所有字段,包括:私有、受保护、默认、公有; * 2.获取单个的: * 1).public Field getField(String fieldName):获取某个"公有的"字段; * 2).public Field getDeclaredField(String fieldName):获取某个字段(可以是私有的) * * 设置字段的值: * Field --> public void set(Object obj,Object value): * 参数说明: * 1.obj:要设置的字段所在的对象; * 2.value:要为字段设置的值; */public class Fields { public static void main(String[] args) throws Exception { //1.获取Class对象 Class stuClass = Class.forName("fanshe.field.Student"); //2.获取字段 System.out.println("************获取所有公有的字段********************"); Field[] fieldArray = stuClass.getFields(); for(Field f : fieldArray){ System.out.println(f); } System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); fieldArray = stuClass.getDeclaredFields(); for(Field f : fieldArray){ System.out.println(f); } System.out.println("*************获取公有字段**并调用***********************************"); Field f = stuClass.getField("name"); System.out.println(f); //获取一个对象 Object obj = stuClass.getConstructor().newInstance();//产生Student对象--》Student stu = new Student(); //为字段设置值 f.set(obj, "刘德华");//为Student对象中的name属性赋值--》stu.name = "刘德华" //验证 Student stu = (Student)obj; System.out.println("验证姓名:" + stu.name); System.out.println("**************获取私有字段****并调用********************************"); f = stuClass.getDeclaredField("phoneNum"); System.out.println(f); f.setAccessible(true);//暴力反射,解除私有限定 f.set(obj, "18888889999"); System.out.println("验证电话:" + stu); } } 控制台输出: 12345678910111213************获取所有公有的字段********************public java.lang.String fanshe.field.Student.name************获取所有的字段(包括私有、受保护、默认的)********************public java.lang.String fanshe.field.Student.nameprotected int fanshe.field.Student.agechar fanshe.field.Student.sexprivate java.lang.String fanshe.field.Student.phoneNum*************获取公有字段**并调用***********************************public java.lang.String fanshe.field.Student.name验证姓名:刘德华**************获取私有字段****并调用********************************private java.lang.String fanshe.field.Student.phoneNum验证电话:Student [name=刘德华, age=0, sex= 6、获取成员方法并调用: Student类: 123456789101112131415161718package fanshe.method; public class Student { //**************成员方法***************// public void show1(String s){ System.out.println("调用了:公有的,String参数的show1(): s = " + s); } protected void show2(){ System.out.println("调用了:受保护的,无参的show2()"); } void show3(){ System.out.println("调用了:默认的,无参的show3()"); } private String show4(int age){ System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age); return "abcd"; }} 测试类: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package fanshe.method;import java.lang.reflect.Method; /* * 获取成员方法并调用: * * 1.批量的: * public Method[] getMethods():获取所有"公有方法";(包含了父类的方法也包含Object类) * public Method[] getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的) * 2.获取单个的: * public Method getMethod(String name,Class<?>... parameterTypes): * 参数: * name : 方法名; * Class ... : 形参的Class类型对象 * public Method getDeclaredMethod(String name,Class<?>... parameterTypes) * * 调用方法: * Method --> public Object invoke(Object obj,Object... args): * 参数说明: * obj : 要调用方法的对象; * args:调用方式时所传递的实参;): */public class MethodClass { public static void main(String[] args) throws Exception { //1.获取Class对象 Class stuClass = Class.forName("fanshe.method.Student"); //2.获取所有公有方法 System.out.println("***************获取所有的”公有“方法*******************"); stuClass.getMethods(); Method[] methodArray = stuClass.getMethods(); for(Method m : methodArray){ System.out.println(m); } System.out.println("***************获取所有的方法,包括私有的*******************"); methodArray = stuClass.getDeclaredMethods(); for(Method m : methodArray){ System.out.println(m); } System.out.println("***************获取公有的show1()方法*******************"); Method m = stuClass.getMethod("show1", String.class); System.out.println(m); //实例化一个Student对象 Object obj = stuClass.getConstructor().newInstance(); m.invoke(obj, "刘德华"); System.out.println("***************获取私有的show4()方法******************"); m = stuClass.getDeclaredMethod("show4", int.class); System.out.println(m); m.setAccessible(true);//解除私有限定 Object result = m.invoke(obj, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参 System.out.println("返回值:" + result); }} 控制台输出: 1234567891011121314151617181920212223***************获取所有的”公有“方法*******************public void fanshe.method.Student.show1(java.lang.String)public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedExceptionpublic final native void java.lang.Object.wait(long) throws java.lang.InterruptedExceptionpublic final void java.lang.Object.wait() throws java.lang.InterruptedExceptionpublic boolean java.lang.Object.equals(java.lang.Object)public java.lang.String java.lang.Object.toString()public native int java.lang.Object.hashCode()public final native java.lang.Class java.lang.Object.getClass()public final native void java.lang.Object.notify()public final native void java.lang.Object.notifyAll()***************获取所有的方法,包括私有的*******************public void fanshe.method.Student.show1(java.lang.String)private java.lang.String fanshe.method.Student.show4(int)protected void fanshe.method.Student.show2()void fanshe.method.Student.show3()***************获取公有的show1()方法*******************public void fanshe.method.Student.show1(java.lang.String)调用了:公有的,String参数的show1(): s = 刘德华***************获取私有的show4()方法******************private java.lang.String fanshe.method.Student.show4(int)调用了,私有的,并且有返回值的,int参数的show4(): age = 20返回值:abcd 7、反射main方法: Student类: 1234567package fanshe.main; public class Student { public static void main(String[] args) { System.out.println("main方法执行了。。。"); }} 测试类: 1234567891011121314151617181920212223242526package fanshe.main;import java.lang.reflect.Method; /** * 获取Student类的main方法、不要与当前的main方法搞混了 */public class Main { public static void main(String[] args) { try { //1、获取Student对象的字节码 Class clazz = Class.forName("fanshe.main.Student"); //2、获取main方法 Method methodMain = clazz.getMethod("main", String[].class);//第一个参数:方法名称,第二个参数:方法形参的类型, //3、调用main方法 // methodMain.invoke(null, new String[]{"a","b","c"}); //第一个参数,对象类型,因为方法是static静态的,所以为null可以,第二个参数是String数组,这里要注意在jdk1.4时是数组,jdk1.5之后是可变参数 //这里拆的时候将 new String[]{"a","b","c"} 拆成3个对象。。。所以需要将它强转。 methodMain.invoke(null, (Object)new String[]{"a","b","c"});//方式一 // methodMain.invoke(null, new Object[]{new String[]{"a","b","c"}});//方式二 } catch (Exception e) { e.printStackTrace(); } }} 控制台输出: 1main方法执行了。。。 8、利用反射创建数值: 数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。 123456789101112public static void testArray() throws ClassNotFoundException { Class<?> cls = Class.forName("java.lang.String"); Object array = Array.newInstance(cls,25); //往数组里添加内容 Array.set(array,0,"hello"); Array.set(array,1,"Java"); Array.set(array,2,"fuck"); Array.set(array,3,"Scala"); Array.set(array,4,"Clojure"); //获取某一项的内容 System.out.println(Array.get(array,3)); } 9、反射方法的其他使用–通过反射运行配置文件内容: Student类: 12345public class Student { public void show(){ System.out.println("is show()"); }} 配置文件以txt文件为例子: 12className = cn.fanshe.StudentmethodName = show 测试类: 1234567891011121314151617181920212223242526272829import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;import java.lang.reflect.Method;import java.util.Properties; /* * 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改 * 我们只需要将新类发送给客户端,并修改配置文件即可 */public class Demo { public static void main(String[] args) throws Exception { //通过反射获取Class对象 Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student" //2获取show()方法 Method m = stuClass.getMethod(getValue("methodName"));//show //3.调用show()方法 m.invoke(stuClass.getConstructor().newInstance()); } //此方法接收一个key,在配置文件中获取相应的value public static String getValue(String key) throws IOException{ Properties pro = new Properties();//获取配置文件的对象 FileReader in = new FileReader("pro.txt");//获取输入流 pro.load(in);//将流加载到配置文件对象中 in.close(); return pro.getProperty(key);//返回根据key获取的value值 }} 10、反射方法的其他使用–通过反射越过泛型检查: 泛型用在编译期,编译过后泛型擦除(消失掉),所以是可以通过反射越过泛型检查的 测试类: 123456789101112131415161718192021222324252627import java.lang.reflect.Method;import java.util.ArrayList; /* * 通过反射越过泛型检查 * 例如:有一个String泛型的集合,怎样能向这个集合中添加一个Integer类型的值? */public class Demo { public static void main(String[] args) throws Exception{ ArrayList<String> strList = new ArrayList<>(); strList.add("aaa"); strList.add("bbb"); // strList.add(100); //获取ArrayList的Class对象,反向的调用add()方法,添加数据 Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象 //获取add()方法 Method m = listClass.getMethod("add", Object.class); //调用add()方法 m.invoke(strList, 100); //遍历集合 for(Object obj : strList){ System.out.println(obj); } }} 控制台输出: 123aaabbb100]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java对日期操作的类]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FJava%E5%AF%B9%E6%97%A5%E6%9C%9F%E6%93%8D%E4%BD%9C%E7%9A%84%E7%B1%BB%2F</url>
<content type="text"><![CDATA[Date表示特定的瞬间,精确到毫秒(因为闰秒的原因,所以其实结果并不是特别的准确,但是如果要求不是特别严格,影响并没有很大。) 构造方法:Date()、Date(Long date) 常用方法: void setTime(Long time):根据毫秒数设置该日期对象,默认构造函数设置该日期对象为当前日期。 Long getTime():获取日期对象毫秒数。毫秒数都是以1970年1月1日0点0分0秒开始计算。 (下面几个方法,在源代码中,实际比较的还是两个日期的毫秒数) int compareTo(Date date):比较俩个日期顺序,比参数date小,返回负数,相等返回0,大则返回正数。 boolean before(Date date):判断是否在参数date之前,是则返回true。 boolean after(Date date):同上,判断在是否在参数之后。 boolean equals(Object date):当且仅当date不为空,是Date对象,而且毫秒数与调用方法的日期对象相等才为true。 SimpleDateFormat继承DateFormat类,主要用来进行格式转换。 构造函数:SimpleDateFormat(String pattern),最常用的构造方法。根据指定格式来转换字符串与日期。 常用的2个格式转换方法: 字符串转换成Date类:Date parse(String date) Date类转换成字符串类型:String format(Date date),都是继承DateFormat类的方法 其中,parse方法会抛出一个转换异常: 12345678public Date parse(String source) throws ParseException{ ParsePosition pos = new ParsePosition(0); Date result = parse(source, pos); if (pos.index == 0) throw new ParseException("Unparseable date: \"" + source + "\"" , pos.errorIndex); return result; } 下面是一个转换的例子: 1234567891011121314public static void main(String[] args) throws ParseException { //对字符串格式类似为2018-09这种进行转换,年月之间用‘-’分隔 SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM"); Date date=(Date)simpleDateFormat.parse("2018-09"); System.out.println(date); String dateStr=simpleDateFormat.format(date); System.out.println(dateStr); //将转换格式应用为201809这种,年月之间没有分隔符号的,只要年月相同,结果就与上面相同 simpleDateFormat.applyPattern("yyyyMM");//applyPattern方法用来切换需要转换的字符串格式。 Date date2=(Date)simpleDateFormat.parse("201809"); System.out.println(date2); String dateStr2=simpleDateFormat.format(date2); System.out.println(dateStr2);} Calendar一个抽象类,为特定瞬间和一组日历字段之间的转换以及操作日历字段提供了方法。 使用Calendar.getInstance获取该对象:Calendar nowTime=Calendar.getInstance(); 部分常用方法: void setTime(Date date):通过该类方法指定Calendar对象表示的日期。 Date getTime():获取一个Date对象。 void setTimeInMillis(Long time):同上。 void add(int field,int amount):给当前对象的指定字段增加指定数值。Calender类为日历中的各种字段都设置了int类型的数值,比如, 123public final static int YEAR = 1;public final static int MONTH = 2;public final static int WEEK_OF_YEAR = 3; 字段‘年’所对应的int值为1,如果你想将当前日历类对象的年份加1年,那么只需这样做: 12345678Calendar nowTime=Calendar.getInstance();System.out.println(nowTime.get(Calendar.YEAR));nowTime.add(YEAR,1);System.out.println(nowTime.get(Calendar.YEAR)); 结果:20182019 int compareTo(Calendar calendar):最终还是使用毫秒数来比较的。 boolean before(Object obj)、boolean after(Object obj):内部都是调用上面的compareTo方法实现。 三个日期类的使用例子(1)一天的毫秒数:Long oneDay=Long.parseLong((24*60*60*1000)+””); (2)接收的字符串格式的时间,转化为Date类型,并且获取对应的毫秒数: 123456789101112public static void main(String[] args) throws ParseException { SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM"); Date date=(Date)simpleDateFormat.parse("2018-09"); //距离1970年1月1日的毫秒数 Long timeMilli=date.getTime(); System.out.println(date); System.out.println(timeMilli); } 结果: Sat Sep 01 00:00:00 CST 2018 1535731200000 获取指定年月份中指定月的天数,比如2018年9月有30天,返回结果就应该为30,8月就应该返回31 例子中,是通过传入字符串日期来进行计算的,如果能直接接收日期对象就少一个转换的过程 123456789101112131415161718192021public static void main(String[] args) throws ParseException { SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd"); //将日期字符串转换为Date对象 Date date=(Date)simpleDateFormat.parse("2018-08-05"); //通过Date对象,创建一个日历对象 Calendar calendar=Calendar.getInstance(); calendar.setTime(date); //通过日历中的字段,获取一个月的天数 Integer daysNumber=calendar.getActualMaximum(Calendar.DAY_OF_MONTH); System.out.println(daysNumber); Date date2=(Date)simpleDateFormat.parse("2018-09-05"); //通过Date对象,创建一个日历对象 calendar.setTime(date2); //通过日历中的字段,获取一个月的天数 daysNumber=calendar.getActualMaximum(Calendar.DAY_OF_MONTH); System.out.println(daysNumber);} 结果: 31 30 获取对应日期的星期数 比如2018-08-19是星期天,就输出星期天,20号星期一就输出星期一 1234567891011121314151617181920212223242526272829303132333435363738public static void main(String[] args) throws ParseException { SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd"); //将日期字符串转换为Date对象 Date date=(Date)simpleDateFormat.parse("2018-08-19"); //通过Date对象,创建一个日历对象 Calendar calendar=Calendar.getInstance(); calendar.setTime(date); //获取当天对应的星期数,星期天为1,星期六为7,详见Calendar类的常量 Integer dayOfWeek=calendar.get(Calendar.DAY_OF_WEEK); String dayOfWeekStr=null; switch(dayOfWeek) { case 1: dayOfWeekStr="星期天"; break; case 2: dayOfWeekStr="星期一"; break; case 3: dayOfWeekStr="星期二"; break; case 4: dayOfWeekStr="星期三"; break; case 5: dayOfWeekStr="星期四"; break; case 6: dayOfWeekStr="星期五"; break; case 7: dayOfWeekStr="星期六"; break; } System.out.println(dayOfWeekStr);} 结果: 星期天 总结: SimpleDateFormat类用于转换格式,String←→Date; Date类用于获取毫秒数,或者作为设置Calendar对象日期的参数,date.getTime()、calendar.setTime(date) Calendar类用于具体操作,比如获取指定年、月的天数,日期对应的星期数,对应的月份数等。]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java数组怎么输出]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FJava%E6%95%B0%E7%BB%84%E6%80%8E%E4%B9%88%E8%BE%93%E5%87%BA%2F</url>
<content type="text"><![CDATA[Java数组怎么输出错误示范:System.out.println(array); 这样输出的是数组的首地址,而不能打印出数组数据。 12345int[] array= {1,2,3,4,5,6};//方式一:for循环for(int i=0;i<array.length;i++){ System.out.println(array[i]);} 1234//方式二:for eachfor(int a:array) System.out.println(a); 1234//方式三:Arrays类中的toString方法(注意,是Arrays不是Array,Arrays类位于java.util包下)int[] array= {1,2,3,4,5,6};System.out.println(Arrays.toString(array));]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SimpleDateFormat线程安全问题]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FSimpleDateFormat%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[SimpleDateFormat线程安全问题使用ExecutorService提交多个任务的方式,模拟并发环境将字符串转换为日期即测试parse方法,代码如下: 12345678910111213141516171819202122@Testpublic void testParse() { ExecutorService executorService = Executors.newCachedThreadPool(); List<String> dateStrList = Lists.newArrayList( "2018-04-01 10:00:01", "2018-04-02 11:00:02", "2018-04-03 12:00:03", "2018-04-04 13:00:04", "2018-04-05 14:00:05" ); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); for (String str : dateStrList) { executorService.execute(() -> { try { simpleDateFormat.parse(str); TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } }); }} 并发环境下使用SimpleDateFormat的parse方法有线程安全问题! 线程安全问题的原因: 在SimpleDateFormat转换日期是通过Calendar对象来操作的,SimpleDateFormat继承DateFormat类,DateFormat类中维护一个Calendar对象通过DateFormat类中的注释可知:此处Calendar实例被用来进行日期-时间计算,既被用于format方法也被用于parse方法! 在parse方法的最后,会调用CalendarBuilder的establish方法,入参就是SimpleDateFormat维护的Calendar实例,在establish方法中会调用calendar的clear方法 可知SimpleDateFormat维护的用于format和parse方法计算日期-时间的calendar被清空了,如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题! 解决方案1、每一个使用SimpleDateFormat对象进行日期-时间进行format和parse方法的时候就创建一个新的SimpleDateFormat对象,用完就销毁即可!代码如下: 123456789101112131415161718192021222324252627/** * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象 */@Testpublic void testParseThreadSafe() { ExecutorService executorService = Executors.newCachedThreadPool(); List<String> dateStrList = Lists.newArrayList( "2018-04-01 10:00:01", "2018-04-02 11:00:02", "2018-04-03 12:00:03", "2018-04-04 13:00:04", "2018-04-05 14:00:05" ); for (String str : dateStrList) { executorService.execute(() -> { try { //创建新的SimpleDateFormat对象用于日期-时间的计算 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); simpleDateFormat.parse(str); TimeUnit.SECONDS.sleep(1); simpleDateFormat = null; //销毁对象 } catch (Exception e) { e.printStackTrace(); } }); } 2、使用 ThreadLocal 12345678910111213141516171819import java.text.SimpleDateFormat;import java.util.Date; public class DateUtil { // anonymous inner class. Each thread will have its own copy of the SimpleDateFormat private final static ThreadLocal<simpledateformat> tl = new ThreadLocal<simpledateformat>() { protected SimpleDateFormat initialValue() { return new SimpleDateFormat("dd/MM/yyyy"); } }; public String formatDate(Date input) { if (input == null) { return null; } return tl.get().format(input); }} 3、同步代码块 synchronized(code) 或者使用装饰器设计模式包装下 SimpleDateFormat ,使之变得线程安全。 简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。 4、使用第三方的日期处理函数: 比如 JODA 来避免这些问题,你也可以使用 commons-lang 包中的 FastDateFormat 工具类。 5、基于JDK1.8的DateTimeFormatter,也是《阿里巴巴开发手册》给我们的解决方案,对之前的代码进行改造: DateTimeFormatter源码上作者也加注释说明了,他的类是不可变的,并且是线程安全的。]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[String=null和空字符串的区别]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2FString%3Dnull%E5%92%8C%E7%A9%BA%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[string s = null; 表示一个空串,没有占用了空间,不在内存中开辟空间 s.length()会出现空指针异常 string s = “”; 在内存中开辟空间,但空间中没有值(“”也是一个字符串) 表示一个空串,被实列化了,占用了内存空间 s.length()长度为0 null 表示一个空引用,“” 表示一个空字符串,string.Empty和“”类似,在内存中分配0个字节如果想声明一个初始值为空的字符串变量最好用 string str = string.Empty;]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[字符流和字节流]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2F%E5%AD%97%E7%AC%A6%E6%B5%81%E5%92%8C%E5%AD%97%E8%8A%82%E6%B5%81%2F</url>
<content type="text"><![CDATA[什么是流 流是个抽象的概念,是对输入输出设备的抽象,输入流可以看作一个输入通道,输出流可以看作一个输出通道。 输入流是相对程序而言的,外部传入数据给程序需要借助输入流。 输出流是相对程序而言的,程序把数据传输到外部需要借助输出流。 字节流字节流–传输过程中,传输数据的最基本单位是字节的流。 字符流字符流–传输过程中,传输数据的最基本单位是字符的流。 字符编码方式不同,有时候一个字符使用的字节数也不一样,比如ASCLL方式编码的字符,占一个字节;而UTF-8方式编码的字符,一个英文字符需要一个字节,一个中文需要三个字节。 字节数据是二进制形式的,要转成我们能识别的正常字符,需要选择正确的编码方式。我们生活中遇到的乱码问题就是字节数据没有选择正确的编码方式来显示成字符。 从本质上来讲,写数据(即输出)的时候,字节也好,字符也好,本质上都是没有标识符的,需要去指定编码方式。 file字节流输入输出例子 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;public class IOTest { public static void main(String[] args) { OutputStream fos = null; InputStream fis = null; try { /********* 输出流写文件 ***********/ fos=new FileOutputStream("d:\\silly.txt"); String str="最好的我们隔了一整个青春"; byte[] words=str.getBytes(); //把字符串编码成字节序列 //写入操作 fos.write(words, 0, words.length); System.out.println("写入成功!"); /********* 输入流读文件 ***********/ fis = new FileInputStream("d:\\silly.txt"); StringBuilder sb = new StringBuilder(); byte[] buf = new byte[1024]; //字节数组缓存数据 int n = 0; //记录读取的字节长度 //循环读取数据 while((n = fis.read(buf)) != -1){ //这里使用三个参数的构造方法,因为最后一次读取的长度可能达不到buf数组的长度 //所以根据实际读取的长度n去构造对象更合理 sb.append(new String(buf, 0, n)); buf = new byte[1024]; //重新初始化,避免数据重复 } System.out.println(sb.toString()); } catch (IOException e) { e.printStackTrace(); }finally{ try { //释放资源 if(fos != null) fos.close(); if(fis != null) fis.close(); } catch (IOException e) { e.printStackTrace(); } } }} file字符流输入输出例子 1234567891011121314151617181920212223242526272829303132333435363738394041import java.io.FileReader;import java.io.FileWriter;import java.io.IOException;public class IOTest2 { /* * FileWriter:用来写入字符文件的便捷类。 * 父类:OutputStreamWriter * FileWriter 用于写入字符流。要写入原始字节流,请考虑使用 FileOutputStream。 * */ public static void main(String[] args) throws IOException { /** * 输入流 //1:创建字符输出便捷流 FileWriter fw = new FileWriter("d:\\a.txt"); //2:写数据 fw.write("你好吗"); //3:关闭流 fw.close(); */ //输出流 //1:创建字符输入便捷流 FileReader fr = new FileReader("d:\\a.txt"); //2:读数据 //2.1 一次读取一个字符 /*int num = 0; while((num = fr.read())!=-1){ System.out.print((char)num); }*/ //2.2 一次读取一个字符数组 char[] ch = new char[1024*1024]; int num = 0; while((num = fr.read(ch))!=-1){ System.out.print(new String(ch,0,num)); } //3:关闭流 fr.close(); }}]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[string和stringbuffer和stringbuilder区别]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2Fstring%E5%92%8Cstringbuffer%E5%92%8Cstringbuilder%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[可变性: 简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。 AbstractStringBuilder.java 12345678abstract class AbstractStringBuilder implements Appendable, CharSequence { char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; } 线程安全性 String 中的对象是不可变的,也就可以理解为常量,线程安全。 AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。 StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。 StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 性能 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。 StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。 相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 对于三者使用的总结: 操作少量的数据: 适用String 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[抽象类和接口的区别]]></title>
<url>%2F2019%2F10%2F02%2FJava%E5%9F%BA%E7%A1%80%2F%E6%8A%BD%E8%B1%A1%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[抽象类总结 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。 接口和抽象类的区别 本质:从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范 区别:1.接口的方法默认是public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法 2.接口中的实例变量默认是static final类型的,而抽象类中则不一定 3.一个类可以实现多个接口,但最多只能实现一个抽象类 4.一个类实现接口的话要实现接口的所有方法,而抽象类不一定(子类可以实现部分方法,所以子类也是抽象类) 5.接口不能用new实例化,但可以声明,但是必须引用一个实现该接口的对象 接口变量、方法修饰符(static 、final、public、private、protected、default) 1.接口中的所有属性默认为:public static final ; 2.接口中的所有方法默认为:public abstract ; 接口中变量只能public ,不可以protected、private,缺省不写默认public 接口中方法只能public,不可以protected、private,缺省不写默认public 接口如果abstract,不可以加方法体,如果是static或default可以加方法体(jdk1.8特性) 抽象类变量、方法修饰符(static 、final、public、private、protected、default) 抽象类中变量修饰符都可以用 抽象类中抽象方法(没有方法体)只能public和protected 抽象类中非抽象方法(有方法体)修饰符都可以用]]></content>
<categories>
<category>Java基础</category>
</categories>
<tags>
<tag>Java基础</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ArrayList遍历与删除]]></title>
<url>%2F2019%2F10%2F01%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2FArrayList%E9%81%8D%E5%8E%86%E4%B8%8E%E5%88%A0%E9%99%A4%2F</url>
<content type="text"><![CDATA[ArrayList遍历三种方式12345672 public void arrayListTraversal(List<Integer> lists){3 /* 第一种遍历方式 */4 System.out.print("for循环的遍历方式:");5 for (int i = 0; i < lists.size(); i++) {6 System.out.print(lists.get(i));7 }8 System.out.println(); 12345610 /* 第二种遍历方式 */11 System.out.print("for each的遍历方式:");12 for (Integer list : lists) {13 System.out.print(list);14 }15 System.out.println(); 1234517 /* 第三种遍历方式 */18 System.out.print("Iterator的遍历方式:");19 for (Iterator<Integer> list = lists.iterator(); list.hasNext();) {20 System.out.print(list.next());21 } ArrayList遍历删除的坑1234567891011121314151617181920212223import java.util.ArrayList;public class ArrayListRemove{ public static void main(String[]args) { ArrayList<String> list=new ArrayList<String>(); list.add("a"); list.add("b"); list.add("b"); list.add("c"); list.add("c"); list.add("c"); remove(list); for(Strings:list) { System.out.println("element : "+s); } } public static void remove(ArrayList<String> list) { // TODO: }} 错误的例子11234567891011public static void remove(ArrayList<String> list){ for(inti=0;i<list.size();i++) { String s=list.get(i); if(s.equals("b")) { list.remove(s); } }} 错误的原因:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。 实例一的错误原因。翻开JDK的ArrayList源码,先看下ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的: 12345678910111213141516public boolean remove(Object o){ if(o==null){ for(int index=0;index<size;index++) if(elementData[index]==null){ fastRemove(index); return true; } }else{ for(intindex=0;index<size;index++) if(o.equals(elementData[index])){ fastRemove(index); return true; } } return false;} 一般情况下程序的执行路径会走到else路径下最终调用faseRemove()方法: 1234567private void fastRemove(int index){ modCount++; int numMoved=size-index-1; if(numMoved>0) System.arraycopy(elementData,index+1,elementData,index,numMoved); elementData[--size]=null;// Let gc do its work} 可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在遍历第一个字符串b时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串b并没有遍历到,所以无法删除。 针对这种情况可以倒序删除的方式来避免或者在删除的时候下标减1 倒序删除的方式 1234567891011public static void remove(ArrayList<String> list){ for(inti=list.size()-1;i>=0;i--) { String s=list.get(i); if(s.equals("b")) { list.remove(s); } }} 正序遍历时在删除时下标减一 12345678/*基本原理是,每次list删除元素后,后面的元素都要往前移动一位,就相当于i多加了1,remove后继续遍历就会错过一个元素,所以就需要代码中的i--,抵消remove后,后面元素前移一位的影响*/for(int i=0; i<list.size(); i++){ System.out.println(i); if(list.get(i).equals("C")){ list.remove(list.get(i)); i--; }} 错误的例子212345678910public static void remove(ArrayList<String> list){ for(Strings:list) { if(s.equals("b")) { list.remove(s); } }} 误的原因:这种for-each写法会报出著名的并发修改异常:java.util.ConcurrentModificationException。 错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中): 123public Iterator<E> iterator() { return new Itr();} 这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法: 1234567891011public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); }} 第一行checkForComodification方法: 1234final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException();} 这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。 123456789101112public static void remove(ArrayList<String> list) { Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { it.remove(); } }} 使用foreach或者迭代器都是会用到迭代器,所以必须使用迭代器来删除,不然会报异常 12345678910 Integer[] arr = {0, 1, 2, 3, 4}; List<Integer> list = new ArrayList<>(); list.addAll(Arrays.asList(arr)); for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) { Integer tar = iter.next(); if (tar == 2) list.remove(tar); //iter.remove(tar); }//这里相当于脱裤子放屁,用了迭代器,却用list删除,报异常, 再多一个例子12345678Integer[] arr = {0, 1, 2, 3, 4};List<Integer> list = new ArrayList<>();list.addAll(Arrays.asList(arr));for (Integer tar : list) { if (tar == 2){ list.remove(tar); }} 答案:报错ConcurrentModificationException位置在下面代码的倒数第三行,下面是 ArrayList中的迭代器。当我们使用foreach时,就是使用这个迭代器工作的,cursor是游标,指示当前已取出元素的下一个元素,lastRet指示当前已取出元素,expectedModCount是期待的修改次数,modCount是实际修改次数,每次循环都会先调用hasNext(),当游标不等于(即小于)list.size()时说明还有下一个元素,再调用next取出下一个值,next()方法的第一个方法就是checkForComodification(),检查期待的修改次数是否与实际相等,不相等就抛异常,expectedModCount变量范围是这个迭代器,使用list.remove(Object obj)只会使modCount++,expectedModCount的值不变自然就出错了。所以采用Iterator遍历是个明智的选择,它的remove()方法里面ArrayList.this.remove(lastRet)会 让modCount++,但随后又把modCount的值赋给了expectedModCount,继续循环不会出问题。 123456789101112131415161718192021222324252627282930313233343536373839404142 private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }} 小提问:把上面的遍历tar == 2改成tar == 3还会错吗?如果你把上面的解释仔细看了的话,想必已经知道答案了,不会。解答:当tar == 3时,当前游标cursor是4,size是5,但当删除了tar,size就变成了4,和cursor相等了,到下一次循环,hasNext()判断时为false,所以结束了循环,不给它抛异常的机会。]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[BlockingQueue]]></title>
<url>%2F2019%2F09%2F28%2F%E5%B9%B6%E5%8F%91%2FBlockingQueue%2F</url>
<content type="text"><![CDATA[BlockingQueue1. BlockingQueue简介在实际编程中,会经常使用到JDK中Collection集合框架中的各种容器类如实现List,Map,Queue接口的容器类,但是这些容器类基本上不是线程安全的,除了使用Collections可以将其转换为线程安全的容器,还可以使用线程安全的容器,如实现List接口的CopyOnWriteArrayList,实现Map接口的ConcurrentHashMap,实现Queue接口的ConcurrentLinkedQueue 最常用的”生产者-消费者“问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。 2. 基本操作BlockingQueue基本操作总结如下 BlockingQueue继承于Queue接口,因此,对数据元素的基本操作有: 插入元素 add(E e) :往队列插入数据,当队列满时,插入元素时会抛出IllegalStateException异常; offer(E e):当往队列插入数据时,插入成功返回true,否则则返回false。当队列满时不会抛出异常; 删除元素 remove(Object o):从队列中删除数据,成功则返回true,否则为false poll:删除数据,当队列为空时,返回null; 查看元素 element:获取队头元素,如果队列为空时则抛出NoSuchElementException异常; peek:获取队头元素,如果队列为空则抛出NoSuchElementException异常 BlockingQueue具有的特殊操作: 插入数据: put:当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用; offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与put方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出; 删除数据 take():当阻塞队列为空时,获取队头数据的线程会被阻塞; poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出 3. 常用的BlockingQueue实现BlockingQueue接口的有ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明: 1.ArrayBlockingQueue ArrayBlockingQueue是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。 当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的ArrayBlockingQueue,可采用如下代码: 1private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true); 2.LinkedBlockingQueue LinkedBlockingQueue是用链表实现的有界阻塞队列,同样满足FIFO的特性,与ArrayBlockingQueue相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue容量迅速增,损耗大量内存。通常在创建LinkedBlockingQueue对象时,会指定其大小,如果未指定,容量等于Integer.MAX_VALUE 3.PriorityBlockingQueue PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。 4.SynchronousQueue SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。 5.LinkedTransferQueue LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法: transfer(E e) 如果当前有线程(消费者)正在调用take()方法或者可延时的poll()方法进行消费数据时,生产者线程可以调用transfer方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出; tryTransfer(E e) tryTransfer方法如果当前有消费者线程(调用take方法或者具有超时特性的poll方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回false。因此,与transfer方法相比,transfer方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而tryTransfer方法能够立即返回结果退出。 tryTransfer(E e,long timeout,imeUnit unit) 与transfer基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回false。 6.LinkedBlockingDeque LinkedBlockingDeque是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为Integer.MAX_VALUE。与LinkedBlockingQueue相比,主要的不同点在于,LinkedBlockingDeque具有双端队列的特性。LinkedBlockingDeque基本操作如下图所示 LinkedBlockingDeque的基本操作可以分为四种类型: 1.特殊情况,抛出异常; 2.特殊情况,返回特殊值如null或者false; 3.当线程不满足操作条件时,线程会被阻塞直至条件满足; 4.操作具有超时特性。 另外,LinkedBlockingDeque实现了BlockingDueue接口而LinkedBlockingQueue实现的是BlockingQueue,这两个接口的主要区别如下图所示 从上图可以看出,两个接口的功能是可以等价使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一样的。 7.DelayQueue DelayQueue是一个存放实现Delayed接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的getDelay(TimeUnit.NANOSECONDS)来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。 ArrayBlockingQueue1. ArrayBlockingQueue简介在多线程编程过程中,为了业务解耦和架构设计,经常会使用并发容器用于存储多线程间的共享数据,这样不仅可以保证线程安全,还可以简化各个线程操作。 例如在“生产者-消费者”问题中,会使用阻塞队列(BlockingQueue)作为数据容器 2. ArrayBlockingQueue实现原理阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)。那么,多线程中消息通知机制最常用的是lock的condition机制,那么ArrayBlockingQueue的实现是不是也会采用Condition的通知机制呢? 2.1 ArrayBlockingQueue的主要属性ArrayBlockingQueue的主要属性如下: 12345678910111213141516171819202122232425/** The queued items */final Object[] items;/** items index for next take, poll, peek or remove */int takeIndex;/** items index for next put, offer, or add */int putIndex;/** Number of elements in the queue */int count;/* * Concurrency control uses the classic two-condition algorithm * found in any textbook. *//** Main lock guarding all access */final ReentrantLock lock;/** Condition for waiting takes */private final Condition notEmpty;/** Condition for waiting puts */private final Condition notFull; ArrayBlockingQueue内部是采用数组进行数据存储的(属性items),为了保证线程安全,采用的是ReentrantLock lock,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建: 12345678public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition();} 2.2 put方法详解put(E e)方法源码如下: 1234567891011121314public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { //如果当前队列已满,将线程移入到notFull等待队列中 while (count == items.length) notFull.await(); //满足插入数据的要求,直接进行入队操作 enqueue(e); } finally { lock.unlock(); }} 当队列已满时(count == items.length)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用enqueue(e)插入数据元素。enqueue方法源码为: 123456789101112private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; //插入数据 items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++; //通知消费者线程,当前队列中有数据可供消费 notEmpty.signal();} enqueue方法先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。 2.3 take方法详解take方法源码如下: 12345678910111213public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { //如果队列为空,没有数据,将消费者线程移入等待队列中 while (count == 0) notEmpty.await(); //获取数据 return dequeue(); } finally { lock.unlock(); }} take方法也主要做了两步: 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中; 若队列不为空则获取数据,即完成出队操作dequeue。 dequeue方法源码为: 1234567891011121314151617private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings("unchecked") //获取数据 E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); //通知被阻塞的生产者线程 notFull.signal(); return x;} dequeue方法也主要做了两件事情: 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]); 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。 put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。 LinkedBlockingQueue1.LinkedBlockingQueue简介LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为Integer.MAX_VALUE。从它的构造方法可以看出: 123public LinkedBlockingQueue() { this(Integer.MAX_VALUE);} 2.LinkedBlockingQueue的主要属性LinkedBlockingQueue的主要属性有: 1234567891011121314151617181920212223242526/** Current number of elements */private final AtomicInteger count = new AtomicInteger();/** * Head of linked list. * Invariant: head.item == null */transient Node<E> head;/** * Tail of linked list. * Invariant: last.next == null */private transient Node<E> last;/** Lock held by take, poll, etc */private final ReentrantLock takeLock = new ReentrantLock();/** Wait queue for waiting takes */private final Condition notEmpty = takeLock.newCondition();/** Lock held by put, offer, etc */private final ReentrantLock putLock = new ReentrantLock();/** Wait queue for waiting puts */private final Condition notFull = putLock.newCondition(); ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(takeLock和putLock)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(notEmpty和notFull)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为: 1234567891011121314static class Node<E> { E item; /** * One of: * - the real successor Node * - this Node, meaning the successor is head.next * - null, meaning there is no successor (this is the last node) */ Node<E> next; Node(E x) { item = x; }} 3.put方法详解put方法源码为: 12345678910111213141516171819202122232425262728293031323334public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ //如果队列已满,则阻塞当前线程,将其移入等待队列 while (count.get() == capacity) { notFull.await(); } //入队操作,插入数据 enqueue(node); c = count.getAndIncrement(); //若队列满足插入数据的条件,则通知被阻塞的生产者线程 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty();} put方法的逻辑基本上和ArrayBlockingQueue的put方法一样。take方法的源码如下: 123456789101112131415161718192021222324public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { //当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件 while (count.get() == 0) { notEmpty.await(); } //移除队头元素,获取数据 x = dequeue(); c = count.getAndDecrement(); //如果当前满足移除元素的条件,则通知被阻塞的消费者线程 if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x;} ArrayBlockingQueue与LinkedBlockingQueue的比较相同点:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性; 不同点: ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构; ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了putLock和takeLock,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ThreadLocal]]></title>
<url>%2F2019%2F09%2F28%2F%E5%B9%B6%E5%8F%91%2FThreadLocal%2F</url>
<content type="text"><![CDATA[ThreadLocal1. ThreadLocal的简介在多线程编程中通常解决线程安全的问题我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。 线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。事实上,这就是一种“空间换时间”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。 ThreadLocal这个类名可以顾名思义的进行理解,表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争。 2. ThreadLocal的实现原理 void set(T value) set方法设置在当前线程中threadLocal变量的值,该方法的源码为: 123456789101112public void set(T value) { //1. 获取当前线程实例对象 Thread t = Thread.currentThread(); //2. 通过当前线程实例获取到ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) //3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入 map.set(this, value); else //4.map为null,则新建ThreadLocalMap并存入value createMap(t, value);} value是存放在了ThreadLocalMap里了,也就是说,数据value是真正的存放在了ThreadLocalMap这个容器中了,并且是以当前threadLocal实例为key。 首先ThreadLocalMap是怎样来的?是通过getMap(t)进行获取: 123ThreadLocalMap getMap(Thread t) { return t.threadLocals;} 该方法直接返回的就是当前线程对象t的一个成员变量threadLocals: 123/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null; 也就是ThreadLocalMap的引用是作为Thread的一个成员变量,被Thread进行维护的。 set方法,当map为Null的时候会通过createMap(t,value)方法: 123void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);} 该方法就是new一个ThreadLocalMap实例对象,然后同样以当前threadLocal实例作为key,值为value存放到threadLocalMap中,然后将当前线程对象的threadLocals赋值为threadLocalMap。 Set方法总结一下: 通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap以threadLocal为键,值为value的键值对存入即可。 T get() get方法是获取当前线程中threadLocal变量的值 123456789101112131415161718public T get() { //1. 获取当前线程的实例对象 Thread t = Thread.currentThread(); //2. 获取当前线程的threadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { //3. 获取map中当前threadLocal实例为key的值的entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //4. 当前entitiy不为null的话,就返回相应的值value T result = (T)e.value; return result; } } //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value return setInitialValue();} initialValue方法: 123protected T initialValue() { return null;} 这个方法是protected修饰的也就是说继承ThreadLocal的子类可重写该方法,实现赋值为其他的初始值。关于get方法来总结一下: 通过当前线程thread实例获取到它所维护的threadLocalMap,然后以当前threadLocal实例为key获取该map中的键值对(Entry),若Entry不为null则返回Entry的value。如果获取threadLocalMap为null或者Entry为null的话,就以当前threadLocal为Key,value为null存入map后,并返回null。 void remove() 1234567public void remove() { //1. 获取当前线程的threadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //2. 从map中删除以当前threadLocal实例为key的键值对 m.remove(this);} get,set方法实现了存数据和读数据。删除数据从map中删除数据,先获取与当前线程相关联的threadLocalMap然后从map中删除该threadLocal实例为key的键值对即可。 3. ThreadLocalMap详解数据其实都放在了threadLocalMap中,threadLocal的get,set和remove方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。 3.1 Entry数据结构ThreadLocalMap是threadLocal一个静态内部类,threadLocalMap内部维护了一个Entry类型的table数组。 12345/** * The table, resized as necessary. * table.length MUST always be a power of two. */private Entry[] table; table数组的长度为2的幂次方,看下Entry是什么: 123456789static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }} Entry是一个以ThreadLocal为key,Object为value的键值对,另外需要注意的是这里的threadLocal是弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadLocal实例包装成一个WeakReferenece。thread,threadLocal,threadLocalMap,Entry之间的关系: 上图中的实线表示强引用,虚线表示弱引用。每个线程实例中可以通过threadLocals获取到threadLocalMap,而threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry数组。 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。 需要注意的是Entry中的key是弱引用,当threadLocal外部强引用被置为null(threadLocalInstance=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。 如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal存在内存泄漏问题。 3.2 set方法与concurrentHashMap,hashMap等容器一样,threadLocalMap也是采用散列表进行实现的。 散列表 理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。 在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式: 分离链表法(separate chaining)和开放定址法(open addressing) 分离链表法 分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素,典型实现为hashMap,concurrentHashMap的拉链法。 开放定址法 开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 – 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。 ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。 set方法的源码为: 1234567891011121314151617181920212223242526272829303132333435private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; //根据threadLocal的hashCode确定Entry应该存放的位置 int i = key.threadLocalHashCode & (len-1); //采用开放地址法,hash冲突的时候使用线性探测 for (Entry e = tab[i] ; e != null ; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //覆盖旧Entry if (k == key) { e.value = value; return; } //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法 //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性 if (k == null) { //用当前插入的值替换掉这个key为null的“脏”entry replaceStaleEntry(key, value, i); return; } } //新建entry并插入table中i处 tab[i] = new Entry(key, value); int sz = ++size; //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();} set方法的关键部分几点需要注意: threadLocal的hashcode? 123456789private final int threadLocalHashCode = nextHashCode();private static final int HASH_INCREMENT = 0x61c88647;private static AtomicInteger nextHashCode =new AtomicInteger();/** * Returns the next hash code. */private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);} threadLocal实例的hashCode是通过nextHashCode()方法实现的,该方法实际上总是用一个AtomicInteger加上0x61c88647来实现的。0x61c88647这个数是有特殊意义的,它能够保证hash表的每个散列桶能够均匀的分布,这是Fibonacci Hashing。也正是能够均匀分布,所以threadLocal选择使用开放地址法来解决hash冲突的问题。 怎样确定新值插入到哈希表中的位置? 该操作源码为:key.threadLocalHashCode & (len-1),同hashMap和ConcurrentHashMap等容器的方式一样,利用当前key(即threadLocal实例)的hashcode与哈希表大小相与,因为哈希表大小总是为2的幂次方,所以相与等同于一个取模的过程,这样就可以通过Key分配到具体的哈希桶中去。而至于为什么取模要通过位与运算的原因就是位运算的执行效率远远高于了取模运算。 怎样解决hash冲突? 源码中通过nextIndex(i, len)方法解决hash冲突的问题,该方法为((i + 1 < len) ? i + 1 : 0);,也就是不断往后线性探测,当到哈希表末尾的时候再从0开始,成环形。 怎样解决“脏”Entry? 在分析threadLocal,threadLocalMap以及Entry的关系的时候,知道使用threadLocal有可能存在内存泄漏(对象创建出来后,在之后的逻辑一直没有使用该对象,但是垃圾回收器无法回收这个部分的内存) 在源码中针对这种key为null的Entry称之为“stale entry”,“脏entry”,在set方法的for循环中寻找和当前Key相同的可覆盖entry的过程中通过replaceStaleEntry方法解决脏entry的问题。如果当前table[i]为null的话,直接插入新entry后也会通过cleanSomeSlots来解决脏entry的问题, 如何进行扩容? threshold的确定 threadLocalMap会有扩容机制,它的threshold是怎样确定的了? 1234567891011121314151617181920private int threshold; // Default to 0/** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }/** * Set the resize threshold to maintain at worst a 2/3 load factor. */ private void setThreshold(int len) { threshold = len * 2 / 3; } 在第一次为threadLocal进行赋值的时候会创建初始大小为16的threadLocalMap,并且通过setThreshold方法设置threshold,其值为当前哈希数组长度乘以(2/3),也就是说加载因子为2/3 加载因子是衡量哈希表密集程度的一个参数,如果加载因子越大的话,说明哈希表被装载的越多,出现hash冲突的可能性越大,反之,则被装载的越少,出现hash冲突的可能性越小。同时如果过小,很显然内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的一个平衡,如hashMap,concurrentHashMap的加载因子都为0.75。 这里threadLocalMap初始大小为16,加载因子为2/3,所以哈希表可用大小为:16*2/3=10,即哈希表可用容量为10。 扩容resize 当hash表的size大于threshold的时候,会通过resize方法进行扩容。 123456789101112131415161718192021222324252627282930313233/** * Double the capacity of the table. */private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; //新数组为原数组的2倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); //遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收 if (k == null) { e.value = null; // Help the GC } else { //重新确定entry在新数组的位置,然后进行插入 int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } //设置新哈希表的threshHold和size属性 setThreshold(newLen); size = count; table = newTab;} 新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。 3.3 getEntry方法getEntry方法源码为: 123456789101112private Entry getEntry(ThreadLocal<?> key) { //1. 确定在散列数组中的位置 int i = key.threadLocalHashCode & (table.length - 1); //2. 根据索引i获取entry Entry e = table[i]; //3. 满足条件则返回该entry if (e != null && e.get() == key) return e; else //4. 未查找到满足条件的entry,额外在做的处理 return getEntryAfterMiss(key, i, e);} 若能当前定位的entry的key和查找的key相同的话就直接返回这个entry,否则的话就是在set的时候存在hash冲突的情况,需要通过getEntryAfterMiss做进一步处理。getEntryAfterMiss方法为: 12345678910111213141516171819private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) //找到和查询的key相同的entry则返回 return e; if (k == null) //解决脏entry的问题 expungeStaleEntry(i); else //继续向后环形查找 i = nextIndex(i, len); e = tab[i]; } return null;} 通过nextIndex往后环形查找,如果找到和查询的key相同的entry的话就直接返回,如果在查找过程中遇到脏entry的话使用expungeStaleEntry方法进行处理。 为了解决潜在的内存泄漏的问题,在set,resize,getEntry这些地方都会对这些脏entry进行处理。 3.4 remove12345678910111213141516171819/** * Remove the entry for key. */private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //将entry的key置为null e.clear(); //将该entry的value也置为null expungeStaleEntry(i); return; } }} 通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。 4. ThreadLocal的使用场景ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此threadLocal只适用于 共享对象会造成线程安全 的业务场景。比如hibernate中通过threadLocal管理Session就是一个典型的案例,不同的请求线程(用户)拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。下面,我们自己来写一个例子,SimpleDateFormat.parse方法会有线程安全的问题,我们可以尝试使用threadLocal包装SimpleDateFormat,将该实例不被多线程共享即可。 1234567891011121314151617181920212223242526272829303132public class ThreadLocalDemo { private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 100; i++) { executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60)); } } static class DateUtil implements Runnable { private String date; public DateUtil(String date) { this.date = date; } @Override public void run() { if (sdf.get() == null) { sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); } else { try { Date date = sdf.get().parse(this.date); System.out.println(date); } catch (ParseException e) { e.printStackTrace(); } } } }} 如果当前线程不持有SimpleDateformat对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,就直接使用。另外,从if (sdf.get() == null){….}else{…..}可以看出为每一个线程分配一个SimpleDateformat对象实例是从应用层面(业务代码逻辑)去保证的。 threadLocal有可能存在内存泄漏,在使用完之后,最好使用remove方法将这个变量移除 ThreadLocal内存泄漏问题1. 造成内存泄漏的原因?threadLocal是为了解决对象不能被多线程共享访问的问题,通过threadLocal.set方法将对象实例保存在每个线程自己所拥有的threadLocalMap中,这样每个线程使用自己的对象实例,彼此不会影响达到隔离的作用,从而就解决了对象在被共享访问带来线程安全问题。 如果将同步机制和threadLocal做一个横向比较的话,同步机制就是通过控制线程访问共享对象的顺序,而threadLocal就是为每一个线程分配一个该对象,各用各的互不影响。 同步机制以“时间换空间”,由于每个线程在同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存,牺牲了时间效率换来了空间效率即“时间换空间”。 threadLocal,为每个线程都分配了一份对象,自然而然内存使用率增加,每个线程各用各的,整体上时间效率要增加很多,牺牲了空间效率换来时间效率即“空间换时间”。 上图中,实线代表强引用,虚线代表的是弱引用,如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。当然,如果线程执行结束后,threadLocal,threadRef会断掉,因此threadLocal,threadLocalMap,entry都会被回收掉。 可是,在实际使用中我们都是会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏需要注意。 2. 已经做出的改进在threadLocal的set和get方法中都有相应的处理。针对key为null的entry,源码注释为stale entry,比如在ThreadLocalMap的set方法中: 1234567891011121314151617181920212223242526272829303132private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();} 在该方法中针对脏entry做了这样的处理: 如果当前table[i]!=null的话说明hash冲突就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry进行处理; 如果当前table[i]==null的话说明新的entry可以直接插入,但是插入后会调用cleanSomeSlots方法检测并清除脏entry 2.1 cleanSomeSlots该方法的源码为: 123456789101112131415161718192021222324252627282930/* @param i a position known NOT to hold a stale entry. The * scan starts at the element after i. * * @param n scan control: {@code log2(n)} cells are scanned, * unless a stale entry is found, in which case * {@code log2(table.length)-1} additional cells are scanned. * When called from insertions, this parameter is the number * of elements, but when from replaceStaleEntry, it is the * table length. (Note: all this could be changed to be either * more or less aggressive by weighting n instead of just * using straight log n. But this version is simple, fast, and * seems to work well.) * * @return true if any stale entries have been removed. */private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed;} 入参: i表示:插入entry的位置i,很显然在上述情况2(table[i]==null)中,entry刚插入后该位置i很显然不是脏entry; 参数n 2.1. n的用途 主要用于扫描控制(scan control),从while中是通过n来进行条件判断的说明n就是用来控制扫描趟数(循环次数)的。在扫描过程中,如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为n >>>= 1,每次n右移一位相当于n除以2。如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len),再扫描log2(n)趟,注意此时n增加就是多增加了循环次数从而通过nextIndex往后搜索的范围扩大,示意图如下 按照n的初始值,搜索范围为黑线,当遇到了脏entry,此时n变成了哈希数组的长度(n取值增大),搜索范围log2(n)增大,红线表示。如果在整个搜索过程没遇到脏entry的话,搜索结束,采用这种方式的主要是用于时间效率上的平衡。 2.2. n的取值 如果是在set方法插入新的entry后调用(上述情况2),n位当前已经插入的entry个数size;如果是在replaceSateleEntry方法中调用n为哈希表的长度len。 2.2 expungeStaleEntry当在搜索过程中遇到了脏entry的话就会调用该方法去清理掉脏entry。源码为: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * Expunge a stale entry by rehashing any possibly colliding entries * lying between staleSlot and the next null slot. This also expunges * any other stale entries encountered before the trailing null. See * Knuth, Section 6.4 * * @param staleSlot index of slot known to have null key * @return the index of the next null slot after staleSlot * (all between staleSlot and this slot will have been checked * for expunging). */private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //清除当前脏entry // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; //2.往后环形继续查找,直到遇到table[i]==null时结束 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //3. 如果在向后搜索过程中再次遇到脏entry,同样将其清理掉 if (k == null) { e.value = null; tab[i] = null; size--; } else { //处理rehash的情况 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i;} 该方法主要做了这么几件事情: 清理当前脏entry,即将其value引用置为null,并且将table[staleSlot]也置为null。value置为null后该value域变为不可达,在下一次gc的时候就会被回收掉,同时table[staleSlot]为null后以便于存放新的entry; 从当前staleSlot位置向后环形(nextIndex)继续搜索,直到遇到哈希桶(tab[i])为null的时候退出; 若在搜索过程再次遇到脏entry,继续将其清除。 也就是说该方法,清理掉当前脏entry后,并没有闲下来继续向后搜索,若再次遇到脏entry继续将其清理,直到哈希桶(table[i])为null时退出。因此方法执行完的结果为 从当前脏entry(staleSlot)位到返回的i位,这中间所有的entry不是脏entry。 为什么是遇到null退出呢?原因是存在脏entry的前提条件是 当前哈希桶(table[i])不为null,只是该entry的key域为null。如果遇到哈希桶为null,很显然它连成为脏entry的前提条件都不具备。 对cleanSomeSlot方法做一下总结,其方法执行示意图如下: cleanSomeSlot方法主要有这样几点: 从当前位置i处(位于i处的entry一定不是脏entry)为起点在初始小范围(log2(n),n为哈希表已插入entry的个数size)开始向后搜索脏entry,若在整个搜索过程没有脏entry,方法结束退出 如果在搜索过程中遇到脏entryt通过expungeStaleEntry方法清理掉当前脏entry,并且该方法会返回下一个哈希桶(table[i])为null的索引位置为i。这时重新令搜索起点为索引位置i,n为哈希表的长度len,再次扩大搜索范围为log2(n’)继续搜索。 以一个例子更清晰的来说一下,假设当前table数组的情况如下图。 如图当前n等于hash表的size即n=10,i=1,在第一趟搜索过程中通过nextIndex,i指向了索引为2的位置,此时table[2]为null,说明第一趟未发现脏entry,则第一趟结束进行第二趟的搜索。 第二趟所搜先通过nextIndex方法,索引由2的位置变成了i=3,当前table[3]!=null但是该entry的key为null,说明找到了一个脏entry,先将n置为哈希表的长度len,然后继续调用expungeStaleEntry方法,该方法会将当前索引为3的脏entry给清除掉(令value为null,并且table[3]也为null) 但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为4,5的位置的entry同样为脏entry,索引为6的位置的entry不是脏entry保持不变,直至i=7的时候此处table[7]位null,该方法就以i=7返回。至此,第二趟搜索结束; 由于在第二趟搜索中发现脏entry,n增大为数组的长度len,因此扩大搜索范围(增大循环次数)继续向后环形搜索; 直到在整个搜索范围里都未发现脏entry,cleanSomeSlot方法执行结束退出。 2.3 replaceStaleEntry该方法源码为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172/* * @param key the key * @param value the value to be associated with key * @param staleSlot index of the first stale entry encountered while * searching for key. */private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). //向前找到第一个脏entry int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null)1. slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { //如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换2. e.value = value;3. tab[i] = tab[staleSlot];4. tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots //的起点 if (slotToExpunge == staleSlot)5. slotToExpunge = i; //搜索脏entry并进行清理6. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. //如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置 //作为起点执行cleanSomeSlots if (k == null && slotToExpunge == staleSlot)7. slotToExpunge = i; } // If key not found, put new entry in stale slot //如果在查找过程中没有找到可以覆盖的entry,则将新的entry插入在脏entry8. tab[staleSlot].value = null;9. tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them10. if (slotToExpunge != staleSlot) //执行cleanSomeSlots11. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);} 首先先看这一部分的代码: 1234int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; 通过PreIndex方法实现往前环形搜索脏entry的功能,初始时slotToExpunge和staleSlot相同,若在搜索过程中发现了脏entry,则更新slotToExpunge为当前索引i。另外,说明replaceStaleEntry并不仅仅局限于处理当前已知的脏entry,它认为在出现脏entry的相邻位置也有很大概率出现脏entry,所以为了一次处理到位,就需要向前环形搜索,找到前面的脏entry。 那么根据在向前搜索中是否还有脏entry以及在for循环后向环形查找中是否找到可覆盖的entry,我们分这四种情况来充分理解这个方法: 1.前向有脏entry 1.1后向环形查找找到可覆盖的entry 该情形如下图所示。 slotToExpunge初始状态和staleSlot相同,当前向环形搜索遇到脏entry时,在第1行代码中slotToExpunge会更新为当前脏entry的索引i,直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束。 在接下来的for循环中进行后向环形查找,若查找到了可覆盖的entry,第2,3,4行代码先覆盖当前位置的entry,然后再与staleSlot位置上的脏entry进行交换。交换之后脏entry就更换到了i处,最后使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程 2.前向没有脏entry 2.1后向环形查找找到可覆盖的entry该情形如下图所示。 slotToExpunge初始状态和staleSlot相同,当前向环形搜索直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束,若在整个过程未遇到脏entry,slotToExpunge初始状态依旧和staleSlot相同。 在接下来的for循环中进行后向环形查找,若遇到了脏entry,在第7行代码中更新slotToExpunge为位置i。若查找到了可覆盖的entry,第2,3,4行代码先覆盖当前位置的entry,然后再与staleSlot位置上的脏entry进行交换,交换之后脏entry就更换到了i处。 如果在整个查找过程中都还没有遇到脏entry的话,会通过第5行代码,将slotToExpunge更新当前i处,最后使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程。 2.2后向环形查找未找到可覆盖的entry该情形如下图所示。 slotToExpunge初始状态和staleSlot相同,当前向环形搜索直到遇到哈希桶(table[i])为null的时候,前向搜索过程结束,若在整个过程未遇到脏entry,slotToExpunge初始状态依旧和staleSlot相同。 在接下来的for循环中进行后向环形查找,若遇到了脏entry,在第7行代码中更新slotToExpunge为位置i。若没有查找到了可覆盖的entry,哈希桶(table[i])为null的时候,后向环形查找过程结束。那么接下来在8,9行代码中,将插入的新entry直接放在staleSlot处即可。 如果发现slotToExpunge被重置,则第10行代码if判断为true,就使用cleanSomeSlots方法从slotToExpunge为起点开始进行清理脏entry的过程。 当前的staleSolt为i=4,首先先进行前向搜索脏entry,当i=3的时候遇到脏entry,slotToExpung更新为3,当i=2的时候tabel[2]为null,因此前向搜索脏entry的过程结束。然后进行后向环形查找,知道i=7的时候遇到table[7]为null,结束后向查找过程,并且在该过程并没有找到可以覆盖的entry。最后只能在staleSlot(4)处插入新entry,然后从slotToExpunge(3)为起点进行cleanSomeSlots进行脏entry的清理。是不是上面的1.2的情况。 当我们调用threadLocal的get方法时,当table[i]不是和所要找的key相同的话,会继续通过threadLocalMap的 getEntryAfterMiss方法向后环形去找,该方法为: 12345678910111213141516private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null;} 当key==null的时候,即遇到脏entry也会调用expungeStleEntry对脏entry进行清理。 当我们调用threadLocal.remove方法时候,实际上会调用threadLocalMap的remove方法,该方法的源码为: 1234567891011121314private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } }} 当遇到了key为null的脏entry的时候,也会调用expungeStaleEntry清理掉脏entry。 从以上set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。 2.4 为什么使用弱引用? 如果使用强引用 假设threadLocal使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉threadLocal实例的目的,但是因为threadLocalMap的Entry强引用threadLocal,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误 如果使用弱引用 假设Entry弱引用threadLocal,尽管会出现内存泄漏的问题,但是在threadLocal的生命周期里(set,getEntry,remove)里,都会针对key为null的脏entry进行处理。 从以上的分析可以看出,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。 2.5 Thread.exit()当线程退出时会执行exit方法: 1234567891011121314private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null;} 当线程结束时,会令threadLocals=null,也就意味着GC的时候就可以将threadLocalMap进行垃圾回收,换句话说threadLocalMap生命周期实际上thread的生命周期相同。 3. threadLocal最佳实践每次使用完ThreadLocal,都调用它的remove()方法,清除数据。 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[并发容器]]></title>
<url>%2F2019%2F09%2F27%2F%E5%B9%B6%E5%8F%91%2F%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8%2F</url>
<content type="text"><![CDATA[ConcurrentHashMap(JDK 1.8版本)1.ConcurrentHashmap简介在使用HashMap时在多线程情况下扩容会出现CPU接近100%的情况,因为hashmap并不是线程安全的,通常我们可以使用在java体系中古老的hashtable类,该类基本上所有的方法都采用synchronized进行线程安全的控制,可想而知,在高并发的情况下,每次只有一个线程能够获取对象监视器锁,这样的并发性能的确不令人满意。另外一种方式通过Collections的Map<K,V> synchronizedMap(Map<K,V> m)将hashmap包装成一个线程安全的map。比如SynchronzedMap的put方法源码为: 123public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);}} 实际上SynchronizedMap实现依然是采用synchronized独占式锁进行线程安全的并发控制的。同样,这种方案的性能也是令人不太满意的。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。 ConcurrentHashMap在JDK1.6的版本网上资料很多,有兴趣的可以去看看。 JDK 1.6版本关键要素: segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障; segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。 而到了JDK 1.8的ConcurrentHashMap就有了很大的变化,光是代码量就足足增加了很多。1.8版本舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。 至于为什么不用ReentrantLock而是Synchronzied呢?实际上,synchronzied做了很多的优化,包括偏向锁,轻量级锁,重量级锁,可以依次向上升级锁状态,但不能降级,因此,使用synchronized相较于ReentrantLock的性能会持平甚至在某些情况更优。另外,底层数据结构改变为采用数组+链表+红黑树的数据形式。 2.关键属性及类 ConcurrentHashMap的关键属性 table volatile Node<K,V>[] table://装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。 nextTable volatile Node<K,V>[] nextTable; //扩容时使用,平时为null,只有在扩容的时候才为非null sizeCtl volatile int sizeCtl; 该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况: 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作; 当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度; 若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor; 当值为0时:即数组长度为默认初始值。 sun.misc.Unsafe U 在ConcurrentHashMap的实现中可以看到大量的U.compareAndSwapXXXX的方法去修改ConcurrentHashMap的一些属性。这些方法实际上是利用了CAS算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。 而CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。 CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中: 12345678static { try { U = sun.misc.Unsafe.getUnsafe(); ....... } catch (Exception e) { throw new Error(e); }} ConcurrentHashMap中关键内部类 1.Node Node类实现了Map.Entry接口,主要存放key-value对,并且具有next域 1234567static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; ......} 可以看出很多属性都是用volatile进行修饰的,也就是为了保证内存可见性。 2.TreeNode 树节点,继承于承载数据的Node类。而红黑树的操作是针对TreeBin类的,从该类的注释也可以看出,也就是TreeBin会将TreeNode进行再一次封装 1234567891011** * Nodes for use in TreeBins */static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; ......} 3.TreeBin 这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象。 1234567891011static final class TreeBin<K,V> extends Node<K,V> { TreeNode<K,V> root; volatile TreeNode<K,V> first; volatile Thread waiter; volatile int lockState; // values for lockState static final int WRITER = 1; // set while holding write lock static final int WAITER = 2; // set when waiting for write lock static final int READER = 4; // increment value for setting read lock ......} 4.ForwardingNode 在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。 12345678static final class ForwardingNode<K,V> extends Node<K,V> { final Node<K,V>[] nextTable; ForwardingNode(Node<K,V>[] tab) { super(MOVED, null, null, null); this.nextTable = tab; } .....} CAS关键操作 1.tabAt 123static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);} 该方法用来获取table数组中索引为i的Node元素。 2.casTabAt 1234static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);} 利用CAS操作设置table数组中索引为i的元素 3.setTabAt 123static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);} 该方法用来设置table数组中索引为i的元素 3.重点方法讲解3.1 实例构造器方法12345678910// 1. 构造一个空的map,即table数组还未初始化,初始化放在第一次插入数据时,默认大小为16ConcurrentHashMap()// 2. 给定map的大小ConcurrentHashMap(int initialCapacity) // 3. 给定一个mapConcurrentHashMap(Map<? extends K, ? extends V> m)// 4. 给定map的大小以及加载因子ConcurrentHashMap(int initialCapacity, float loadFactor)// 5. 给定map大小,加载因子以及并发度(预计同时操作数据的线程)ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) ConcurrentHashMap一共给我们提供了5中构造器方法,看看第2种构造器,传入指定大小时的情况,该构造器源码为: 1234567891011public ConcurrentHashMap(int initialCapacity) { //1. 小于0直接抛异常 if (initialCapacity < 0) throw new IllegalArgumentException(); //2. 判断是否超过了允许的最大值,超过了话则取最大值,否则再对该值进一步处理 int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); //3. 赋值给sizeCtl this.sizeCtl = cap;} 如果小于0就直接抛出异常,如果指定值大于了所允许的最大值的话就取最大值,否则,在对指定值做进一步处理。最后将cap赋值给sizeCtl,关于sizeCtl的说明请看上面的说明,当调用构造器方法之后,sizeCtl的大小应该就代表了ConcurrentHashMap的大小,即table数组长度。tableSizeFor做了哪些事情了?源码为: 12345678910111213/** * Returns a power of two table size for the given desired capacity. * See Hackers Delight, sec 3.2 */private static final int tableSizeFor(int c) { int n = c - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;} 该方法会将调用构造器方法时指定的大小转换成一个2的幂次方数,也就是说ConcurrentHashMap的大小一定是2的幂次方,比如,当指定大小为18时,为了满足2的幂次方特性,实际上concurrentHashMapd的大小为2的5次方(32) 另外,需要注意的是,调用构造器方法的时候并未构造出table数组(可以理解为ConcurrentHashMap的数据容器),只是算出table数组的长度,当第一次向ConcurrentHashMap插入数据的时候才真正的完成初始化创建table数组的工作。 3.2 initTable方法1234567891011121314151617181920212223242526private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) // 1. 保证只有一个线程正在进行初始化操作 Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { // 2. 得出数组的大小 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") // 3. 这里才真正的初始化数组 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; // 4. 计算数组中可用的大小:实际大小n*0.75(加载因子) sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab;} 有可能存在一个情况是多个线程同时走到这个方法中,为了保证能够正确初始化,在第1步中会先通过if进行判断,若当前已经有一个线程正在初始化即sizeCtl值变为-1,这个时候其他线程在If判断为true从而调用Thread.yield()让出CPU时间片。 正在进行初始化的线程会调用U.compareAndSwapInt方法将sizeCtl改为-1即正在初始化的状态。 另外还需要注意的事情是,在第四步中会进一步计算数组中可用的大小即为数组实际大小n乘以加载因子0.75.可以看看这里乘以0.75是怎么算的,0.75为四分之三,这里n - (n >>> 2)是不是刚好是n-(1/4)n=(3/4)n。 如果选择是无参的构造器的话,这里在new Node数组的时候会使用默认大小为DEFAULT_CAPACITY(16),然后乘以加载因子0.75为12,也就是说数组的可用大小为12。 3.3 put方法调用put方法时实际具体实现是putVal方法,源码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); //1. 计算key的hash值 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //2. 如果当前table还没有初始化先调用initTable方法将tab进行初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); //3. tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } //4. 当前正在扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { //5. 当前为链表,在链表中插入新的键值对 if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 6.当前为红黑树,将新的键值对插入到红黑树中 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 7.插入完键值对后再根据实际大小看是否需要转换成红黑树 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } //8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容 addCount(1L, binCount); return null;} 从整体而言,为了解决线程安全的问题,ConcurrentHashMap使用了synchronzied和CAS的方式。HashMap以及1.8版本之前的ConcurrenHashMap结构图 ConcurrentHashMap是一个哈希桶数组,如果不出现哈希冲突的时候,每个元素均匀的分布在哈希桶数组中。当出现哈希冲突的时候,是标准的链地址的解决方式,将hash值相同的节点构成链表的形式,称为“拉链法”,另外,在1.8版本中为了防止拉链过长,当链表的长度大于8的时候会将链表转换成红黑树。table数组中的每个元素实际上是单链表的头结点或者红黑树的根节点。当插入键值对时首先应该定位到要插入的桶,即插入table数组的索引i处。据key的hashCode值计算得出索引i。 spread()重哈希,以减小Hash冲突 对于一个hash表来说,hash值分散的不够均匀会大大增加哈希冲突的概率,从而影响到hash表的性能。因此通过spread方法进行了一次重hash从而大大减小哈希冲突的可能性。spread方法为: 123static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS;} 该方法主要是将key的hashCode的低16位于高16位进行异或运算,这样不仅能够使得hash值能够分散能够均匀减小hash冲突的概率,另外只用到了异或运算,在性能开销上也能兼顾,做到平衡的trade-off。 2.初始化table 第2步会判断当前table数组是否初始化了,没有的话就调用initTable进行初始化 123//2. 如果当前table还没有初始化先调用initTable方法将tab进行初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); 3.能否直接将新值插入到table数组中 从上面的结构示意图就可以看出存在这样一种情况,如果插入值待插入的位置刚好所在的table数组为null的话就可以直接将值插入即可。根据hash确定在table中待插入的索引i,可以通过hash值与数组的长度取模操作,从而确定新值插入到数组的哪个位置。 而之前我们提过ConcurrentHashMap的大小总是2的幂次方,(n - 1) & hash运算等价于对长度n取模,也就是hash%n,但是位运算比取模运算的效率要高很多。 确定好数组的索引i后,就可以可以tabAt()方法,获取该位置上的元素,如果当前Node f为null的话,就可以直接用casTabAt方法将新值插入即可。 123456//3. tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } 4.当前是否正在扩容 如果当前节点不为null,且该节点为特殊节点(forwardingNode)的话,就说明当前concurrentHashMap正在进行扩容操作,关于扩容操作,下面会作为一个具体的方法进行讲解。那么怎样确定当前的这个Node是不是特殊的节点了?是通过判断该节点的hash值是不是等于-1(MOVED),代码为(fh = f.hash) == MOVED,对MOVED的解释在源码上也写了: 123//4. 当前正在扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); 1static final int MOVED = -1; // hash for forwarding nodes 5.当table[i]为链表的头结点,在链表中插入新值 在table[i]不为null并且不为forwardingNode时,并且当前Node f的hash值大于0(fh >= 0)的话说明当前节点f为当前桶的所有的节点组成的链表的头结点。那么接下来,要想向ConcurrentHashMap插入新值的话就是向这个链表插入新值。通过synchronized (f)的方式进行加锁以实现线程安全性。往链表中插入节点的部分代码为: 123456789101112131415161718192021if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; // 找到hash值相同的key,覆盖旧值即可 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { //如果到链表末尾仍未找到,则直接将新值插入到链表末尾即可 pred.next = new Node<K,V>(hash, key, value, null); break; } }} 两种情况: 在链表中如果找到了与待插入的键值对的key相同的节点,就直接覆盖即可; 如果直到找到了链表的末尾都没有找到的话,就直接将待插入的键值对追加到链表的末尾即可 6.当table[i]为红黑树的根节点,在红黑树中插入新值 按照之前的数组+链表的设计方案,这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,甚至在极端情况下,查找一个节点会出现时间复杂度为O(n)的情况,则会严重影响ConcurrentHashMap的性能 在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高ConcurrentHashMap的性能,其中会用到红黑树的插入、删除、查找等算法。当table[i]为红黑树的树节点时的操作为: 123456789if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ( (TreeBin<K,V>) f ).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; }} 首先在if中通过f instanceof TreeBin判断当前table[i]是否是树节点,也正好验证了TreeBin会对TreeNode做进一步封装,对红黑树进行操作的时候针对的是TreeBin而不是TreeNode。 调用putTreeVal方法完成向红黑树插入新节点,同样的逻辑,如果在红黑树中存在于待插入键值对的Key相同(hash值相等并且equals方法判断为true)的节点的话,就覆盖旧值,否则就向红黑树追加新节点。 7.根据当前节点个数进行调整 当完成数据新节点插入之后,会进一步对当前链表大小进行调整,这部分代码为: 1234567if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break;} 如果当前链表节点个数大于等于8(TREEIFY_THRESHOLD)的时候,就会调用treeifyBin方法将tabel[i](第i个散列桶)拉链转换成红黑树。 关于Put方法的逻辑做一些总结: 整体流程: 首先对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在 table中的位置; 如果当前table数组还未初始化,先将table数组进行初始化操作; 如果这个位置是null的,那么使用CAS操作直接放入; 如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果该节点fh==MOVED(代表forwardingNode,数组正在进行扩容)的话,说明正在进行扩容; 如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到key相同的节点,则只需要覆盖该结点的value值即可。否则依次向后遍历,直到链表尾插入这个结点; 如果这个节点的类型是TreeBin的话,直接调用红黑树的插入方法进行插入新的节点; 插入完节点之后再次检查链表长度,如果长度大于8,就把这个链表转换成红黑树; 对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容。 3.4 get方法get方法源码为: 1234567891011121314151617181920212223public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // 1. 重hash int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 2. table[i]桶节点的key与查找的key相同,则直接返回 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } // 3. 当前节点hash小于0说明为树节点,在红黑树中查找即可 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { // 4. 从链表中查找,查找到则返回该节点的value,否则就返回null即可 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null;} 首先先看当前的hash桶数组节点即table[i]是否为查找的节点,若是则直接返回; 若不是,则继续再看当前是不是树节点?通过看节点的hash值是否为小于0,如果小于0则为树节点。如果是树节点在红黑树中查找节点; 如果不是树节点,那就只剩下为链表的形式的一种可能性了,就向后遍历查找节点,若查找到则返回节点的value即可,若没有找到就返回null。 3.5 transfer方法当ConcurrentHashMap容量不足的时候,需要对table进行扩容。这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。 原因是它支持多线程进行扩容操作,而并没有加锁。我想这样做的目的不仅仅是为了满足concurrent的要求,而是希望利用并发处理去减少扩容带来的时间影响。transfer方法源码为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range //1. 新建Node数组,容量为之前的两倍 if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; } int nextn = nextTab.length; //2. 新建forwardingNode引用,在之后会用到 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true; boolean finishing = false; // to ensure sweep before committing nextTab for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; // 3. 确定遍历中的索引i while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } //4.将原数组中的元素复制到新数组中去 //4.5 for循环退出,扩容结束修改sizeCtl属性 if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } //4.1 当前数组中第i个元素为null,用CAS设置成特殊节点forwardingNode(可以理解成占位符) else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); //4.2 如果遍历到ForwardingNode节点 说明这个点已经被处理过了 直接跳过 这里是控制并发扩容的核心 else if ((fh = f.hash) == MOVED) advance = true; // already processed else { synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; if (fh >= 0) { //4.3 处理当前节点为链表的头结点的情况,构造两个链表,一个是原链表 另一个是原链表的反序排列 int runBit = fh & n; Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } //在nextTable的i位置上插入一个链表 setTabAt(nextTab, i, ln); //在nextTable的i+n的位置上插入另一个链表 setTabAt(nextTab, i + n, hn); //在table的i位置上插入forwardNode节点 表示已经处理过该节点 setTabAt(tab, i, fwd); //设置advance为true 返回到上面的while循环中 就可以执行i--操作 advance = true; } //4.4 处理当前节点是TreeBin时的情况,操作和上面的类似 else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } }} 整个扩容操作分为两个部分: 第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。新建table数组的代码为:Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1],在原容量大小的基础上右移一位。 第二个部分就是将原来table中的元素复制到nextTable中,主要是遍历复制的过程。 根据运算得到当前遍历的数组的位置i,然后利用tabAt方法获得i位置的元素再进行判断: 如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点; 如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上 如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。设置为新容量的0.75倍代码为 sizeCtl = (n << 1) - (n >>> 1),仔细体会下是不是很巧妙,n<<1相当于n右移一位表示n的两倍即2n,n>>>1左右一位相当于n除以2即0.5n,然后两者相减为2n-0.5n=1.5n,是不是刚好等于新容量的0.75倍即2n*0.75=1.5n。最后用一个示意图来进行总结: 3.6 与size相关的一些方法对于ConcurrentHashMap来说,这个table里到底装了多少东西其实是个不确定的数量,因为不可能在调用size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值。对于这个估计值,ConcurrentHashMap也是大费周章才计算出来的。 为了统计元素个数,ConcurrentHashMap定义了一些变量和一个内部类 12345678910111213141516171819202122232425/** * A padded cell for distributing counts. Adapted from LongAdder * and Striped64. See their internal docs for explanation. */@sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; }}/******************************************/ /** * 实际上保存的是hashmap中的元素个数 利用CAS锁进行更新 但它并不用返回当前hashmap的元素个数 */private transient volatile long baseCount;/** * Spinlock (locked via CAS) used when resizing and/or creating CounterCells. */private transient volatile int cellsBusy;/** * Table of counter cells. When non-null, size is a power of 2. */private transient volatile CounterCell[] counterCells; mappingCount与size方法 mappingCount与size方法的类似 从给出的注释来看,应该使用mappingCount代替size方法 两个方法都没有直接返回basecount 而是统计一次这个值,而这个值其实也是一个大概的数值,因此可能在统计的时候有其他线程正在执行插入或删除操作。 1234567891011121314151617181920212223242526272829303132public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);} /** * Returns the number of mappings. This method should be used * instead of {@link #size} because a ConcurrentHashMap may * contain more mappings than can be represented as an int. The * value returned is an estimate; the actual count may differ if * there are concurrent insertions or removals. * * @return the number of mappings * @since 1.8 */public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; // ignore transient negative values} final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value;//所有counter的值求和 } } return sum;} addCount方法 在put方法结尾处调用了addCount方法,把当前ConcurrentHashMap的元素个数+1这个方法一共做了两件事,更新baseCount的值,检测是否进行扩容。 123456789101112131415161718192021222324252627282930313233343536373839404142private final void addCount(long x, int check) { CounterCell[] as; long b, s; //利用CAS方法更新baseCount的值 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } //如果check值大于等于0 则需要检验是否需要进行扩容操作 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); // if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; //如果已经有其他线程在执行扩容操作 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } //当前线程是唯一的或是第一个发起扩容的线程 此时nextTable=null else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } }} 4. 总结JDK6,7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性,当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。 1.8之前put定位节点时要先定位到具体的segment,然后再在segment中定位到具体的桶。而在1.8的时候摒弃了segment臃肿的设计,直接针对的是Node[] tale数组中的每一个桶,进一步减小了锁粒度。并且防止拉链过长导致性能下降,当链表长度大于8的时候采用红黑树的设计。 主要设计上的变化有以下几点: 不采用segment而采用node,锁住node来实现减小锁粒度。 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。 sizeCtl的不同值来代表不同含义,起到了控制的作用。 采用synchronized而不是ReentrantLock CopyOnWriteArrayList1. CopyOnWriteArrayList的简介ArrayList并不是线程安全的,在读线程在读取ArrayList的时候如果有写线程在写数据的时候,基于fast-fail机制,会抛出ConcurrentModificationException异常,也就是说ArrayList并不是一个线程安全的容器 可以用Vector,或者使用Collections的静态方法将ArrayList包装成一个线程安全的类,但是这些方式都是采用java关键字synchronzied对方法进行修饰,利用独占式锁来保证线程安全的。但是,由于独占式锁在同一时刻只有一个线程能够获取到对象监视器,很显然这种方式效率并不是太高。 回到业务场景中,有很多业务往往是读多写少的,比如系统配置的信息,除了在初始进行系统配置的时候需要写入数据,其他大部分时刻其他模块之后对系统信息只需要进行读取,又比如白名单,黑名单等配置,只需要读取名单配置然后检测当前用户是否在该配置范围以内。 类似的还有很多业务场景,它们都是属于读多写少的场景。如果在这种情况用到上述的方法,使用Vector,Collections转换的这些方式是不合理的,因为尽管多个读线程从同一个数据容器中读取数据,但是读线程对数据容器的数据并不会发生发生修改。很自然而然的我们会联想到ReenTrantReadWriteLock,通过读写分离的思想,使得读读之间不会阻塞,无疑如果一个list能够做到被多个读线程读取的话,性能会大大提升不少。 但是,如果仅仅是将list通过读写锁(ReentrantReadWriteLock)进行再一次封装的话,由于读写锁的特性,当写锁被写线程获取后,读写线程都会被阻塞。如果仅仅使用读写锁对list进行封装的话,这里仍然存在读线程在读数据的时候被阻塞的情况,如果想list的读效率更高的话,如果我们保证读线程无论什么时候都不被阻塞,效率会更高? 提供CopyOnWriteArrayList容器可以保证线程安全,保证读读之间在任何时候都不会被阻塞,CopyOnWriteArrayList也被广泛应用于很多业务场景之中,CopyOnWriteArrayList值得被我们好好认识一番。 2. COW的设计思想如果简单的使用读写锁的话,在写锁被获取之后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在读线程的角度来看,即读线程任何时候都是获取到最新的数据,满足数据实时性。既然要进行优化,必然有trade-off,就可以牺牲数据实时性满足数据的最终一致性即可。而CopyOnWriteArrayList就是通过Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。 COW通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。 3. CopyOnWriteArrayList的实现原理CopyOnWriteArrayList内部维护的就是一个数组 12/** The array, accessed only via getArray/setArray. */private transient volatile Object[] array; 且该数组引用是被volatile修饰,注意这里仅仅是修饰的是数组引用。关于volatile很重要的一条性质是它能够够保证可见性,对list来说,就是读写的时候,分别为get和add方法的实现。 3.1 get方法实现原理get方法的源码为: 12345678910111213public E get(int index) { return get(getArray(), index);}/** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */final Object[] getArray() { return array;}private E get(Object[] a, int index) { return (E) a[index];} get方法实现非常简单,几乎就是一个“单线程”程序,没有对多线程添加任何的线程安全控制,也没有加锁也没有CAS操作等等,原因是,所有的读线程只是会读取数据容器中的数据,并不会进行修改。 3.2 add方法实现原理add方法的源码为: 12345678910111213141516171819public boolean add(E e) { final ReentrantLock lock = this.lock; //1. 使用Lock,保证写线程在同一时刻只有一个 lock.lock(); try { //2. 获取旧数组引用 Object[] elements = getArray(); int len = elements.length; //3. 创建新的数组,并将旧数组的数据复制到新数组中 Object[] newElements = Arrays.copyOf(elements, len + 1); //4. 往新数组中添加新的数据 newElements[len] = e; //5. 将旧数组引用指向新的数组 setArray(newElements); return true; } finally { lock.unlock(); }} add方法的逻辑需要注意这么几点: 采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据; 前面说过数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。 由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。 4. 总结COW和读写锁都是通过读写分离的思想实现的,但两者还是有些不同,可以进行比较: COW vs 读写锁 相同点:1. 两者都是通过读写分离的思想实现;2.读线程间是互不阻塞的 不同点:对读线程而言,为了实现数据实时性,在写锁被获取后,读线程会等待或者当读锁被获取后,写线程会等待,从而解决“脏读”等问题。也就是说如果使用读写锁依然会出现读线程阻塞等待的情况。而COW则完全放开了牺牲数据实时性而保证数据最终一致性,即读线程对数据的更新是延时感知的,因此读线程不会存在等待的情况。 add方法核心代码为: 123451.Object[] elements = getArray();2.int len = elements.length;3.Object[] newElements = Arrays.copyOf(elements, len + 1);4.newElements[len] = e;5.setArray(newElements); 假设COW的变化如下图所示: 数组中已有数据1,2,3,现在写线程想往数组中添加数据4,我们在第5行处打上断点,让写线程暂停。读线程依然会“不受影响”的能从数组中读取数据,可是还是只能读到1,2,3。如果读线程能够立即读到新添加的数据的话就叫做能保证数据实时性。当对第5行的断点放开后,读线程才能感知到数据变化,读到完整的数据1,2,3,4,而保证数据最终一致性,尽管有可能中间间隔了好几秒才感知到。 还有这样一个问题: 为什么需要复制呢? 如果将array 数组设定为volitile的, 对volatile变量写happens-before读,读线程不是能够感知到volatile变量的变化。 原因是,这里volatile的修饰的仅仅只是数组引用,数组中的元素的修改是不能保证可见性的。因此COW采用的是新旧两个数据容器,通过第5行代码将数组引用指向新的数组。 这也是为什么concurrentHashMap只具有弱一致性的原因 COW的缺点 CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。 内存占用问题:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对 象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的minor GC和major GC。 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。 ConcurrentLinkedQueue1.ConcurrentLinkedQueue简介在单线程编程中我们会经常用到一些集合类,比如ArrayList,HashMap等,但是这些类都不是线程安全的类。比如ArrayList不是线程安全的,Vector是线程安全。 而保障Vector线程安全的方式,是非常粗暴的在方法上用synchronized独占锁,将多线程执行变成串行化。 要想将ArrayList变成线程安全的也可以使用Collections.synchronizedList(List<T> list)方法ArrayList转换成线程安全的,但这种转换方式依然是通过synchronized修饰方法实现的,这不是一种高效的方式,同时,队列也是我们常用的一种数据结构,为了解决线程安全的问题,ConcurrentLinkedQueue这个线程安全的队列。从类名就可以知道实现队列的数据结构是链式。 1.1 Node从它的节点类看起,明白底层数据结构,Node类的源码为: 12345private static class Node<E> { volatile E item; volatile Node<E> next; .......} Node节点主要包含了两个域:一个是数据域item,另一个是next指针,用于指向下一个节点从而构成链式队列。并且都是用volatile进行修饰的,以保证内存可见性,另外ConcurrentLinkedQueue含有这样两个成员变量: 12private transient volatile Node<E> head;private transient volatile Node<E> tail; ConcurrentLinkedQueue通过持有头尾指针进行管理队列。当我们调用无参构造器时,其源码为: 123public ConcurrentLinkedQueue() { head = tail = new Node<E>(null);} head和tail指针会指向一个item域为null的节点,此时ConcurrentLinkedQueue状态如下图所示: 如图,head和tail指向同一个节点Node0,该节点item域为null,next域为null。 1.2 操作Node的几个CAS操作在队列进行出队入队的时候免不了对节点需要进行操作,在多线程就很容易出现线程安全的问题。可以看出在处理器指令集能够支持CMPXCHG指令后,在java源码中涉及到并发处理都会使用CAS操作,那么在ConcurrentLinkedQueue对Node的CAS操作有几个: 1234567891011121314//更改Node中的数据域item boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);}//更改Node中的指针域nextvoid lazySetNext(Node<E> val) { UNSAFE.putOrderedObject(this, nextOffset, val);}//更改Node中的指针域nextboolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);} 通过调用UNSAFE实例的方法,UNSAFE为sun.misc.Unsafe类,该类是hotspot底层方法,知道CAS的操作归根结底是由该类提供就好。 2.offer方法对一个队列来说,插入满足FIFO特性,插入元素总是在队列最末尾的地方进行插入,而取(移除)元素总是从队列的队头。所有要想能够彻底弄懂ConcurrentLinkedQueue从offer方法和poll方法开始。那么为了能够理解offer方法,另外,在看多线程的代码时,可采用这样的思维方式: 单个线程offer,多个线程offer,部分线程offer,部分线程poll —-offer的速度快于poll ——– 队列长度会越来越长,由于offer节点总是在对队列队尾,而poll节点总是在队列对头,也就是说offer线程和poll线程两者并无“交集”,也就是说两类线程间并不会相互影响,这种情况站在相对速率的角度来看,也就是一个”单线程offer” —-offer的速度慢于poll ——– poll的相对速率快于offer,也就是队头删的速度要快于队尾添加节点的速度,导致的结果就是队列长度会越来越短,而offer线程和poll线程就会出现“交集”,即那一时刻就可以称之为offer线程和poll线程同时操作的节点为 临界点 ,且在该节点offer线程和poll线程必定相互影响。 根据在临界点时offer和poll发生的相对顺序又可从两个角度去思考: 1. 执行顺序为offer–>poll–>offer,即表现为当offer线程在Node1后插入Node2时,此时poll线程已经将Node1删除,这种情况很显然需要在offer方法中考虑; 2.执行顺序可能为:poll–>offer–>poll,即表现为当poll线程准备删除的节点为null时(队列为空队列),此时offer线程插入一个节点使得队列变为非空队列 先看这么一段代码: 1231. ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();2. queue.offer(1);3. queue.offer(2); 创建一个ConcurrentLinkedQueue实例,先offer 1,然后再offer 2。offer的源码为: 1234567891011121314151617181920212223242526272829public boolean offer(E e) {1. checkNotNull(e);2. final Node<E> newNode = new Node<E>(e);3. for (Node<E> t = tail, p = t;;) {4. Node<E> q = p.next;5. if (q == null) {6. // p is last node7. if (p.casNext(null, newNode)) { // Successful CAS is the linearization point // for e to become an element of this queue, // and for newNode to become "live".8. if (p != t) // hop two nodes at a time9. casTail(t, newNode); // Failure is OK.10. return true; } // Lost CAS race to another thread; re-read next }11. else if (p == q) // We have fallen off list. If tail is unchanged, it // will also be off-list, in which case we need to // jump to head, from which all live nodes are always // reachable. Else the new tail is a better bet.12. p = (t != (t = tail)) ? t : head; else // Check for tail updates after two hops.13. p = (p != t && t != (t = tail)) ? t : q; }} 单线程执行角度分析: 先从单线程执行的角度看起,分析offer 1的过程。第1行代码会对是否为null进行判断,为null的话就直接抛出空指针异常,第2行代码将e包装成一个Node类,第3行为for循环,只有初始化条件没有循环结束条件,这很符合CAS的“套路”,在循环体CAS操作成功会直接return返回,如果CAS操作失败的话就在for循环中不断重试直至成功。这里实例变量t被初始化为tail,p被初始化为t即tail。 为了方便下面的理解,p被认为队列真正的尾节点,tail不一定指向对象真正的尾节点,因为在ConcurrentLinkedQueue中tail是被延迟更新的。 代码走到第3行的时候,t和p都分别指向初始化时创建的item域为null,next域为null的Node0。第4行变量q被赋值为null,第5行if判断为true,在第7行使用casNext将插入的Node设置成当前队列尾节点p的next节点,如果CAS操作失败,此次循环结束在下次循环中进行重试。CAS操作成功走到第8行,此时p==t,if判断为false,直接return true返回。如果成功插入1的话,此时ConcurrentLinkedQueue的状态如下图所示: 如图,此时队列的尾节点应该为Node1,而tail指向的节点依然还是Node0,因此可以说明tail是延迟更新的。那么我们继续来看offer 2的时候的情况,很显然此时第4行q指向的节点不为null了,而是指向Node1,第5行if判断为false,第11行if判断为false,代码会走到第13行。好了,再插入节点的时候我们会问自己这样一个问题?上面已经解释了tail并不是指向队列真正的尾节点,那么在插入节点的时候,我们是不是应该最开始做的就是找到队列当前的尾节点在哪里才能插入?那么第13行代码就是找出队列真正的尾节点。 定位队列真正的对尾节点 1p = (p != t && t != (t = tail)) ? t : q; 如果这段代码在单线程环境执行时,很显然由于p==t,此时p会被赋值为q,而q等于Node<E> q = p.next,即Node1。在第一次循环中指针p指向了队列真正的队尾节点Node1,那么在下一次循环中第4行q指向的节点为null,那么在第5行中if判断为true,那么在第7行依然通过casNext方法设置p节点的next为当前新增的Node,接下来走到第8行,这个时候p!=t,第8行if判断为true,会通过casTail(t, newNode)将当前节点Node设置为队列的队尾节点,此时的队列状态示意图如下图所示: tail指向的节点由Node0改变为Node2,这里的casTail失败不需要重试的原因是,offer代码中主要是通过p的next节点q(Node<E> q = p.next)决定后面的逻辑走向的,当casTail失败时状态示意图如下: 如图,如果这里casTail设置tail失败即tail还是指向Node0节点的话,无非就是多循环几次通过13行代码定位到队尾节点。 通过对单线程执行角度进行分析,我们可以了解到poll的执行逻辑为: 如果tail指向的节点的下一个节点(next域)为null的话,说明tail指向的节点即为队列真正的队尾节点,因此可以通过casNext插入当前待插入的节点,但此时tail并未变化,如图2; 如果tail指向的节点的下一个节点(next域)不为null的话,说明tail指向的节点不是队列的真正队尾节点。通过q(Node q = p.next)指针往前递进去找到队尾节点,然后通过casNext插入当前待插入的节点,并通过casTail方式更改tail,如图3。 我们回过头再来看p = (p != t && t != (t = tail)) ? t : q;这行代码在单线程中,这段代码永远不会将p赋值为t,那么这么写就不会有任何作用,那我们试着在多线程的情况下进行分析。 多线程执行角度分析 多个线程offer t != (t = tail)这个操作并非一个原子操作,有这样一种情况: 如图,假设线程A此时读取了变量t,线程B刚好在这个时候offer一个Node后,此时会修改tail指针,那么这个时候线程A再次执行t=tail时t会指向另外一个节点,很显然线程A前后两次读取的变量t指向的节点不相同,即t != (t = tail)为true,并且由于t指向节点的变化p != t也为true,此时该行代码的执行结果为p和t最新的t指针指向了同一个节点,并且此时t也是队列真正的对尾节点。那么,现在已经定位到队列真正的队尾节点,就可以执行offer操作了。 offer->poll->offer 那么还剩下第11行的代码我们没有分析,大致可以猜想到应该就是回答一部分线程offer,一部分poll的这种情况。当if (p == q)为true时,说明p指向的节点的next也指向它自己,这种节点称之为哨兵节点,这种节点在队列中存在的价值不大,一般表示为要删除的节点或者是空节点。 3.poll方法poll方法源码如下: 123456789101112131415161718192021222324public E poll() { restartFromHead: 1. for (;;) { 2. for (Node<E> h = head, p = h, q;;) { 3. E item = p.item; 4. if (item != null && p.casItem(item, null)) { // Successful CAS is the linearization point // for item to be removed from this queue. 5. if (p != h) // hop two nodes at a time 6. updateHead(h, ((q = p.next) != null) ? q : p); 7. return item; } 8. else if ((q = p.next) == null) { 9. updateHead(h, p); 10. return null; } 11. else if (p == q) 12. continue restartFromHead; else 13. p = q; } }} 先站在单线程的角度去理清该方法的基本逻辑。假设ConcurrentLinkedQueue初始状态如下图所示: 参数offer时的定义,我们还是先将变量p作为队列要删除真正的队头节点,h(head)指向的节点并不一定是队列的队头节点。先来看poll出Node1时的情况,由于p=h=head,参照上图,很显然此时p指向的Node1的数据域不为null,在第4行代码中item!=null判断为true后接下来通过casItem将Node1的数据域设置为null。如果CAS设置失败则此次循环结束等待下一次循环进行重试。若第4行执行成功进入到第5行代码,此时p和h都指向Node1,第5行if判断为false,然后直接到第7行return回Node1的数据域1,方法运行结束,此时的队列状态如下图。 下面继续从队列中poll,很显然当前h和p指向的Node1的数据域为null,那么第一件事就是要定位准备删除的队头节点(找到数据域不为null的节点)。 定位删除的队头节点 第三行代码item为null,第4行代码if判断为false,走到第8行代码(q = p.next)if也为false,由于q指向了Node2,在第11行的if判断也为false,因此代码走到了第13行,这个时候p和q共同指向了Node2,也就找到了要删除的真正的队头节点。可以总结出,定位待删除的队头节点的过程为:如果当前节点的数据域为null,很显然该节点不是待删除的节点,就用当前节点的下一个节点去试探。在经过第一次循环后,此时状态图为下图: 进行下一次循环,第4行的操作同上述,当前假设第4行中casItem设置成功,由于p已经指向了Node2,而h还依旧指向Node1,此时第5行的if判断为true,然后执行updateHead(h, ((q = p.next) != null) ? q : p),此时q指向的Node3,所有传入updateHead方法的分别是指向Node1的h引用和指向Node3的q引用。updateHead方法的源码为: 1234final void updateHead(Node<E> h, Node<E> p) { if (h != p && casHead(h, p)) h.lazySetNext(h);} 该方法主要是通过casHead将队列的head指向Node3,并且通过 h.lazySetNext将Node1的next域指向它自己。最后在第7行代码中返回Node2的值。此时队列的状态如下图所示: Node1的next域指向它自己,head指向了Node3。如果队列为空队列的话,就会执行到代码的第8行(q = p.next) == null,if判断为true,因此在第10行中直接返回null。以上的分析是从单线程执行的角度去看,也可以让我们了解poll的整体思路,现在来做一个总结: 如果当前head,h和p指向的节点的Item不为null的话,说明该节点即为真正的队头节点(待删除节点),只需要通过casItem方法将item域设置为null,然后将原来的item直接返回即可。 如果当前head,h和p指向的节点的item为null的话,则说明该节点不是真正的待删除节点,那么应该做的就是寻找item不为null的节点。通过让q指向p的下一个节点(q = p.next)进行试探,若找到则通过updateHead方法更新head指向的节点以及构造哨兵节点(通过updateHead方法的h.lazySetNext(h))。 多线程执行情况分析: 多个线程poll 现在回过头来看poll方法的源码,有这样一部分: 12else if (p == q) continue restartFromHead; 这一部分就是处理多个线程poll的情况,q = p.next也就是说q永远指向的是p的下一个节点,那么什么情况下会使得p,q指向同一个节点呢?根据上面我们的分析,只有p指向的节点在poll的时候转变成了哨兵节点(通过updateHead方法中的h.lazySetNext)。当线程A在判断p==q时,线程B已经将执行完poll方法将p指向的节点转换为哨兵节点并且head指向的节点已经发生了改变,所以就需要从restartFromHead处执行,保证用到的是最新的head。 poll->offer->poll 试想,还有这样一种情况,如果当前队列为空队列,线程A进行poll操作,同时线程B执行offer,然后线程A在执行poll,那么此时线程A返回的是null还是线程B刚插入的最新的那个节点呢?我们来写一代demo: 123456789101112public static void main(String[] args) { Thread thread1 = new Thread(() -> { Integer value = queue.poll(); System.out.println(Thread.currentThread().getName() + " poll 的值为:" + value); System.out.println("queue当前是否为空队列:" + queue.isEmpty()); }); thread1.start(); Thread thread2 = new Thread(() -> { queue.offer(1); }); thread2.start();} 输出结果为: Thread-0 poll 的值为:null queue当前是否为空队列:false 通过debug控制线程thread1和线程thread2的执行顺序,thread1先执行到第8行代码if ((q = p.next) == null),由于此时队列为空队列if判断为true,进入if块,此时先让thread1暂停,然后thread2进行offer插入值为1的节点后,thread2执行结束。再让thread1执行,这时thread1并没有进行重试,而是代码继续往下走,返回null,尽管此时队列由于thread2已经插入了值为1的新的节点。所以输出结果为thread0 poll的为null,然队列不为空队列。因此,在判断队列是否为空队列的时候是不能通过线程在poll的时候返回为null进行判断的,可以通过isEmpty方法进行判断。 4. offer方法中部分线程offer部分线程poll offer->poll->offer 在offer方法的第11行代码if (p == q),能够让if判断为true的情况为p指向的节点为哨兵节点,而什么时候会构造哨兵节点呢?即当head指向的节点的item域为null时会寻找真正的队头节点,等到待插入的节点插入之后,会更新head,并且将原来head指向的节点设置为哨兵节点。假设队列初始状态如下图所示: 因此在线程A执行offer时,线程B执行poll就会存在如下一种情况: 如图,线程A的tail节点存在next节点Node1,因此会通过引用q往前寻找队列真正的队尾节点,当执行到判断if (p == q)时,此时线程B执行poll操作,在对线程B来说,head和p指向Node0,由于Node0的item域为null,同样会往前递进找到队列真正的队头节点Node1,在线程B执行完poll之后,Node0就会转换为哨兵节点,也就意味着队列的head发生了改变,此时队列状态为下图。 此时线程A在执行判断if (p == q)时就为true,会继续执行p = (t != (t = tail)) ? t : head;,由于tail指针没有发生改变所以p被赋值为head,重新从head开始完成插入操作。 5. HOPS的设计通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为: tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。 head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。 并且在更新操作时,源码中会有注释为:hop two nodes at a time。所以这种延迟更新的策略就被叫做HOPS,从上面更新时的状态图可以看出,head和tail的更新是“跳着的”即中间总是间隔了一个。那么这样设计的意图是什么呢? 如果让tail永远作为队列的队尾节点,实现的代码量会更少,而且逻辑更易懂。但是,这样做有一个缺点,如果大量的入队操作,每次都要执行CAS进行tail的更新,汇总起来对性能也会是大大的损耗。如果能减少CAS更新的操作,无疑可以大大提升入队的操作效率,所以doug lea大师每间隔1次(tail和队尾节点的距离为1)进行才利用CAS更新tail。对head的更新也是同样的道理,虽然,这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能,因此,多出来的在循环中定位尾节点的操作的性能损耗相对而言是很小的。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Lock体系]]></title>
<url>%2F2019%2F05%2F27%2F%E5%B9%B6%E5%8F%91%2FLock%E4%BD%93%E7%B3%BB%2F</url>
<content type="text"><![CDATA[初识Lock与AbstractQueuedSynchronizer(AQS)1. concurrent包的结构层次 其中包含了两个子包:atomic以及lock,另外在concurrent下的阻塞队列以及executors, 从整体上来看concurrent包的整体实现图如下图所示: 2. lock简介锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。通常使用显示使用lock的形式如下: 1234567Lock lock = new ReentrantLock();lock.lock();try{ .......}finally{ lock.unlock();} synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁。 2.1 Lock接口APIlock接口定义的五个方法: void lock(); //获取锁 void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 boolean tryLock();//尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的 情况下能够获取锁 void unlock();//释放锁 Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时 会先释放锁,当再次获取锁时才能从等待中返回 ReentrantLock并没有多少源码,另外有一个很明显的特点是:基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。 2.2 初识AQS同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这三个方法。 子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。 2.3 AQS的模板方法设计模式AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire: 123protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException();} ReentrantLock中NonfairSync(继承AQS)会重写该方法为: 123protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);} 而AQS中的模板方法acquire(): 12345public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();} 会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点: 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类; AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法; AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义; 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState()方法进行修改同步状态 AQS提供的模板方法可以分为3类: 独占式获取与释放同步状态; 共享式获取与释放同步状态; 查询同步队列中等待线程情况; 3. 一个例子下面使用一个例子来进一步理解下AQS的使用。这个例子也是来源于AQS源码中的example。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778class Mutex implements Lock, java.io.Serializable { // Our internal helper class // 继承AQS的静态内部类 // 重写方法 private static class Sync extends AbstractQueuedSynchronizer { // Reports whether in locked state protected boolean isHeldExclusively() { return getState() == 1; } // Acquires the lock if state is zero public boolean tryAcquire(int acquires) { assert acquires == 1; // Otherwise unused if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // Releases the lock by setting state to zero protected boolean tryRelease(int releases) { assert releases == 1; // Otherwise unused if (getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // Provides a Condition Condition newCondition() { return new ConditionObject(); } // Deserializes properly private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } } // The sync object does all the hard work. We just forward to it. private final Sync sync = new Sync(); //使用同步器的模板方法实现自己的同步语义 public void lock() { sync.acquire(1); } public boolean tryLock() { return sync.tryAcquire(1); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }} MutexDemo: 12345678910111213141516171819public class MutextDemo { private static Mutex mutex = new Mutex(); public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { mutex.lock(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } finally { mutex.unlock(); } }); thread.start(); } }} 执行情况: 上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。MutexDemo新建了10个线程,分别睡眠3s。从执行情况也可以看出来当前Thread-6正在执行占有锁而其他Thread-7,Thread-8等线程处于WAIT状态。按照推荐的方式,Mutex定义了一个继承AQS的静态内部类Sync,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。 从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是: 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法; 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。 通俗点说,因为AQS整体设计思路采用模板方法设计模式,同步组件以及AQS的功能实际上别切分成各自的两部分: 同步组件实现者的角度: 通过可重写的方法:独占式: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);共享式 :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。 AQS的角度 而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。 总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理 深入理解AbstractQueuedSynchronizer(AQS)1. AQS简介在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法,归纳整理如下: 独占式锁 void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待; void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断; boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false; boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点 共享式锁: void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态; void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能; boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能; boolean releaseShared(int arg):共享式释放同步状态 2. 同步队列当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。接下来,疑问:1. 节点的数据结构是什么样的?2. 是单向还是双向?3. 是带头结点的还是不带头节点的? 在AQS有一个静态内部类Node,其中有这样一些属性: volatile int waitStatus //节点状态 volatile Node prev //当前节点/线程的前驱节点 volatile Node next; //当前节点/线程的后继节点 volatile Thread thread;//加入同步队列的线程引用 Node nextWaiter;//等待队列中的下一个节点 节点的状态有以下这些: int CANCELLED = 1//节点从同步队列中取消 int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; int CONDITION = -2//当前节点进入等待队列中 int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去 int INITIAL = 0;//初始状态 每个节点拥有其前驱和后继节点,很显然这是一个双向队列。可以用一段demo看一下。 12345678910111213141516171819public class LockDemo { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { for (int i = 0; i < 5; i++) { Thread thread = new Thread(() -> { lock.lock(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); thread.start(); } }} 实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示: Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3,Thread-4)获取锁失败进入同步队列,同时也可以很清楚的看出来每个节点有两个域:prev(前驱)和next(后继),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态等信息。另外AQS中有两个重要的成员变量: 12private transient volatile Node head;private transient volatile Node tail; 也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下: 通过对源码的理解以及做实验的方式,现在我们可以清楚的知道这样几点: 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息; 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列; 3. 独占锁3.1 独占锁的获取(acquire方法)调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的acquire()方法,源码如下 1234567public final void acquire(int arg) { //先看同步状态是否获取成功,如果成功则方法结束返回 //若失败则先调用addWaiter()方法再调用acquireQueued()方法 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();} acquire根据当前获得同步状态成功与否做了两件事情: 成功,则方法结束返回, 失败,则先调用addWaiter()然后在调用acquireQueued()方法。 当线程获取独占式锁失败后就会将当前线程加入同步队列,那么加入队列的方式是看addWaiter()和acquireQueued()。addWaiter()源码如下: 123456789101112131415161718private Node addWaiter(Node mode) { // 1. 将当前线程构建成Node类型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure // 2. 当前尾节点是否为null? Node pred = tail; if (pred != null) { // 2.2 将当前节点尾插入的方式插入同步队列中 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程 enq(node); return node;} 程序的逻辑主要分为两个部分:1. 当前同步队列的尾节点为null,调用方法enq()插入;2. 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。 另外还会有另外一个问题:如果 if (compareAndSetTail(pred, node))为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。 因此,经过这样的分析,enq()方法可能承担两个任务:1. 处理当前同步队列尾节点为null时进行入队操作;2. 如果CAS尾插入节点失败后负责自旋进行尝试。那么是不是真的就像我们分析的一样了?只有源码会告诉我们答案:),enq()源码如下: 1234567891011121314151617private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize //1. 构造头结点 if (compareAndSetHead(new Node())) tail = head; } else { //2. 尾插入,CAS操作失败自旋尝试 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }} 在上面的分析中可以看出在第1步中会先创建头结点,说明同步队列是带头结点的链式存储结构。带头结点与不带头结点相比,会在入队和出队的操作中获得更大的便捷性,因此同步队列选择了带头结点的链式存储结构。那么带头节点的队列初始化时机是什么?自然而然是在tail为null时,即当前线程是第一次插入同步队列。compareAndSetTail(t, node)方法会利用CAS操作设置尾节点,如果CAS操作失败会在for (;;)for死循环中不断尝试,直至成功return返回为止。因此,对enq()方法可以做这样的总结: 在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化; 自旋不断尝试CAS尾插入节点直至成功为止。 在同步队列中的节点(线程)会做什么事情了来保证自己能够有机会获得独占式锁了?带着这样的问题我们就来看看acquireQueued()方法,从方法名就可以很清楚,这个方法的作用就是排队获取锁的过程,源码如下: 123456789101112131415161718192021222324252627final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // 1. 获得当前节点的先驱节点 final Node p = node.predecessor(); // 2. 当前节点能否获取独占式锁 // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁 if (p == head && tryAcquire(arg)) { //队列头指针用指向当前节点 setHead(node); //释放前驱节点 p.next = null; // help GC failed = false; return interrupted; } // 2.2 获取锁失败,线程进入等待状态等待获取独占式锁 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }} 整体来看这是一个这又是一个自旋的过程(for (;;)),代码首先获取当前节点的先驱节点,如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁。反之,获取锁失败进入等待状态。整体示意图为下图: 获取锁的节点出队的逻辑是: 123456//队列头结点引用指向当前节点setHead(node);//释放前驱节点p.next = null; // help GCfailed = false;return interrupted; setHead()方法为: 12345private void setHead(Node node) { head = node; node.thread = null; node.prev = null;} 将当前节点通过setHead()方法设置为队列的头结点,然后将之前的头结点的next域设置为null并且pre域也为null,即与队列断开,无任何引用方便GC时能够将内存进行回收。示意图如下: 那么当获取锁失败的时候会调用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他们做了什么事情。shouldParkAfterFailedAcquire()方法源码为: 123456789101112131415161718192021222324252627private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;} shouldParkAfterFailedAcquire()方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS将节点状态由INITIAL设置成SIGNAL,表示当前线程阻塞。 当compareAndSetWaitStatus设置失败则说明shouldParkAfterFailedAcquire方法返回false,然后会在acquireQueued()方法中for (;;)死循环中会继续重试,直至compareAndSetWaitStatus设置节点状态位为SIGNAL时shouldParkAfterFailedAcquire返回true时才会执行方法parkAndCheckInterrupt()方法,该方法的源码为: 12345private final boolean parkAndCheckInterrupt() { //使得该线程阻塞 LockSupport.park(this); return Thread.interrupted();} 该方法的关键是会调用LookSupport.park()方法,该方法是用来阻塞当前线程的。因此到这里就应该清楚了,acquireQueued()在自旋过程中主要完成了两件事情: 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出; 获取锁失败的话,先将节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞。 经过上面的分析,独占式锁的获取过程也就是acquire()方法的执行流程如下图所示: 3.2 独占锁的释放(release()方法)独占锁的释放就相对来说比较容易理解了,废话不多说先来看下源码: 123456789public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false;} 这段代码逻辑就比较容易理解了,如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。unparkSuccessor方法源码: 1234567891011121314151617181920212223242526272829private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ //头节点的后继节点 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //后继节点不为null时唤醒该线程 LockSupport.unpark(s.thread);} 源码的关键信息请看注释,首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。 通过学习源码的方式非常深刻的学习到了独占式锁的获取和释放的过程以及同步队列。可以做一下总结: 1.线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试; 2.线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞 3.释放锁的时候会唤醒后继节点; 总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。 3.3 可中断式获取锁(acquireInterruptibly方法)lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性,通过学习源码的方式来看看能够响应中断是怎么实现的。 可响应中断式锁可调用方法lock.lockInterruptibly(); 而该方法其底层会调用AQS的acquireInterruptibly方法,源码为: 12345678public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) //线程获取锁失败 doAcquireInterruptibly(arg);} 在获取同步状态失败后就会调用doAcquireInterruptibly方法: 12345678910111213141516171819202122232425private void doAcquireInterruptibly(int arg) throws InterruptedException { //将节点插入到同步队列中 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); //获取锁出队 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //线程中断抛异常 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); }} 与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。 3.4 超时等待式获取锁(tryAcquireNanos()方法)通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回: 在超时时间内,当前线程成功获取了锁; 当前线程在超时时间内被中断; 超时时间结束,仍未获得锁返回false。 我们仍然通过采取阅读源码的方式来学习底层具体是怎么实现的,该方法会调用AQS的方法tryAcquireNanos(),源码为: 12345678public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || //实现超时等待的效果 doAcquireNanos(arg, nanosTimeout);} 很显然这段源码最终是靠doAcquireNanos方法实现超时等待的效果,该方法源码如下: 123456789101112131415161718192021222324252627282930313233343536private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; //1. 根据超时时间和当前时间计算出截止时间 final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); //2. 当前线程获得锁出队列 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // 3.1 重新计算超时时间 nanosTimeout = deadline - System.nanoTime(); // 3.2 已经超时返回false if (nanosTimeout <= 0L) return false; // 3.3 线程阻塞等待 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); // 3.4 线程被中断抛出被中断异常 if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); }} 程序逻辑如图所示: 程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上,在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是8h10min,超时时间是10min,那么根据deadline = System.nanoTime() + nanosTimeout计算出刚好达到超时时间时的系统时间就是8h 10min+10min = 8h 20min。 然后根据deadline - System.nanoTime()就可以判断是否已经超时了,比如,当前系统时间是8h 30min很明显已经超过了理论上的系统时间8h 20min,deadline - System.nanoTime()计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。 如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过LockSupport.parkNanos使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。 4. 共享锁4.1 共享锁的获取(acquireShared()方法)共享锁的获取方法为acquireShared,源码为: 1234public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg);} 这段源码的逻辑很容易理解,在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法,该方法的源码为: 12345678910111213141516171819202122232425262728private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { // 当该节点的前驱节点是头结点且成功获取同步状态 setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }} 逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态。 4.2 共享锁的释放(releaseShared()方法)共享锁的释放在AQS中会调用方法releaseShared: 1234567public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;} 当成功释放同步状态之后即tryReleaseShared会继续执行doReleaseShared方法: 1234567891011121314151617181920212223242526272829private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; }} 这段方法跟独占式锁释放过程有点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。 4.3 可中断(acquireSharedInterruptibly()方法),超时等待(tryAcquireSharedNanos()方法)关于可中断锁以及超时等待的特性其实现和独占式锁可中断获取锁以及超时等待的实现几乎一致 彻底理解ReentrantLock1. ReentrantLock的介绍ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。 2. 重入性的实现原理要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire: 123456789101112131415161718192021final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //1. 如果该锁未被任何线程占有,该锁能被当前线程获取 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //2.若被占有,检查占有线程是否是当前线程 else if (current == getExclusiveOwnerThread()) { // 3. 再次获取,计数加一 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false;} 这段代码的逻辑也很简单,具体请看注释。为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?(依然还是以非公平锁为例)核心方法为tryRelease: 123456789101112131415protected final boolean tryRelease(int releases) { //1. 同步状态减1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //2. 只有当同步状态为0时,锁成功被释放,返回true free = true; setExclusiveOwnerThread(null); } // 3. 锁未被完全释放,返回false setState(c); return free;} 代码的逻辑请看注释,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。到现在可以理清ReentrantLock重入性的实现了,也就是理解了同步语义的第一条。 3. 公平锁与公平锁ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的构造方法无参时是构造非公平锁,源码为: 123public ReentrantLock() { sync = new NonfairSync();} 另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为: 123public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 在上面非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。公平锁的处理逻辑的核心方法为: 1234567891011121314151617181920protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }} 这段代码的逻辑与nonfairTryAcquire基本上一致,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。 公平锁 VS 非公平锁 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。 深入理解读写锁ReentrantReadWriteLock1.读写锁的介绍在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。 而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。 在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。更多关于读写锁特性介绍大家可以看源码上的介绍,这里做一个归纳总结: 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平; 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁; 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁 要想彻底的理解读写锁必须能够理解这样几个问题: 读写锁是怎样实现分别记录读写状态的? 写锁是怎样获取和释放的? 读锁是怎样获取和释放的? 2.写锁详解2.1.写锁的获取同步组件的实现聚合了同步器(AQS),并通过重写同步器(AQS)中的方法实现同步组件的同步语义。因此,写锁的实现依然也是采用这种方式。 在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为: 12345678910111213141516171819202122232425262728293031323334353637protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 2. If count would saturate, fail. (This can only * happen if count is already nonzero.) * 3. Otherwise, this thread is eligible for lock if * it is either a reentrant acquire or * queue policy allows it. If so, update state * and set owner. */ Thread current = Thread.currentThread(); // 1. 获取写锁当前的同步状态 int c = getState(); // 2. 获取写锁获取的次数 int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) // 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话 // 当前线程获取写锁失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire // 3.2 当前线程获取写锁,支持可重复加锁 setState(c + acquires); return true; } // 3.3 写锁未被任何线程获取,当前线程可获取写锁 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true;} 这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount(c)方法,该方法源码为: 123static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } 其中EXCLUSIVE_MASK为: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。 而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数。同时还有一个方法值得我们注意: 123static int sharedCount(int c) { return c >>> SHARED_SHIFT; } 该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出 另外一个结论同步状态的高16位用来表示读锁被获取的次数。现在还记得我们需要弄懂的第一个问题吗?读写锁 是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示: 现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。 2.2.写锁的释放写锁释放通过重写AQS的tryRelease方法,源码为: 12345678910111213protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); //1. 同步状态减去写状态 int nextc = getState() - releases; //2. 当前写状态是否为0,为0则释放写锁 boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); //3. 不为0则更新同步状态 setState(nextc); return free;} 源码的实现逻辑请看注释,不难理解与ReentrantLock基本一致,这里需要注意的是,减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的。 3.读锁详解3.1.读锁的获取读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍,实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. If write lock held by another thread, fail. * 2. Otherwise, this thread is eligible for * lock wrt state, so ask if it should block * because of queue policy. If not, try * to grant by CASing state and updating count. * Note that step does not check for reentrant * acquires, which is postponed to full version * to avoid having to check hold count in * the more typical non-reentrant case. * 3. If step 2 fails either because thread * apparently not eligible or CAS fails or count * saturated, chain to version with full retry loop. */ Thread current = Thread.currentThread(); int c = getState(); //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前线程获取读锁失败返回-1 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && //2. 当前线程获取读锁 compareAndSetState(c, c + SHARED_UNIT)) { //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法 //返回当前获取读锁的次数 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } //4. 处理在第二步中CAS操作失败的自旋已经实现重入性 return fullTryAcquireShared(current);} 代码的逻辑请看注释,需要注意的是 当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。另外,当前同步状态需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的 3.2.读锁的释放读锁释放的实现主要通过方法tryReleaseShared,源码如下,主要逻辑请看注释: 1234567891011121314151617181920212223242526272829303132protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); // 前面还是为了实现getReadHoldCount等新功能 if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); // 读锁释放 将同步状态减去读状态即可 int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; }} 4.锁降级读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中: 123456789101112131415161718192021222324252627void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } }} 详解Condition的await和signal等待/通知机制1.Condition简介任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制 同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同: Condition能够支持不响应中断,而通过使用Object方式不支持; Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个; Condition能够支持超时时间的设置,而Object不支持 针对Object的wait方法: void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常; long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时; boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位 boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间。 针对Object的notify/notifyAll方法: void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。 void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程 2.Condition实现原理分析2.1 等待队列要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS的一个内部类。 condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。 同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量: 1234/** First node of condition queue. */private transient Node firstWaiter;/** Last node of condition queue. */private transient Node lastWaiter; ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性和AQS的实现原理差不多,Node类有这样一个属性: 12//后继节点Node nextWaiter; 进一步说明,等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。接下来我们用一个demo,去看是不是符合我们的猜想: 123456789101112131415public static void main(String[] args) { for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { lock.lock(); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }); thread.start(); }} 新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下: 从这个图我们可以很清楚的看到这样几点: 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2….Thread-8; 等待队列是一个单向队列。通过我们的猜想然后进行实验验证,我们可以得出等待队列的示意图如下图所示: 同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。示意图如下: 如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。 2.2 await实现原理当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。await()方法源码为: 1234567891011121314151617181920212223public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 1. 将当前线程包装成Node,尾插入到等待队列中 Node node = addConditionWaiter(); // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点 int savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { // 3. 当前线程进入到等待状态 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 4. 自旋等待获取到同步状态(即获取到lock) if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); // 5. 处理被中断的情况 if (interruptMode != 0) reportInterruptAfterWait(interruptMode);} 我们都知道当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。 那么关于这个实现过程我们会有这样几个问题: 是怎样将当前线程添加到等待队列中去的? 释放锁的过程? 怎样才能从await方法退出?而这段代码的逻辑就是告诉我们这三个问题的答案。 在第1步中调用addConditionWaiter将当前线程添加到等待队列中,该方法源码为: 123456789101112131415161718private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } //将当前线程包装成Node Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else //尾插入 t.nextWaiter = node; //更新lastWaiter lastWaiter = node; return node;} 将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。就是通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。 将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现,fullyRelease源码为: 1234567891011121314151617final int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); if (release(savedState)) { //成功释放同步状态 failed = false; return savedState; } else { //不成功释放同步状态抛出异常 throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; }} 调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。到目前为止,这两段代码已经解决了前面的两个问题的答案了,还剩下第三个问题,怎样从await方法退出?现在回过头再来看await方法有这样一段逻辑: 123456while (!isOnSyncQueue(node)) { // 3. 当前线程进入到等待状态 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;} 很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方: 1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false。 出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第2种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。 总结下,就是当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。 当退出while循环后就会调用acquireQueued(node, savedState),这个方法在介绍AQS的底层实现时说过了,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。 await方法示意图如下图: 如图,调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。 超时机制的支持 condition还额外支持了超时机制,使用者可调用方法awaitNanos,awaitUtil。这两个方法的实现原理,基本上与AQS中的tryAcquire方法如出一辙。 不响应中断的支持 要想不响应中断可以调用condition.awaitUninterruptibly()方法,该方法的源码为: 123456789101112public final void awaitUninterruptibly() { Node node = addConditionWaiter(); int savedState = fullyRelease(node); boolean interrupted = false; while (!isOnSyncQueue(node)) { LockSupport.park(this); if (Thread.interrupted()) interrupted = true; } if (acquireQueued(node, savedState) || interrupted) selfInterrupt();} 这段方法与上面的await方法基本一致,只不过减少了对中断的处理,并省略了reportInterruptAfterWait方法抛被中断的异常。 2.3 signal/signalAll实现原理调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。 signal方法源码为: 123456789public final void signal() { //1. 先检测当前线程是否已经获取lock if (!isHeldExclusively()) throw new IllegalMonitorStateException(); //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点 Node first = firstWaiter; if (first != null) doSignal(first);} signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的doSignal方法也是基于该节点。下面我们来看看doSignal方法做了些什么事情,doSignal方法源码为: 12345678910private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; //1. 将头结点从等待队列中移除 first.nextWaiter = null; //2. while中transferForSignal方法对头结点做真正的处理 } while (!transferForSignal(first) && (first = firstWaiter) != null);} 真正对头节点做处理的逻辑在transferForSignal方法,该方法源码为: 123456789101112131415161718192021final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ //1. 更新状态为0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; /* * Splice onto queue and try to set waitStatus of predecessor to * indicate that thread is (probably) waiting. If cancelled or * attempt to set waitStatus fails, wake up to resync (in which * case the waitStatus can be transiently and harmlessly wrong). */ //2.将该节点移入到同步队列中去 Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true;} 这段代码主要做了两件事情 1.将头结点的状态更改为CONDITION; 2.调用enq方法,将该节点尾插入到同步队列中 现在我们可以得出结论:调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。 signal执行示意图如下图: signalAll sigllAll与sigal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,,而doSignalAll的源码为: 123456789private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null);} 该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。 3. await与signal/signalAll的结合思考等待/通知机制,通过使用condition提供的await和signal/signalAll方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”,await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切: 如图,线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个 线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程 awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock, 从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同 步队列。 4. 一个例子12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class AwaitSignal { private static ReentrantLock lock = new ReentrantLock(); private static Condition condition = lock.newCondition(); private static volatile boolean flag = false; public static void main(String[] args) { Thread waiter = new Thread(new waiter()); waiter.start(); Thread signaler = new Thread(new signaler()); signaler.start(); } static class waiter implements Runnable { @Override public void run() { lock.lock(); try { while (!flag) { System.out.println(Thread.currentThread().getName() + "当前条件不满足等待"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "接收到通知条件满足"); } finally { lock.unlock(); } } } static class signaler implements Runnable { @Override public void run() { lock.lock(); try { flag = true; condition.signalAll(); } finally { lock.unlock(); } } }} 输出结果为: 12Thread-0当前条件不满足等待Thread-0接收到通知,条件满足 开启了两个线程waiter和signaler,waiter线程开始执行的时候由于条件不满足,执行condition.await方法使该线 程进入等待状态同时释放锁,signaler线程获取到锁之后更改条件,并通知所有的等待线程后释放锁。这时, waiter线程获取到锁,并由于signaler线程更改了条件此时相对于waiter来说条件满足,继续执行。 LockSupport工具1. LockSupport简介在之前介绍AQS的底层实现,已经在介绍java中的Lock时,比如ReentrantLock,已经在介绍线程间等待/通知机制使用的Condition时都会调用LockSupport.park()方法和LockSupport.unpark()方法。 而这个在同步组件的实现中被频繁使用的LockSupport。LockSupport位于java.util.concurrent.locks包下,LockSupprot是线程的阻塞原语,用来阻塞线程和唤醒线程。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在线程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞。 2. LockSupport方法介绍 阻塞线程 void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回 void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性; void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; void parkUntil(long deadline):阻塞当前线程,知道deadline; void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; 唤醒线程 void unpark(Thread thread):唤醒处于阻塞状态的指定线程 实际上LockSupport阻塞和唤醒线程的功能是依赖于sun.misc.Unsafe,这是一个很底层的类,比如park()方法的功能实现则是靠unsafe.park()方法。另外在阻塞线程这一系列方法中还有一个现象就是,每个方法都会新增一个带有Object的阻塞对象的重载方法。那么增加了一个Object对象的入参会有什么不同的地方了?示例代码很简单就不说了,直接看dump线程的信息。 调用park()方法dump线程: 12345"main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) at learn.LockSupportDemo.main(LockSupportDemo.java:7) 调用park(Object blocker)方法dump线程: 123456"main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x048c2d18> (a java.lang.String) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at learn.LockSupportDemo.main(LockSupportDemo.java:7) 通过分别调用这两个方法然后dump线程信息可以看出,带Object的park方法相较于无参的park方法会增加 parking to wait for (a java.lang.String)的信息,这种信息就类似于记录“案发现场”,有助于工程人员能够迅速发现问题解决问题。有个有意思的事情是,我们都知道如果使用synchronzed阻塞了线程dump线程时都会有阻塞对象的描述,在java 5推出LockSupport时遗漏了这一点,在java 6时进行了补充。还有一点需要需要的是:synchronzed致使线程阻塞,线程会进入到BLOCKED状态,而调用LockSupprt方法阻塞线程会致使线程进入到WAITING状态。 3. 一个例子12345678910111213141516public class LockSupportDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { LockSupport.park(); System.out.println(Thread.currentThread().getName() + "被唤醒"); }); thread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.unpark(thread); }} thread线程调用LockSupport.park()致使thread阻塞,当mian线程睡眠3秒结束后通过LockSupport.unpark(thread)方法唤醒thread线程,thread线程被唤醒执行后续操作。 另外,LockSupport.unpark(thread)可以指定线程对象唤醒指定的线程。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JVM之内存区域]]></title>
<url>%2F2019%2F05%2F26%2Fjvm%2FJVM%E4%B9%8B%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%2F</url>
<content type="text"><![CDATA[JVM之内存区域 了解如何通过参数来控制各区域的内存大小 控制参数: -Xms:设置堆的最小空间大小。 -Xmx:设置堆的最大空间大小。 -XX:NewSize:设置新生代最小空间大小。 -XX:MaxNewSize:设置新生代最大空间大小。 -XX:PermSize:设置永久代最小空间大小。 -XX:MaxPermSize:设置永久代最大空间大小。 -Xss:设置每个线程的堆栈大小。 没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。 老年代空间大小=堆空间大小-年轻代大空间大小 程序计数器(program counter register)程序计数器的作用: 当前线程所执行的字节码的行号指示器,字节码的解析工作就是通过改变这个计数器的值来选取下一个需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。(线程私有的) 程序计数器的记录内容: 如果线程正在执行一个java方法,则程序计数器里面记录着正在执行的虚拟机字节码指令的地址; 如果正在执行的是native方法,则程序计数器里面为null。 特殊例子: 此内存区域是唯一一个在java虚拟机规范中没有OutOfMemoryError情况的区域。 java虚拟机栈(JVM Stacks)生命周期:与线程相同,线程私有。 虚拟机栈描述的是java方法执行的内存模型。 JVM栈的作用:每个方法被执行的时候,都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用至执行完成的过程,其实就是一个栈帧从入栈到出栈的过程。 局部变量表存放了的内容: 存放着编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。 理解性内容: 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 该区域的异常状况: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常; 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。 本地方法栈(native method stacks)本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。 java堆(java heap)对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。(线程共享) Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 作用:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。 如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细分年轻代的有Eden空间、From Survivor空间、To Survivor空间等。(默认比例8:1:1)(详情在GC算法和回收中提到) 理解性: 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。 异常状况: 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。 方法区(method area)方法区和Java堆一样,是各个线程共享的内存区域 作用:它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。 这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载 异常状况: 根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。 运行时常量池(runtime constant pool)运行时的常量池属于方法区的一部分。 作用:用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。编译器和运行期(String 的 intern() )都可以将常量放入池中。 异常状况 当常量池无法再申请到内存时会抛出OutOfMemoryError 异常。 直接内存(direct memory)直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。 本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。 对象访问Object obj = new Object(); 最简单的访问,却涉及Java 栈、Java 堆、方法区这三个最重要内存区 假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。 “new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。 在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。 由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。 如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息 如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址 使用句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。 就主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。 OutOfMemoryError分析(OOM)java堆溢出 1java.lang.OutOfMemoryError:Java heap space java栈溢出 1java.lang.StackOverflowError 运行时常量池溢出 1java.lang.OutOfMemoryError:PermGen space 方法区溢出 1java.lang.OutOfMemoryError:PermGen space 本机直接内存溢出 1java.lang.OutOfMemoryError]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JVM之垃圾收集器与内存分配]]></title>
<url>%2F2019%2F05%2F26%2Fjvm%2FJVM%E4%B9%8B%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%2F</url>
<content type="text"><![CDATA[JVM之垃圾收集器与内存分配垃圾回收区域: 程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。 而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是java堆和方法区。 判断对象死活引用计数法: 给对象添加一个引用计数器。但是难以解决循环引用问题。 对象objA和objB都有字段instance,赋值令objA.instance = objB及objB.instance = objA,除此以外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。 可达性分析(根搜索算法): 通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。 可作为 GC Roots 的对象: 虚拟机栈(栈帧中的本地变量表)中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象 引用的类别: 强引用:类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。 软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。 弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。 虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 回收方法区在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。 永久代垃圾回收主要两部分内容:废弃的常量和无用的类。 判断废弃常量:一般是判断没有该常量的引用。 判断无用的类:要以下三个条件都满足 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例 加载该类的 ClassLoader 已经被回收 该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法 垃圾回收算法标记-清除算法: 标记所有需要回收的对象,标记完成后统一进行回收被标记的对象 缺点: 效率不高 空间产生大量碎片 复制算法: 思路:把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。 解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。 但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。 标记-整理算法: 与标记-清除算法不同的一点是,会把存活对象移动一端,解决空间产生大量碎片的缺点。 分代收集算法: 当前商业虚拟机的垃圾收集都采用分代收集算法。 把java堆分成新生代和老年代,根据各个年代的特点采用最合适的收集算法。 在新生代中,每次垃圾回收时都发现大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 在老年代中,对象存活率高,没有额外空间对他进行分配担保,就使用标记清除或者标记整理算法回收。 垃圾收集器 说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。 serial收集器 这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。 ParNew收集器 Serial 收集器的多线程版本,许多运行在server模式下的虚拟机中首选的新生代收集器,原因是处理Serial收集器,只有它能与CMS收集器配合工作(真正意义上的并发收集器,实现让垃圾收集线程和用户线程同时工作) Parallel Scavenge收集器 并行和并发区别: 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户线程继续运行,而垃圾收集程序运行于另一个CPU上。 这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。 CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) )。 Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数和直接设置吞吐量大小的-XX:GCTimeRatio参数。 -XX:MaxGCPauseMillis:允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的(并不是把参数设置越小越好) -XX:GCTimeRatio:是一个大于0小于100的整数,是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。 -XX:+UserAdaptiveSIzePolicy:作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。 Serial Old收集器 收集器的老年代版本,单线程,使用 标记 —— 整理。 Parallel Old收集器 Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理 CMS收集器 CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。 运作步骤: 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象 并发标记(CMS concurrent mark):进行 GC Roots Tracing(根搜索算法) 重新标记(CMS remark):修正并发标记期间的变动部分 并发清除(CMS concurrent sweep) 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)。 优点: 并发收集、低停顿缺点: 对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片 G1收集器 G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点: 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。 上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(物理可以不连续、逻辑连续的一段内存)Region的集合。 G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。 收集步骤: 1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark) 2、Root Region Scanning根区域扫描,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。 3、Concurrent Marking并发标记,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。 4、Remark, 最终标记,会有短暂停顿(STW:stop the world)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。 5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。 6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。 内存分配与回收策略对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动本地线程分配缓冲(TLAB),将线程上优先在TLAB上分配少数情况下也可能会直接分配在老年代中,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数配置。 对象优先在Eden分配 对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。 新生代和老年代: 新生代GC(minor GC):发生在新生代的垃圾回收动作,频繁,速度快。 老年代GC(Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。 大对象直接进入老年代 大对象指的是需要大量连续内存空间的java对象,最典型的大对象是那种很长字符串及数组(byte[]数组就是典型大对象) 虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。 长期存活的对象直接进入老年代 虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。 动态对象年龄判断 如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 空间分配担保 每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。]]></content>
<categories>
<category>jvm</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JVM之类的加载机制]]></title>
<url>%2F2019%2F05%2F26%2Fjvm%2FJVM%E4%B9%8B%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[JVM之类的加载机制什么是类的加载类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象。 Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。 加载.class文件的方式 从本地系统中直接加载 通过网络下载.class文件 从zip,jar等归档文件中加载.class文件 从专有数据库中提取.class文件 将Java源文件动态编译为.class文件 类的生命周期 其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。 加载在加载阶段,虚拟机需要完成以下三件事情: 通过一个类的全限定名来获取其定义的二进制字节流。 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 在Java堆中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。 连接验证:确保被加载的类的正确性 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作: 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 符号引用验证:确保解析动作能正确执行。 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。 准备:为类的静态变量分配内存,并将其初始化为默认值 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: 1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。 假设一个类变量的定义为: public static int value=3; 那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的 public static指令是在程序编译后,存放于类构造器 <clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。 1234567891011这里还需要注意如下几点:1、对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。2、对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。3、对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。4、如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。一句话总结以上几点,类变量、引用数据类型赋予默认值,常量、局部变量必须在声明或者初始化之前显示赋值,不然不通过编译 3、如果类字段的字段属性表中存在 ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。 假设上面的类变量value被定义为: public static final int value=3; 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中 解析:把类中的符号引用转换为直接引用 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 初始化初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式: ①声明类变量是指定初始值 ②使用静态代码块为类变量指定初始值 JVM初始化步骤: 1、假如这个类还没有被加载和连接,则程序先加载并连接该类 2、假如该类的直接父类还没有被初始化,则先初始化其直接父类 3、假如类中有初始化语句,则系统依次执行这些初始化语句 类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种: 创建类的实例,也就是new的方式 访问某个类或接口的静态变量,或者对该静态变量赋值 调用类的静态方法 反射(如 Class.forName(“com.shengsiyuan.Test”)) 初始化某个类的子类,则其父类也会被初始化 Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类 结束生命周期在如下几种情况下,Java虚拟机将结束生命周期 执行了 System.exit()方法 程序正常执行结束 程序在执行过程中遇到了异常或错误而异常终止 由于操作系统出现错误而导致Java虚拟机进程终止 类加载器 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类: 启动类加载器: BootstrapClassLoader,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点: 1、在执行非置信代码之前,自动验证数字签名。 2、动态地创建符合用户特定需要的定制化构建类。 3、从特定的场所取得java class,例如数据库中和网络中。 JVM加载机制 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效 类的加载三种方式: 1、命令行启动应用时候由JVM初始化加载 2、通过Class.forName()方法动态加载 3、通过ClassLoader.loadClass()方法动态加载 Class.forName()和ClassLoader.loadClass()区别 Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块; ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。 双亲委派模型双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。 双亲委派机制: 1、当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。 2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader`去完成。 3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载; 4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException。 ClassLoader源码分析: 双亲委派模型意义: 系统类防止内存中出现多份同样的字节码 保证Java程序安全稳定运行]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java并发编程]]></title>
<url>%2F2019%2F05%2F26%2F%E5%B9%B6%E5%8F%91%2FJava%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%2F</url>
<content type="text"><![CDATA[Java并发编程并发基础知识为什么用到并发编程 充分利用多核cpu的计算能力 方便进行业务拆分 并发编程的缺点 频繁的上下文切换 无锁并发编程 可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。 CAS算法 利用Atomic包下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换 使用最少的线程 避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态 协程 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换 线程安全的问题 死锁的代码(会写) 死锁的概念介绍 如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进,这种情况就是死锁,处于死锁状态的进程称为死锁进程 死锁产生的原因 因竞争资源发生死锁现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象 进程推进顺序不当发生死锁 产生死锁的四个必要条件 互斥条件(线程的特性,不可解决) 进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源 请求和保持条件 进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放 不可剥夺的条件 是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放 循环等待条件 是指进程发生死锁后,必然存在一个进程–资源之间的环形链 处理死锁的基本策略 预防死锁 通过设置一些限制条件,去破坏产生死锁的必要条件 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。 避免死锁 在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁 银行家算法 最大分配矩阵 已分配矩阵 求出需求分配矩阵 资源最大矩阵 剩余待分配矩阵 检测死锁 允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉 解除死锁 该方法与检测死锁配合使用 java层面的避免死锁 避免一个线程同时获得多个锁; 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源; 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞; 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况 了解基本概念 同步与异步 同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。 并发与并行 并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。 阻塞与非阻塞 阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。 临界区 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。 线程状态与操作新建线程 继承Thread类,重写run方法 实现runable接口 实现callable接口 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)将callable进行包装然后FeatureTask提交给ExecutorsService 可以通过Executors将Runable转换成Callable,具体方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。 线程状态转换 初始状态 运行状态 阻塞状态 等待状态 超时等待状态 终止状态 线程状态基本操作 interrupted 它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法 interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。 join 如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法: sleep public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。 wait和sleep区别 sleep()方法是Thread的静态方法,而wait是Object实例方法 wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁; sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notify/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。 yield public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。 sleep和yield区别 sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。 守护线程 守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。 Java内存模型JMM线程安全介绍 概念:当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。 线程安全出现的原因:出现线程安全的问题一般是因为主内存和工作内存数据不一致性和重排序导致的, 线程通信 共享内存 消息传递 共享变量 在java程序中所有实例域,静态域和数组元素都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。 JMM抽象结构模型 CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。 1-线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中; 2-线程B从主存中读取最新的共享变量 重排序 在不改变程序执行结果的前提下,尽可能提高并行度。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。 编译器重排序 1-编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; 处理器重排序 2-指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序; 3-内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。 针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。 编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序 happens-before规则 JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见) as-if-serial VS happens-before as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 具体规则 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。 并发关键字Synchronized synchronized实现原理 方法 实例方法 锁住类的实例对象 静态方法 锁住类对象 代码块 实例对象 锁住类的实例对象 class对象 锁住类对象 任意实例对象Object 实例对象Object 对象锁(Monitor)机制 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态 Synchronized优化 CAS操作 CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。 无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 Synchronized VS CAS Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。 而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 CAS应用场景 在Lock实现中会有CAS改变state变量 在atomic包中的实现类也几乎都是用CAS实现 CAS问题 ABA问题 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题 自旋时间过长 使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。 只能保证一个共享变量的原子操作 当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。 对象头 在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。 锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 偏向锁 偏向锁的获取 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程 偏向锁的撤销 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 轻量级锁 加锁 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 解锁 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。 因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。 各种锁的比较 volatile 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。 volatile实现原理 在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,那么Lock前缀的指令在多核处理器下主要有这两个方面的影响:1.将当前处理器缓存行的数据写回系统内存;2.这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论: 1、Lock前缀的指令会引起处理器缓存写回内存; 2、一个处理器的缓存回写到内存会导致其他处理器的缓存失效; 3、当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。 这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。 volatile的happens-before关系 volatile的内存语义 为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。 JMM内存屏障分为四类 为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略: 1、在每个volatile写操作的前面插入一个StoreStore屏障;2、在每个volatile写操作的后面插入一个StoreLoad屏障;3、在每个volatile读操作的后面插入一个LoadLoad屏障;4、在每个volatile读操作的后面插入一个LoadStore屏障。 需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障 StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序 final final的简介 final可以修饰变量,方法和类,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类。 final的使用场景 变量 当final变量未初始化时系统不会进行隐式初始化,会出现报错。 final成员变量 类变量(static修饰的变量) 必须要在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在这两个地方之一进行指定; 实例变量 必须要在非静态初始化块,声明该实例变量时或者在构造器中指定初始值,而且只能在这三个地方进行指定。 final局部变量 final局部变量由程序员进行显式初始化,如果final局部变量已经进行了初始化则后面就不能再次进行更改,如果final变量未进行初始化,可以进行赋值,当且仅有一次赋值,一旦赋值之后再次赋值就会出错。 当final修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。而对于引用类型变量而言,它仅仅保存的是一个引用,final只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的 方法 被final修饰的方法不能够被子类所重写(覆盖)。 final方法是可以被重载的 类 当一个类被final修饰时,表名该类是不能被子类继承的 不变类 使用private和final修饰符来修饰该类的成员变量 提供带参的构造器用于初始化类的成员变量; 仅为该类的成员变量提供getter方法,不提供setter方法,因为普通方法无法修改fina修饰的成员变量; 如果有必要就重写Object类 的hashCode()和equals()方法,应该保证用equals()判断相同的两个对象其Hashcode值也是相等的。 final域重排序规则 final域是基本类型 写final域重排序规则 写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:1、JMM禁止编译器把final域的写重排序到构造函数之外;2、编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。 写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。 读final域重排序规则 在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。 读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。 final域为引用类型 对final修饰的对象的成员域写操作 针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使用。 对final修饰的对象的成员域读操作 JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。 final重排序总结 基本数据类型: final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。 final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。 引用数据类型: 额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序 final的实现原理 为什么final引用不能从构造函数中“溢出” 并发三大性质 两个核心 JMM抽象内存模型 happens-before规则 三大性质 原子性 一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉 java内存模型8个操作是原子性的 lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态; unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用; load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本 use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作; assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作; store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用; write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 有序性 可见性]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多线程(一)]]></title>
<url>%2F2019%2F05%2F26%2F%E5%B9%B6%E5%8F%91%2F%E5%A4%9A%E7%BA%BF%E7%A8%8B1%2F</url>
<content type="text"><![CDATA[多线程(一)进程和线程的基本概念进程:系统中能独立运行并作为资源分配的基本单位 进程的特征: 1.动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。2.并发性:任何进程都可以同其他进程一起并发执行。3.独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。4.异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。 线程:是进程中的一个实体,作为系统调度和分派的基本单位。 线程的性质: 1.线程是进程内的一个相对独立的可执行的单元。若把进程称为任务的话,那么线程则是应用中的一个子任务的执行。2.由于线程是被调度的基本单元,而进程不是调度单元。所以,每个进程在创建时,至少需要同时为该进程创建一个线程。即进程中至少要有一个或一个以上的线程,否则该进程无法被调度执行。3.进程是被分给并拥有资源的基本单元。同一进程内的多个线程共享该进程的资源,但线程并不拥有资源,只是使用他们。4.线程是操作系统中基本调度单元,因此线程中应包含有调度所需要的必要信息,且在生命周期中有状态的变化。5.由于共享资源【包括数据和文件】,所以线程间需要通信和同步机制,且需要时线程可以创建其他线程,但线程间不存在父子关系。 进程与线程的区别: 1、调度:在传统的操作系统中,CPU调度和分派的基本单位是进程。在引入线程的操作系统中,则把线程作为CPU调度和分派的基本单位,进程则作为资源拥有的基本单位,将线程作为cpu调度和分派,进程则作为资源拥有的基本单位,显著提高系统的并发性(同一进程中的线程切换不会引起进程切换,不同进程中的线程切换才会引起进程切换) 2、并发性:进程之间可以并发执行,同一个进程之间的线程也可以并发执行,提高系统资源和系统吞吐量 (例如,在一个为引入线程的单CPU操作系统中,若仅设置一个文件服务进程,当它由于某种原因被封锁时,便没有其他的文件服务进程来提供服务。在引入线程的操作系统中,可以在一个文件服务进程设置多个服务线程。当第一个线程等待时,文件服务进程中的第二个线程可以继续运行;当第二个线程封锁时,第三个线程可以继续执行,从而显著地提高了文件服务的质量以及系统的吞吐量。) 3、拥有资源:进程拥系统资源的独立单位,线程不能拥有自己的资源,但可以访问隶属进程的资源(代码段、数据段以及系统资源,可供同一个进程的其他线程共享) 4、独立性:在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多,这是因为为了防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其它资源,除了共享全局变量外,不允许其它进程的访问。 5、系统开销:由于在创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统为此所付出的开销将显著地大于在创建或撤消线程时的开销。 6、支持多处理机系统: 在多处理机系统中,对于传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。但对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,这无疑将加速进程的完成。 创建和启动线程1、继承Thread类创建线程类 步骤: 定义一个继承Thread类的子类,并重写该类的run()方法 创建子类的实例,即创建线程对象 调用该线程对象的start()方法启动线程 1234567891011class SomeThead extends Thraad { public void run() { //do something here } } public static void main(String[] args){ SomeThread oneThread = new SomeThread(); //启动线程: oneThread.start(); } 2、实现Runnable接口创建线程类 步骤: 定义Runnable接口的实现类,并重写该接口的的run()方法 创建该实现类的实例 以此实例作为创建Thread对象的参数,该Thread对象即是线程对象 12345678class SomeRunnable implements Runnable { public void run() { //do something here } } Runnable oneRunnable = new SomeRunnable(); Thread oneThread = new Thread(oneRunnable); oneThread.start(); 3、通过Callable和Future创建对象 步骤: 创建Callable接口的实现类,并实现call()方法,该call()方法作为线程执行体,并且有返回值 创建Callable实现类的实例 使用FutureTask类来包装Callable对象,该FutureTask对象封装Callable对象的call()方法的返回值 使用FutureTask对象作为Thread对象的target创建并启动新线程 调用FutureTask对象的get()方法获取子线程执行结束后的返回值 12345678910111213public interface Callable { V call() throws Exception; } 步骤1:创建实现Callable接口的类SomeCallable(略); 步骤2:创建一个类对象: Callable oneCallable = new SomeCallable(); 步骤3:由Callable创建一个FutureTask对象: FutureTask oneTask = new FutureTask(oneCallable); 注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。 步骤4:由FutureTask创建一个Thread对象: Thread oneThread = new Thread(oneTask); 步骤5:启动线程: oneThread.start(); 123456789101112131415161718192021222324252627282930313233343536373839404142434445public class ThreadTest { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程 thread.start(); //线程进入到就绪状态 } } System.out.println("主线程for循环执行完毕.."); try { int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }}class MyCallable implements Callable<Integer> { private int i = 0; // 与run()方法不同的是,call()方法具有返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; }} 线程的生命周期 1、新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。 2、就绪状态 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。 提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。 3、运行状态 处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。 运行状态->就绪状态:1.失去cpu资源 2.调用yield()方法 运行状态->阻塞状态: 1.调用sleep()方法主动放弃cpu资源 2.线程调用一个阻塞式IO方法,在该方法返回之前,线程被阻塞 3.线程试图获取一个同步监视器(锁),但该同步监视器正被其他线程所持有 4.线程在等待某个通知(notify) 5.程序调用线程的suspend方法被线程挂起(容易导致死锁) 4、阻塞状态 只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。 5、死亡状态 当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 线程管理1、线程睡眠sleep 让当前正在执行的线程暂停一段时间,并进入阻塞状态,且当前线程不会失去锁 sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效 123456789101112public class Test1 { public static void main(String[] args) throws InterruptedException { System.out.println(Thread.currentThread().getName()); MyThread myThread=new MyThread(); myThread.start(); myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程 Thread.sleep(10); for(int i=0;i<100;i++){ System.out.println("main"+i); } } } 使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。 2、线程让步yield() yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。 3、线程合并join() 线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,它不是静态方法。从上面的方法的列表可以看到,它有3个重载的方法: 123456void join() 当前线程等该加入该线程后面,等待该线程终止。 void join(long millis) 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 void join(long millis,int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 4、设置线程优先级 每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。 12345MAX_PRIORITY =10MIN_PRIORITY =1NORM_PRIORITY =5 注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。 5、后台(守护)线程 JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。 守护线程的用途为: • 守护线程通常用于执行一些后台作业,例如在应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。 • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。 123456789public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: on - 如果为 true,则将该线程标记为守护线程。 抛出: IllegalThreadStateException - 如果该线程处于活动状态。 SecurityException - 如果当前线程无法修改该线程。 6、正确结束线程 Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法: • 正常执行完run方法,然后结束掉; • 控制循环条件和判断条件的标识符来结束掉线程。]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多线程(二)]]></title>
<url>%2F2019%2F05%2F26%2F%E5%B9%B6%E5%8F%91%2F%E5%A4%9A%E7%BA%BF%E7%A8%8B2%2F</url>
<content type="text"><![CDATA[多线程(二)线程同步java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。 1、同步方法 即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 public synchronized void save(){} 注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类 2、同步代码块 即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 12345678910111213141516171819202122232425public class Bank { private int count = 0;//账户余额 //存钱 public void addMoney(int money){ synchronized (this) { count +=money; } System.out.println(System.currentTimeMillis()+"存进:"+money); } //取钱 public void subMoney(int money){ synchronized (this) { if(count-money < 0){ System.out.println("余额不足"); return; } count -= money; } System.out.println(+System.currentTimeMillis()+"取出:"+money); } //查询 public void lookMoney(){ System.out.println("账户余额:"+count); } } 同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 3、使用特殊域变量(volatile)实现线程同步 volatile关键字为域变量的访问提供了一种免锁机制 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 每次使用该域就要重新计算,而不是使用寄存器中的值; volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960public class SynchronizedThread { class Bank { private volatile int account = 100; public int getAccount() { return account; } /** * 用同步方法实现 * * @param money */ public synchronized void save(int money) { account += money; } /** * 用同步代码块实现 * * @param money */ public void save1(int money) { synchronized (this) { account += money; } } } class NewThread implements Runnable { private Bank bank; public NewThread(Bank bank) { this.bank = bank; } @Override public void run() { for (int i = 0; i < 10; i++) { // bank.save1(10); bank.save(10); System.out.println(i + "账户余额为:" +bank.getAccount()); } } } /** * 建立线程,调用内部类 */ public void useThread() { Bank bank = new Bank(); NewThread new_thread = new NewThread(bank); System.out.println("线程1"); Thread thread1 = new Thread(new_thread); thread1.start(); System.out.println("线程2"); Thread thread2 = new Thread(new_thread); thread2.start(); } public static void main(String[] args) { SynchronizedThread st = new SynchronizedThread(); st.useThread(); }} 多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。 4、使用重入锁(Lock)实现线程同步 在javaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有: 123ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁 unlock() : 释放锁 ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 123456789101112131415161718//只给出要修改的代码,其余代码与上同 class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } } 线程通信1、借助于Object类的wait()、notify()和notifyAll()实现通信 线程执行wait()后,就放弃了运行资格,处于冻结状态; 线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。notifyAll(), 唤醒线程池中所有线程。注: (1) wait(), notify(),notifyAll()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中; (2) wait(),notify(),notifyAll(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。 单个消费者生产者例子: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253class Resource{ //生产者和消费者都要操作的资源 private String name; private int count=1; private boolean flag=false; public synchronized void set(String name){ if(flag) try{wait();}catch(Exception e){} this.name=name+"---"+count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag=true; this.notify(); } public synchronized void out(){ if(!flag) try{wait();}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); flag=false; this.notify(); } } class Producer implements Runnable{ private Resource res; Producer(Resource res){ this.res=res; } public void run(){ while(true){ res.set("商品"); } } } class Consumer implements Runnable{ private Resource res; Consumer(Resource res){ this.res=res; } public void run(){ while(true){ res.out(); } } } public class ProducerConsumerDemo{ public static void main(String[] args){ Resource r=new Resource(); Producer pro=new Producer(r); Consumer con=new Consumer(r); Thread t1=new Thread(pro); Thread t2=new Thread(con); t1.start(); t2.start(); } }//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。 但是如果有多个生产者和多个消费者,上面的代码就会有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r, 而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。 1234567891011121314151617181920212223242526272829303132333435class Resource{ private String name; private int count=1; private boolean flag=false; public synchronized void set(String name){ while(flag) /*原先是if,现在改成while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/ try{wait();}catch(Exception e){} this.name=name+"---"+count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag=true; this.notifyAll();/*原先是notity(), 现在改成notifyAll(),这样生产者线程生产完一个商品后可以将等待中的消费者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/ } public synchronized void out(){ while(!flag) /*原先是if,现在改成while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/ try{wait();}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); flag=false; this.notifyAll(); /*原先是notity(), 现在改成notifyAll(),这样消费者线程消费完一个商品后可以将等待中的生产者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/ } } public class ProducerConsumerDemo{ public static void main(String[] args){ Resource r=new Resource(); Producer pro=new Producer(r); Consumer con=new Consumer(r); Thread t1=new Thread(pro); Thread t2=new Thread(con); Thread t3=new Thread(pro); Thread t4=new Thread(con); t1.start(); t2.start(); t3.start(); t4.start(); } } 2、使用Condition控制线程通信 ]]></content>
<categories>
<category>并发</category>
</categories>
<tags>
<tag>并发</tag>
</tags>
</entry>
<entry>
<title><![CDATA[mysql概述]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2Fmysql%E6%A6%82%E8%BF%B0%2F</url>
<content type="text"><![CDATA[mysql六大约束 primary key foreign key NOT NULL Default Unique check 事务和隔离级别事务T1、T2,当T1读取一个表的字段,T2此时插入多几行数据,T1再次读取,数据多了几行幻读和不可重复读区别:不可重复读针对一个字段更新前后数据不一致,幻读是针对一个表数据读取获取的数据数量不想等 事务ACID 原子性(atomicity) 一致性(consistency) 隔离性(isolation) 持久性(durability) 事务结束四个标志 commit或rollback DDL或DCL自动提交 用户会话正常结束 系统异常终止 事务隔离级别和对应的问题事务的隔离级别: 脏读-不可重复读-幻读 read uncommitted:√ √ √read committed: × √ √repeatable read: × × √serializable × × × 丢失修改: 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。 读未提交一个事务读取到另一个未提交的数据 脏读事务T1、T2,当T2读取到T1已更新但还没有提交的字段,若T2回滚,T1读取的数据临时且无效 读已提交一个事务必须等到另一个事务提交后才可以读取数据 不可重复读事务T1、T2,T1读取一个字段,然后T2更新这个字段之后,T1再次读取同一个字段,数据已经发生改变脏读和不可重复读的区别:脏读是在一个事务里面更新了还没提交导致前后数据不一致,不可重复读是在提交数据之后,事务再次更新操作,导致数据不一致 重复读在开始读取数据(事务开启时),不允许修改操作 幻读事务T1、T2,T1读取表中一个范围的值,然后T2插入或删除这个字段之后,T1再次读取,数据的数量发生了变化幻读和不可重复读的区别:幻读是插入或删除导致的前后数据不一致(数量上)不可重复读是修改前后导致前后的字段值不一致(内容上) 序列化事务串行化顺序执行解决脏读、不可重复读、幻读的并发问题 存储引擎(Innodb、Myisam)支持行锁MyISAM 只有表级锁(table-level locking)InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 支持事务和崩溃后的安全恢复MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。 但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 支持外键MyISAM不支持而InnoDB支持 支持MVCC仅InnoDB支持。应对高并发事务, MVCC比单纯的加锁更高效; MVCC只在READ COMMITTED和 REPEATABLE READ 两个隔离级别下工作; MVCC可以使用乐观(optimistic)锁和悲观(pessimistic)锁来实现; 各数据库中MVCC实现并不统一 视图视图:一种虚拟存在的表,在使用时动态生成,只保存sql逻辑不保存结果应用场景 多个地方重复用到的结果l sql语句较复杂的语句 视图不可更新情况 包含关键字:distinct、分组函数、group by、having、union、union all 常量视图 select中包含子查询 join from一个不能更新的视图 where子句的子查询引用了from子句中的表 存储程序存储过程和函数:实现经过编译并存储在数据库中的一段sql语句的集合 存储例程 存储函数 存储过程 触发器事件锁机制当一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之如果请求不兼容,则该事物就等待锁释放。 表级锁MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。 其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。 InnoDB表级锁当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。 而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。 如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。 意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。 这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。 意向共享锁(IS)表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。 意向排他锁(IX)表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。 MyISAM表级锁页级锁MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。 表级锁速度快,但冲突多,行级冲突少,但速度慢。 页级进行了折衷,一次锁定相邻的一组记录。BDB支持页级锁。开销和加锁时间界于表锁和行锁之间,会出现死锁。锁定粒度界于表锁和行锁之间,并发度一般。 行级锁(InnoDB专有)MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。 其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB 行锁是通过给索引上的索引项加锁来实现的,InnoDB 这种行锁实现的特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁。 Record Lock对索引项加锁 行锁锁定的是索引记录,而不是行数据,也就是说锁定的是key。 其他事务不能修改和删除加锁项; 索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引; 如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。 Gap Lock对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁)不包含索引项本身(对非索引项)。 其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行 锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别而已的。 间隙锁(Gap Lock)一般是针对非唯一索引而言的 Next-key Lock锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。 发生死锁当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。 MyISAM 中是不会产生死锁的,因为 MyISAM 总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在 InnoDB 中,锁是逐步获得的,就造成了死锁的可能。 发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个获取锁完成事务。 避免死锁有多种方法可以避免死锁,这里介绍常见的三种 1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。 2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; 3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率; 是否可写锁分类读锁(共享锁)其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。 共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A; 其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。 写锁(互斥锁)排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。 它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。 两者之间的区别共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。 排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。 大表优化限定数据范围读写分离经典的数据库拆分方案,主库负责写,从库负责读; 垂直分区根据数据库里面数据表的相关性进行拆分。例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。 简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。 优点垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。 缺点垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; 水平分区保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。 水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。 水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。 客户端代理客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。 中间件代理中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。 超一万行数据量的写操作产生的问题大批量操作可能会造成严重的主从延迟主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间, 而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况 binlog 日志为 row 格式时会产生大量的日志大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因 解决方案避免产生大事务操作,分批操作大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL的性能产生非常大的影响。 特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。 对于大表使用pt-online-schema-change修改表的结构避免大表修改产生的主从延迟避免在对表字段进行修改时进行锁表 对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。 pt-online-schema-change 它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。 把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。把原来一个 DDL 操作,分解成多个小的批次进行。]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[oracle和mysql的区别]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2Foracle%E5%92%8Cmysql%E7%9A%84%E5%8C%BA%E5%88%AB%2F</url>
<content type="text"><![CDATA[默认隔离级别不同:mysql默认可重复读,oracle默认读已提交 隔离级别种类不同:oracle只有读已提交和串行化,只读 对事务的提交:mysql自动提交,oracle默认手动提交 并发性:mysql有索引行级锁,没索引表级锁,oracle使用行级锁对资源锁定的粒度小得很,只是锁定sql需要得资源,不依赖索引 分页查询:MySQL用到limit,oracle用到伪列ROWNUM和嵌套查询 逻辑备份:mysql逻辑备份数据需要锁定数据,oracle不需要]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[String和包装类的equals和==]]></title>
<url>%2F2019%2F05%2F26%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2FString%E5%92%8C%E5%8C%85%E8%A3%85%E7%B1%BB%E7%9A%84equals%E5%92%8C%3D%3D%2F</url>
<content type="text"><![CDATA[StringString对象创建两种方式: 第一种方式是在常量池中拿对象; 第二种方式是直接在堆内存空间创建一个新的对象。 String 类型的常量池比较特殊。它的主要使用方法有两种: 直接使用双引号声明出来的 String 对象会直接存储在常量池中。 如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7之前(不包含1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。 123456String s1 = new String("计算机");String s2 = s1.intern();String s3 = "计算机";System.out.println(s2);//计算机System.out.println(s1 == s2);//false,因为一个是堆内存中的 String 对象一个是常量池中的 String 对象,System.out.println(s3 == s2);//true,因为两个都是常量池中的 String 对象 字符串拼接 123456789String str1 = "str";String str2 = "ing";String str3 = "str" + "ing";//常量池中的对象String str4 = str1 + str2; //在堆上创建的新的对象 String str5 = "string";//常量池中的对象System.out.println(str3 == str4);//falseSystem.out.println(str3 == str5);//trueSystem.out.println(str4 == str5);//false 1234567891011121314151617181920212223public class IntegerTest { public static void main(String[] args) { String a = "123"; String b = new String("123"); String c = "1"+"23"; String d = "1"+new String("23"); String e = new String("1")+new String("23"); String f = "1"; String h = "23"; String i = f+h; System.out.println(a==b);//false System.out.println(a==c);//true System.out.println(a==d);//false System.out.println(a==e);//false System.out.println(b==d);//false System.out.println(b==e);//false System.out.println(d==e);//false System.out.println("------------------------------"); System.out.println(a==i);//false System.out.println(b==i);//false }} 只要涉及到对象引用如上f、h或者其中new一个,整体都会new一个对象,所以都是false 包装类 Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 为啥把缓存设置为[-128,127]区间?性能和资源之间的权衡。 两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。 123456Integer i1 = 33;Integer i2 = 33;System.out.println(i1 == i2);// 输出 trueInteger i11 = 333;Integer i22 = 333;System.out.println(i11 == i22);// 输出 false Integer 缓存源代码: 12345678/***此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。*/ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 应用场景: Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。 Integer i1 = new Integer(40);这种情况下会创建新的对象。 123Integer i1 = 40;Integer i2 = new Integer(40);System.out.println(i1==i2);//输出 false Integer 比较更丰富的一个例子: 12345678910111213Integer i1 = 40;Integer i2 = 40;Integer i3 = 0;Integer i4 = new Integer(40);Integer i5 = new Integer(40);Integer i6 = new Integer(0);System.out.println("i1=i2 " + (i1 == i2));System.out.println("i1=i2+i3 " + (i1 == i2 + i3));System.out.println("i1=i4 " + (i1 == i4));System.out.println("i4=i5 " + (i4 == i5));System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); System.out.println("40=i5+i6 " + (40 == i5 + i6)); 结果: 123456i1=i2 truei1=i2+i3 truei1=i4 falsei4=i5 falsei4=i5+i6 true40=i5+i6 true 解释: 语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。 12345678910111213141516171819202122232425262728293031public class IntegerTest { public static void main(String[] args) { Integer a = 40; Integer b = new Integer(40); Integer c = 10+30; Integer d = 10+new Integer(30); Integer e = new Integer(10)+new Integer(30); Integer f = 10; Integer h = 30; Integer i = f+h; int j = 40; System.out.println(a==b);//false System.out.println(a==c);//true System.out.println(a==d);//true System.out.println(a==e);//true System.out.println(b==c);//false System.out.println(b==d);//false System.out.println(b==e);//false System.out.println(d==e);//true System.out.println("------------------------------"); System.out.println(a==i);//true System.out.println(b==i);//false System.out.println(c==i);//true System.out.println(d==i);//true System.out.println(e==i);//true System.out.println(a==j);//true System.out.println(b==j);//true }} 包装类需要考虑自动拆箱问题,当有加减操作时候,就会自动拆箱,相当于在常量池中创建 equals方法比较内容,所以都相等]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git的基本操作]]></title>
<url>%2F2019%2F05%2F26%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2Fgit%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C%2F</url>
<content type="text"><![CDATA[git的通用操作流程 主要涉及到四个关键点: 工作区:本地电脑存放项目文件的地方,比如learnGitProject文件夹; 暂存区(Index/Stage):在使用git管理项目文件的时候,其本地的项目文件会多出一个.git的文件夹,将这个.git文件夹称之为版本库。其中.git文件夹中包含了两个部分,一个是暂存区(Index或者Stage),顾名思义就是暂时存放文件的地方,通常使用add命令将工作区的文件添加到暂存区里; 本地仓库:.git文件夹里还包括git自动创建的master分支,并且将HEAD指针指向master分支。使用commit命令可以将暂存区中的文件添加到本地仓库中; 远程仓库:不是在本地仓库中,项目代码在远程git服务器上,比如项目放在github上,就是一个远程仓库,通常使用clone命令将远程仓库拷贝到本地仓库中,开发后推送到远程仓库中即可; 更细节的来看: 日常开发时代码实际上放置在工作区中,也就是本地的XXX.java这些文件,通过add等这些命令将代码文教提交给暂存区(Index/Stage),也就意味着代码全权交给了git进行管理,之后通过commit等命令将暂存区提交给master分支上,也就是意味打了一个版本,也可以说代码提交到了本地仓库中。另外,团队协作过程中自然而然还涉及到与远程仓库的交互。 因此,经过这样的分析,git命令可以分为这样的逻辑进行理解和记忆: git管理配置的命令; 几个核心存储区的交互命令: 工作区与暂存区的交互; 暂存区与本地仓库(分支)上的交互; 本地仓库与远程仓库的交互。 git配置命令 查询配置信息 列出当前配置:git config --list; 列出repository配置:git config --local --list; 列出全局配置:git config --global --list; 列出系统配置:git config --system --list; 第一次使用git,配置用户信息 配置用户名:git config --global user.name "your name"; 配置用户邮箱:git config --global user.email "youremail@github.com"; 其他配置 配置解决冲突时使用哪种差异分析工具,比如要使用vimdiff:git config --global merge.tool vimdiff; 配置git命令输出为彩色的:git config --global color.ui auto; 配置git使用的文本编辑器:git config --global core.editor vi; 工作区上的操作命令 新建仓库 将工作区中的项目文件使用git进行管理,即创建一个新的本地仓库:git init; 从远程git仓库复制项目:git clone <url>,如:git clone git://github.com/wasd/example.git;克隆项目时如果想定义新的项目名,可以在clone命令后指定新的项目名:git clone git://github.com/wasd/example.git mygit; 提交 提交工作区所有文件到暂存区:git add .; 提交工作区中指定文件到暂存区:git add <file1> <file2> ...; 提交工作区中某个文件夹中所有文件到暂存区:git add [dir]; 撤销 删除工作区文件,并且也从暂存区删除对应文件的记录:git rm <file1> <file2>; 从暂存区中删除文件,但是工作区依然还有该文件:git rm --cached <file>; 取消暂存区已经暂存的文件:git reset HEAD <file>...; 撤销上一次对文件的操作:git checkout --<file>。要确定上一次对文件的修改不再需要,如果想保留上一次的修改以备以后继续工作,可以使用stashing和分支来处理; 隐藏当前变更,以便能够切换分支:git stash; 查看当前所有的储藏:git stash list; 应用最新的储藏:git stash apply,如果想应用更早的储藏:git stash apply stash@{2};重新应用被暂存的变更,需要加上--index参数:git stash apply --index; 使用apply命令只是应用储藏,而内容仍然还在栈上,需要移除指定的储藏:git stash drop stash{0};如果使用pop命令不仅可以重新应用储藏,还可以立刻从堆栈中清除:git stash pop; 在某些情况下,你可能想应用储藏的修改,在进行了一些其他的修改后,又要取消之前所应用储藏的修改。Git没有提供类似于 stash unapply 的命令,但是可以通过取消该储藏的补丁达到同样的效果:git stash show -p stash@{0} | git apply -R;同样的,如果你沒有指定具体的某个储藏,Git 会选择最近的储藏:git stash show -p | git apply -R; 更新文件 重命名文件,并将已改名文件提交到暂存区:git mv [file-original] [file-renamed]; 查新信息 查询当前工作区所有文件的状态:git status; 比较工作区中当前文件和暂存区之间的差异,也就是修改之后还没有暂存的内容:git diff;指定文件在工作区和暂存区上差异比较:git diff <file-name>; 暂存区上的操作命令 提交文件到版本库 将暂存区中的文件提交到本地仓库中,即打上新版本:git commit -m "commit_info"; 将所有已经使用git管理过的文件暂存后一并提交,跳过add到暂存区的过程:git commit -a -m "commit_info"; 提交文件时,发现漏掉几个文件,或者注释写错了,可以撤销上一次提交:git commit --amend; 查看信息 比较暂存区与上一版本的差异:git diff --cached; 指定文件在暂存区和本地仓库的不同:git diff <file-name> --cached; 查看提交历史:git log;参数-p展开每次提交的内容差异,用-2显示最近的两次更新,如git log -p -2; 打标签 Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用。而含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。一般我们都建议使用含附注型的标签,以便保留相关信息;当然,如果只是临时性加注标签,或者不需要旁注额外信息,用轻量级标签也没问题。 列出现在所有的标签:git tag; 使用特定的搜索模式列出符合条件的标签,例如只对1.4.2系列的版本感兴趣:git tag -l "v1.4.2.*"; 创建一个含附注类型的标签,需要加-a参数,如git tag -a v1.4 -m "my version 1.4"; 使用git show命令查看相应标签的版本信息,并连同显示打标签时的提交对象:git show v1.4; 如果有自己的私钥,可以使用GPG来签署标签,只需要在命令中使用-s参数:git tag -s v1.5 -m "my signed 1.5 tag"; 验证已签署的标签:git tag -v ,如git tag -v v1.5; 创建一个轻量级标签的话,就直接使用git tag命令即可,连-a,-s以及-m选项都不需要,直接给出标签名字即可,如git tag v1.5; 将标签推送到远程仓库中:git push origin ,如git push origin v1.5; 将本地所有的标签全部推送到远程仓库中:git push origin --tags; 分支管理 创建分支:git branch <branch-name>,如git branch testing; 从当前所处的分支切换到其他分支:git checkout <branch-name>,如git checkout testing; 新建并切换到新建分支上:git checkout -b <branch-name>; 删除分支:git branch -d <branch-name>; 将当前分支与指定分支进行合并:git merge <branch-name>; 显示本地仓库的所有分支:git branch; 查看各个分支最后一个提交对象的信息:git branch -v; 查看哪些分支已经合并到当前分支:git branch --merged; 查看当前哪些分支还没有合并到当前分支:git branch --no-merged; 把远程分支合并到当前分支:git merge <remote-name>/<branch-name>,如git merge origin/serverfix;如果是单线的历史分支不存在任何需要解决的分歧,只是简单的将HEAD指针前移,所以这种合并过程可以称为快进(Fast forward),而如果是历史分支是分叉的,会以当前分叉的两个分支作为两个祖先,创建新的提交对象;如果在合并分支时,遇到合并冲突需要人工解决后,再才能提交; 在远程分支的基础上创建新的本地分支:git checkout -b <branch-name> <remote-name>/<branch-name>,如git checkout -b serverfix origin/serverfix; 从远程分支checkout出来的本地分支,称之为跟踪分支。在跟踪分支上向远程分支上推送内容:git push。该命令会自动判断应该向远程仓库中的哪个分支推送数据;在跟踪分支上合并远程分支:git pull; 将一个分支里提交的改变移到基底分支上重放一遍:git rebase <rebase-branch> <branch-name>,如git rebase master server,将特性分支server提交的改变在基底分支master上重演一遍;使用rebase操作最大的好处是像在单个分支上操作的,提交的修改历史也是一根线;如果想把基于一个特性分支上的另一个特性分支变基到其他分支上,可以使用--onto操作:git rebase --onto <rebase-branch> <feature branch> <sub-feature-branch>,如git rebase --onto master server client;使用rebase操作应该遵循的原则是:一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行rebase操作; 本地仓库上的操作 查看本地仓库关联的远程仓库:git remote;在克隆完每个远程仓库后,远程仓库默认为origin;加上-v的参数后,会显示远程仓库的url地址; 添加远程仓库,一般会取一个简短的别名:git remote add [remote-name] [url],比如:git remote add example git://github.com/example/example.git; 从远程仓库中抓取本地仓库中没有的更新:git fetch [remote-name],如git fetch origin;使用fetch只是将远端数据拉到本地仓库,并不自动合并到当前工作分支,只能人工合并。如果设置了某个分支关联到远程仓库的某个分支的话,可以使用git pull来拉去远程分支的数据,然后将远端分支自动合并到本地仓库中的当前分支; 将本地仓库某分支推送到远程仓库上:git push [remote-name] [branch-name],如git push origin master;如果想将本地分支推送到远程仓库的不同名分支:git push <remote-name> <local-branch>:<remote-branch>,如git push origin serverfix:awesomebranch;如果想删除远程分支:git push [romote-name] :<remote-branch>,如git push origin :serverfix。这里省略了本地分支,也就相当于将空白内容推送给远程分支,就等于删掉了远程分支。 查看远程仓库的详细信息:git remote show origin; 修改某个远程仓库在本地的简称:git remote rename [old-name] [new-name],如git remote rename origin org; 移除远程仓库:git remote rm [remote-name]; 忽略文件.gitignore一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。如下例: 12345678910111213# 此为注释 – 将被 Git 忽略# 忽略所有 .a 结尾的文件*.a# 但 lib.a 除外!lib.a# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO/TODO# 忽略 build/ 目录下的所有文件build/# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txtdoc/*.txt# 忽略 doc/ 目录下所有扩展名为 txt 的文件doc/**/*.txt]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[jvm在操作系统的哪个区域]]></title>
<url>%2F2019%2F05%2F26%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2Fjvm%E5%9C%A8%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E5%93%AA%E4%B8%AA%E5%8C%BA%E5%9F%9F%2F</url>
<content type="text"><![CDATA[操作系统的基本结构 操作系统中的jvm 为什么jvm的内存是分布在操作系统的堆中呢??因为操作系统的栈是操作系统管理的,它随时会被回收,所以如果jvm放在栈中,那java的一个null对象就很难确定会被谁回收了,那gc的存在就一点意义都没有了,而要对栈做到自动释放也是jvm需要考虑的,所以放在堆中就最合适不过了。 操作系统+jvm的内存简单布局 jvm的设计的模型其实就是操作系统的模型,基于操作系统的角度,jvm也就是一个应用(java.exe/javaw.exe),而基于class文件来说,jvm就是个操作系统,而jvm的方法区,也就相当于操作系统的硬盘区,所以方法区也被叫做permanent区,因为这个单词是永久的意思,也就是永久区。而java栈和操作系统栈是一致的,无论是生长方向还是管理的方式,至于堆,虽然概念上一致目标也一致,分配内存的方式也一直(new,或者malloc等等),但是由于他们的管理方式不同,jvm是gc回收,而操作系统是程序员手动释放,所以在算法上有很多的差异. 看下面的图。 将这个图和上面的图对比多了什么?没错,多了一个pc寄存器,所谓pc寄存器,无论是在虚拟机中还是在我们虚拟机所寄宿的操作系统中功能目的是一致的,计算机上的pc寄存器是计算机上的硬件,本来就是属于计算机,计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址,它甚至可以是操作系统指令的本地地址,当虚拟机正在执行的方法是一个本地方法的时候,jvm的pc寄存器存储的值是undefined,所以虚拟机的pc寄存器是用于存放下一条将要执行的指令的地址(字节码流)。 这个图是要告诉你,当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader如下图。 ————————————————版权声明:本文为CSDN博主「Kevinten10」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/wsh596823919/article/details/82669460 https://blog.csdn.net/yfqnihao/article/details/8289363]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[sizeOf]]></title>
<url>%2F2019%2F05%2F26%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2FsizeOf%2F</url>
<content type="text"><![CDATA[https://www.cnblogs.com/qiulinzhang/p/9570867.html]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[占用多少个字节数]]></title>
<url>%2F2019%2F05%2F26%2F%E9%9B%B6%E6%95%A3%E8%AE%B0%2F%E5%8D%A0%E7%94%A8%E5%A4%9A%E5%B0%91%E4%B8%AA%E5%AD%97%E8%8A%82%E6%95%B0%2F</url>
<content type="text"><![CDATA[不同的字符需要看不同的编码,char 字符占2个字节,默认unicode 中文 英文 unicode 2(还需要额外加多2个字节) 2(还需要额外加多2个字节) GBK 2 1 utf-8 3 1 utf-16 2(还需要额外加多2个字节) 2(还需要额外加多2个字节)]]></content>
<categories>
<category>零散记</category>
</categories>
<tags>
<tag>零散记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[集合类之MAP]]></title>
<url>%2F2019%2F05%2F26%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BMAP%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BMAP%2F</url>
<content type="text"><![CDATA[集合类之MAPmap架构 如上图:(1) Map 是映射接口,Map中存储的内容是键值对*(key-value)*。(2) AbstractMap 是继承于Map的抽象类,它实现了Map中的大部分API。其它Map的实现类可以通过继承AbstractMap来减少重复编码。(3) SortedMap 是继承于Map的接口。SortedMap中的内容是排序的键值对,排序的方法是通过比较器(Comparator)。(4) NavigableMap 是继承于SortedMap的接口。相比于SortedMap,NavigableMap有一系列的导航方法;如”获取大于/等于某对象的键值对”、“获取小于/等于某对象的键值对”等等。(5) TreeMap 继承于AbstractMap,且实现了NavigableMap接口;因此,TreeMap中的内容是“有序的键值对”!(6) HashMap 继承于AbstractMap,但没实现NavigableMap接口;因此,HashMap的内容是“键值对,但不保证次序”!(7) Hashtable 虽然不是继承于AbstractMap,但它继承于Dictionary(Dictionary也是键值对的接口),而且也实现Map接口;因此,Hashtable的内容也是“键值对,也不保证次序”。但和HashMap相比,Hashtable是线程安全的,而且它支持通过Enumeration去遍历。(8) WeakHashMap 继承于AbstractMap。它和HashMap的键类型不同,WeakHashMap的键是“弱键”。 HashMap1、HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 2、HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 3、HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。 4、HashMap中的映射不是有序的。 HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。 从图中可以看出:(01) HashMap继承于AbstractMap类,实现了Map接口。Map是”key-value键值对”接口,AbstractMap实现了”键值对”的通用函数接口。(02) HashMap是通过”拉链法”实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。 table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的”key-value键值对”都是存储在Entry数组中的。 size是HashMap的大小,它是HashMap保存的键值对的数量。 threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=”容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。 loadFactor就是加载因子。 modCount是用来实现fail-fast机制的。 1、存储结构 hashmap底层是以数组方式进行存储。将key-value对作为数组中的一个元素进行存储。 key-value都是Map.Entry中的属性。其中将key的值进行hash之后进行存储,即每一个key都是计算hash值,然后再存储。每一个Hash值对应一个数组下标,数组下标是根据hash值和数组长度计算得来。 由于不同的key有可能hash值相同,即该位置的数组中的元素出现两个,对于这种情况,hashmap采用链表形式进行存储。 下图描述了hashmap的存储结构图 Entry结构分析 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051static class Entry<K,V> implements Map.Entry<K,V> { final K key;// map中key值,可以为null。 V value; // map中的value值,可以为null。 Entry<K,V> next;// 链表引用,防止key值不同,hash值相同。 int hash; // 每个key的hash值 // 构造函数 Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } // 同一个key时,新值替换旧值,返回旧值 public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // key值重写equals方法 public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } // 重写hashCode值 public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } // 其他方法省略 } HashMap属性分析 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{ /** *默认情况下,hashmap大小为16.即1<<4就是1乘以2的4次幂=16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * hashMap的最大值 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 默认加载加载因子,即使用空间达到总空间的0.75时,需要扩容。 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 声明hashmap一个空数组。 */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * 最开始时,hashmap是一个空数组。 */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** * map的元素的个数 */ transient int size; /* * hashmap的实际存储空间大小。这个空间是总空间*加载因子得出的大小。 * 比如默认是16,加载因子是0.74。则threshold就是12。 */ int threshold; /** * 加载因子,即使用空间达到总空间的0.75时,需要扩容。 */ final float loadFactor; /** * */ transient int modCount; /** * threshold这个值的最大值就是Integer.MAX_VALUE */ static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; put方法 put(key,value)方法是hashmap中最重要的方法,使用hashmap最主要的就是使用put,get两个方法。可以从put方法的源码进行分析 1234567891011121314151617181920212223242526272829303132public V put(K key, V value) { // 首次存储元素,初始化存储空间 if (table == EMPTY_TABLE) { inflateTable(threshold); } // 如果key为null,则将null放入元素的第一个位置 if (key == null) return putForNullKey(value); // 计算key的hash值 int hash = hash(key); // 根据key的hash值,数组长度计算该Entry<key,value>的数组下标 int i = indexFor(hash, table.length); /** **如果当前key的已经存在于map中,则将新值替换成旧值。 **/ for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 判断同一个key,既要判断hash值相同,还要判断key是同一个key,因为 // 相同的key有可能hash值也相同。双重判断保证是同一个key。 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 如果是新的key需要存储,则增加操作次数modCount++ modCount++; // 将新增key-value键值对添加中map中。 addEntry(hash, key, value, i); return null; } addEntry方法 addEntry方法是将新增的key-value键值对存入到map中。该方法主要完成两个功能:1.1. 添加新元素前, 判断是否需要对map的数组进行扩容,如果需要扩容,则扩容空间大小是原来的两倍1.2. 对于新增key-value键值对,如果key的hash值相同,则构造单向列表。 从源码分析结果如下: 123456789101112131415/**** hash:key的hash值** key:存储的键** value:存储的value对象值*** bucketIndex:数组下标位置,即key-value在数组中的位置。**/void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//扩容两倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 往数组中添加新的key-value键值对 createEntry(hash, key, value, bucketIndex); } createEntry方法 该方法主要完成两个功能 1、添加新的key到Entry数组中 2、对于不同key的hash值相同的情况下,在同一个数组下标处,构建单向链表进行存储。 1234567void createEntry(int hash, K key, V value, int bucketIndex) { // 取出当前位置的元素,如果是新添加的key,则e为null,已经有的元素为不为空。 Entry<K,V> e = table[bucketIndex]; // 添加新的key-value值或构建链表 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } 遍历HashMap的键 根据keySet()获取HashMap的“键”的Set集合。 通过Iterator迭代器遍历“第一步”得到的集合 1234567891011// 假设map是HashMap对象// map中的key是String类型,value是Integer类型String key = null;Integer integ = null;Iterator iter = map.keySet().iterator();while (iter.hasNext()) { // 获取key key = (String)iter.next(); // 根据key,获取value integ = (Integer)map.get(key);} 遍历HashMap的值 根据value()获取HashMap的“值”的集合。 通过Iterator迭代器遍历“第一步”得到的集合。 12345678// 假设map是HashMap对象// map中的key是String类型,value是Integer类型Integer value = null;Collection c = map.values();Iterator iter= c.iterator();while (iter.hasNext()) { value = (Integer)iter.next();} TreeMap TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。 TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。 TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。 TreeMap 实现了Cloneable接口,意味着它能被克隆。 TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。 TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。 TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n)(jdk1.8之后加入红黑树由o(n)变为o( log(n) ) ) 。 TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的 从图中可以看出:(1) TreeMap实现继承于AbstractMap,并且实现了NavigableMap接口。(2) TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量: root, size, comparator。 root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。 红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。size是红黑数中节点的个数。 数据结构 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051static final class Entry<K,V> implements Map.Entry<K,V> { K key; //键 V value; //值 Entry<K,V> left = null; //左孩子节点 Entry<K,V> right = null; //右孩子节点 Entry<K,V> parent; //父节点 boolean color = BLACK; //节点的颜色,在红黑树种,只有两种颜色,红色和黑色 //构造方法,用指定的key,value ,parent初始化,color默认为黑色 Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } //返回key public K getKey() { return key; } //返回该节点对应的value public V getValue() { return value; } //替换节点的值,并返回旧值 public V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } //重写equals()方法 public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>)o; //两个节点的key相等,value相等,这两个节点才相等 return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); } //重写hashCode()方法 public int hashCode() { int keyHash = (key==null ? 0 : key.hashCode()); int valueHash = (value==null ? 0 : value.hashCode()); //key和vale hash值得异或运算,相同则为零,不同则为1 return keyHash ^ valueHash; } //重写toString()方法 public String toString() { return key + "=" + value; }} 构造方法 1234567891011121314151617181920212223242526//构造方法,comparator用键的顺序做比较public TreeMap() { comparator = null;}//构造方法,提供比较器,用指定比较器排序public TreeMap(Comparator<? super K> comparator) { his.comparator = comparator;}//将m中的元素转化daoTreeMap中,按照键的顺序做比较排序public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m);}//构造方法,指定的参数为SortedMap//采用m的比较器排序public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException cannotHappen) { } catch (ClassNotFoundException cannotHappen) { }} TreeMap提供了四个构造方法,实现了方法的重载。无参构造方法中比较器的值为null,采用自然排序的方法,如果指定了比较器则称之为定制排序. 自然排序:TreeMap的所有key必须实现Comparable接口,所有的key都是同一个类的对象 定制排序:创建TreeMap对象传入了一个Comparator对象,该对象负责对TreeMap中所有的key进行排序,采用定制排序不要求Map的key实现Comparable接口。 Put()方法 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108public V put(K key, V value) { Entry<K,V> t = root; //红黑树的根节点 if (t == null) { //红黑树是否为空 compare(key, key); // type (and possibly null) check //构造根节点,因为根节点没有父节点,传入null值。 root = new Entry<>(key, value, null); size = 1; //size值加1 modCount++; //改变修改的次数 return null; //返回null } int cmp; Entry<K,V> parent; //定义节点 Comparator<? super K> cpr = comparator; //获取比较器 if (cpr != null) { //如果定义了比较器,采用自定义比较器进行比较 do { parent = t; //将红黑树根节点赋值给parent cmp = cpr.compare(key, t.key); //比较key, 与根节点的大小 if (cmp < 0) //如果key < t.key , 指向左子树 t = t.left; //t = t.left , t == 它的做孩子节点 else if (cmp > 0) t = t.right; //如果key > t.key , 指向它的右孩子节点 else return t.setValue(value); //如果它们相等,替换key的值 } while (t != null); //循环遍历 } else { //自然排序方式,没有指定比较器 if (key == null) throw new NullPointerException(); //抛出异常 Comparable<? super K> k = (Comparable<? super K>) key; //类型转换 do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) // key < t.key t = t.left; //左孩子 else if (cmp > 0) // key > t.key t = t.right; //右孩子 else return t.setValue(value); //t == t.key , 替换value值 } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); //创建新节点,并制定父节点 //根据比较结果,决定新节点为父节点的左孩子或者右孩子 if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); //新插入节点后重新调整红黑树 size++; modCount++; return null;}//比较方法,如果comparator==null ,采用comparable.compartTo进行比较,否则采用指定比较器比较大小final int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2);}private void fixAfterInsertion(Entry<K,V> x) { //插入的节点默认的颜色为红色 x.color = RED; // //情形1: 新节点x 是树的根节点,没有父节点不需要任何操作 //情形2: 新节点x 的父节点颜色是黑色的,也不需要任何操作 while (x != null && x != root && x.parent.color == RED) { //情形3:新节点x的父节点颜色是红色的 //判断x的节点的父节点位置,是否属于左孩子 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //获取x节点的父节点的兄弟节点,上面语句已经判断出x节点的父节点为左孩子,所以直接取右孩子 Entry<K,V> y = rightOf(parentOf(parentOf(x))); //判断是否x节点的父节点的兄弟节点为红色。 if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); // x节点的父节点设置为黑色 setColor(y, BLACK); // y节点的颜色设置为黑色 setColor(parentOf(parentOf(x)), RED); // x.parent.parent设置为红色 x = parentOf(parentOf(x)); // x == x.parent.parent ,进行遍历。 } else { //x的父节点的兄弟节点是黑色或者缺少的 if (x == rightOf(parentOf(x))) { //判断x节点是否为父节点的右孩子 x = parentOf(x); //x == 父节点 rotateLeft(x); //左旋转操作 } //x节点是其父的左孩子 setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); //上面两句将x.parent 和x.parent.parent的颜色做调换 rotateRight(parentOf(parentOf(x))); //进行右旋转 } } else { Entry<K,V> y = leftOf(parentOf(parentOf(x))); //y 是x 节点的祖父节点的左孩子 if (colorOf(y) == RED) { //判断颜色 setColor(parentOf(x), BLACK); //父节点设置为黑色 setColor(y, BLACK); //父节点的兄弟节点设置为黑色 setColor(parentOf(parentOf(x)), RED); //祖父节点设置为红色 x = parentOf(parentOf(x)); //将祖父节点作为新插入的节点,遍历调整 } else { if (x == leftOf(parentOf(x))) { //x 是其父亲的左孩子 x = parentOf(x); rotateRight(x); //以父节点为旋转点,进行右旋操作 } setColor(parentOf(x), BLACK); //父节点为设置为黑色 setColor(parentOf(parentOf(x)), RED); //祖父节点设置为红色 rotateLeft(parentOf(parentOf(x))); //以父节点为旋转点,进行左旋操作 } } } root.color = BLACK; //通过节点位置的调整,最终将红色的节点条调换到了根节点的位置,根节点重新设置为黑色} 增加删除节点都运用了红黑树的原理,红黑树有五个特点: 每个节点只能是红色或者黑色 根节点永远是黑色的 所有的叶子的子节点都是空节点,并且都是黑色的 每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点) 从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同) 红黑树插入新节点的三个关键地方:1、插入新节点总是红色节点。2、插入节点的父节点是黑色,能维持性质。3、如果插入节点的父节点是红色,破坏了性质。故插入算法就是通过重新着色或旋转,来维持性质 deleteEntry()方法 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364private void deleteEntry(Entry<K,V> p) { modCount++; //修改次数 +1 size--; //元素个数 -1 /* * 被删除节点的左子树和右子树都不为空,那么就用 p节点的中序后继节点代替 p 节点 * successor(P)方法为寻找P的替代节点。规则是右分支最左边,或者 左分支最右边的节点 * ---------------------(1) */ if (p.left != null && p.right != null) { Entry<K,V> s = successor(p); p.key = s.key; p.value = s.value; p = s; } //replacement为替代节点,如果P的左子树存在那么就用左子树替代,否则用右子树替代 Entry<K,V> replacement = (p.left != null ? p.left : p.right); /* * 删除节点,分为上面提到的三种情况 * -----------------------(2) */ //如果替代节点不为空 if (replacement != null) { replacement.parent = p.parent; /* *replacement来替代P节点 */ //若P没有父节点,则跟节点直接变成replacement if (p.parent == null) root = replacement; //如果P为左节点,则用replacement来替代为左节点 else if (p == p.parent.left) p.parent.left = replacement; //如果P为右节点,则用replacement来替代为右节点 else p.parent.right = replacement; //同时将P节点从这棵树中剔除掉 p.left = p.right = p.parent = null; /* * 若P为红色直接删除,红黑树保持平衡 * 但是若P为黑色,则需要调整红黑树使其保持平衡 */ if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { //p没有父节点,表示为P根节点,直接删除即可 root = null; } else { //P节点不存在子节点,直接删除即可 if (p.color == BLACK) //如果P节点的颜色为黑色,对红黑树进行调整 fixAfterDeletion(p); //删除P节点 if (p.parent != null) { if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } } TreeMap还有很多地方没有写全,后续再来补学。 TreeMap问题集锦 1、TreeMap的键、值能否为null value是可以为null的 当未实现 Comparator 接口时,key 不可以为null,否则抛 NullPointerException 异常; 当实现 Comparator 接口时,若未对 null 情况进行判断,则可能抛 NullPointerException 异常。如果针对null情况实现了,可以存入,但是却不能正常使用get()访问,只能通过遍历去访问。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748测试Value能否为null public static void main(String[] args) { TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("1",1); treeMap.put("2",null); System.out.println(treeMap.get("2")); } 结果:null 测试key能否为null public static void main(String[] args) { TreeMap<String, Integer> treeMap = new TreeMap<>(); treeMap.put("1",1); treeMap.put(null,null); System.out.println(treeMap.get("2")); } 结果:Exception in thread "main" java.lang.NullPointerException at java.util.TreeMap.put(TreeMap.java:563) at com.crazy_june.test_treemap.main(test_treemap.java:9) 测试当自己实现一个comparator接口时 public static void main(String[] args) { TreeMap<String, Integer> treeMap = new TreeMap<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { if(o1==null){ return 1; }else { return o2.charAt(0)-o1.charAt(0); } } }); treeMap.put("1",1); treeMap.put(null,12); treeMap.put("2",2); System.out.println(treeMap.get(null)); } 结果:null 证明不能通过get()取出来 测试通过遍历entry可以取出来不 for(Map.Entry<String,Integer> entry:treeMap.entrySet()){ System.out.println(entry.getKey()+":"+entry.getValue()); } 结果: 2:2 1:1 null:12]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JDBC知识总结]]></title>
<url>%2F2019%2F05%2F26%2FJavaEE%2FJDBC%2FJDBC%E7%9F%A5%E8%AF%86%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[JDBC知识总结JDBC的接口和类JDBC API主要位于java.sql包中,关键的接口和类包括以下几种。 类/接口 描述 Driver接口、DriverManager类 前者表示驱动器、后者表示驱动管理器 Connection接口 表示数据库的链接 Statement接口 负责执行SQL语句 PreparedStatement接口 负责执行预备的SQL语句 CallableStatement接口 负责执行SQL存储过程 ResultSet接口 表示SQL查询语句返回的结果集 JDBC步骤1、注册驱动 2、获取与数据库的链接 3、创建代表SQL语句的对象 4、执行SQL语句 5、如果是查询语句,需要遍历结果集 6、释放占用的资源 DriverManager接口和DriverManager类DriverManager类用来建立和数据库的连接及管理JDBC驱动器。DriverManager类的方法都是静态的,主要包括以下几种 类/接口 描述 registerDriver(Driver driver) 在DriverManeger中注册JDBC驱动器 getConnection(String url,String user,String password) 建立和数据的连接 setLoginTime(int seconds) 设定等待建立数据连接的超时时间 setlogWriter(PrintWriter out) 设定输出JDBC日志的PrintWriter对象 1234567891011//注册数据库的驱动Class.forName("com.MySQL.jdbc.Driver"); //创建数据库的链接信息(指定要连接那个数据库):数据库路径、数据库的账号和密码String url = "jdbc:mysql://localhost:3306/dataBase_Name";//jdbc:数据库://ip地址:端口号;数据库名 String username = "root";String password = "123456"; //连接数据库,返回连接结果,该结果的类型是ConnectionConnection conn = DriverManager.getConnection(url,username,password);//获得连接 注意,上面代码可能出现的两种异常: 1、ClassNotFoundException:这个异常是在加载数据库驱动的时候,出现这个异常有两个可能: a、检查是否导入了Mysql的jar包 b、将数据库的驱动名打错,检查是否是com.MySQL.jdbc.Driver 2、SQLException:这个异常出现在连接数据库的过程,出现这个异常就是三个参数的问题。 Class.forName(“com.mysql.jdbc.Driver”)和DriverManager.registerDriver(new com.mysql.jdbc.Driver())的区别 Class.forName(“com.mysql.jdbc.Driver”)的源码 123456789static { try { DriverManager.registerDriver(new Driver());//静态代码块,加载即初始化 } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } DriverManager.registerDriver(new com.mysql.jdbc.Driver()) Driver类的静态代码块会注册一次,那么此时new Driver的时候就会注册一次,然后外层又会注册一次,所以注册了两次驱动(加载一次、初始化一次) connection接口Connection接口代表Java程序和数据库的连接,主要包括以下方法。 类/接口 描述 getMetaData() 返回表示数据库的元数据的DatabaseMetaData对象,元数据包含了描述数据库的相关信息 createStatement() 创建并返回Statement对象 prepareStatement() 创建并返回prepareStatement对象 Connection最为重要的方法就是获取Statement或者prepareStatement对象 1234567//Statement的用法Statement st = conn.createStatement(sql);ResultSet rs = stmt.executeQuery(); //prepareStatement的用法PreparedStatement ps = conn.prepareStatement(sql);ResultSet rs = ps.executeQuery(); Statement接口Statement接口提供了3个执行SQL语句的方法 类/接口 描述 execute(String sql) 执行各种SQL语句,该方法返回一个boolean类型的值。该方法返回的是boolean类型,表示SQL语句是否有结果集。 如果执行的是更新语句,那么还要调用int getUpdateCount()来获取insert、update、delete语句所影响的行数。 如果执行的是查询语句,那么还要调用ResultSet对象的getResultSet()来获取select语句的查询结果。 | | executeUpdate(String sql) | 执行SQL的insert、update和delet**等语句,适用于不需要返回结果的SQL语句。该方法返回一个int类型的值,表示数据库中受该SQL语句影响的记录的数目。 || executeQuery(String sql) | 执行SQL的select语句。查询操作会返回ResultSet对象,即结果集。 | PrepareStatement接口 PrepareStatement接口继承了Statement接口,用来执行准备的SQL语句。在访问数据库时,可能会遇到某条SQL语句被多次执行,但是其中的参数却不同的情况。 使用PrepareStatement,而不是Statement来执行SQL语句,这样做具有以下优点: 简化程序代码,是程序更加灵活。 123456//创建SQL语句String sql = "Select * From users Where name = ? And sex = ? And age = ?";//设置参数值ps.setString(1,"LaoYe"); //此处的1表示name中的?,而LaoYe表示name的值,下面同理ps.isBoolean(2,true);ps.setInt(3,18); 提高访问数据库的性能。PrepareStatement执行预准备的SQL语句,数据库只需对这种SQL语句编译一次,然后就可以多次执行。而每次用Statement执行SQL语句时,数据库都需要对该SQL语句进行编译。 123456789101112131415161718192021//1、创建SQL语句String sql = "Insert Into users(name,sex,age) Values(?,?,?)";//2、连接数据库,获取连接对象Connection conn = DriverManager.getConnection(url,account,password);//3、预准备SQL语句PrepareStatement ps = conn.prepareStatement(sql); //第一次插入ps.setString(1,"LaoYe"); //此处的1表示name中的?,而LaoYe表示name的值,下面同理ps.isBoolean(2,true);ps.setInt(3,18);ps.executeUpdate(); 第二次插入ps.setString(1,"LaoCheng"); //此处的1表示name中的?,而LaoYe表示name的值,下面同理ps.isBoolean(2,false);ps.setInt(3,18);ps.executeUpdate(); //结论:可以看出除了值,几乎是重复的,所以视情况可以循环插入,提高开发效率 作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。同时,三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数。 preparestatement预防SQL注入的原因: 因为sql语句是预编译的,而且语句中使用了占位符,规定了sql语句的结构。用户可以设置”?”的值,但是不能改变sql语句的结构,因此想在sql语句后面加上如“or 1=1”实现sql注入是行不通的。 ResultSet接口结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名或得数据。 循环输出数据库信息: while(rs.next()){ System.out.println(rs.getString(1)); System.out.println(rs.getString(2)); System.out.println(rs.getString(3)); System.out.println(rs.getString(4));}]]></content>
<categories>
<category>JavaEE</category>
</categories>
<tags>
<tag>JavaEE</tag>
</tags>
</entry>
<entry>
<title><![CDATA[集合类之List]]></title>
<url>%2F2019%2F05%2F26%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BList%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BList%2F</url>
<content type="text"><![CDATA[集合类之ListList接口扩展自Collection,定义一个允许重复的有序集合,从List接口中的方法来看,List接口主要是增加了面向位置的操作,允许在指定位置上操作元素,同时增加了一个能够双向遍历线性表的新列表迭代器ListIterator。List接口的两个重要的具体实现类,也是我们可能最常用的类,ArrayList和LinkedList。 1.ArrayList它是用数组存储元素的,这个数组可以动态创建,如果元素个数超过了数组的容量,那么就创建一个更大的新数组(通过移位运算符>>1扩大1倍再加上自己原本的容量即扩充1.5倍),并将当前数组中的所有元素都复制到新数组中。假设第一次是集合没有任何元素,下面以插入一个元素为例看看源码的实现。 123456789101112131415161718192021222324252627282930313233343536371、找到add()实现方法。 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 2、此方法主要是确定将要创建的数组大小。 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//取出两个较大的容量 } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++;//记录了结构性改变的次数。结构性改变指的是那些修改了列表大小的操作,在迭代过程中可能会造成错误的结果。 if (minCapacity - elementData.length > 0) grow(minCapacity); } 3、最后是创建数组,可以明显的看到先是确定了添加元素后的大小之后将元素复制到新数组中。 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍扩容 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } ArrayList遍历三种方法 1、通过迭代器Iterator() 12345Iterator iter = list.iterator();while (iter.hasNext()){ System.out.println(iter.next());} 2、随机访问,通过索引值去遍历。 12345int size = list.size();for (int i=0; i<size; i++) { System.out.println(list.get(i)); } 3、for循环遍历 1234for(String str:list){System.out.println(str); } ArrayList常见问题 1、ArrayList如何实现自动增加 当试图在arraylist中增加一个对象的时候,Java会去检查arraylist,以确保已存在的数组中有足够的容量来存储这个新的对象。如果没有足够容量的话,那么就会新建一个长度更长的数组,旧的数组就会使用Arrays.copyOf方法被复制到新的数组中去,现有的数组引用指向了新的数组。 2、当传递ArrayList到某个方法中,或者某个方法返回ArrayList,什么时候要考虑安全隐患?如何修复安全违规这个问题呢? 当array被当做参数传递到某个方法中,如果array在没有被复制的情况下直接被分配给了成员变量,那么就可能发生这种情况,即当原始的数组被调用的方法改变的时候,传递到这个方法中的数组也会改变。 3、什么情况下你会使用ArrayList?什么时候你会选择LinkedList? 多数情况下,当你遇到访问元素比插入或者是删除元素更加频繁的时候,你应该使用ArrayList。另外一方面,当你在某个特别的索引中,插入或者是删除元素更加频繁,或者你根本就不需要访问元素的时候,你会选择LinkedList。这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是”1″,而在LinkedList中可能就是”n”了。在ArrayList中增加或者删除某个元素,通常会调用System.arraycopy方法,这是一种极为消耗资源的操作,因此,在频繁的插入或者是删除元素的情况下,LinkedList的性能会更加好一点。 System.arraycopy方法消耗资源原因: 123456789101112131415public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)代码解释: Object src : 原数组 int srcPos : 从元数据的起始位置开始 Object dest : 目标数组 int destPos : 目标数组的开始起始位置 int length : 要copy的数组的长度我们使用System.arraycopy进行转换(copy)System.arrayCopy(srcBytes,0,destBytes ,0,5)上面这段代码就是 : 创建一个一维空数组,数组的总长度为 12位,然后将srcBytes源数组中 从0位 到 第5位之间的数值 copy 到 destBytes目标数组中,在目标数组的第0位开始放置.那么这行代码的运行效果应该是 2,4,0,0,0,增加或者删除都要进行一次copy,消耗资源多。 4、如何复制某个ArrayList到另一个ArrayList中去?写出你的代码? 下面就是把某个ArrayList复制到另一个ArrayList中去的几种技术: 使用clone()方法,比如ArrayList newArray = oldArray.clone(); 使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject); 使用Collection的copy方法。 注意1和2是浅拷贝(shallow copy)。 浅拷贝和深拷贝区别: | 浅拷贝:只复制引用,另一处修改,你当下的对象也会修改。 | 深拷贝:引用对象的值等信息,复制一份一样的。 浅拷贝—能复制变量,如果对象内还有对象,则只能复制对象的地址(指针指向同一个内存空间) 深拷贝—能复制变量,也能复制当前对象的内部对象 利用序列化实现深拷贝 把对象写到流里的过程是序列化过程(Serialization),而把对象从流中读出来的过程则叫做反序列化过程(Deserialization)。 在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。 2.LinkedList 1、继承了AbstractSequentialList抽象类:在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用我们的迭代器。(因为LinkedList底层是通过一个双向链表来实现的)(虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每一的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。 至于上面的说从链表的头部后尾部进行遍历:官方源码对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向)(所以这里遍历LinkedList推荐使用迭代器)。 2、实现了List接口。(提供List接口中所有方法的实现)实现了Cloneable接口,它支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。 3、实现了Deque接口。实现了Deque所有的可选的操作。 4、实现了Serializable接口。表明它支持序列化。(和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,底层只序列化节点的个数和节点的值) 底层重要方法分析: addAll(int index, Collection) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071// 首先调用一下空的构造器。//然后调用addAll(c)方法。 public LinkedList(Collection<? extends E> c) { this(); addAll(c); }//通过调用addAll(int index, Collection<? extends E> c) 完成集合的添加。 public boolean addAll(Collection<? extends E> c) { return addAll(size, c); }//几乎所有的涉及到在指定位置添加或者删除或修改操作都需要判断传进来的参数是否合法。// checkPositionIndex(index)方法就起这个作用。 public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index);//先把集合转化为数组,然后为该数组添加一个新的引用(Objext[] a)。 Object[] a = c.toArray();//新建一个变量存储数组的长度。 int numNew = a.length;//如果待添加的集合为空,直接返回,无需进行后面的步骤。后面都是用来把集合中的元素添加到//LinkedList中。 if (numNew == 0) return false;//Node<E> succ:指代待添加节点的位置。//Node<E> pred:指代待添加节点的前一个节点。//下面的代码是依据新添加的元素的位置分为两个分支://①新添加的元素的位置位于LinkedList最后一个元素的后面。//新添加的元素的位置位于LinkedList中。//如果index==size;说明此时需要添加LinkedList中的集合中的每一个元素都是在LinkedList//最后面。所以把succ设置为空,pred指向尾节点。//否则的话succ指向插入待插入位置的节点。这里用到了node(int index)方法,这个方法//后面会详细分析,这里只需要知道该方法返回对应索引位置上的Node(节点)。pred指向succ节点的前一个节点。 Node<E> pred, succ; if (index == size) { succ = null; pred = last; } else { succ = node(index); pred = succ.prev; }//接着遍历数组中的每个元素。在每次遍历的时候,都新建一个节点,该节点的值存储数组a中遍历//的值,该节点的prev用来存储pred节点,next设置为空。接着判断一下该节点的前一个节点是否为//空,如果为空的话,则把当前节点设置为头节点。否则的话就把当前节点的前一个节点的next值//设置为当前节点。最后把pred指向当前节点,以便后续新节点的添加。 for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; Node<E> newNode = new Node<>(pred, e, null); if (pred == null) first = newNode; else pred.next = newNode; pred = newNode; }//这里仍然和上面一样,分两种情况对待://①当succ==null(也就是新添加的节点位于LinkedList集合的最后一个元素的后面),//通过遍历上面的a的所有元素,此时pred指向的是LinkedList中的最后一个元素,所以把//last指向pred指向的节点。//当不为空的时候,表明在LinkedList集合中添加的元素,需要把pred的next指向succ上,//succ的prev指向pred。//最后把集合的大小设置为新的大小。//modCount(修改的次数)自增。 if (succ == null) { last = pred; } else { pred.next = succ; succ.prev = pred; } size += numNew; modCount++; return true; } 虽然是增加一个集合的元素,但是modCount只增加了一次 将LinkedList写入到流中。(也就是把LinkedList状态保存到流中)(序列化) 123456789101112private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // Write out any hidden serialization magic s.defaultWriteObject(); // Write out size s.writeInt(size); // Write out all elements in the proper order. for (Node<E> x = first; x != null; x = x.next) s.writeObject(x.item);} 从流中把LinkedList读取出来(读取流,拼装成LinkedList)(反序列化) 12345678910111213@SuppressWarnings("unchecked")private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // Read in any hidden serialization magic s.defaultReadObject(); // Read in size int size = s.readInt(); // Read in all elements in the proper order. for (int i = 0; i < size; i++) linkLast((E)s.readObject());} LinkedList提供了两种迭代器,一种是返回Iterator,另一种返回ListIterator。 ①返回ListIterator迭代器: 1234public ListIterator<E> listIterator(int index) { checkPositionIndex(index); return new ListItr(index);} ②返回Iterator迭代器: 123public Iterator<E> descendingIterator() { return new DescendingIterator();}]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[集合类之SET]]></title>
<url>%2F2019%2F05%2F26%2Fjava%E9%9B%86%E5%90%88%E7%B1%BB%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BSET%2F%E9%9B%86%E5%90%88%E7%B1%BB%E4%B9%8BSET%2F</url>
<content type="text"><![CDATA[集合类之SETset集合可以存储多个对象,但并不会记住元素的存储顺序,也不允许集合中有重复元素(不同的set集合有不同的判断方法)。 1.HashSetHashSet按照Hash算法存储集合中的元素,具有很好的存取和查找性能。当向HashSet中添加一些元素时,HashSet会根据该对象的HashCode()方法来得到该对象的HashCode值,然后根据这些HashCode的值来决定元素的位置。(HashSet的底层原理是HashMap) HashSet的特点:1.存储顺序和添加的顺序不同 2.HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或更多的线程修改了 集合中的值,则必须通过代码使线程同步。 3.HastSet允许集合中的元素为null。 4.非线程安全 在Hashset集合中,判断两个元素相同的标准是:两个对象通过equals()方法相等,且HashCode()方法的返回值也相等。如果有两个元素通过equals()方法比较相等,而HashCode()的返回值不同,HashSet会将这两个对象保存在不同的地方。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; // 底层使用HashMap来保存HashSet中所有元素。 private transient HashMap<E,Object> map; // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。 private static final Object PRESENT = new Object(); /** * 默认的无参构造器,构造一个空的HashSet。 * * 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 */ public HashSet() { map = new HashMap<E,Object>(); } /** * 构造一个包含指定collection中的元素的新set。 * * 实际底层使用默认的加载因子0.75和足以包含指定 * collection中所有元素的初始容量来创建一个HashMap。 * @param c 其中的元素将存放在此set中的collection。 */ public HashSet(Collection<? extends E> c) { map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } /** * 以指定的initialCapacity和loadFactor构造一个空的HashSet。 * * 实际底层以相应的参数构造一个空的HashMap。 * @param initialCapacity 初始容量。 * @param loadFactor 加载因子。 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E,Object>(initialCapacity, loadFactor); } /** * 以指定的initialCapacity构造一个空的HashSet。 * * 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。 * @param initialCapacity 初始容量。 */ public HashSet(int initialCapacity) { map = new HashMap<E,Object>(initialCapacity); } /** * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。 * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。 * * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。 * @param initialCapacity 初始容量。 * @param loadFactor 加载因子。 * @param dummy 标记。 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor); } /** * 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 * * 底层实际调用底层HashMap的keySet来返回所有的key。 * 可见HashSet中的元素,只是存放在了底层HashMap的key上, * value使用一个static final的Object对象标识。 * @return 对此set中元素进行迭代的Iterator。 */ public Iterator<E> iterator() { return map.keySet().iterator(); } /** * 返回此set中的元素的数量(set的容量)。 * * 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。 * @return 此set中的元素的数量(set的容量)。 */ public int size() { return map.size(); } /** * 如果此set不包含任何元素,则返回true。 * * 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。 * @return 如果此set不包含任何元素,则返回true。 */ public boolean isEmpty() { return map.isEmpty(); } /** * 如果此set包含指定元素,则返回true。 * 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e)) * 的e元素时,返回true。 * * 底层实际调用HashMap的containsKey判断是否包含指定key。 * @param o 在此set中的存在已得到测试的元素。 * @return 如果此set包含指定元素,则返回true。 */ public boolean contains(Object o) { return map.containsKey(o); } /** * 如果此set中尚未包含指定元素,则添加指定元素。 * 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2)) * 的元素e2,则向此set 添加指定的元素e。 * 如果此set已包含该元素,则该调用不更改set并返回false。 * * 底层实际将将该元素作为key放入HashMap。 * 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key * 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true), * 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变, * 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中, * 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。 * @param e 将添加到此set中的元素。 * @return 如果此set尚未包含指定元素,则返回true。 */ public boolean add(E e) { return map.put(e, PRESENT)==null; } /** * 如果指定元素存在于此set中,则将其移除。 * 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e, * 则将其移除。如果此set已包含该元素,则返回true * (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。 * * 底层实际调用HashMap的remove方法删除指定Entry。 * @param o 如果存在于此set中则需要将其移除的对象。 * @return 如果set包含指定元素,则返回true。 */ public boolean remove(Object o) { return map.remove(o)==PRESENT; } /** * 从此set中移除所有元素。此调用返回后,该set将为空。 * * 底层实际调用HashMap的clear方法清空Entry中所有元素。 */ public void clear() { map.clear(); } /** * 返回此HashSet实例的浅表副本:并没有复制这些元素本身。 * * 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。 */ public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } } } HashSet所有方法都直接在HashMap上运用,了解HashMap自然可以了解HashSet。 其中需要了解一下HashSet的加载因子和容量: 在HashSet中我们new对象的时候会创建一个初始默认容量是16的HashSet集合;其中默认的一个值loadFactor: 加载因子:0.75 加载因子是数组的长度的百分比;16*0.75 = 12; 意思就是数组中的桶数达到12个时数组就要扩容;(复制),扩容到原来的2倍; 0.75是一个折中的数据;是增删改查的最优速度; new的时候可以直接初始化数组长度和loadFactor(加载因子)来改变加载因子; 注意: 对于HashSet中保存的对象,主要要正确重写equals方法和hashCode方法,以保证放入Set对象的唯一性 虽说是Set是对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了 HashSet没有提供get()方法,愿意是同HashMap一样,Set内部是无序的,只能通过迭代的方式获得 2.LinkedHashSetLinkedHashSet是继承自HashSet,底层实现是LinkedHashMap。并且其初始化时直接super(......) 查看了LinkedHashMap的构造方法后,发现其因为继承自HashMap,所以其底层实现也是HashMap!!!,然后发现了LinkedHashMap调用父类构造方法初始化时,还顺便设置了变量accessOrder = false,看上面得源码可以知道,这是给了迭代器一个参数,false代表迭代时使用插入得顺序 3.TreeSet1、TreeSet(树集)是一个有序集合,可以按照任何顺序将元素插入该集合,当对该集合进行迭代时,各个值将自动以排序后的顺序出现。TreeSet中的元素按照升序排列,缺省是按照自然顺序进行排序,意味着TreeSet中的元素要实现Comparable接口,或者有一个自定义的比较器Comparator。 2、TreeSet底层使用的是TreeMap,TreeMap的底层实现是红黑树 1234public TreeSet(){ this(new TreeMap<E,Object>());} 注意: 1、TreeSet的排列顺序必须是全局顺序,也就是说任何两个元素都是必须可比的,同时只有当他们比较相同时才返回0。 2、如果树集包含了n个元素,那么平均需要进行log2n次比较,才能找到新元素的正确位置。]]></content>
<categories>
<category>集合框架</category>
</categories>
<tags>
<tag>集合框架</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2FRedis%2FRedis%2F</url>
<content type="text"><![CDATA[RedisNoSQLNoSQL = not only SQL 非关系型数据库 为什么需要NoSQL High Performance - 高并发读写 Huge Storage - 海量数据的高效率存储和访问 HIgh Scalability&&High Availability - 高可扩展性和高可用性 NoSQL数据库的四大分类 键值对(key-value)存储 列存储 文档数据库 图形数据库 NoSQL的特点: 易扩展 灵活的数据模型 大数据量 高可用 Redis支持的键值数据类型: 字符串类型 散列类型 列表类型 集合类型 有序集合类型 读每秒11万次,写每秒8万次 Redis应用场景: 缓存 任务队列 应用排行榜 网站访问统计 数据过期处理 分布式集群架构中的session分离 JedisJedis是Redis官方首选的java客户端开发包 Redis的数据结构 字符串(String) 哈希(hash) 字符串列表(list) 字符串集合(set) 有序字符串集合(sorted set) Redis持久化需要持久化的原因:Redis将数据存在内存中,容易丢失数据,需要将数据存进硬盘,称为持久化 两种持久化的方式 RDB方式 AOF方式 持久化的四种方式: RDB方式:默认支持、不需要进行配置,在指定的时间间隔内,将内存中的数据集快照写入硬盘 AOF方式:以日记的方式记录服务器处理的每一个操作,当重启时,会读取文件进行重建数据库,保证启动时数据库中的数据的完整 无持久化:通过日志禁止数据库持久化的功能 同时使用RDB方式和AOF方式]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库之事务和锁机制]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E4%BA%8B%E5%8A%A1%E5%92%8C%E9%94%81%E6%9C%BA%E5%88%B6%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E4%BA%8B%E5%8A%A1%E5%92%8C%E9%94%81%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[数据库之事务和锁机制 事务四大特性事务的概念:事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元,要么全部执行,要么全部不执行。 1、原子性(Atomicity) 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,如果操作失败则不能对数据库有任何影响,任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。 2、一致性(Consistency) 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。 3、隔离性(lsolation) 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。 4、持久性(durability) 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。 事务并发引起的问题1、脏读(dirty read) 当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。 例如:用户A向用户B转账100元 1234update account set money=money+100 where name=’B’; (此时A通知B)update account set money=money - 100 where name=’A’;以上两条sql语句为转账事务 转账是一个事务,通知查看是一个事务。 当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读) 而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚 那么当B以后再次查看账户时就会发现钱其实并没有转。 脏读就是一个事务读取了另一个事务未提交的脏数据 2、不可重复读(unrepeatable reading) 不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。 例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。 不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。 在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就产生矛盾。 3、幻读(Phantom read) 幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。 幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。 事务的四种隔离级别 Serializable (串行化):可避免脏读、不可重复读、幻读的发生。 Repeatable read (可重复读):可避免脏读、不可重复读的发生。 Read committed (读已提交):可避免脏读的发生。 Read uncommitted (读未提交):最低级别,任何情况都无法保证。 以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别。级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。 隔离级别的设置只对当前链接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效。 对于JDBC操作数据库来说,一个Connection对象相当于一个链接,而对于Connection对象设置的隔离级别只对该Connection对象有效,与其他链接Connection对象无关。 ###]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库之基础知识]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%2F</url>
<content type="text"><![CDATA[数据库之基础知识命令行操作MySQLDOS命令 盘符+冒号 切换逻辑盘 如:d:dir 显示目录和文件列表cd 目录名 进入目录cd ../ 进入上一级目录cd ./ 进入当前目录cd / 进入根目录exit 退出命令行 连接数据库 语法:mysql -h主机名 -u用户名 -p密码如:mysql -hlocalhost -uroot -proot注意:当提示符变成 mysql> 说明我们已经进入mysql命令行模式,只能使用sql指令sql指令都需要以分号 ; 结束quit 退出mysql exit 退出命令行 数据库操作创建数据库 1create database 数据库名 显示数据库 12345678show database;显示服务器上的所有的数据库列表破show database like '%a%'; like 子命令,显示所有名字里带a的数据库名show databases like '___'; like 子命令,显示数据库名是三个字符的所有数据库名% 匹配0个或者多个任意字符_ 下划线,匹配任意一个字符show create database 数据库名; 查看建立数据库的语句 修改数据库 1alter database 数据库名 charset=gbk; 修改指定的数据库的字符集,只能修改数据的字符集,数据库名不能修改。 删除数据库 1drop database 数据库名; 删除指定的数据库 MySQL里面的三个数据库mysql、information_schema、performance_schema是系统默认自带的,不可删除,删了需重装MySQL 数据库操作创建数据表 1create table 表名(id int,username varchar(30),password varchar(30)); 查看表 123show tables; 查看所有的数据表show create table 表名; 查看数据表的建表语句desc 表名; 查看数据表的结构 删除表 1drop table 表名; 查看数据表的结构 修改表 123alter table 表名 engine=innodb; 可以修改默认引擎alter table 表名 charset=gbk; 可以修改字符集rename table 表名 to 另一个表名; 可以修改表名 字段操作增加新字段 12alter table student add column gender varchar(2);在student数据表中新增gender字段 修改字段的类型和属性 123456alter table student modify column gender varchar(10);修改student数据表中gender字段数据类型alter table student modify column gender varchar(10) after 另一个字段;修改student数据表中gender字段数据类型,并且在数据表中排在 “另一个字段” 后面alter table student modify column gender varchar(10) first;修改student数据表中gender字段数据类型,并且在数据表中排在第一位 修改字段的名字和定义 12alter table student change column gender sex varchar(2);把student数据表中gender字段名字改为sex,数据类型改为varchar(2); 删除字段 12alter table student drop column gender;删除student数据表中的gender字段; 记录操作新增记录 12345678insert into 表名(字段列表) values (值的列表);例子:insert into student(username) values('中文');insert into student(id,username,age) values(3,'中文',20);批量新增:insert into student(id,username,age) values (3,'张三',18),(4,'李四',20),(5,'刘五',22);值的个数与数据表中的字段个数一样时,可以省略字段列表insert into student values(4,'中文',20); 查询记录 完整语句:select [字段列表] [from子句] [inner join子句] [where子句] [group by子句] [having子句] [order by子句] [limit子句]; 一般使用: select [字段列表] [from子句] [where子句] [order by子句] [limit子句]; [where子句] 用于过滤数据,只取出满足条件的记录 1select * from student where age >= 20; 查询出 age >= 20的记录 [order by子句] 用于对查询出的数据进行排序 1select * from student where age >= 20 order by age asc; 年龄升序排列 asc升序排列 (从小到大)、desc 降序排序 (从大到小) [limit子句] 用于限制输出数据的条数 123limit n; 提取前 n 条数据limit m,n; 从 第 m 条开始提取 n 条数据**(m 从 0 开始)**select * from student where age >= 20 order by age asc limit 2; 查询出 age >= 20的记录年龄,升序排列,提取前 2 条 修改记录 123update 表名 set 字段名1=新的值1,字段名2=新的值2,字段名n=新的值n where 子句;例子:update student set age=25 where id=8; 删除记录 123delete from 表名 where 子句;例子:delete from student where id=8; mysql中常用数据类型 数据类型 描述 INT(size) 4字节整数类型,-2147483648 到 2147483647 常规。0 到 4294967295 无符号*。在括号中规定最大位数,默认是11。 BIGINT(size) 8字节整数类型,-9223372036854775808 到 9223372036854775807 常规。0 到 18446744073709551615 无符号*。在括号中规定最大位数。默认是20。 FLOAT(size,d) 字节浮点数,带有浮动小数点的小数字。在括号中规定最大位数。在 d 参数中规定小数点右侧的最大位数。 CHAR(size) 保存固定长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的长度。最多 255 个字符。 VARCHAR(size) 保存可变长度的字符串(可包含字母、数字以及特殊字符)。在括号中指定字符串的最大长度。最多 255 个字符。注释:如果值的长度大于 255,则被转换为 TEXT 类型。 TEXT 存放最大长度为 65,535 个字符的字符串。 DATE() 日期。格式:YYYY-MM-DD 注释:支持的范围是从 ‘1000-01-01’ 到 ‘9999-12-31’ DATETIME() *日期和时间的组合。格式:YYYY-MM-DD HH:MM:SS 注释:支持的范围是从 ‘1000-01-01 00:00:00’ 到 ‘9999-12-31 23:59:59’ TIMESTAMP() *时间戳。TIMESTAMP 值使用 Unix 纪元(‘1970-01-01 00:00:00’ UTC) 至今的描述来存储。格式:YYYY-MM-DD HH:MM:SS 注释:支持的范围是从 ‘1970-01-01 00:00:01’ UTC 到 ‘2038-01-09 03:14:07’ UTC]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库之索引]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E5%BA%93%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E7%B4%A2%E5%BC%95%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B9%8B%E7%B4%A2%E5%BC%95%2F</url>
<content type="text"><![CDATA[数据库之索引创建索引在创建表的时候添加索引 12345CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, INDEX [indexName] (username(length)) ); 在创建表以后添加索引 123ALTER TABLE my_table ADD [UNIQUE] INDEX index_name(column_name);或者CREATE INDEX index_name ON my_table(column_name); 注意: 1、索引需要占用磁盘空间,因此在创建索引时要考虑到磁盘空间是否足够 2、创建索引时需要对表加锁,因此实际操作中需要在业务空闲期间进行 根据索引进行查询1234567891011121314151617181920212223具体查询:SELECT * FROM table_name WHERE column_1=column_2;(为column_1建立了索引)或者模糊查询SELECT * FROM table_name WHERE column_1 LIKE '%三'SELECT * FROM table_name WHERE column_1 LIKE '三%'SELECT * FROM table_name WHERE column_1 LIKE '%三%' SELECT * FROM table_name WHERE column_1 LIKE '_好_' 如果要表示在字符串中既有A又有B,那么查询语句为:SELECT * FROM table_name WHERE column_1 LIKE '%A%' AND column_1 LIKE '%B%'; SELECT * FROM table_name WHERE column_1 LIKE '[张李王]三'; //表示column_1中有匹配张三、李三、王三的都可以SELECT * FROM table_name WHERE column_1 LIKE '[^张李王]三'; //表示column_1中有匹配除了张三、李三、王三的其他三都可以//在模糊查询中,%表示任意0个或多个字符;_表示任意单个字符(有且仅有),通常用来限制字符串长度;[]表示其中的某一个字符;[^]表示除了其中的字符的所有字符 或者在全文索引中模糊查询SELECT * FROM table_name WHERE MATCH(content) AGAINST('word1','word2',...); 删除索引 123DROP INDEX my_index ON tablename;或者ALTER TABLE table_name DROP INDEX index_name; 查看表中的索引 1SHOW INDEX FROM tablename 查看查询语句查询索引的情况 12//explain 加查询语句explain SELECT * FROM table_name WHERE column_1='123'; 索引的优缺点优点: 可以快速检索,加快检索速度 根据索引分组和排序,可以加快分组和排序 缺点: 索引本身也是表,会占据存储空间 索引表的创建和维护需要时间,随数据量增大而增大 降低数据表的修改操作(删除、添加、修改)的效率,因为在修改数据表的同时也要修改索引表 索引的分类常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引 1、主键索引:即主索引,根据主键pk_clolum(length)建立索引,不允许重复,不允许空值; 1ALTER TABLE 'table_name' ADD PRIMARY KEY pk_index('col'); 2、唯一索引:用来建立索引的列的值必须是唯一的,允许空值 1ALTER TABLE 'table_name' ADD UNIQUE index_name('col'); 3、普通索引:用表中的普通列构建的索引,没有任何限制 1ALTER TABLE 'table_name' ADD INDEX index_name('col'); 4、全文索引:用大文本对象的列构建的索引 1ALTER TABLE 'table_name' ADD FULLTEXT INDEX ft_index('col'); 5、组合索引:用多个列组合构建的索引,这多个列中的值不允许有空值 1ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3'); 遵循“最左前缀”原则,把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了col1,col1col2,col1col2col3三个索引,而col2或者col3是不能使用索引的。 在使用组合索引的时候可能因为列名长度过长而导致索引的key太大,导致效率降低,在允许的情况下,可以只取col1和col2的前几个字符作为索引 1ALTER TABLE 'table_name' ADD INDEX index_name(col1(4),col2(3)); 表示使用col1的前4个字符和col2的前3个字符作为索引。 索引的选取类型1、越小的数据类型通常更好:越小的数据类型通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快 2、简单的数据类型更好:整型数据比起字符,处理开销更小,因为字符串的比较更复杂 3、尽量避免NULL:应该指定列为NOT nuLL,在MySQL中, 含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂 什么场景不适合创建索引1、很少使用查询或者只是作为参考的列,因为这些列很少用到,添加索引只会增加开销和维护成本 2、很少数据集的列也不应该加索引,例如班级的同学的性别,只有男或女,利用索引相当于全表搜索,没意义 3、当修改性能远远大于检索性能时,因为修改性能和检索性能互相矛盾 4、不会出现在where条件中的字段不该建立索引 什么的字段适合索引1、表的主键和外键必须有索引,外键唯一,且经常查询 2、数据量比较多的超过300需要索引 3、经常需要和其他表进行连接查询的字段应该建立索引 4、经常出现在where子句中的字段,加快判断速度 5、经常用到排序的列上,因为索引已经排序 6、经常用在范围内搜索的列上创建索引,因为索引已经排序,指定范围是连续(B+树) MySQL索引的底层原理索引是帮助MySQL高效获取数据的数据结构 上图展示的是一种可能的索引方式 左边是数据表,一共有两列14条记录,最左边是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上并不一定物理相邻),但实际数据库系统几乎没有使用二叉查找树或其进化品种红黑树(red-black tree)实现 目前大部分数据库系统及文件系统都采用B Tree或其变种B+Tree作为索引结构,MySQL普遍使用B+树实现索引 在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的 MyISAM和InnoDB对索引和数据的存储在磁盘上是如何体现的 role表使用的存储引擎是MyISAM,而user使用的是InnoDB: role表有三个文件,对应如下: role.frm:表结构文件 role.MYD:数据文件(MyISAM Data) role.MYI:索引文件(MyISAM Index) user表有两个文件,对应如下: user.frm:表结构文件 user.ibd:索引和数据文件(InnoDB Data) 由于两种引擎对索引和数据的存储方式的不同,我们也称MyISAM的索引为非聚集索引,InnoDB的索引为聚集索引。 MyISAM索引实现 MyISAM引擎使用B+Tree作为索引结构,叶节点data域存放数据记录的地址 设Col1为主键,上图是一个MyISAM表的主索引(Primary key)示例。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中,主索引和辅索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅索引的key可以重复(为什么没有区别,是因为SQL语句中where子句可以使用的是其他字段来作为条件,如clo2>…之类的语句,所以主索引和辅助索引在MyISAM没有区别)如果我们在Col2上建立一个辅索引,则此索引的结构如下图所示: MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分 InnoDB索引实现 MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址 而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引 与MyISAM的区别两点: 一、因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有) 如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键 如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形 二、InnoDB的辅索引data域存储相应记录主键的值而不是地址。 InnoDB的所有辅助索引都引用主键作为data域 索引存在但未使用情况(索引失效、索引优化)1CREATE INDEX idx_test_a1234 ON test(a1,a2,a3,a4);//创建联合索引来说明索引失效的一下情况 1.字符串为使用引号,导致索引失效(不能在索引列上干任何操作(计算,函数,类型转换) ) 123select * from test where a1 = a1;//可以查到数据,数据库自动转换类型,但索引失效select * from test where a1 = 'a1'; 2、索引最左原则使用不当,导致索引失效(where子句后面的顺序无关,只要用到就可以) 12345678910111213141516171819select * from test where a1 = 'a1' and a2 = 'a2' and a3 = 'a3' and a4 = 'a4';select * from test where a4= 'a4' and a3 = 'a3' and a2 = 'a2' and a1= 'a1';以上两个查询中,where条件中的索引位置是相反的,但是执行结果是一致的,这个是由mysql优化器来处理的,因为两个查询中都出现了联合索引a1,a2,a3,a4,MySQL优化器底层会进行优化处理。select * from test where a1 = 'a1' and a3 = 'a3' and a4 = 'a4';select * from test where a4= 'a4' and a3 = 'a3' and a1= 'a1';以上两个查询只用到一个索引a1select * from test where a1 = 'a1' and a2 = 'a2' and a4 = 'a4';select * from test where a4= 'a4' and a2 = 'a2' and a1= 'a1';以上两个查询用到两个索引select * from test where a2 = 'a2' and a3 = 'a3' and a4 = 'a4';select * from test where a4= 'a4' and a3 = 'a3' and a2 = 'a2'以上两个查询未使用索引 3、范围查找导致索引失效(存储引擎不能使用索引中范围条件右边的列) 123456select * from test where a1 = 'a1' and a2 = 'a2' and a3 >'a3' and a4 = 'a4';以上用到a1、a2、a3,3个索引,因为a3右边是a4,导致a4的索引不可用select * from test where a1 = 'a1' and a2 = 'a2' and a4 >'a4' and a3= 'a3';以上用到a1、a2、a3、a4,4个索引,因为a4后面已经没有索引了,不影响 4、order by使用不当,导致索引失效 12345select * from test where a1 = 'a1' and a2 = 'a2' and a4 = 'a4' order by a3explain语句显示两个索引,严格来说以上用到两个索引查询,三个索引排序因为查找的where条件中跳过了a3,违背了索引的最左原则,导致索引a4失效严格意义上来说,以上查询使用了3个索引,a3并未用于查找,但是在排序中使用到了,只是为统计到explain中,即满足索引的两大功能:查找和排序 以下排序中,索引使用不当,导致产生了文件内排序,影响性能 a2之后a3断开,导致a4索引失效,mysql 为了将结果展现出来,进行了内部排序 1select * from test where a1 ='a1' and a2 = 'a2' order by a4; 123456789101112select * from test where a1 ='a1' and a5 = 'a5' order by a2,a3;使用到a1一个索引select * from test where a1 ='a1' and a5= 'a5' order by a3,a2;使用到a1索引,并且产生文件内排序select * from test where a1 ='a1' and a2 = 'a2' order by a2,a3;使用到a1、a2两个索引,没有产生文件内排序原文:https://blog.csdn.net/weixin_39539399/article/details/80842750 5、group by使用不当导致索引失效 123select * from test where a1 ='a1'and a4 = 'a4' group by a2,a3;select * from test where a1 ='a1'and a4 = 'a4' group by a3,a2; 分组之前必排序,group by 表面上是分组,但是对索引的使用和order by 的使用大致相同,所以group by后面如果索引错乱,会产生临时表,导致mysql内部进行排序 6、通配符like的使用不当导致索引失效(like以通配符开头(“%abc…”) ) 1234567891011select * from test where a1 like '%a';索引失效select * from test where a1 like 'a%';索引不失效select a1 from test where a1 like '%a';覆盖索引,解决最左匹配不当的索引失效问题select * from test where a1 like 'c%';索引失效select * from test where a1 like '%c';索引失效select * from test where a1 like 'abc%';索引不失效 6、MYSQL 中!=,<>导致索引失效(is null, is not null 也无法使用索引) 尽量使用覆盖索引(只访问索引的查询),减少select *,可以解决索引失效的以上问题 7、少用or,用它来连接时索引会失效 关于or导致的索引失效,是有存在这种情况的,即or的左右边的查询条件,有一个列没有加索引,那么另一个列的索引会失效。要想使得索引生效,需要保证or两边的列都有索引,且一个列是主键。 小总结 索引的底层原理其实没有我总结那么简单,水平有限,B+B-树随缘再总结]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[算法复杂度]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F1%20%E7%AE%97%E6%B3%95%E7%9A%84%E5%A4%8D%E6%9D%82%E5%BA%A6%2F%E7%AE%97%E6%B3%95%E5%A4%8D%E6%9D%82%E5%BA%A6%2F</url>
<content type="text"><![CDATA[算法复杂度算法是对问题求解步骤的描述,通过有限序列的指令来实现 五大特征 有穷性:有限步之后结束 确定性:不存在二义性 可行性 输入 输出 时间复杂度用来衡量算法随着问题规模增大,算法执行时间的增大的快慢 时间复杂度是问题规模的函数:T(n) T(n)=O(f(n)),大O记法 计算方法: 算法时间增长最快的那个函数项,把它的系数改为1 空间复杂度 用来衡量算法随问题规模增大,算法所需空间的增长的快慢 是问题规模的函数:S(n)=O(g(n)) 常见的时间复杂度大小关系 O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[线性表]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F2%20%E7%BA%BF%E6%80%A7%E8%A1%A8%2F%E7%BA%BF%E6%80%A7%E8%A1%A8%2F</url>
<content type="text"><![CDATA[线性表线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列 线性表的顺序存储是用一组地址连续的存储单元,依次存储线性表中的数据元素,顺序存储的线性表也叫顺序表 静态建表: 存储空间的起始位置 顺序表最大存储容量 顺序表当前的长度 数组是静态分配的(大小固定) 其实存储空间(数组)还可以动态分配,也就是存储数组的空间是在程序执行过程中通过动态分配语句来分配的 1234567891011typedef int Elemtype;typedef struct{ ElemType *data;//指示动态分配数组的指针 int MaxSize,length;//数组的最大容量和当前个数}SeqList;动态分配语句#define InitSize 100SeqList L;L.data=(ElemType*)malloc(sezeof(ElemType)*InitSize); 动态分配并不是链式存储,同样还是属于顺序存储结构,只是分配的空间大小可以在运行时决定 顺序表的操作插入12345678910111213在顺序表L的第i(1<=i<=L.length)个位置插入新元素e。如果i的输入不合法,则返回false,表示插入失败;否则,将顺序表的第i个元素以及其后的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回truebool ListInsert(SqlList &L,int i,ElemType e){ if(i<1||i>L.length+1)//判断i的范围是否有效 return false; if(L.length>=MaxSize)//当前的存储空间已满,不能插入 return false; for(int j=L.length;j>=i;j--)//将第i个元素及之后的元素后移 L.data[j]=L.data[j-1]; L.data[i-1]=e; L.lengt++; return true;} 删除1234567891011删除顺序表L中第i(1<=i<=L.length)个位置的元素,成功则返回true,并将被删除的元素用应用变量e返回;否则返回falsebool ListDelete(SqList &L,int i,ElemType &e){ if(i<1||i>L.length+1)//判断i的范围是否有效 return false; e=L.data[i-1]; for(int j=i;j<L.length;j++) L.data[j-1]=L.data[j]; L.length--; return true;} 优点: 存储密度大,不需要为表中元素之间的逻辑关系增加额外存储空间 随机存取:可以快速存取表中任一位置的元素 缺点: 插入和删除需要移动大量元素 对存储空间要求高,会产生存储空间的碎片 链式存储 线性表的链式存储是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立起数据元素之间的线性关系,每个链表结点,除了存放元素自身的信息之外,还需要存放一个指向其后继的指针。 1234typedef struct LNode{ ElemType data;//数据域 struct LNode *next;//指针域}LNode,*LinkList; 通常用“头指针”来标识一个单链表,例如LinkList L,那么头指针L代表一个单链表 单链表第一个结点之前附加一个结点,称为头结点,头结点的数据域可以不设任何信息,也可以记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点。 单链表操作头插法建立单链表建立新的结点分配内存空间,将新结点插入当前链表的表头 12345678910111213141516//默认有空的头结点LinkList CreatList1(LinkList &L){ LNode *s; int x;//存储插入结点的数据的值 L=(LinkList)malloc(sizeof(LNode));//创建头结点 L->next=NULL;//初始化为空链表 scanf("%d",&x);//输入结点的值 while(x!=9999){//输入9999表示结束 s=(LNode*)malloc(sizeif(LNode));//创建新结点 s->data=x;//对新结点的数据域赋值 s->next=L->next;//新结点的后继指向第一个结点 L->next=s;//头结点的后继指向新结点 sanf("%d",&x); } return L;} 尾插法建立单链表建立新的结点分配内存空间,将新的结点插入到当前链表的表尾 123456789101112131415LinkList CreatList2(LinkList &L){ int x;//存储插入结点的数据的值 L=(LinkList)malloc(sizeof(LNode));//创建头结点 LNode *s,*r=L;//r为表尾指针,指向表尾 scanf("%d",&x);//输入结点的值 while(x!=9999){//输入9999表示结束 s=(LNode*)malloc(sizeif(LNode));//创建新结点 s->data=x;//对新结点的数据域赋值 r->next=s; r=s; sanf("%d",&x); } r->next=NULL; return L;} 按序号查找结点在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL 1234567891011LNode *GEtElem(LinkList L,int i){ int j=1;//计数,初始为1 LNode *p=L->next;//第一个结点指针赋给p if(i==0) return L;//若i等于0,返回头结点 if(i<1) return NULL; while(p&&j<i){//从第一个结点开始找,查找第i个结点 p=p->next; j++; } return p;} 按值查找结点 1234567LNode *Locate(LinkList L,ElemType e){ LNode *p=L->next; while(p!=NULL&&p->data!=e){//从第一个结点开始找 p=p->next; } return p;} 插入新结点插入操作是将值为x的新结点插入到单链表的第i个位置上。先检查插入位置和合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新结点 算法思路: 取指向插入位置的前驱结点的指针p=GetElem(L,i-1); 取新结点s的指针域指向 p的后继结点s->next=p->next 令结点p的指针域指向新插入的结点 s p->next=s; 删除一个结点删除操作是将单链表的第i个结点删除,先检查删除位置的合法性,然后查找表中第i-1个结点,即被删除结点的前驱结点,再将其删除 算法思路: 取指向删除位置的前驱结点的指针 p=GetElem(L,i-1); 取指向删除位置的指针 q=p->next; p指向结点的后继指向被删除结点的后继 p->next=q->next 双链表123456789typedef struct LNode{ ElemType data; structLNode *node;}LNode,*LinkList;typedef struct DNode{ ElemType data; struct DNode *prior,*next;//前驱和后继指针}DNode,*DLinkList; 插入 s->next=p->next p-next->prior=s s->prior=p p->next=s 删除 p->next=q->next q->next->prior=p free(q) 循环链表&静态链表循环单链表:循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环 循环单链表的判空条件不是头结点的后继指针是否为空,而是它是否等于头指针 循环双链表 静态链表 使用数组来描述线性表的链式存储结构 123456#define MaxSize 50//静态链表的最大长度typedef int ElemType//静态链表的数据类型假定为inttypedef struct{ ElemType data;//数据域,存储数据元素 int next;//指针域,下一个元素的数组下标}SLinkList[MaxSize]; 数组第一个元素不存储数据,它的指针域存储第一个元素所在的数组下标。 链表最后一个元素的指针域值为-1]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[栈和队列]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F3%20%E6%A0%88%E5%92%8C%E9%98%9F%E5%88%97%2F%E6%A0%88%E5%92%8C%E9%98%9F%E5%88%97%2F</url>
<content type="text"><![CDATA[栈和队列栈只允许在一端进行插入或删除操作的线性表 栈顶:栈中允许进行插入和删除的哪一端 栈底:固定的,不允许进行插入和删除的另一端 12345#define MaxSize 50typedef struct{ Elemtype data[MaxSize];//存放栈中的元素 int top;//栈顶指针}SqStack; top值不能超过MaxSize 空栈的判定条件通常定为top==-1,满栈的判定条件通常为top==MaxSize-1,栈中数据元素个数为top+1 顺序栈的操作判空1234bool StackEmpty(SqStack S){ if(s.top==-1) return true; else return false;} 进栈12345bool Push(SqStack &S,ElemType x){ if(S.top==MaxSize-1) return false; S.data[++S.top]=x; return true;} 出栈12345bool Pop(SqStack &S,ElemType &x){ if(S.top==-1) return false; x=S.data[S.top--]; return true;} 获取栈顶元素12345bool GetTop(SqStack S,ElemType &x){ if(S.top==-1) return false; x=S.data[S.top]; return true;} 共享栈 123456#define MaxSize 100typedef struct{ Elemtype data[MaxSize];//存放栈中的元素 int top1;//栈1栈顶指针 int top2;//栈2栈顶指针}SqDoubleStack; 进栈1234567bool Push(SqDoubleStack &S,ElemType x,int stackNum){ if(S.top1+1==S.top2) return false;//栈满 if(stackNum==1) S.data[++S.top1]=x;//栈1有元素进栈 else if(stackNum==2) S.data[--S.top2]=x;//栈2有元素进栈 return true;} 链式栈 123456789typedef struct SNode{ Elemtype data;//存放栈中的元素 struct SNode *next//栈顶指针}SNode,*SLink//链栈结点 typedef struct LinkStack{ SLink top;//栈顶指针 int count;//链栈结点数}LinkStack 链栈没有栈满的情况 链栈空为top==null 进栈12345678bool Push(LinkStack *S,ElemType x){ SLink p=(SLink)malloc(sizeof(SNode));//给新元素分配空间 p->data=x;//新元素的值 p-next=S->top;//p的后继指向栈顶元素 S->top=p;//栈顶指针指向新的元素 S->count++;//栈中元素个数加1 return true;} 出栈123456789bool Pop(LinkStack *S,ElemType &x){ if(S->top==NULL) return false; x=S->top->data;//栈顶元素值 SLink p=S->top;//辅助指针 S->top=S->top->next;//栈顶指针后移 free(p);//释放被删除数据的存储空间 S->count--;//栈中元素个数减一 return true;} 栈的应用括号配对假设有两种括号,一种圆的(),一种方的[],嵌套的顺序是任意的 算法思路: 若是左括号,入栈;若是右括号,出栈一个左括号判断是否与之匹配;检验到字符串尾,还要检查栈是否为空。只有栈空整个字符串才是括号匹配 12345678910111213141516171819202122bool Check(char *str){ stack s; InitStack(s); int len = strlen(str);//字符串长度 for(int i=0;i<len;i++){ char a=str[i]; switch(a){ case '('; case '['; Push(s,a); break; case ')'; if(Pop(s)!='(') return false;//出栈顶,如果不匹配直接返回不合法 break; case ']'; if(Pop(s)!=']')return false; break; } } if(Empty(s)) return true;//匹配完所有括号最后要求栈中为空 else return false;} 表达式求值从左到右,先乘除后加减,右括号先算括号 后缀表达式做法: 规则:从左到右扫描表达式的每个数字和符号,遇到数字就进栈,遇到符号就将处于栈顶的两个数字出栈然后跟这个符号进行运算,最后将结果进栈,直到最终获得结果 如何将中缀表达式转换成后缀表达式(计算机也是用到栈取转换,下面是手动方法) 递归递归最重要的是递归式和递归边界 使用递归求解n的阶乘 1234int F(int n){ if(n==0) return 1;//递归边界 else return n*F(n-1);//递归式} 求斐波拉契数列的第n项 12345int Fib(int n){ if(n==0) return 0; else if(n==1) return 1; else return Fib(n-1)+Fib(n-2);} 队列队列是只允许在一端进行插入,而在另一端进行删除的线性表 队头(Front):允许删除的一端,队首 队尾(Rear):允许插入的一端 顺序队列用数组实现队列,可以将队首放在数组下标为0的位置 12345#define MaxSize 50typedef struct{ ElemType data[MaxSize];//存放队列的元素 int front,rear;//队头指针和队尾指针} 循环队列入队:rear=(rear+1)%MaxSize 出队:front=(front+1)%MaxSize 判断队列是空是满 设置标志位flag,当flag=0且rear等于front时为队列空,当flag=1且rear等于front时为队列满(入队时flag等于1,出队时flag等于0) 把front==rear仅作为队空的判定条件。当队列满的时候,令数组中仍然保留一个空余单元。认为这种情况就是队列满了 队满关系 (rear+1)%MaxSize==front 队列中元素个数 (rear-front+MaxSize)%MaxSize 入队123456bool EnQueue(SqQueue &Q,ElemType x){ if((Q.rear+1)%MaxSize==Q.front) return flase;//队满 Q.data[Q.rear]=x; Q.rear=(Q.rear+1)%MaxSize; return true;} 出队123456bool DeQueue(SqQueue &Q,ElemType &x){ if(Q.rear==Q.front) return false;//队空,报错 x=Q.data[Q.front]; Q.front=(Q.front+1)%MaxSize; return true;} 链式队列队列的链式存储结构,其实就是线性表的单链表,只不过需要加点限制,只能表尾插入元素,表头删除元素 分别设置队头指针和队尾指针,队头指针指向头结点,队尾指针指向队尾结点 12345678typedef struct{//链式队列结点 ElemType data; struct LinkNode *next;}LinkNode;typedef struct{//链式队列 LinkNode *front,*rear;//队头和队尾指针}LinkQueue; 入队1234567void EnQueue(LinkQueue &Q,ElemType x){ s=(LinkNode*)malloc(sizeof(LinkNode)); s->data=x; s->next=NULL; Q.rear->next=s; Q.rear=s;} 出队出队就是头结点的后继结点出队,然后将头结点的后继改为它后面的结点 123456789bool DeQueue(LinkQueue &Q,ElemType &x){ if(Q.front==Q.rear) return false;//空队 p=Q.front->next; x=p->data; Q.front->next=p->next; if(Q.rear==p) Q.rear=Q.front;//若原队列中只有一个结点,删除后变空 free(p); return true;} 双端队列双端队列是指允许两端都可以进行入队和出队操作的队列 矩阵对于二维数组,两种映射方法:按行优先和按列优先 行优先 列优先 矩阵的压缩存储对称矩阵 三角矩阵 三对角矩阵 稀疏矩阵]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[树与二叉树]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F4%20%E6%A0%91%E5%92%8C%E4%BA%8C%E5%8F%89%E6%A0%91%2F%E6%A0%91%E4%B8%8E%E4%BA%8C%E5%8F%89%E6%A0%91%2F</url>
<content type="text"><![CDATA[树与二叉树一对多的树形结构 树的性质 树中的结点等于所有结点的度数加1 度为m的树中第i层上至多有mi-1个结点(i>=1) 高度为h的m叉树至多有(mh-1)/(m-1)个结点 具有n个结点的m叉树的最小高度为取上整[logm(n(m-1)+1)] 解上一个方程的h即可 树的存储结构顺序存储结构双亲表示法:用一组连续的存储空间存储树的结点,同时在每个结点中,用一个变量存储该结点的双亲结点在数组中的位置 1234567891011typedef char ElemType;typedef struct TNode{ ElemType data;//结点数据 int parent;//该结点双亲在数组的下标}TNode;//结点的数据类型#define MaxSize 100typedef struct{ TNode nodes[MaxSize];//结点数组 int n;//结点的数量}Tree;//结点双亲表示结构 双亲表示法可以根据parent找到该结点的双亲结点,时间复杂度为O(1)。但如果找到某节点的孩子结点就需要遍历 链式存储结构孩子表示法:把每个结点的孩子结点排列起来存储成一个单链表。所以n个结点就有n个链表;如果时叶子结点,那这个结点的孩子单链表就是空的;然后n个单链表的头指针又存储在一个顺序表(数组)中。 需要设计两种结合结构类型: 孩子链表的结点 每个孩子链表的表头结点(存在数组中) 12345678910typedef char ElemType;typedef struct CNode{ int child;//该孩子在表头数组的下标 struct CNode *next;//指向该结点的下一个孩子结点}CNode,*Child;//孩子结点数据结构typedef struct{ Elemtype data;//结点数据域 Child firstchild;//指向该结点的第一个孩子结点}TNode//孩子结点的数据类型 12345#define MaxSize 100typedef struct{ TNode nodes[MaxSize];//结点数据域 int n;//树中结点个数}Tree;//树的孩子表示结构 孩子兄弟表示法:要存储孩子结点和兄弟结点,就是设置两个指针,分别指向该结点的第一个孩子结点和该结点的兄弟结点。 12345typedef char ElemType;typedef struct CSNode{ ElemType data;//该结点的数据域 struct CSNode *firstchild,*rightsib//指向该结点的第一个孩子结点和该结点的右兄弟结点}CSNode;//孩子兄弟结点数据类型 二叉树每个结点最多有两颗子树 左右子树有顺序 五种基本形态 特殊二叉树 二叉树性质 非空二叉树上叶子结点等于度为2的结点数加1 非空二叉树上第K层上至多有2k-1个结点(k>=1) 高度为H的二叉树至多有2H-1个结点(H>=1) 具有N个(N>0)结点的完全二叉树的高度为上取整[log2(N+1)]或下取整[log2N]+1 二叉树的存储结构顺序存储结构二叉树的顺序存储结构就是用一组地址连续的存储单元依次自上而下,自左而右存储完全二叉树上的结点元素 链式存储结构二叉树每个结点最多两个孩子,所以设计二叉树的结点结构时考虑两个指针指向该结点的两个孩子 1234typedef struct BiTNode{ ElemType data;//数据域 struct BiTNode *lchild,*rchild;//指向该结点的左右孩子指针}BiTNode,*BiTNode;//二叉树结点结构 二叉树遍历(递归)二叉树的遍历是指按某种次序依次访问树中的每个结点,使得每个结点均被访问一次,而且仅被访问一次 递归先序遍历操作过程: 访问根结点 先序遍历左子树 先序遍历右子树 1234567void PreOrder(BiTree T){ if(T!=NULL){ printf("%c",T->data)//根节点 PreOrder(T->lchild);//左子树 PreOrder(T->rchild);//右子树 }} 递归中序遍历操作过程: 中序遍历左子树 访问根节点 中序遍历右子树 1234567void InOrder(BiTree T){ if(T!=NULL){ InOrder(T->lchild); printf("%c",T->data); InOrder(T->rchild); }} 递归后序遍历操作过程: 后序遍历左子树 后序遍历右子树 访问根节点 1234567void PostOrder(BiTree T){ if(T!=NULL){ PostOrder(T->lchild); PostOrder(T->rchild); printf("%c",T->data); }} 二叉树遍历(非递归)非递归先序遍历123456789101112131415void PreOrderTraverse(BiTree b){ InitStack(S); BitTree p=b;//工作指针p while(p || !IsEmpty(S)){ while(p){ printf("%c",p->data);//先序先遍历结点 Push(S,p);//进栈保存 p=p->lchild; } if(!IsEmpty(S)){ p=Pop(S); p=p-rchild; } }} 非递归中序遍历12345678910111213void InOrderTraverse(BiTree b){ InitStack(S); BitTree p=b;//工作指针p while(p || !IsEmpty(S)){ while(p){ Push(S,p);//进栈保存 p=p->lchild; } p=Pop(S); printf("%c",p->data); p=p-rchild; }} 非递归后序遍历 层序遍历操作过程: 若树为空,则什么都不做直接返回; 否则从树的第一层开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问 出队->访问->左右孩子入队 12345678910111213void LevelOrder(BiTree b){ InitQueue(Q); BiTree p; EnQueue(Q,b);//根节点入队 while(!IsEmpty(Q)){ DeQueue(Q,p);//队头元素出队 printf("%c",p->data); if(p->lchild!=NULL) EnQueue(Q,p->lchild); if(p->rchild!=NULL) EnQueue(Q,p->rchild); }} 线索二叉树二叉链表表示的二叉树存在大量空指针 N个结点的二叉树,每个结点都有指向左右孩子的结点指针,所以一共有2N个指针,而N个结点的二叉树一共有N-1个分支,也就是说存在2N-(N-1)=N+1个空指针。 指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树 对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。 如何区分指针是指向左孩子还是前驱,右孩子还是后继? 在二叉链表结点的结构基础上增加两个标志位ltag和rtag 12345typedef struct ThreadNode{ ElemType data; struct ThreadNode *lchild,*rchild; int ltag,rtag;}ThreadNode,*ThreadTree;//线索链表 ltag==0表示lchild指向该结点左孩子 ltag==1表示rchild指向该结点前驱 rtag==0表示rchild指向该结点右孩子 rtag==1表示指向该结点后继 构造线索二叉树 遍历线索二叉树 哈夫曼树和哈夫曼编码概念: 权:树中结点相关的数值 路径长度:从树中某个结点到另一个结点之间的分支数目(经过的边数) 带权路径长度:从树的根节点到任意结点的路径长度(经过的边数)与该结点上权值的乘积称为该结点的带权路径长度 哈夫曼树:含有N个带权叶子结点的二叉树中,带有带权路径长度(WPL)最小的二叉树,也成为最优二叉树。 设计哈夫曼树 将这N个结点分别作为N颗仅含一个结点的二叉树,构成森林F 构造一个新结点,并从F中选取两颗根节点权值最小的树作为新结点的左右子树,并且将新结点的权值置为左右子树上根节点的权值之和 从F中删除刚才选出的两棵树,同时将新得到的树加入F中 重复步骤2和3,直至F中只剩下一棵树为止 哈夫曼编码左子树为0,右子树为1 哈夫曼编码性质 哈夫曼编码是前缀编码 哈夫曼编码是最优前缀编码 二叉树、树和森林树转化成二叉树用到一个孩子兄弟表示法(回归本章树的链式存储) 二叉树转树 森林转二叉树 二叉树转森林 树和森林的遍历树的先序遍历 树的后序遍历 森林遍历]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[图]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F5%20%E5%9B%BE%2F%E5%9B%BE%2F</url>
<content type="text"><![CDATA[图基本概念图G由顶点集V和边集E组成,记为G=(V,E) 简单图和多重图简单图 不存在顶点到自身的边 同一条边不重复出现 多重图 若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联 完全图无向完全图:如果任意两个顶点之间都存在边 有向完全图:如果任意两个顶点之间都存在方向相反的两条弧 子图 连通图 强连通 连通图的生成树 度 权和网 基本概念总结 图的存储结构 邻接矩阵(顺序存储) 邻接表(链式存储) 十字链表(有向图) 邻接多重表(无向图) 邻接矩阵顶点:用一维数组来存储 边或弧:用二维数组来存储 二维数组就是一维数组的扩展,相当于一维数组中每个元素也是一维数组,二维数组也叫做邻接矩阵 无向图的邻接矩阵 有向图的邻接矩阵 邻接表对于稀疏图(E远小于V),顺序存储结构存在预先分配内存可能浪费的问题 无向图邻接表 有向图邻接表 十字链表十字链表是针对有向图的存储方式,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点 十字链表数据结构 邻接多重表 边表结构 图的遍历图的遍历:从图中某一个顶点出发遍历图中其余的顶点,且使每一个顶点仅访问一次,这个过程叫做图的遍历 图中顶点没有特殊性,可能存在沿着某条路径搜索后回到原起点,而有些顶点没有访问到。 解决办法:设置一个访问数组,记录遍历过程中访问过的顶点。 广度优先遍历(BFS) BFS算法实例 BFS空间复杂度 BFS需要借助一个队列,n个顶点均需要入队一次,所以最坏情况下n个顶点在队列,那么则需要O(|V|)的空间复杂度 BFS时间复杂度 邻接表:每个顶点入队一次,时间复杂度为O(|V|),对于每个顶点,搜索它的邻接点,就需要访问这个顶点的所有边,所以时间复杂度为O(|E|)。所以总的时间复杂度为O(|V|+|E|)。 邻接矩阵:每个顶点入队一次,时间复杂度为O(|V|),对于每个顶点,搜索它的邻接点,需要遍历一遍矩阵,所以时间复杂度为O(|V|),所以总的时间复杂度为O(|V|2)。 BFS应用 BFS解决单源非带权图最短路径问题:按照距离由近到远来遍历图中每个顶点 广度优先生成树 深度优先遍历(DFS)深度优先遍历(DFS:Depth-First-Serch):深度优先遍历类似于树的先序遍历算法 遍历过程:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1,再访问与w1邻接且未被访问的任一顶点w2,。。。。重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直到图中所有顶点均被访问过为止。 深度优先复杂度 空间复杂度: 由于DFS是一个递归算法,递归是一个需要工作栈来辅助工作,最多需要图中所有顶点进栈,所以空间复杂度为O(|V|) 时间复杂度: 邻接表:遍历过程的主要操作是对顶点遍历它的邻接点,由于通过访问边表来查找邻接点,所以时间复杂度为O(|E|),访问访问顶点时间为O(|V|),所以总的时间复杂度为O(|V|+|E|)。 邻接矩阵:查找每个顶点的邻接点时间复杂度为O(|V|),对每个顶点都进行查找,所以总的时间复杂度O(|V|2)。 深度优先生成树 图的应用最小生成树(Prim、Kruskal)连通图的生成树,是一个极小的连通子图。包含图中全部的顶点,但只有足以构成一棵树的n-1条边 普里姆(Prim)算法 克鲁斯卡尔(Kruskal)算法 普里姆算法 初始化 i=1 i=2 i=3 i=4 i=5 i=6 Prim算法时间复杂度 克鲁斯卡尔算法(Kruskal) 并查集 算法思路 初始化,将边权值进行排序 第一次 第二次 第三次 第四次 第五次 第六次 第七次 4-6之后的边循环都不进行操作了,已经形成六条边(n-1),形成最小生成树 克鲁斯卡尔算法复杂度 最短路径(Dijkstra、floyd) 迪杰斯特拉算法:一个源点到其余顶点的最短路径 弗洛伊德算法:所有顶点到所有顶点的最短路径 迪杰斯特拉算法思路: 迪杰斯特拉算法实例 第一次 第二次 第三次 第四次 第五次 迪杰斯特拉算法代码 迪杰斯特拉复杂度 弗洛伊德算法 弗洛伊德算法是求图中任意一对顶点间的最短路径的算法 算法思想: 佛洛依德算法实例 初始化 第一次 第二次 第三次 第四次 弗洛伊德算法代码和复杂度 拓扑排序AOV网 拓扑排序算法思路 第一轮: 第二轮: 第三轮: 第四轮: 第五轮: 第六轮: 第七轮: 拓扑排序算法代码 拓扑排序算法复杂度 拓扑排序规律 关键路径AOE网 关键路径 寻找关键路径步骤]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[排序]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F7%20%E6%8E%92%E5%BA%8F%2F%E6%8E%92%E5%BA%8F%2F</url>
<content type="text"><![CDATA[排序排序就是将原本无序的序列重新排列成有序的序列 排序的稳定性 内部排序:指的是待排序记录全部存放在计算机内存中进行排序的过程 外部排序:指的是待排序的记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需要对外存进行访问的排序过程 内部排序分类 插入类:将无序子序列中的一个或几个记录插入到有序序列中,从而增加记录的有序子序列的长度,包括直接插入排序、折半插入排序、希尔排序 交换类:通过交换无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度,包括冒泡排序和快速排序 选择类:从记录的无序子序列中选择关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加有序子序列的长度,包括简单选择排序、树形选择排序、堆排序 归并类:通过归并两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度,包括2路归并排序 分配类:是唯一一类不需要关键字之间的比较的排序方法,排序时主要利用分配和收集两种基本操作完成。基数排序是主要的分配类排序 插入排序直接插入排序 1234567891011121314151617181920212223242526272829303132333435363738package 插入排序;import java.util.Scanner;/** * 直接插入排序 * @param array */public class InsertSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } InsertSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void InsertSort(int array[]) { int i,j; for(i=2;i<array.length;i++) { if(array[i]<array[i-1]) { array[0]=array[i]; for(j=i-1;array[j]>array[0];j--) { array[j+1] = array[j]; } array[j+1] = array[0]; } } }} 空间时间复杂度 时间复杂度最好O(n),最坏O(n2) 直接插入排序是稳定的 折半查找排序 折半排序代码 1234567891011121314151617181920212223242526272829303132333435363738394041424344package 插入排序;import java.util.Scanner;/** * 折半插入排序 * @author 11053 * */public class BinaryInsertSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } BinaryInsertSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void BinaryInsertSort(int array[]) { int i,j,mid,low,high; for(i=2;i<array.length;i++) { if(array[i]<array[i-1]) { array[0] = array[i]; low = 1; high = i-1; while(low<=high) { mid = (low+high)/2; if(array[mid]>array[0]) high = mid-1; else low = mid+1; } for(j=i-1;j>=high+1;j--) array[j+1] = array[j]; array[high+1] = array[0]; } } }} 时间复杂度 时间复杂度O(n2) 折半插入排序是稳定的 希尔排序 希尔排序过程 第一趟:10个元素取增量10/2=5 第二趟:5/2向下取整=2 第三趟:最后增量为1 希尔排序的时间复杂度 12345678910111213141516171819202122232425262728293031323334353637383940package 插入排序;import java.util.Scanner;/** * 希尔排序 * @author 11053 * */public class ShellSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } ShellSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void ShellSort(int array[]) { int d,i,j; for(d=array.length/2;d>=1;d=d/2) { for(i=d+1;i<=array.length-1;i++) { if(array[i]<array[i-d]) { array[0] = array[i]; for(j=i-d;j>0&&array[j]>array[0];j-=d) { array[j+d] = array[j]; } array[j+d] = array[0]; } } } }} 时间复杂度最好O(n1.3),最坏O(n2). 希尔排序不稳定 交换排序交换类排序:根据序列中两个元素关键字的比较结果来交换它两在序列中的位置 冒泡排序 冒泡排序代码 123456789101112131415161718192021222324252627282930313233343536373839404142package 交换排序;import java.util.Scanner;/** * 冒泡排序 * @author 11053 * */public class BubbleSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } BubbleSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void BubbleSort(int array[]) { int i,j,temp; boolean flag; for(i=0;i<array.length;i++) { flag=false; for(j=array.length-1;j>i;j--) { if(array[j-1]>array[j]) { temp = array[j]; array[j] = array[j-1]; array[j-1] = temp; flag = true; } } if(flag==false) return ; } }} 冒泡排序时间复杂度最好O(n),最坏O(n2). 冒泡排序稳定. 快速排序快速排序是一种基于分治法的排序方法 快速排序代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445package 交换排序;import java.util.Scanner;/** * 快速排序 * @author 11053 * */public class QuickSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } QuickSort(array,1,array.length-1); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void QuickSort(int array[],int low,int high) { if(low<high) { int pivotloc = Partition(array,low,high); QuickSort(array,low,pivotloc-1); QuickSort(array,pivotloc+1,high); } } public static int Partition(int array[],int low,int high) { int pivot = array[low]; while(low<high) { while(low<high&&array[high]>=pivot) high--; array[low] = array[high]; while(low<high&&array[low]<=pivot) low++; array[high] = array[low]; } array[low] = pivot; return low; }} 快速排序时间复杂度最好O(nlogn),最坏O(n2). 序列越乱序,效率越高;序列越有序,效率越低 空间复杂度最好O(logn),最坏O(n). 快速排序不稳定. 选择排序 简单选择排序 123456789101112131415161718192021222324252627282930313233343536package 选择排序;/** * 简单选择排序 */import java.util.Scanner;public class SelectSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } SelectSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void SelectSort(int array[]) { int i,j,min; for(i=1;i<array.length-1;i++) { min = i; for(j=i+1;j<array.length;j++) if(array[min]>array[j]) min = j; if(min!=i) { array[0] = array[min]; array[min] = array[i]; array[i] = array[0]; } } }} 简单选择排序时间复杂度O(n2) 不稳定,交换会打破顺序 堆排序堆的定义 大顶堆过程 堆排序算法 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061package 选择排序;/** * 堆排序 */import java.util.Scanner;public class HeadSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } HeadSort(array); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void HeadSort(int array[]) { int len = array.length-1; int i; BuildMaxHead(array, len); for(i=len;i>0;i--) { Swap(array,1,i); HeadAjust(array,1,i-1); } } public static void BuildMaxHead(int array[],int len) { //len=10 int i; for(i=len/2;i>0;i--) HeadAjust(array,i,len); } public static void HeadAjust(int array[],int k,int len) { int i; array[0] = array[k]; for(i=k*2;i<=len;i=i*2) { if(i<len&&array[i]<array[i+1]) i++; if(array[i]<array[0]) break; else { array[k] = array[i]; k=i; } } array[k] = array[0]; } public static void Swap(int array[],int top,int bottom) { array[0] = array[top]; array[top] = array[bottom]; array[bottom] = array[0]; }} 堆排序时间复杂度O(nlog2n),空间复杂度O(1)。 堆排序不稳定. 归并排序 归并排序代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package 归并排序;import java.util.Scanner;/** * 归并排序 * @author 11053 * */public class MergeSort { public static void main(String[] args) { int i=1; Scanner sc = new Scanner(System.in); System.out.println("请输入需要排序的数字"); System.out.println("01 02 03 04 05 06 07 08 09 10"); int array[] = new int[11]; while(i<11) { array[i] = sc.nextInt(); i++; } MergeSort(array,1,array.length-1); for(int a=1;a<11;a++) System.out.print(array[a]+","); } public static void MergeSort(int array[],int low,int high) { int mid; if(low<high) { mid = (low+high)/2; MergeSort(array,low,mid); MergeSort(array,mid+1,high); Merge(array,low,mid,high); } } public static void Merge(int array[],int low,int mid,int high) { int i,j,k; int len = array.length; int temp[] = new int[len]; for(k=low;k<=high;k++) temp[k] = array[k]; for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++) { if(temp[i]<temp[j]) array[k]=temp[i++]; else array[k]=temp[j++]; } while(i<=mid) array[k++]=temp[i++]; while(j<=high) array[k++]=temp[j++]; }} 时间复杂度O(nlogn),空间复杂度O(n) 归并排序稳定 非比较排序基数排序(桶排序) MSD]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[查找]]></title>
<url>%2F2019%2F05%2F26%2F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%2F6%20%E6%9F%A5%E6%89%BE%2F%E6%9F%A5%E6%89%BE%2F</url>
<content type="text"><![CDATA[查找查找:在数据集合中寻找满足某种条件的数据元素的过程 关键字:数据元素中某个可以唯一标识该元素的数据项 平均查找长度 顺序查找 折半查找 分块查找分块查找又称为索引顺序查找 二叉排序树 二叉排序树查找递归代码 二叉排序树查找非递归代码 二叉排序树插入关键字代码 时间复杂度 有n个结点就需要插入n个结点操作,插入一个的时间复杂度为O(log2n),构造复杂度为O(nlog2n) 二叉排序树构造代码 二叉排序树删除结点 删除叶子结点 删除只有左子树或者右子树的结点 删除左右子树都有的结点 找到该结点的前驱和后继(即中序遍历的前驱和后继) 前驱替代结点 后继替代结点 二叉排序树分析 平衡二叉树(AVL树) 最小不平衡子树 不平衡二叉树类型 构建平衡二叉树过程 LL调整 RR调整 RL调整 平衡二叉树结点规律 B树和B+树2-3树: 2-3是一种多路查找树:2和3的意思就是2-3树包含两种结点 2-3-4树: B树 磁盘管理系统中的目录管理,以及数据库系统中的索引组织多数都采用B树数据结构 B树查找操作 B树插入操作 B树删除操作 删除的关键字在终端结点上(最底层非叶子结点) 第一种情况 第二种情况 第三中情况 删除关键字不在终端结点上(最底层非叶子结点) 第一种情况 第二种情况 B+树 B+树是B树的变形树,适合用于文件索引系统 散列表散列表基本概念 散列函数和冲突处理方法 常用Hash函数的构造方法 常用Hash函数的冲突处理方法 开放地址法 拉链法(链地址法) 散列表的查找过程和性能 散列表实例 ASL计算方式:查找成功ASL看关键字,查找失败ASL看地址个数 开放地址法(线性探测再散列法)计算ASL:https://blog.csdn.net/wangran51/article/details/8826633/ KMP算法KMP算法是用于解决字符串模式匹配的问题,字符串的模式匹配,是求一个字符串(模式串)在另一个字符串(主串)中的位置 BF(Brute-Force)算法 BF算法效率O(n*m) KMP算法 KMP算法next数组 next例子情况 当P[k] == P[j]时, 有next[j+1] == next[j] + 1 当P[k] != P[j] 像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。 KMP算法代码 KMP算法效率 手动求解next数组的值]]></content>
<categories>
<category>数据结构</category>
</categories>
<tags>
<tag>数据结构</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JVM概述]]></title>
<url>%2F2019%2F05%2F16%2Fjvm%2FJVM%E6%A6%82%E8%BF%B0%2F</url>
<content type="text"><![CDATA[JVM内存区域(运行时数据区)线程私有虚拟机栈存储着方法出口,局部变量表、动态链接、操作数栈 局部变量表存储的是方法里面的变量操作数栈的作用就是变量和常量或变量和变量之间的计算, 栈帧操作数栈主要用于方法内对变量与变量之间的计算,用于计算 方法出口局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 本地方法栈程序计数器介绍当前线程所执行的字节码的行号指示器 作用:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 线程共享堆此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 新生区eden 80%from survivor 10%to survivor 10%老年区方法区它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 运行时常量池包装类和String:是存在常量池还是堆中的问题 字面量####### 文本字符串 ####### final常量池 ####### 基本数据类型 符号引用####### 类和结构的完全限定名 ####### 字段名称和描述符 ####### 方法名称和描述符 类信息常量静态变量直接内存(非运行时数据区)垃圾回收堆空间基本结构新生代对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1(“To”),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,经过这次GC后,Eden区和”From”区已经被清空。这个时候,”From”和”To”会交换他们的角色,也就是新的”To”就是上次GC前的“From”,新的”From”就是上次GC前的”To”。“To”区被填满之后,会将所有对象移动到年老代中。 eden区from survivorto survivor老年代堆内存分配策略对象优先在eden区mirror GC在新生代Full/Major GC在老年区 大对象直接进入老年区长期存活的对象进入老年代动态对象年龄判断判断对象死亡对象无效判断方法引用计数法难以解决对象相互引用问题 可达性分析通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。 引用强引用以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 软引用如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 弱引用弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 虚引用与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。 判断废弃常量假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池。 判断无用的类方法区主要回收的是无用的类 堆中不存在该类的任何实例加载该类的类加载器被回收该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法垃圾回收算法标志-清除算法效率问题空间问题产生大量的空间碎片 复制算法解决效率问题分成两块,经过GC之后将存活的对象转移到另一块,该块被全部清除 标志-整理算法解决了空间碎片问题与标记清除之后,把存活的对象移到一端,直接清理边界以外的对象 分代收集算法垃圾收集器Serial收集器单线程垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ) 新生代复制算法老年代标记-整理算法Parnew收集器GC线程多线程并发 新生代复制算法老年代标记-整理算法Paraller Scavenge收集器Paraller Scavenge和Parnew的区别:Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU) 新生代复制算法老年代标记-整理算法Serial Old收集器Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 Parallel Scavenge 收集器Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 CMS收集器是什么?CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器 CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。CMS 收集器是一种 “标记-清除”算法实现的 初始标记 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; 并发标记同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。 因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 重新标记重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 并发清除开启用户线程,同时 GC 线程开始对为标记的区域做清扫。 优缺点主要优点:并发收集、低停顿。 但是它有下面三个明显的缺点:对 CPU 资源敏感;(需要多cpu)无法处理浮动垃圾;它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。 G1算法G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征. 特点:1.并行与并发2.分代收集3.空间整合(整体看是标记-整理,局部是复制算法)4.可预测停顿 初始标记并发标记最终标记筛选回收JDK监控和故障处理类文件结构在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。 魔数每个 Class 文件的头四个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。 class文件版本紧接着魔数的四个字节存储的是 Class 文件的版本号:第五和第六是次版本号,第七和第八是主版本号。 高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件 常量池字面量字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等 符号引用类和接口的全限定名字段的名称和描述符方法的名称和描述符访问标志这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 类索引类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。 接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按implents(如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。 当前类索引父类索引接口索引集合字段表集合字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。 方法表集合Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 属性表集合在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。 类加载过程Class 文件需要加载到虚拟机中之后才能运行和使用 系统加载 Class 类型的文件主要三步:加载->连接->初始化。 连接过程又可分为三步:验证->准备->解析。 加载一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段 这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。 通过全限定名加载字符流通过静态的字符流转换方法区运行时的数据结构在内存中生成一个代表该类的对象,作为方法区数据访问接口连接验证文件格式验证元数据验证字节码验证符号引用验证准备准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。 对于该阶段有以下几点需要注意: 1.这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。2.这里所设置的初始值”通常情况”下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被复制为 111。 解析解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 初始化初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 ()方法的过程。 对于() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。 五种情况必须对类初始化1.当遇到 new 、 getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。 2.使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。 3.初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 4.当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。 5.当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。 类加载器JVM 中内置了三个重要的ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader: BootstrapClassLoader1、BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。 ExtensionClassLoader2、ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。 AppClassLoader3、AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。 双亲委派模型每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。 即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。 当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。 双亲委派的好处1.双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。) 2.如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。]]></content>
<categories>
<category>JVM</category>
</categories>
<tags>
<tag>JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaWeb知识总结]]></title>
<url>%2F2019%2F01%2F26%2FJavaWeb%E7%9F%A5%E8%AF%86%2FJavaWeb%2F</url>
<content type="text"><![CDATA[XMLXML的作用 1231.可以用来保存数据2.可以用来配置文件3.数据传输载体 定义xml 1234567文档声明 简单声明,version:解析这个xml使用什么版本的解析器解析 <?xml version="1.0" ?> encoding:解析xml文字使用什么编码来翻译,电脑上的文件在保存时是存储文字对应的二进制,这些文字对应 的二进制使用编码解析得到 <?xml version="1.0" encoding="gbk(UTF-8)"?> standalone:no---文档会依赖关联其他文档,yes---这是一个独立的文档 <?xml version="1.0" encoding="gbk(UTF-8)" standalone="no"?> 元素定义 12341.<>里面都是元素,成对出现,如:<stu></stu>2.文档声明第一个出现的元素是根元素3.空标签<age/>,开始也是结束,里面可以写属性4.标签可以自定义 注释 1<!-- -->与HTML注释一样 CDATA区 XML解析方式(常用两种DOM&SAX) 1获取元素里面的字符或者属性数据 XML解析手段(DOM4J&JDOM) DOM4J 基本用法 123456element.element("stu");//返回该元素下的第一个stu元素element.elements();//返回该元素下的所有子元素1.创建SAXReader对象2.指定解析的xml3.获取根元素4.根据根元素获取子元素或者下面的子孙元素 12345678910111213141516171819202122232425262728293031323334353637package com.itheima.test;import java.util.*;import java.io.File;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;public class MainTest { public static void main(String[] args) { try { //1.创建sax读取对象 SAXReader reader = new SAXReader();//jdbc --classloader //2.指定解析的xml源 Document document = reader.read(new File("src/xml/stus.xml")); //3.得到元素 Element rootElement = document.getRootElement(); //获取根元素下面的子元素 //System.out.println(rootElement.element("stu").element("age").getText()); List<Element> elements = rootElement.elements(); for(Element element : elements) { String name = element.element("name").getText(); String age = element.element("age").getText(); String address = element.element("address").getText(); System.out.println("name:"+name+"--"+"age:"+age+"--"+"address:"+address); } } catch (Exception e) { e.printStackTrace(); } }} Dom4J的Xpath使用 12345> dom4j里面支持Xpath的写法。xpath其实是xml的路径语言,支持我们在解析xml的时候定位到一个具体的元素。1.添加jar包依赖 jaxen-1.1-beta-6.jar2.在查找指定节点的时候,根据xpath语法规则来查找3.后续的代码与以前的解析代码一样 12345678910111213141516171819202122232425262728293031323334353637package com.itheima.test;import java.util.*;import java.io.File;import org.dom4j.Document;import org.dom4j.Element;import org.dom4j.io.SAXReader;public class XpathTest { public static void main(String[] args) { try { //1.创建sax读取对象 SAXReader reader = new SAXReader();//jdbc --classloader //2.指定解析的xml源 Document document = reader.read(new File("src/xml/stus.xml")); //3.得到元素 Element rootElement = document.getRootElement(); // 要想使用Xpath,还得添加支持的jar 获取第一个只返回第一个 Element nameElement = (Element) rootElement.selectSingleNode("//name"); System.out.println(nameElement.getText()); System.out.println("####################"); //获取文档中所有的name元素 List<Element> list = rootElement.selectNodes("//name"); for(Element element : list) { System.out.println(element.getText()); } } catch (Exception e) { e.printStackTrace(); } }} 约束 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556属性ID规定唯一或者元素只能出现一次DTD: 语法可读性差 1. 引入网络上的DTD <!-- 引入dtd 来约束这个xml --> <!-- 文档类型 根标签名字 网络上的dtd dtd的名称 dtd的路径 <!DOCTYPE stus PUBLIC "//UNKNOWN/" "unknown.dtd"> --> 1. 引入本地的DTD <!-- 引入本地的DTD : 根标签名字 引入本地的DTD dtd的位置 --> <!-- <!DOCTYPE stus SYSTEM "stus.dtd"> --> 2. 直接在XML里面嵌入DTD的约束规则 <!-- xml文档里面直接嵌入DTD的约束法则 --> <!DOCTYPE stus [ <!ELEMENT stus (stu)> <!ELEMENT stu (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> ]> <stus> <stu> <name>张三</name> <age>18</age> </stu> </stus><!ELEMENT stus (stu)> : stus 下面有一个元素 stu , 但是只有一个 <!ELEMENT stu (name , age)> stu下面有两个元素 name ,age 顺序必须name-age <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> <!ATTLIST stu id CDATA #IMPLIED> stu有一个属性 文本类型, 该属性可有可无 元素的个数: + 一个或多个 * 零个或多个 ? 零个或一个 属性的类型定义 CDATA : 属性是普通文字 ID : 属性的值必须唯一 <!ELEMENT stu (name , age)> 按照顺序来 <!ELEMENT stu (name | age)> 两个中只能包含一个子元素 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364Schema: 其实就是一个xml,使用xml的语法规则,xml解析器解析方便,是为了替代DTD 但是Scheme约束文本内容比DTD的内容还要多,所以没有真正意思上的替代DTD约束文档: <!-- xmlns : xml namespace : 名称空间 / 命名空间 targetNamespace : 目标名称空间 。 下面定义的那些元素都与这个名称空间绑定上。 elementFormDefault : 元素的格式化情况。 --> <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.itheima.com/teacher" elementFormDefault="qualified"> <element name="teachers"> <complexType> <sequence maxOccurs="unbounded"> <!-- 这是一个复杂元素 --> <element name="teacher"> <complexType> <sequence> <!-- 以下两个是简单元素 --> <element name="name" type="string"></element> <element name="age" type="int"></element> </sequence> </complexType> </element> </sequence> </complexType> </element> </schema>实例文档: <?xml version="1.0" encoding="UTF-8"?> <!-- xmlns:xsi : 这里必须是这样的写法,也就是这个值已经固定了。 xmlns : 这里是名称空间,也固定了,写的是schema里面的顶部目标名称空间 xsi:schemaLocation : 有两段: 前半段是名称空间,也是目标空间的值 , 后面是约束文档的路径。 --> <teachers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.itheima.com/teacher" xsi:schemaLocation="http://www.itheima.com/teacher teacher.xsd" > <teacher> <name>zhangsan</name> <age>19</age> </teacher> <teacher> <name>lisi</name> <age>29</age> </teacher> <teacher> <name>lisi</name> <age>29</age> </teacher> </teachers>##名称空间的作用一个xml如果想指定它的约束规则, 假设使用的是DTD ,那么这个xml只能指定一个DTD,不能指定多个DTD 。 但是如果一个xml的约束是定义在schema里面,并且是多个schema,那么是可以的。简单的说: 一个xml 可以引用多个schema约束。 但是只能引用一个DTD约束。名称空间的作用就是在 写元素的时候,可以指定该元素使用的是哪一套约束规则。 默认情况下 ,如果只有一套规则,那么都可以这么写<name>张三</name><aa:name></aa:name><bb:name></bb:name> Tomcat程序架构 1234C/S(Client/Server)QQ微信优点:有一部分代码写在客户端,用户体现差缺点:服务器更新,客户端跟着更新,占用资源大 123B/S(browser/Server)优点:客户端只要有浏览器就行,占用资源少,不用更新缺点:用户体现不佳 Web服务器 1234>其实服务器就是一台电脑,配置比一般的好>客户端在浏览器的地址栏输入地址,然后web服务器软件,接受请求,然后响应消息>处理客户端的请求,返回资源|信息web应用 需要服务器支撑 index.html Tomcat目录 123456789101112bin>>jar bat startup.batconf>>tomcat的配置,server.xml web.xmllib>>tomcat运行所需的jar文件temp>>临时文件webapps>>存放着发布到tomcat服务器上的项目work>>jsp翻译成java文件存放地 发布项目到tomcat 1234567>>需求:如何能让其他电脑访问这台电脑上的资源 stu.xml>>localhost:8080:本地地址>>1.拷贝文件到这个webapps/root下,在浏览器访问: localhost:8080/stu.xml>>2.在webapps下新建文件夹xml,然后拷贝文件到该文件夹中 http://localhost:8080/xml/stu.xml http://localhost:8080/xml:对应到webapps/xml 123456789>>3.使用localhost:8080/docs 打开tomcat首页,找到configuration点击进入,再进入左侧找到context(http://localhost:8080/docs/config/context.html)>>>>配置虚拟路径>>>>1.在conf/server.xml找到host元素节点 2. 加入以下内容。 <!-- docBase : 项目的路径地址 如: D:\xml02\person.xml path : 对应的虚拟路径 一定要以/打头。 对应的访问方式为: http://localhost:8080/a/person.xml --> <Context docBase="D:\xml02" path="/a"></Context> 3. 在浏览器地址栏上输入: http://localhost:8080/a/person.xml 1234567>>4. 配置虚拟路径>>>>1. 在tomcat/conf/catalina/localhost/ 文件夹下新建一个xml文件,名字 可以自己定义。 person.xml>>>>2. 在这个文件里面写入以下内容 <?xml version='1.0' encoding='utf-8'?> <Context docBase="D:\xml02"></Context>>>>>3. 在浏览器上面访问 http://localhost:8080/person/xml的名字即可 eclipse配置tomcat 123456789101112131. 在server里面 右键新建一个服务器, 选择到apache分类, 找到对应的tomcat版本, 接着一步一步配置即可。2. 配置完毕后, 在server 里面, 右键刚才的服务器,然后open , 找到上面的Server Location , 选择中间的 Use Tomcat installation...3. 创建web工程, 在WebContent下定义html文件, 右键工程, run as server ##总结:xml 1. 会定义xml 2. 会解析xml dom4j 基本解析 Xpath手法tomcat 1. 会安装 ,会启动 , 会访问。 2. 会设置虚拟路径 3. 给eclipse配置tomcat JDBCJDBC 123JAVA DataBase Connectivity:java数据库连接>>为什么出现JDBC>>>>sun公司规定的一种数据访问规范,由于数据库种类较多,java语言使用广泛,sun公司提供一种,让其他的数据库提供商去实现底层的访问规则。我们的Java程序只要使用sun公司提供的jdbc驱动即可 JDBC入门 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849package com.itheima.test;import java.net.URI;import java.sql.Connection;import java.sql.Driver;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;public class MainTest { public static void main(String[] args) { try { //1.注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver()); //2.建立连接 参数一:协议+访问的数据库, 参数二:用户名,参数三:密码 //DriverManager.getConnection("jdbc:mysql://localhost/test? user=monty&password=greatsqldb); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/student","root","123456"); //3.创建statement,跟数据库读取一定需要这个对象 Statement st = conn.createStatement(); //4.执行查询,得到结果集 String sql = "select * from t_stu"; ResultSet rs = st.executeQuery(sql); while(rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); int age = rs.getInt("age"); System.out.println("id"+id+"----"+"name"+name+"----"+age+"age"); } rs.close(); st.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } }} JDBC工具类 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162JDBCUtil.javapackage com.itheima.util;import java.sql.Connection;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;/**释放资源 * @author Administrator * */public class JDBCUtil { public static void release(Connection conn, Statement st, ResultSet rs) { closeRs(rs); closeSt(st); closeConn(conn); } private static void closeRs(ResultSet rs) { try { if(rs != null) { rs.close(); } } catch (SQLException e1) { e1.printStackTrace(); }finally { rs = null; } } private static void closeSt(Statement st) { try { if(st != null) { st.close(); } } catch (SQLException e1) { e1.printStackTrace(); }finally { st = null; } } private static void closeConn(Connection conn) { try { if(conn != null) { conn.close(); } } catch (SQLException e1) { e1.printStackTrace(); }finally { conn = null; } }} 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354MainTest.javapackage com.itheima.test;import java.net.URI;import java.sql.Connection;import java.sql.Driver;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import com.itheima.util.JDBCUtil;public class MainTest { public static void main(String[] args) { Connection conn = null; Statement st = null; ResultSet rs = null; try { //1.注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver()); //2.建立连接 参数一:协议+访问的数据库, 参数二:用户名,参数三:密码 //DriverManager.getConnection("jdbc:mysql://localhost/test?user=monty&password=greatsqldb); conn = DriverManager.getConnection("jdbc:mysql://localhost/student","root","123456"); //3.创建statement,跟数据库读取一定需要这个对象 st = conn.createStatement(); //4.执行查询,得到结果集 String sql = "select * from t_stu"; rs = st.executeQuery(sql); System.out.println("id"+"---- "+"name"+"---- "+"age"); while(rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); int age = rs.getInt("age"); System.out.println(+id+"----"+name+"----"+age); } } catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, st, rs); } }} JDBC使用步骤 1234567891011121314151617181920212223242526272829303132333435363738391. 注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver());2. 建立连接 //DriverManager.getConnection("jdbc:mysql://localhost/test?user=monty&password=greatsqldb"); //2. 建立连接 参数一: 协议 + 访问的数据库 , 参数二: 用户名 , 参数三: 密码。 conn = DriverManager.getConnection("jdbc:mysql://localhost/student", "root", "root");3. 创建statement //3. 创建statement , 跟数据库打交道,一定需要这个对象 st = conn.createStatement();4. 执行sql ,得到ResultSet //4. 执行查询 , 得到结果集 String sql = "select * from t_stu"; rs = st.executeQuery(sql);5. 遍历结果集 //5. 遍历查询每一条记录 while(rs.next()){ int id = rs.getInt("id"); String name = rs.getString("name"); int age = rs.getInt("age"); System.out.println("id="+id + "===name="+name+"==age="+age); } 6. 释放资源 if (rs != null) { try { rs.close(); } catch (SQLException sqlEx) { } // ignore rs = null; } ... JDBC工具类构建 12345678910>>注册驱动1. 资源释放工作的整合2. 驱动防二次注册 DriverManager.registerDriver(new com.mysql.jdbc.Driver()); Driver 这个类里面有静态代码块,一上来就执行了,所以等同于我们注册了两次驱动。 其实没这个必要的。 //静态代码块 ---> 类加载了,就执行。 //java.sql.DriverManager.registerDriver(new Driver());3.最后形成以下代码即可。 Class.forName("com.mysql.jdbc.Driver"); >>连接对象整合 代码重整 123456789101112131415161718192021222324252627282930>>使用properties配置文件1. 在src底下声明一个文件 xxx.properties ,里面的内容吐下: driverClass=com.mysql.jdbc.Driver url=jdbc:mysql://localhost/student name=root password=root2. 在工具类里面,使用静态代码块,读取属性>>>>static{ try { //1. 创建一个属性配置对象 Properties properties = new Properties(); InputStream is = new FileInputStream("jdbc.properties"); //对应文件位于工程根目录 //使用类加载器,去读取src底下的资源文件。 后面在servlet //对应文件位于src目录底下 //InputStream is = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"); //导入输入流。 properties.load(is); //读取属性 driverClass = properties.getProperty("driverClass"); url = properties.getProperty("url"); name = properties.getProperty("name"); password = properties.getProperty("password"); } catch (Exception e) { e.printStackTrace(); } } SQL的CRUD语句 123456789>>insertINSERT INTO t_stu (NAME , age) VALUES ('wangqiang',28)INSERT INTO t_stu VALUES (NULL,'wangqiang2',28)>>deleteDELETE FROM t_stu WHERE id = 6>>querySELECT * FROM t_stu>>updateUPDATE t_stu SET age = 38 WHERE id = 1; 12345678910>>使用单元测试,测试代码1. 定义一个类, TestXXX , 里面定义方法 testXXX.2. 添加junit的支持。 右键工程 --- add Library --- Junit --- Junit43. 在方法的上面加上注解 , 其实就是一个标记。 @Test public void testQuery() { ... }4. 光标选中方法名字,然后右键执行单元测试。 或者是打开outline视图, 然后选择方法右键执行。 Dao模式 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849新建一个dao的接口, 里面声明数据库访问规则>>/** * 定义操作数据库的方法 */ public interface UserDao { /** * 查询所有 */ void findAll(); } 新建一个dao的实现类,具体实现早前定义的规则>>public class UserDaoImpl implements UserDao{ @Override public void findAll() { Connection conn = null; Statement st = null; ResultSet rs = null; try { //1. 获取连接对象 conn = JDBCUtil.getConn(); //2. 创建statement对象 st = conn.createStatement(); String sql = "select * from t_user"; rs = st.executeQuery(sql); while(rs.next()){ String userName = rs.getString("username"); String password = rs.getString("password"); System.out.println(userName+"="+password); } } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, st, rs); } }}直接使用实现>>@Test public void testFindAll(){ UserDao dao = new UserDaoImpl(); dao.findAll(); } Statement安全问题 12345678910Statement执行 ,其实是拼接sql语句的。 先拼接sql语句,然后在一起执行。 >>String sql = "select * from t_user where username='"+ username +"' and password='"+ password +"'"; UserDao dao = new UserDaoImpl(); dao.login("admin", "100234khsdf88' or '1=1"); SELECT * FROM t_user WHERE username='admin' AND PASSWORD='100234khsdf88' or '1=1' 前面先拼接sql语句, 如果变量里面带有了 数据库的关键字,那么一并认为是关键字。 不认为是普通的字符串。 rs = st.executeQuery(sql); PreparedStatement 123456789>>该对象就是替换前面的statement对象。>>相比较以前的statement, 预先处理给定的sql语句,对其执行语法检查。 在sql语句里面使用 ? 占位符来替代后续要传递进来的变量。 后面进来的变量值,将会被看成是字符串,不会产生任何的关键字。>>>>String sql = "insert into t_user values(null , ? , ?)"; ps = conn.prepareStatement(sql); //给占位符赋值 从左到右数过来,1 代表第一个问号, 永远你是1开始。 ps.setString(1, userName); ps.setString(2, password); HTTP&ServletHttp协议 1234567什么是协议>>双方在交互、通讯的时候, 遵守的一种规范、规则。http协议>>针对网络上的客户端 与 服务器端在执行http请求的时候,遵守的一种规范。 其实就是规定了客户端在访问服务器端的时候,要带上哪些东西, 服务器端返回数据的时候,也要带上什么东西。 >>版本 1.0 请求数据,服务器返回后, 将会断开连接 1.1 请求数据,服务器返回后, 连接还会保持着。 除非服务器 | 客户端 关掉。 有一定的时间限制,如果都空着这个连接,那么后面会自己断掉。 演示客户端如何与服务器端通讯 123456789在地址栏中键入网络地址 回车 或者是平常注册的时候,点击了注册按钮 , 浏览器都能显示出来一些东西。那么背地里到底浏览器和服务器是怎么通讯。 它们都传输了哪些数据。1. 安装抓包工具 HttpWatch (IE插件)2. 打开tomcat. 输入localhost:8080 打开首页3. 在首页上找到Example 字样 > 6.x 和 7.x 的文档页面有所不同,但是只要找到example就能够找到例子工程1. 选择 servlet 例子 ---> Request Parameter接着点击Request Parameters 的 Execute超链接执行tomcat的例子,然后查看浏览器和 tomcat服务器的对接细节 Http请求数据解释 12345678910111213141516171819202122232425262728293031323334353637请求的数据里面包含三个部分内容 : 请求行 、 请求头 、请求体>>请求行POST /examples/servlets/servlet/RequestParamExample HTTP/1.1 POST : 请求方式 ,以post去提交数据/examples/servlets/servlet/RequestParamExample请求的地址路径 , 就是要访问哪个地方。HTTP/1.1 协议版本>>请求头Accept: application/x-ms-application, image/jpeg, application/xaml+xml, image/gif, image/pjpeg, application/x-ms-xbap, */* Referer: http://localhost:8080/examples/servlets/servlet/RequestParamExample Accept-Language: zh-CN User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E) Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate Host: localhost:8080 Content-Length: 31 Connection: Keep-Alive Cache-Control: no-cacheAccept: 客户端向服务器端表示,我能支持什么类型的数据。 Referer : 真正请求的地址路径,全路径Accept-Language: 支持语言格式User-Agent: 用户代理 向服务器表明,当前来访的客户端信息。 Content-Type: 提交的数据类型。经过urlencoding编码的form表单的数据Accept-Encoding: gzip, deflate : 压缩算法 。 Host : 主机地址Content-Length: 数据长度Connection : Keep-Alive 保持连接Cache-Control : 对缓存的操作>>请求体浏览器真正发送给服务器的数据 发送的数据呈现的是key=value ,如果存在多个数据,那么使用 & firstname=zhang&lastname=sansan Http响应数据解析 12345678910111213141516171819202122232425262728请求的数据里面包含三个部分内容 : 响应行 、 响应头 、响应体HTTP/1.1 200 OKServer: Apache-Coyote/1.1Content-Type: text/html;charset=ISO-8859-1Content-Length: 673Date: Fri, 17 Feb 2017 02:53:02 GMT...这里还有很多数据...>>响应行HTTP/1.1 200 OK协议版本 状态码 咱们这次交互到底是什么样结果的一个code. 200 : 成功,正常处理,得到数据。 403 : for bidden 拒绝 404 : Not Found 500 : 服务器异常OK 对应前面的状态码 >>响应头Server: 服务器是哪一种类型。 TomcatContent-Type : 服务器返回给客户端你的内容类型Content-Length : 返回的数据长度Date : 通讯的日期,响应的时间 Get和Post请求区别 123456>>post数据是以流的方式写过去,不会在地址栏上面显示。 现在一般提交数据到服务器使用的都是POST以流的方式写数据,所以数据没有大小限制。>>get会在地址栏后面拼接数据,所以有安全隐患。 一般从服务器获取数据,并且客户端也不用提交上面数据的时候,可以使用GET能够带的数据有限, 1kb大小 Web资源 12345678910在http协议当中,规定了请求和响应双方, 客户端和服务器端。与web相关的资源。 有两种分类>>静态资源 html 、 js、 css>>动态资源 servlet/jspServlet>>servlet是什么?>>>>其实就是一个java程序,运行在我们的web服务器上,用于接收和响应 客户端的http请求。 >>>>更多的是配合动态资源来做。 当然静态资源也需要使用到servlet,只不过是Tomcat里面已经定义好了一个 DefaultServlet Hello Servlet 12345678910111213141516171. 得写一个Web工程 , 要有一个服务器。2. 测试运行Web工程 1. 新建一个类, 实现Servlet接口 1. 配置Servlet , 用意: 告诉服务器,我们的应用有这么些个servlet。 在webContent/WEB-INF/web.xml里面写上以下内容。 <!-- 向tomcat报告, 我这个应用里面有这个servlet, 名字叫做HelloServlet , 具体的路径是com.itheima.servlet.HelloServlet --> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.itheima.servlet.HelloServlet</servlet-class> </servlet> <!-- 注册servlet的映射。 servletName : 找到上面注册的具体servlet, url-pattern: 在地址栏上的path 一定要以/打头 --> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/a</url-pattern> </servlet-mapping> 3. 在地址栏上输入 http://localhost:8080/项目名称/a Servlet执行过程 Servlet的通用写法 1234567Servlet (接口) | |GenericServlet | |HttpServlet (用于处理http的请求) 定义一个类,继承HttpServlet 复写doGet和doPost Servlet的生命周期 1234567891011121314151617生命周期>>从创建到销毁的一段时间生命周期方法>>从创建到销毁,所调用的那些方法。- init方法 在创建该servlet的实例时,就执行该方法。 一个servlet只会初始化一次, init方法只会执行一次 默认情况下是 : 初次访问该servlet,才会创建实例。 - service方法 只要客户端来了一个请求,那么就执行这个方法了。 该方法可以被执行很多次。 一次请求,对应一次service方法的调用- destroy方法 servlet销毁的时候,就会执行该方法 1. 该项目从tomcat的里面移除。 2. 正常关闭tomcat就会执行 shutdown.bat doGet 和 doPost不算生命周期方法,所谓的生命周期方法是指,从对象的创建到销毁一定会执行的方法, 但是这两个方法,不一定会执行。 让Servlet创建实例的时机提前 123456781. 默认情况下,只有在初次访问servlet的时候,才会执行init方法。 有的时候,我们可能需要在这个方法里面执行一些初始化工作,甚至是做一些比较耗时的逻辑。 2. 那么这个时候,初次访问,可能会在init方法中逗留太久的时间。 那么有没有方法可以让这个初始化的时机提前一点。 3. 在配置的时候, 使用load-on-startup元素来指定, 给定的数字越小,启动的时机就越早。 一般不写负数, 从2开始即可。 <servlet> <servlet-name>HelloServlet04</servlet-name> <servlet-class>com.itheima.servlet.HelloServlet04</servlet-class> <load-on-startup>2</load-on-startup> </servlet> ServletConfig 123456789101112131415161718192021>>Servlet的配置,通过这个对象,可以获取servlet在配置的时候一些信息>>先说 , 在写怎么用, 最后说有什么用。//1. 得到servlet配置对象 专门用于在配置servlet的信息 ServletConfig config = getServletConfig();//获取到的是配置servlet里面servlet-name 的文本内容 String servletName = config.getServletName(); System.out.println("servletName="+servletName); //2、。 可以获取具体的某一个参数。 String address = config.getInitParameter("address"); System.out.println("address="+address);//3.获取所有的参数名称 Enumeration<String> names = config.getInitParameterNames(); //遍历取出所有的参数名称 while (names.hasMoreElements()) { String key = (String) names.nextElement(); String value = config.getInitParameter(key); System.out.println("key==="+key + " value="+value); } 为什么需要有这个ServletConfig 123456789101112131415161718192021221. 未来我们自己开发的一些应用,使用到了一些技术,或者一些代码,我们不会。 但是有人写出来了。它的代码放置在了自己的servlet类里面。 2. 刚好这个servlet 里面需要一个数字或者叫做变量值。 但是这个值不能是固定了。 所以要求使用到这个servlet的公司,在注册servlet的时候,必须要在web.xml里面,声明init-params在开发当中比较少用。##总结- Http协议 1. 使用HttpWacht 抓包看一看http请求背后的细节。 1. 基本了解 请求和响应的数据内容 请求行、 请求头 、请求体 响应行、响应头、响应体 2. Get和Post的区别- Servlet【重点】 1. 会使用简单的servlet 1.写一个类,实现接口Servlet 2. 配置Servlet 3. 会访问Setvlet 1. Servlet的生命周期 init 一次 创建对象 默认初次访问就会调用或者可以通过配置,让它提前 load-on-startup service 多次,一次请求对应一次service destory 一次 销毁的时候 从服务器移除 或者 正常关闭服务器 2. ServletConfig 获取配置的信息, params HTTPServletReauest&HTTPServletResponseServlet配置方式 123456789--全路径匹配> 以 / 开始 /a /aa/bb> localhost:8080/项目名称/aa/bb --路径匹配 , 前半段匹配> 以 / 开始 , 但是以 * 结束 /a/* /* > - 其实是一个通配符,匹配任意文字> localhost:8080/项目名称/aa/bb --以扩展名匹配> 写法: 没有/ 以 * 开始 *.扩展名 *.aa *.bb ServletContext 12> Servlet 上下文> 每个web工程都只有一个ServletContext对象。 说白了也就是不管在哪个servlet里面,获取到的这个类的对象都是同一个。 如何得到对象 12//1. 获取对象 ServletContext context = getServletContext(); 有什么作用 1231. 获取全局配置参数2. 获取web工程中的资源3. 存取数据,servlet间共享数据 域对象 可以获取全局配置参数 获取全局参数 可以获取Web应用中的资源 1234567891. 获取资源在tomcat里面的绝对路径 先得到路径,然后自己new InpuStream context.getRealPath("") //这里得到的是项目在tomcat里面的根目录。 D:\tomcat\apache-tomcat-7.0.52\apache-tomcat-7.0.52\wtpwebapps\Demo03\ String path = context.getRealPath("file/config.properties"); D:\tomcat\apache-tomcat-7.0.52\apache-tomcat-7.0.52\wtpwebapps\Demo03\file\config.properties 2. getResourceAsStream 获取资源 流对象 直接给相对的路径,然后获取流对象。 通过classloader去获取web工程下的资源 使用ServletContext存取数据 1.定义一个登陆的html页面, 定义一个form表单 2.定义一个Servlet,名为LoginServlet 3.针对成功或者失败,进行判断,然后跳转到不一样的网页 ServletContext存取值分析 1234567891011<!-- A路径: Servlet的路径 http://localhost:8080/Demo4/loginB路径: 当前这个html的路径: http://localhost:8080/Demo4/login.html --><form action="login" method="get"> 账号:<input type="text" name="username"/><br> 密码:<input type="text" name="password"/><br> <input type="submit" value="登录"/></form> ServletContext 何时创建, 何时销毁? 12服务器启动的时候,会为托管的每一个web应用程序,创建一个ServletContext对象从服务器移除托管,或者是关闭服务器。 HttpServletRequest 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061这个对象封装了客户端提交过来的一切数据。可以获取客户端请求头信息1.获取客户端提交过来的数据//得到一个枚举集合 Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = (String) headerNames.nextElement(); String value = request.getHeader(name); System.out.println(name+"="+value); }2.获取客户端提交过来的数据 String name = request.getParameter("name"); String address = request.getParameter("address"); System.out.println("name="+name); System.out.println("address="+address); ------------------------------------------------- //name=zhangsan&name=lisi&name=wangwu 一个key可以对应多个值。 Map<String, String[]> map = request.getParameterMap(); Set<String> keySet = map.keySet(); Iterator<String> iterator = keySet.iterator(); while (iterator.hasNext()) { String key = (String) iterator.next(); System.out.println("key="+key + "--的值总数有:"+map.get(key).length); String value = map.get(key)[0]; String value1 = map.get(key)[1]; String value2 = map.get(key)[2]; System.out.println(key+" ======= "+ value + "=" + value1 + "="+ value2); }3.获取中文数据客户端提交数据给服务器端,如果数据中带有中文的话,有可能会出现乱码情况,那么可以参照以下方法解决。如果是GET方式1. 代码转码 String username = request.getParameter("username"); String password = request.getParameter("password"); >>System.out.println("userName="+username+"==password="+password);//get请求过来的数据,在url地址栏上就已经经过编码了,所以我们取到的就是乱码,//tomcat收到了这批数据,getParameter 默认使用ISO-8859-1去解码//先让文字回到ISO-8859-1对应的字节数组 , 然后再按utf-8组拼字符串username = new String(username.getBytes("ISO-8859-1") , "UTF-8");System.out.println("userName="+username+"==password="+password);直接在tomcat里面做配置,以后get请求过来的数据永远都是用UTF-8编码。 2.可以在tomcat里面做设置处理 conf/server.xml 加上URIEncoding="utf-8" <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8"/> 如果是POST方式这个说的是设置请求体里面的文字编码。 get方式,用这行,有用吗? ---> 没用 request.setCharacterEncoding("UTF-8"); 这行设置一定要写在getParameter之前。 HttpServletResponse 12345678负责返回数据给客户端。输出数据到页面上 //以字符流的方式写数据 //response.getWriter().write("<h1>hello response...</h1>"); //以字节流的方式写数据 response.getOutputStream().write("hello response2222...".getBytes()); 响应的数据中有中文,那么有可能出现中文乱码 1234567891011121314151617--以字符流输出> response.getWriter()//1. 指定输出到客户端的时候,这些文字使用UTF-8编码 response.setCharacterEncoding("UTF-8"); //2. 直接规定浏览器看这份数据的时候,使用什么编码来看。 response.setHeader("Content-Type", "text/html; charset=UTF-8"); response.getWriter().write("我爱黑马训练营...");--以字节流输出> response.getOutputStream() //1. 指定浏览器看这份数据使用的码表 response.setHeader("Content-Type", "text/html;charset=UTF-8"); //2. 指定输出的中文用的码表 response.getOutputStream().write("我爱深圳黑马训练营..".getBytes("UTF-8")); 不管是字节流还是字符流,直接使用一行代码就可以了。 12response.setContentType("text/html;charset=UTF-8");然后在写数据即可。 演练下载资源 12345671.直接以超链接的方式下载,不写任何代码。 也能够下载东西下来。 让tomcat的默认servlet去提供下载:<br><a href="download/aa.jpg">aa.jpg</a><br><a href="download/bb.txt">bb.txt</a><br><a href="download/cc.rar">cc.rar</a><br>原因是tomcat里面有一个默认的Servlet -- DefaultServlet 。这个DefaultServlet 专门用于处理放在tomcat服务器上的静态资源。 总结 1234567891011121314151617181. Servlet注册方式 2. ServletContext【重点】>>作用:1. 获取全局参数2. 获取工程里面的资源。3. 资源共享。 ServletContext 域对象>>有几个 一个 >>什么时候创建 ? 什么时候销毁服务器启动的时候给每一个应用都创建一个ServletContext对象, 服务器关闭的时候销毁简单登录3. HttpServletRequest【重点】 1. 获取请求头 1. 获取提交过来的数据4. HttpServletResponse【重点】 负责输出数据到客户端,其实就是对之前的请求作出响应5. 中文乱码问题。【重点】6. 下载 作业: 完成注册 完成登录 V1.1 最好配合上数据库,完成注册和登录的功能。 Cookie&&Session 中文文件下载 1234567891011121314151617针对浏览器类型,对文件名字做编码处理 Firefox (Base64) , IE、Chrome ... 使用的是URLEncoder /* * 如果文件的名字带有中文,那么需要对这个文件名进行编码处理 * 如果是IE ,或者 Chrome (谷歌浏览器) ,使用URLEncoding 编码 * 如果是Firefox , 使用Base64编码 */ //获取来访的客户端类型 String clientType = request.getHeader("User-Agent"); if(clientType.contains("Firefox")){ fileName = DownLoadUtil.base64EncodeFileName(fileName); }else{ //IE ,或者 Chrome (谷歌浏览器) , //对中文的名字进行编码处理 fileName = URLEncoder.encode(fileName,"UTF-8"); } 请求转发和重定向 12345678910111213141516171819202122232425262728293031323334重定向 /* 之前的写法 response.setStatus(302); response.setHeader("Location", "login_success.html");*/ //重定向写法: 重新定位方向 参数即跳转的位置 response.sendRedirect("login_success.html"); 1. 地址上显示的是最后的那个资源的路径地址 2. 请求次数最少有两次, 服务器在第一次请求后,会返回302 以及一个地址, 浏览器在根据这个地址,执行第二次访问。 3. 可以跳转到任意路径。 不是自己的工程也可以跳。 4. 效率稍微低一点, 执行两次请求。 5. 后续的请求,没法使用上一次的request存储的数据,或者 没法使用上一次的request对象,因为这是两次不同的请求。---------------------------------------------------------------------------------请求转发 //请求转发的写法: 参数即跳转的位置 request.getRequestDispatcher("login_success.html").forward(request, response); 1. 地址上显示的是请求servlet的地址。 返回200 ok 2. 请求次数只有一次, 因为是服务器内部帮客户端执行了后续的工作。 3. 只能跳转自己项目的资源路径 。 4. 效率上稍微高一点,因为只执行一次请求。 5. 可以使用上一次的request对象。 Cookie 1234饼干. 其实是一份小数据, 是服务器给客户端,并且存储在客户端上的一份小数据### 应用场景> 自动登录、浏览记录、购物车。 为什么要有这个Cookie 1http的请求是无状态。 客户端与服务器在通讯的时候,是无状态的,其实就是客户端在第二次来访的时候,服务器根本就不知道这个客户端以前有没有来访问过。 为了更好的用户体验,更好的交互 [自动登录],其实从公司层面讲,就是为了更好的收集用户习惯[大数据] Cookie怎么用 12345678#### 简单使用:- 添加Cookie给客户端 1. 在响应的时候,添加cookie Cookie cookie = new Cookie("aa", "bb");//给响应,添加一个cookieresponse.addCookie(cookie); 2.客户端收到的信息里面,响应头中多了一个字段 Set-Cookie 3. 123456789101112131415161718192021222324252627获取客户端带过来的Cookie//获取客户端带过来的cookie Cookie[] cookies = request.getCookies(); if(cookies != null){ for (Cookie c : cookies) { String cookieName = c.getName(); String cookieValue = c.getValue(); System.out.println(cookieName + " = "+ cookieValue); } } 常用方法 //关闭浏览器后,cookie就没有了。 ---> 针对没有设置cookie的有效期。 // expiry: 有效 以秒计算。 //正值 : 表示 在这个数字过后,cookie将会失效。 //负值: 关闭浏览器,那么cookie就失效, 默认值是 -1 cookie.setMaxAge(60 * 60 * 24 * 7); //赋值新的值 //cookie.setValue(newValue); //用于指定只有请求了指定的域名,才会带上该cookie cookie.setDomain(".itheima.com"); //只有访问该域名下的cookieDemo的这个路径地址才会带cookie cookie.setPath("/CookieDemo"); 例子一 显示最近访问的时间。 12345678910111213141516171819202122232425262728293031323334351.判断账号是否正确2. 如果正确,则获取cookie。 但是得到的cookie是一个数组, 我们要从数组里面找到我们想要的对象。3. 如果找到的对象为空,表明是第一次登录。那么要添加cookie4. 如果找到的对象不为空, 表明不是第一次登录。 if("admin".equals(userName) && "123".equals(password)){ //获取cookie last-name --- > Cookie [] cookies = request.getCookies(); //从数组里面找出我们想要的cookie Cookie cookie = CookieUtil.findCookie(cookies, "last"); //是第一次登录,没有cookie if(cookie == null){ Cookie c = new Cookie("last", System.currentTimeMillis()+""); c.setMaxAge(60*60); //一个小时 response.addCookie(c); response.getWriter().write("欢迎您, "+userName); }else{ //1. 去以前的cookie第二次登录,有cookie long lastVisitTime = Long.parseLong(cookie.getValue()); //2. 输出到界面, response.getWriter().write("欢迎您, "+userName +",上次来访时间是:"+new Date(lastVisitTime)); //3. 重置登录的时间 cookie.setValue(System.currentTimeMillis()+""); response.addCookie(cookie); } }else{ response.getWriter().write("登陆失败 "); } 例子二: 显示商品浏览记录。 1234567891011121314151617###准备工作1. 拷贝基础课第一天的 htmll原型文件,到工程的WebContent里面。2. 在WebContent目录下新建一个jsp文件, product_list.jsp, 然后拷贝原来product_list.html的内容到jsp里面。 建好之后,jsp里面的所有ISO-8859-1 改成 UTF-8 拷贝html标签的所有内容。 替换jsp的html标签即可3. 修改product_info.htm里面的手机数码超链接地址 <li class="active"><a href="product_list.jsp">手机数码<span class="sr-only">(current)</span></a></li>4. 修改首页(index.html)顶部的手机数码跳转的位置为 product_list.jsp <li class="active"><a href="product_list.jsp">手机数码<span class="sr-only">(current)</span></a></li>###分析 Jsp 里面使用Java代码 123456789- jsp> Java Server Pager ---> 最终会翻译成一个类, 就是一个Servlet- 定义全局变量 <%! int a = 99; %>- 定义局部变量 <% int b = 999; %>- 在jsp页面上,显示 a 和 b的值, <%=a %> <%=b %> jsp显示浏览记录 清除浏览记录 123456其实就是清除Cookie, 删除cookie是没有什么delete方法的。只有设置maxAge 为0 。 Cookie cookie = new Cookie("history",""); cookie.setMaxAge(0); //设置立即删除 cookie.setPath("/CookieDemo02"); response.addCookie(cookie); Cookie总结 1234567891011121314151617181920211. 服务器给客户端发送过来的一小份数据,并且存放在客户端上。2. 获取cookie, 添加cookie request.getCookie(); response.addCookie();3. Cookie分类 会话Cookie 默认情况下,关闭了浏览器,那么cookie就会消失。 持久Cookie 在一定时间内,都有效,并且会保存在客户端上。 cookie.setMaxAge(0); //设置立即删除cookie.setMaxAge(100); //100 秒4. Cookie的安全问题。由于Cookie会保存在客户端上,所以有安全隐患问题。 还有一个问题, Cookie的大小与个数有限制。 为了解决这个问题 ---> Session . Session 1234567891011121314151617181920会话 , Session是基于Cookie的一种会话机制。 Cookie是服务器返回一小份数据给客户端,并且存放在客户端上。 Session是,数据存放在服务器端。常用API //得到会话ID String id = session.getId(); //存值 session.setAttribute(name, value); //取值 session.getAttribute(name); //移除值 session.removeAttribute(name); - Session何时创建 , 何时销毁?- 创建> 如果有在servlet里面调用了 request.getSession()- 销毁> session 是存放在服务器的内存中的一份数据。 当然可以持久化. Redis . 即使关了浏览器,session也不会销毁。> 1. 关闭服务器> 1. session会话时间过期。 有效期过了,默认有效期: 30分钟。 简单购物车 CartServlet 代码 1234567891011121314151617181920212223242526272829response.setContentType("text/html;charset=utf-8"); //1. 获取要添加到购物车的商品id int id = Integer.parseInt(request.getParameter("id")); // 0 - 1- 2 -3 -4 String [] names = {"Iphone7","小米6","三星Note8","魅族7" , "华为9"}; //取到id对应的商品名称 String name = names[id]; //2. 获取购物车存放东西的session Map<String , Integer> iphoen7 3 //把一个map对象存放到session里面去,并且保证只存一次。 Map<String, Integer> map = (Map<String, Integer>) request.getSession().getAttribute("cart"); //session里面没有存放过任何东西。 if(map == null){ map = new LinkedHashMap<String , Integer>(); request.getSession().setAttribute("cart", map); } //3. 判断购物车里面有没有该商品 if(map.containsKey(name)){ //在原来的值基础上 + 1 map.put(name, map.get(name) + 1 ); }else{ //没有购买过该商品,当前数量为1 。 map.put(name, 1); } //4. 输出界面。(跳转) response.getWriter().write("<a href='product_list.jsp'><h3>继续购物</h3></a><br>"); response.getWriter().write("<a href='cart.jsp'><h3>去购物车结算</h3></a>"); 移除Session中的元素 12345//强制干掉会话,里面存放的任何数据就都没有了。session.invalidate();//从session中移除某一个数据//session.removeAttribute("cart"); 总结: 1234567891011121314151617181920212223242526272829303132- 请求转发和重定向(面试经常问。)- Cookie 服务器给客户端发送一小份数据, 存放在客户端上。 基本用法: 添加cookie 获取cookie。演练例子:1. 获取上一次访问时间2. 获取商品浏览记录。- 什么时候有cookie response.addCookie(new Cookie())- Cookie 分类>>会话Cookie 关闭浏览器,就失效>>持久cookie 存放在客户端上。 在指定的期限内有效。 setMaxAge(); - Session也是基于cookie的一种会话技术, 数据存放存放在服务器端会在cookie里面添加一个字段 JSESSIONID . 是tomcat服务器生成。 >>setAttribute 存数据>>getAttribute 取数据>>removeAttribute 移除数据>>getSessionId(); 获取会话id>>invalidate() 强制让会话失效。- 创建和销毁 ,调用request.getSesion创建 服务器关闭 , 会话超时(30分) setAttribute 存放的值, 在浏览器关闭后,还有没有。 有!,就算客户端把电脑砸了也还有。 JSP & EL & JSTL jsp 1234567Java Server Page - 什么是jsp> 从用户角度看待 ,就是是一个网页 , 从程序员角度看待 , 其实是一个java类, 它继承了servlet,所以可以直接说jsp 就是一个Servlet.- 为什么会有jsp?> html 多数情况下用来显示静态内容 , 一成不变的。 但是有时候我们需要在网页上显示一些动态数据, 比如: 查询所有的学生信息, 根据姓名去查询具体某个学生。 这些动作都需要去查询数据库,然后在网页上显示。 html是不支持写java代码 , jsp里面可以写java代码。 怎么用JSP 12指令写法<%@ 指令名字 %> 123456789101112131415161718page指令- language>>>>表明jsp页面中可以写java代码- contentType>>>>其实即使说这个文件是什么类型,告诉浏览器我是什么内容类型,以及使用什么编码 contentType="text/html; charset=UTF-8" text/html MIMEType 这是一个文本,html网页- pageEncoding jsp内容编码- extends 用于指定jsp翻译成java文件后,继承的父类是谁,一般不用改。- import 导包使用的,一般不用手写。- session >>>>值可选的有true or false . 用于控制在这个jsp页面里面,能够直接使用session对象。 具体的区别是,请看翻译后的java文件 如果该值是true , 那么在代码里面会有getSession()的 调用,如果是false : 那么就不会有该方法调用,也就是没有session对象了。在页面上自然也就不 能使用session了。- errorPage>>>>指的是错误的页面, 值需要给错误的页面路径- isErrorPage>>>>上面的errorPage 用于指定错误的时候跑到哪一个页面去。 那么这个isErroPage , 就是声明某一个 页面到底是不是错误的页面。 12345include指令> 包含另外一个jsp的内容进来。 <%@ include file="other02.jsp"%>- 背后细节:> 把另外一个页面的所有内容拿过来一起输出。 所有的标签元素都包含进来。 1234taglib指令<%@ taglib prefix="" uri=""%> uri: 标签库路径prefix : 标签库的别名 JSP 动作标签 123456789101112131415161718192021222324<jsp:include page=""></jsp:include><jsp:param value="" name=""/><jsp:forward page=""></jsp:forward>jsp:include<jsp:include page="other02.jsp"></jsp:include>包含指定的页面, 这里是动态包含。 也就是不把包含的页面所有元素标签全部拿过来输出,而是把它的运行结果拿过来。 jsp:forward<jsp:forward page=""></jsp:forward>前往哪一个页面。<% //请求转发 request.getRequestDispatcher("other02.jsp").forward(request, response);%> jsp:param意思是: 在包含某个页面的时候,或者在跳转某个页面的时候,加入这个参数。 <jsp:forward page="other02.jsp"> <jsp:param value="beijing" name="address"/> </jsp:forward> 在other02.jsp中获取参数 <br>收到的参数是:<br><%= request.getParameter("address")%> JSP内置对象 12345678910111213141516171819202122232425262728> 所谓内置对象,就是我们可以直接在jsp页面中使用这些对象。 不用创建。- pageContext- request- session- application以上4个是作用域对象,- 作用域 表示这些对象可以存值,他们的取值范围有限定。 setAttribute 和 getAttribute使用作用域来存储数据<br> <% pageContext.setAttribute("name", "page"); request.setAttribute("name", "request"); session.setAttribute("name", "session"); application.setAttribute("name", "application"); %> 取出四个作用域中的值<br> <%=pageContext.getAttribute("name")%> <%=request.getAttribute("name")%> <%=session.getAttribute("name")%> <%=application.getAttribute("name")%> - 作用域范围大小:pageContext -- request --- session -- application 四个作用域的区别 12345678910111213141516171819202122- pageContext 【PageContext】> 作用域仅限于当前的页面。 > 还可以获取到其他八个内置对象。- request 【HttpServletRequest】> 作用域仅限于一次请求, 只要服务器对该请求做出了响应。 这个域中存的值就没有了。- session 【HttpSession】> 作用域限于一次会话(多次请求与响应) 当中。 - application 【ServletContext】> 整个工程都可以访问, 服务器关闭后就不能访问了。 - out 【JspWriter】- response 【HttpServletResponse】- exception 【Throwable】- page 【Object】 ---就是这个jsp翻译成的java类的实例对象- config 【ServletConfig】 EL表达式 1234> 是为了简化咱们的jsp代码,具体一点就是为了简化在jsp里面写的那些java代码。- 写法格式${表达式 }> 如果从作用域中取值,会先从小的作用域开始取,如果没有,就往下一个作用域取。 一直把四个作用域取完都没有, 就没有显示。 如何使用 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657581. 取出4个作用域中存放的值。<% pageContext.setAttribute("name", "page"); request.setAttribute("name", "request"); session.setAttribute("name", "session"); application.setAttribute("name", "application"); %> 按普通手段取值<br> <%= pageContext.getAttribute("name")%> <%= request.getAttribute("name")%> <%= session.getAttribute("name")%> <%= application.getAttribute("name")%> <br>使用EL表达式取出作用域中的值<br> ${ pageScope.name } ${ requestScope.name } ${ sessionScope.name } ${ applicationScope.name } 2.如果域中所存的是数组<% String [] a = {"aa","bb","cc","dd"}; pageContext.setAttribute("array", a);%>使用EL表达式取出作用域中数组的值<br> ${array[0] } , ${array[1] },${array[2] },${array[3] }3.如果域中锁存的是集合 使用EL表达式取出作用域中集合的值<br> ${li[0] } , ${li[1] },${li[2] },${li[3] } <br>-------------Map数据----------------<br> <% Map map = new HashMap(); map.put("name", "zhangsna"); map.put("age",18); map.put("address","北京.."); map.put("address.aa","深圳.."); pageContext.setAttribute("map", map); %> 4.取出Map集合的值<% Map map = new HashMap(); map.put("name", "zhangsna"); map.put("age",18); map.put("address","北京.."); map.put("address.aa","深圳.."); pageContext.setAttribute("map", map); %> 使用EL表达式取出作用域中Map的值<br> ${map.name } , ${map.age } , ${map.address } , ${map["address.aa"] } 取值细节: 12345678910111213141516171819202122232425262728291. 从域中取值。得先存值。 <% //pageContext.setAttribute("name", "zhangsan"); session.setAttribute("name", "lisi..."); %> <br>直接指定说了,到这个作用域里面去找这个name<br> ${ pageScope.name } <br>//先从page里面找,没有去request找,去session,去application <br> ${ name } <br>指定从session中取值<br> ${ sessionScope.name } 2. 取值方式如果这份值是有下标的,那么直接使用[]<% String [] array = {"aa","bb","cc"} session.setAttribute("array",array);%>${ array[1] } --> 这里array说的是attribute的name 如果没有下标, 直接使用 .的方式去取<% User user = new User("zhangsan",18); session.setAttribute("u", user);%>${ u.name } , ${ u.age } 一般使用EL表达式,用的比较多的,都是从一个对象中取出它的属性值,比如取出某一个学生的姓名。 EL表达式 的11个内置对象。 123456789101112131415161718192021${ 对象名.成员 }- pageContext 作用域相关对象- pageScope- requestScope- sessionScope- applicationScope头信息相关对象- header- headerValues参数信息相关对象- param- paramValues- cookie 全局初始化参数- initParam JSTL 123456789> 全称 : JSP Standard Tag Library jsp标准标签库> 简化jsp的代码编写。 替换 <%%> 写法。 一般与EL表达式配合###怎么使用1. 导入jar文件到工程的WebContent/Web-Inf/lib jstl.jar standard.jar2. 在jsp页面上,使用taglib 指令,来引入标签库3. 注意: 如果想支持 EL表达式,那么引入的标签库必须选择1.1的版本,1.0的版本不支持EL表达式。<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 常用标签 1234567891011121314151617181920212223242526272829303132333435363738394041<c:set></c:set><c:if test=""></c:if><c:forEach></c:forEach>- c:set <!-- 声明一个对象name, 对象的值 zhangsan , 存储到了page(默认) , 指定是session --> <c:set var="name" value="zhangsan" scope="session"></c:set> ${sessionScope.name } - c:if> 判断test里面的表达式是否满足,如果满足,就执行c:if标签中的输出 , c:if 是没有else的。 <c:set var="age" value="18" ></c:set> <c:if test="${ age > 26 }"> 年龄大于了26岁... </c:if> <c:if test="${ age <= 26 }"> 年龄小于了26岁... </c:if> ------------------------------ 定义一个变量名 flag 去接收前面表达式的值,然后存在session域中 <c:if test="${ age > 26 }" var="flag" scope="session"> 年龄大于了26岁... </c:if> - c:forEach 从1 开始遍历到10 ,得到的结果 ,赋值给 i ,并且会存储到page域中, step , 增幅为2, <c:forEach begin="1" end="10" var="i" step="2"> ${i } </c:forEach> ----------------------------------------------- <!-- items : 表示遍历哪一个对象,注意,这里必须写EL表达式。 var: 遍历出来的每一个元素用user 去接收。 --> <c:forEach var="user" items="${list }"> ${user.name } ----${user.age } </c:forEach> 学生信息管理系统 需求分析 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711. 先写 login.jsp , 并且搭配一个LoginServlet 去获取登录信息。2. 创建用户表, 里面只要有id , username 和 password3. 创建UserDao, 定义登录的方法 /** - 该dao定义了对用户表的访问规则 */ public interface UserDao { 1. - - 这里简单就返回一个Boolean类型, 成功或者失败即可。 - - 但是开发的时候,登录的方法,一旦成功。这里应该返回该用户的个人信息 - @param userName - @param password - - @return true : 登录成功, false : 登录失败。 */ boolean login(String userName , String password); }4. 创建UserDaoImpl , 实现刚才定义的登录方法。 public class UserDaoImpl implements UserDao { @Override public boolean login(String userName , String password) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { //1. 得到连接对象 conn = JDBCUtil.getConn(); String sql = "select * from t_user where username=? and password=?"; //2. 创建ps对象 ps = conn.prepareStatement(sql); ps.setString(1, userName); ps.setString(2, password); //3. 开始执行。 rs = ps.executeQuery(); //如果能够成功移到下一条记录,那么表明有这个用户。 return rs.next(); } catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, ps, rs); } return false; } }5. 在LoginServlet里面访问UserDao, 判断登录结果。 以区分对待6. 创建stu_list.jsp , 让登录成功的时候跳转过去。7. 创建学生表 , 里面字段随意。 8. 定义学生的Dao . StuDao public interface StuDao { /** * 查询出来所有的学生信息 * @return List集合 */ List<Student> findAll(); } 9.对上面定义的StuDao 做出实现 StuDaoImplpublic class StuDaoImpl implements StuDao { @Override public List<Student> findAll() { List<Student> list = new ArrayList<Student>(); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { //1. 得到连接对象 conn = JDBCUtil.getConn(); String sql = "select * from t_stu"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); //数据多了,用对象装, 对象也多了呢? 用集合装。 while(rs.next()){ //10 次 ,10个学生 Student stu = new Student(); stu.setId(rs.getInt("id")); stu.setAge(rs.getInt("age")); stu.setName(rs.getString("name")); stu.setGender(rs.getString("gender")); stu.setAddress(rs.getString("address")); list.add(stu); } } catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, ps, rs); } return list; } } 10. 在登录成功的时候,完成三件事情。11. 查询所有的学生 1. 把这个所有的学生集合存储到作用域中。 2. 跳转到stu_list.jsp protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //提交的数据有可能有中文, 怎么处理。 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=utf-8"); //1. 获取客户端提交的信息 String userName = request.getParameter("username"); String password = request.getParameter("password"); //2. 去访问dao , 看看是否满足登录。 UserDao dao = new UserDaoImpl(); boolean isSuccess = dao.login(userName, password); //3. 针对dao的返回结果,做出响应 if(isSuccess){ //response.getWriter().write("登录成功."); //1. 查询出来所有的学生信息。 StuDao stuDao = new StuDaoImpl(); List<Student> list = stuDao.findAll(); //2. 先把这个集合存到作用域中。 request.getSession().setAttribute("list", list); //2. 重定向 response.sendRedirect("stu_list.jsp"); }else{ response.getWriter().write("用户名或者密码错误!"); } }11.在stu_list.jsp中,取出域中的集合,然后使用c标签 去遍历集合。 <table border="1" width="700"> <tr align="center"> <td>编号</td> <td>姓名</td> <td>年龄</td> <td>性别</td> <td>住址</td> <td>操作</td> </tr> <c:forEach items="${list }" var="stu"> <tr align="center"> <td>${stu.id }</td> <td>${stu.name }</td> <td>${stu.age }</td> <td>${stu.gender }</td> <td>${stu.address }</td> <td><a href="#">更新</a> <a href="#">删除</a></td> </tr> </c:forEach></table> 总结: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253JSP三大指令 page include taglib三个动作标签 <jsp:include> <jsp:forward> <jsp:param> >>>>九个内置对象>>四个作用域pageContextrequestsessionapplicationoutexceptionresponsepageconfig>>EL${ 表达式 }取4个作用域中的值${ name }>>>有11个内置对象。 pageContextpageScoperequestScopesessionScopeapplicationScopeheaderheaderValuesparamparamValuescookieinitParamJSTL> 使用1.1的版本, 支持EL表达式, 1.0不支持EL表达式> 拷贝jar包, 通过taglib 去引入标签库<c:set><c:if><c:forEach> 事务&数据库连接池&DBUtils事务 1234> Transaction 其实指的一组操作,里面包含许多个单一的逻辑。只要有一个逻辑没有执行成功,那么都算失败。 所有的数据都回归到最初的状态(回滚) - 为什么要有事务?为了确保逻辑的成功。 例子: 银行的转账。 使用命令行方式演示事务。 12345- 开启事务 start transaction;- 提交或者回滚事务 commit; 提交事务, 数据将会写到磁盘上的数据库 rollback ; 数据回滚,回到最初的状态。 1.关闭自动提交功能。 2.演示事务 使用代码方式演示事务 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849> 代码里面的事务,主要是针对连接来的。 > 1. 通过conn.setAutoCommit(false )来关闭自动提交的设置。> 2. 提交事务 conn.commit();> 3. 回滚事务 conn.rollback();@Testpublic void testTransaction(){ Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = JDBCUtil.getConn(); //连接,事务默认就是自动提交的。 关闭自动提交。 conn.setAutoCommit(false); String sql = "update account set money = money - ? where id = ?"; ps = conn.prepareStatement(sql); //扣钱, 扣ID为1 的100块钱 ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate(); int a = 10 /0 ; //加钱, 给ID为2 加100块钱 ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate(); //成功: 提交事务。 conn.commit(); } catch (SQLException e) { try { //事变: 回滚事务 conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }finally { JDBCUtil.release(conn, ps, rs); }} 事务的特性 1234567891011- 原子性> 指的是 事务中包含的逻辑,不可分割。 - 一致性> 指的是 事务执行前后。数据完整性- 隔离性> 指的是 事务在执行期间不应该受到其他事务的影响- 持久性> 指的是 事务执行成功,那么数据应该持久保存到磁盘上。 事务的安全隐患 1234567891011121314> 不考虑隔离级别设置,那么会出现以下问题。- 读> 脏读 不可重读读 幻读.>>>脏读> 一个事务读到另外一个事务还未提交的数据>>>不可重复读 > 一个事务读到了另外一个事务提交的数据 ,造成了前后两次查询结果不一致。写丢失更新 读未提交 演示 1.设置A窗口的隔离级别为 读未提交 2.两个窗口都分别开启事务 读已提交演示 1.设置A窗口的隔离级别为 读已提交 2.A B 两个窗口都开启事务, 在B窗口执行更新操作 3.在A窗口执行的查询结果不一致。 一次是在B窗口提交事务之前,一次是在B窗口提交事务之后。 这个隔离级别能够屏蔽 脏读的现象, 但是引发了另一个问题 ,不可重复读。 可串行化 1234567> 如果有一个连接的隔离级别设置为了串行化 ,那么谁先打开了事务, 谁就有了先执行的权利, 谁后打开事务,谁就只能得着,等前面的那个事务,提交或者回滚后,才能执行。 但是这种隔离级别一般比较少用。 容易造成性能上的问题。 效率比较低。- 按效率划分,从高到低> 读未提交 > 读已提交 > 可重复读 > 可串行化- 按拦截程度 ,从高到底> 可串行化 > 可重复读 > 读已提交 > 读未提交 事务总结 12345678910111213141516171819202122232425262728293031323334353637383940414243需要掌握的1.在代码里面会使用事务 conn.setAutoCommit(false); conn.commit(); conn.rollback(); 2 事务只是针对连接连接对象,如果再开一个连接对象,那么那是默认的提交。3. 事务是会自动提交的。 需要了解的>>安全隐患 读 脏读 一个事务读到了另一个事务未提交的数据 不可重复读 一个事务读到了另一个事务已提交的数据,造成前后两次查询结果不一致 幻读 一个事务读到了另一个事务insert的数据 ,造成前后查询结果不一致 。 写 丢失更新。>>隔离级别读未提交> 引发问题: 脏读 读已提交> 解决: 脏读 , 引发: 不可重复读可重复读> 解决: 脏读 、 不可重复读 , 未解决: 幻读可串行化> 解决: 脏读、 不可重复读 、 幻读。 mySql 默认的隔离级别是 可重复读Oracle 默认的隔离级别是 读已提交>>丢失更新 解决丢失更新 悲观锁 可以在查询的时候,加入 for update 乐观锁 要求程序员自己控制。 数据库连接池 123> 1. 数据库的连接对象创建工作,比较消耗性能。 > 2.一开始现在内存中开辟一块空间(集合) , 一开先往池子里面放置 多个连接对象。 后面需要连接的话,直接从池子里面去。不要去自己创建连接了。 使用完毕, 要记得归还连接。确保连接对象能循环利用。 自定义数据库连接池 12345678910111213- 代码实现- 出现的问题: 1. 需要额外记住 addBack方法 2. 单例。 3. 无法面向接口编程。 UserDao dao = new UserDaoImpl(); dao.insert(); DataSource dataSource = new MyDataSource(); 因为接口里面没有定义addBack方法。 4.怎么解决? 以addBack 为切入点。 解决自定义数据库连接池出现的问题。 123456789101112> 由于多了一个addBack 方法,所以使用这个连接池的地方,需要额外记住这个方法,并且还不能面向接口编程。> 我们打算修改接口中的那个close方法。 原来的Connection对象的close方法,是真的关闭连接。 > 打算修改这个close方法,以后在调用close, 并不是真的关闭,而是归还连接对象。###如何扩展某一个方法?> 原有的方法逻辑,不是我们想要的。 想修改自己的逻辑1. 直接改源码 无法实现。2. 继承, 必须得知道这个接口的具体实现是谁。 3. 使用装饰者模式。 开源连接池 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758#### DBCP1. 导入jar文件2. 不使用配置文件:public void testDBCP01(){ Connection conn = null; PreparedStatement ps = null; try { //1. 构建数据源对象 BasicDataSource dataSource = new BasicDataSource(); //连的是什么类型的数据库, 访问的是哪个数据库 , 用户名, 密码。。 //jdbc:mysql://localhost/bank 主协议:子协议 ://本地/数据库 dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost/bank"); dataSource.setUsername("root"); dataSource.setPassword("root"); //2. 得到连接对象 conn = dataSource.getConnection(); String sql = "insert into account values(null , ? , ?)"; ps = conn.prepareStatement(sql); ps.setString(1, "admin"); ps.setInt(2, 1000); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, ps); } } 2.使用配置文件方式: Connection conn = null; PreparedStatement ps = null; try { BasicDataSourceFactory factory = new BasicDataSourceFactory(); Properties properties = new Properties(); InputStream is = new FileInputStream("src//dbcpconfig.properties"); properties.load(is); DataSource dataSource = factory.createDataSource(properties); //2. 得到连接对象 conn = dataSource.getConnection(); String sql = "insert into account values(null , ? , ?)"; ps = conn.prepareStatement(sql); ps.setString(1, "liangchaowei"); ps.setInt(2, 100); ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, ps); } C3P0 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748拷贝jar文件 到 lib目录不使用配置文件方式 Connection conn = null; PreparedStatement ps = null; try { //1. 创建datasource ComboPooledDataSource dataSource = new ComboPooledDataSource(); //2. 设置连接数据的信息 dataSource.setDriverClass("com.mysql.jdbc.Driver"); //忘记了---> 去以前的代码 ---> jdbc的文档 dataSource.setJdbcUrl("jdbc:mysql://localhost/bank"); dataSource.setUser("root"); dataSource.setPassword("root"); //2. 得到连接对象 conn = dataSource.getConnection(); String sql = "insert into account values(null , ? , ?)"; ps = conn.prepareStatement(sql); ps.setString(1, "admi234n"); ps.setInt(2, 103200); ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtil.release(conn, ps); } 使用配置文件方式 //默认会找 xml 中的 default-config 分支。 ComboPooledDataSource dataSource = new ComboPooledDataSource(); //2. 设置连接数据的信息 dataSource.setDriverClass("com.mysql.jdbc.Driver"); //忘记了---> 去以前的代码 ---> jdbc的文档 dataSource.setJdbcUrl("jdbc:mysql://localhost/bank"); dataSource.setUser("root"); dataSource.setPassword("root"); //2. 得到连接对象 conn = dataSource.getConnection(); String sql = "insert into account values(null , ? , ?)"; ps = conn.prepareStatement(sql); ps.setString(1, "admi234n"); ps.setInt(2, 103200); DBUtils 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748增删改 //dbutils 只是帮我们简化了CRUD 的代码, 但是连接的创建以及获取工作。 不在他的考虑范围 QueryRunner queryRunner = new QueryRunner(new ComboPooledDataSource()); //增加 //queryRunner.update("insert into account values (null , ? , ? )", "aa" ,1000); //删除 //queryRunner.update("delete from account where id = ?", 5); //更新 //queryRunner.update("update account set money = ? where id = ?", 10000000 , 6); 查询1.直接new接口的匿名实现类 QueryRunner queryRunner = new QueryRunner(new ComboPooledDataSource()); Account account = queryRunner.query("select * from account where id = ?", new ResultSetHandler<Account>(){ @Override public Account handle(ResultSet rs) throws SQLException { Account account = new Account(); while(rs.next()){ String name = rs.getString("name"); int money = rs.getInt("money"); account.setName(name); account.setMoney(money); } return account; } }, 6); System.out.println(account.toString()); 2.直接使用框架已经写好的实现类。* 查询单个对象 QueryRunner queryRunner = new QueryRunner(new ComboPooledDataSource()); //查询单个对象 Account account = queryRunner.query("select * from account where id = ?", new BeanHandler<Account>(Account.class), 8);* 查询多个对象 QueryRunner queryRunner = new QueryRunner(new ComboPooledDataSource()); List<Account> list = queryRunner.query("select * from account ", new BeanListHandler<Account>(Account.class)); ResultSetHandler 常用的实现类 12345678910111213以下两个是使用频率最高的BeanHandler, 查询到的单个数据封装成一个对象BeanListHandler, 查询到的多个数据封装 成一个List<对象>ArrayHandler, 查询到的单个数据封装成一个数组ArrayListHandler, 查询到的多个数据封装成一个集合 ,集合里面的元素是数组。MapHandler, 查询到的单个数据封装成一个mapMapListHandler,查询到的多个数据封装成一个集合 ,集合里面的元素是map。 ColumnListHandlerKeyedHandlerScalarHandler 总结 123456789101112131415161718192021222324252627282930313233事务使用命令行演示使用代码演示脏读、不可重复读、幻读丢失更新悲观锁乐观锁4个隔离级别 读未提交 读已提交 可重复读 可串行化 数据连接池- DBCP 不使用配置 使用配置- C3P0 不使用配置 使用配置 (必须掌握)- 自定义连接池 装饰者模式 DBUtils> 简化了我们的CRUD , 里面定义了通用的CRUD方法。 queryRunner.update();queryRunner.query MVC设计模式元数据 12345Meata data 描述数据的数据 String sql , 描述这份sql字符串的数据叫做元数据数据库元数据 DatabaseMetaData参数元数据 ParameterMetaData结果集元数据 ResultSetMetaData JSP的开发模式 三层架构&MVC练习 学生信息管理系统–数据库准备–查询 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485CREATE DATABASE stus;USE stus;CREATE TABLE stu ( sid INT PRIMARY KEY AUTO_INCREMENT, sname VARCHAR (20), gender VARCHAR (5), phone VARCHAR (20), birthday DATE, hobby VARCHAR(50), info VARCHAR(200));查询1. 先写一个JSP 页面, 里面放一个超链接 。 <a href="StudentListServlet"> 学生列表显示</a>2. 写Servlet, 接收请求, 去调用 Service , 由service去调用dao3. 先写Dao , 做Dao实现。public interface StudentDao {/**- 查询所有学生 - @return List<Student> */ List<Student> findAll() throws SQLException ; } public class StudentDaoImpl implements StudentDao { /** * 查询所有学生 * @throws SQLException */ @Override public List<Student> findAll() throws SQLException { QueryRunner runner = new QueryRunner(JDBCUtil02.getDataSource()); return runner.query("select * from stu", new BeanListHandler<Student>(Student.class)); } }4.再Service , 做Service的实现。/** * 这是学生的业务处理规范 * @author xiaomi * */ public interface StudentService { /** * 查询所有学生 * @return List<Student> */ List<Student> findAll() throws SQLException ; } ------------------------------------------ /** * 这是学生业务实现 * @author xiaomi * */ public class StudentServiceImpl implements StudentService{ @Override public List<Student> findAll() throws SQLException { StudentDao dao = new StudentDaoImpl(); return dao.findAll(); } }5.在servlet 存储数据,并且做出页面响应。protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { //1. 查询出来所有的学生 StudentService service = new StudentServiceImpl(); List<Student> list = service.findAll(); //2. 先把数据存储到作用域中 request.setAttribute("list", list); //3. 跳转页面 request.getRequestDispatcher("list.jsp").forward(request, response); } catch (SQLException e) { e.printStackTrace(); } }在list.jsp上显示数据EL + JSTL + 表格 增加 1234567891011121314151. 先跳转到增加的页面 , 编写增加的页面2. 点击添加,提交数据到AddServlet . 处理数据。3. 调用service4. 调用dao, 完成数据持久化。5. 完成了这些存储工作后,需要跳转到列表页面。 这里不能直接跳转到列表页面,否则没有什么内容显示。 应该先跳转到查询所有学生信息的那个Servlet, 由那个Servlet再去跳转到列表页面。6. 爱好的value 值有多个。 request.getParameter("hobby"); String[] hobby = request.getParameterValues("hobby") ---> String[] String value = Arrays.toString(hobby): // [爱好, 篮球, 足球] 删除 123456789101112131415161718191.点击超链接,弹出一个询问是否删除的对话框,如果点击了确定,那么就真的删除。<a href="#" onclick="doDelete(${stu.sid})">删除</a>2.让超链接,执行一个js方法<script type="text/javascript">function doDelete(sid) { /* 如果这里弹出的对话框,用户点击的是确定,就马上去请求Servlet。 如何知道用户点击的是确定。 如何在js的方法中请求servlet。 */ var flag = confirm("是否确定删除?"); if(flag){ //表明点了确定。 访问servlet。 在当前标签页上打开 超链接, //window.location.href="DeleteServlet?sid="+sid; location.href="DeleteServlet?sid="+sid; } }</script>3.在js访问里面判断点击的选项,然后跳转到servlet。4.servlet收到了请求,然后去调用service , service去调用dao 更新 12345678910111213141516171819202122232425262728293031323334353637383940411. 点击列表上的更新, 先跳转到一个EditServlet > 在这个Servlet里面,先根据ID 去查询这个学生的所有信息出来。2. 跳转到更新的页面。 ,然后在页面上显示数据 <tr> <td>姓名</td> <td><input type="text" name="sname" value="${stu.sname }"></td> </tr> <tr> <td>性别</td> <td> <!-- 如果性别是男的, 可以在男的性别 input标签里面, 出现checked , 如果性别是男的, 可以在女的性别 input标签里面,出现checked --> <input type="radio" name="gender" value="男" <c:if test="${stu.gender == '男'}">checked</c:if>>男 <input type="radio" name="gender" value="女" <c:if test="${stu.gender == '女'}">checked</c:if>>女 </td> </tr> <tr> <td>爱好</td> <td> <!-- 爱好: 篮球 , 足球 , 看书 因为爱好有很多个, 里面存在包含的关系 --> <input type="checkbox" name="hobby" value="游泳" <c:if test="${fn:contains(stu.hobby,'游泳') }">checked</c:if>>游泳 <input type="checkbox" name="hobby" value="篮球" <c:if test="${fn:contains(stu.hobby,'篮球') }">checked</c:if>>篮球 <input type="checkbox" name="hobby" value="足球" <c:if test="${fn:contains(stu.hobby,'足球') }">checked</c:if>>足球 <input type="checkbox" name="hobby" value="看书" <c:if test="${fn:contains(stu.hobby,'看书') }">checked</c:if>>看书 <input type="checkbox" name="hobby" value="写字" <c:if test="${fn:contains(stu.hobby,'写字') }">checked</c:if>>写字 </td> </tr> 3.修改完毕后,提交数据到UpdateServlet提交上来的数据是没有带id的,所以我们要手动创建一个隐藏的输入框, 在这里面给定id的值, 以便提交表单,带上id。 <form method="post" action="UpdateServlet"> <input type="hidden" name="sid" value="${stu.sid }"> ... </form> 4.获取数据,调用service, 调用dao. 分页功能 1234567891011- 物理分页 (真分页)> 来数据库查询的时候,只查一页的数据就返回了。 优点 内存中的数据量不会太大 缺点:对数据库的访问频繁了一点。 SELECT * FROM stu LIMIT 5 OFFSET 2 - 逻辑分页 (假分页)> 一口气把所有的数据全部查询出来,然后放置在内存中。 优点: 访问速度快。缺点: 数据库量过大,内存溢出。 Ajax & JqueryAjax 12345678910- 是什么?> “Asynchronous Javascript And XML”(异步JavaScript和XML),> 并不是新的技术,只是把原有的技术,整合到一起而已。 1.使用CSS和XHTML来表示。 2. 使用DOM模型来交互和动态显示。 3.使用XMLHttpRequest来和服务器进行异步通信。 4.使用javascript来绑定和调用。 - 有什么用?> 咱们的网页如果想要刷新局部内容。 那么需要重新载入整个网页。用户体验不是很好。 就是为了解决局部刷新的问题。 保持其他部分不动,只刷新某些地方。 数据请求 Get 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263641.创建对象 function ajaxFunction(){ var xmlHttp; try{ // Firefox, Opera 8.0+, Safari xmlHttp=new XMLHttpRequest(); } catch (e){ try{// Internet Explorer xmlHttp=new ActiveXObject("Msxml2.XMLHTTP"); } catch (e){ try{ xmlHttp=new ActiveXObject("Microsoft.XMLHTTP"); } catch (e){} } } return xmlHttp; } 2. 发送请求 //执行get请求function get() { //1. 创建xmlhttprequest 对象 var request = ajaxFunction(); //2. 发送请求。 // http://localhost:8080/day16/demo01.jsp //http://localhost:8080/day16/DemoServlet01 /* 参数一: 请求类型 GET or POST 参数二: 请求的路径 参数三: 是否异步, true or false */ request.open("GET" ,"/day16/DemoServlet01" ,true ); request.send();}如果发送请求的同时,还想获取数据,那么代码如下//执行get请求function get() { //1. 创建xmlhttprequest 对象 var request = ajaxFunction(); //2. 发送请求 request.open("GET" ,"/day16/DemoServlet01?name=aa&age=18" ,true ); //3. 获取响应数据 注册监听的意思。 一会准备的状态发生了改变,那么就执行 = 号右边的方法 request.onreadystatechange = function(){ //前半段表示 已经能够正常处理。 再判断状态码是否是200 if(request.readyState == 4 && request.status == 200){ //弹出响应的信息 alert(request.responseText); } } request.send();} 数据请求 Post 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465<script type="text/javascript">//1. 创建对象function ajaxFunction(){ var xmlHttp; try{ // Firefox, Opera 8.0+, Safari xmlHttp=new XMLHttpRequest(); } catch (e){ try{// Internet Explorer xmlHttp=new ActiveXObject("Msxml2.XMLHTTP"); } catch (e){ try{ xmlHttp=new ActiveXObject("Microsoft.XMLHTTP"); } catch (e){} } } return xmlHttp; }function post() { //1. 创建对象 var request = ajaxFunction(); //2. 发送请求 request.open( "POST", "/day16/DemoServlet01", true ); //如果不带数据,写这行就可以了 //request.send(); //如果想带数据,就写下面的两行 //如果使用的是post方式带数据,那么 这里要添加头, 说明提交的数据类型是一个经过url编码的form表单数据 request.setRequestHeader("Content-type","application/x-www-form-urlencoded"); //带数据过去 , 在send方法里面写表单数据。 request.send("name=aobama&age=19");}</script>需要获取数据function post() { //1. 创建对象 var request = ajaxFunction(); //2. 发送请求 request.open( "POST", "/day16/DemoServlet01", true ); //想获取服务器传送过来的数据, 加一个状态的监听。 request.onreadystatechange=function(){ if(request.readyState==4 && request.status == 200){ alert("post:"+request.responseText); } } //如果使用的是post方式带数据,那么 这里要添加头, 说明提交的数据类型是一个经过url编码的form表单数据 request.setRequestHeader("Content-type","application/x-www-form-urlencoded"); //带数据过去 , 在send方法里面写表单数据。 request.send("name=aobama&age=19"); } 校验用户名是否可用 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566671. 搭建环境页面准备<body> <table border="1" width="500"> <tr> <td>用户名:</td> <td><input type="text" name="name" id="name" onblur="checkUserName()"><span id="span01"></span></td> </tr> <tr> <td>密码</td> <td><input type="text" name=""></td> </tr> <tr> <td>邮箱</td> <td><input type="text" name=""></td> </tr> <tr> <td>简介</td> <td><input type="text" name=""></td> </tr> <tr> <td colspan="2"><input type="submit" value="注册"></td> </tr> </table> </body> 2.数据库准备 Servlet代码 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { request.setCharacterEncoding("UTF-8"); //1. 检测是否存在 String name = request.getParameter("name"); System.out.println("name="+name); UserDao dao = new UserDaomImpl(); boolean isExist = dao.checkUserName(name); //2. 通知页面,到底有还是没有。 if(isExist){ response.getWriter().println(1); //存在用户名 }else{ response.getWriter().println(2); //不存在该用户名 } } catch (SQLException e) { e.printStackTrace(); }}3. Dao代码public class UserDaomImpl implements UserDao{ @Override public boolean checkUserName(String username) throws SQLException { QueryRunner runner = new QueryRunner(JDBCUtil02.getDataSource()); String sql = "select count(*) from t_user where username =?"; runner.query(sql, new ScalarHandler(), username); Long result = (Long) runner.query(sql, new ScalarHandler(), username); return result > 0 ; } } jsp页面显示 1234567891011121314151617181920212223242526272829function checkUserName() {//获取输入框的值 document 整个网页 var name = document.getElementById("name").value; // value value() val val() //1. 创建对象 var request = ajaxFunction(); //2. 发送请求 request.open("POST" ,"/day16/CheckUserNameServlet" , true ); //注册状态改变监听,获取服务器传送过来的数据 request.onreadystatechange = function(){ if(request.readyState == 4 && request.status == 200){ //alert(request.responseText); var data = request.responseText; if(data == 1){ //alert("用户名已存在"); document.getElementById("span01").innerHTML = "<font color='red'>用户名已存在!</font>"; }else{ document.getElementById("span01").innerHTML = "<font color='green'>用户名可用!</font>"; //alert("用户名未存在"); } } } request.setRequestHeader("Content-type","application/x-www-form-urlencoded"); request.send("name="+name);} JQuery 123456- 是什么?> javascript 的代码框架。 - 有什么用?> 简化代码,提高效率。 - 核心 > write less do more , 写得更少,做的更多。 load 123456789<a href="" onclick="load()">使用JQuery执行load方法</a>有两次刷新, 先走 onClick的方法,取到数据回来之后,赋值显示。 接着 走 href=""的路径,但是这个属性没有给值,所以会把当前的页面重新再刷新一次。所以导致看不见值。//找到这个元素, 去执行加载的动作, 加载/day16/DemoServlet02 , 得到的数据,赋值显示$("#aaa").load("/day16/DemoServlet02" , function(responseText , statusTXT , xhr) { //找到id为text01的元素, 设置它的value属性值为 responseText 对应的值 $("#aaa").val(responseText); }); Get 123456789$.get("/day16/DemoServlet02" , function(data ,status) { $("#div01").text(data); });赋值显示- val("aa"); > 只能放那些标签带有value属性- html("aa"); ---写html代码- text("aa");> 其实没有什么区别,如果想针对这分数据做html样式处理,那么只能用html() load & get 12345678910111213141516load$("#元素id").load(url地址);$("#div1").load(serlvet); ---> 使用的get请求,回来赋值的时候, 使用text();去赋值get语法格式 : $.get(URL,callback); 使用案例: $.get("/day16/DemoServlet02" , function(data ,status) { $("#div01").text(data); });post语法格式:$.post(URL,data,callback); function post() { $.post("/day16/DemoServlet02", {name:"zhangsan",age:18},function(data,status) { //想要放数据到div里面去。 --- 找到div $("#div01").html(data); }); } 使用JQuery去实现校验用户名 1234567891011121314151617function checkUserName() { //1. 获取输入框的内容 var name = $("#name").val(); //2. 发送请求 $.post("/day16/CheckUserNameServlet" , {name:name} , function(data , status){ //alert(data); if(data == 1){//用户名存在 //alert("用户名存在"); $("#span01").html("<font color='red'>用户名已被注册</font>"); }else{ //alert("用户名可用"); $("#span01").html("<font color='green'>用户名可以使用</font>"); } } ); //3. 输出响应的数据到页面上。} 实现百度搜索提示 12345678910111213141516171819202122232425262728293031323334353637383940搭建环境1.定义首页<body> <center> <h2>黑马</h2> <input type="text" name="word" id="word" style="width: 600px ; height: 50px ;font-size: 20px;"> <input type="button" value="黑马一下" style="height: 55px ; width: 100px ; "> <div id="div01" style="position:relative; left : -54px; width: 600px; height: 200px ; border: 1px solid blue; display: none"></div></center></body>2.定义数据库###捕获键盘弹起$(function(){ $("#word").keyup(function() { alert("键盘弹起了.."); })});###JS请求$(function(){ $("#word").keyup(function() { //2。 获取输入框的值 //var word = $("#word").val(); //this 对应就是执行这个方法的那个对象, $("#word") var word = $(this).val(); if(word == ""){ $("#div01").hide(); }else{ //3. 请求数据。 $.post("find",{word:word} ,function(data , status){ //alert(data); $("#div01").show(); $("#div01").html(data); }); } })}); Servlet代码 123456789101112131415161718192021222324252627protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); try { //1. 先获取参数 String word = request.getParameter("word"); System.out.println("word="+word); //2. 让dao执行查询 WordsDao dao = new WordsDaoImpl(); List<WordBean> list = dao.findWords(word); for (WordBean wordBean : list) { System.out.println("==="+wordBean.toString()); } request.setAttribute("list", list); //3. 返回数据 response.setContentType("text/html;charset=utf-8"); //response.getWriter().write("数据是:"); request.getRequestDispatcher("list.jsp").forward(request, response); } catch (SQLException e) { e.printStackTrace(); } } list.jsp 1234567891011<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <table style="width: 100%"> <c:forEach items="${list }" var="wordBean"> <tr> <td>${wordBean.words}</td> </tr> </c:forEach> </table> 使用JQuery实现 省市联动 1234567891011121314151617181920环境准备1. 准备数据库2 。 准备页面<script type="text/javascript" src="js/jquery-1.11.3.min.js"></script> <script type="text/javascript" src="js/city.js"></script> </head> <body> 省份: <select name="province" id ="province"> <option value="" >-请选择 - <option value="1" >广东 <option value="2" >湖南 <option value="3" >湖北 <option value="4" >四川 </select> 城市: <select name="city" id="city"> <option value="" >-请选择 - </select> </body> XStream的使用 1234567891011//3. 返回数据。手动拼 ---> XStream 转化 bean对象成 xml XStream xStream = new XStream(); //想把id做成属性 xStream.useAttributeFor(CityBean.class, "id"); //设置别名 xStream.alias("city", CityBean.class); //转化一个对象成xml字符串 String xml = xStream.toXML(list); JS代码 12345678910111213141516171819202122232425262728293031323334353637383940$(function() { //1。找到省份的元素 $("#province").change(function() { //2. 一旦里面的值发生了改变,那么就去请求该省份的城市数据 //$("#province").varl(); var pid = $(this).val(); /*<list> <city> <id>1<id> <pid>1</pid> <cname>深圳</cname> </city> <city > <id>2<id> <pid>1</pid> <cname>东莞</cname> </city> </list>*/ $.post( "CityServlet",{pid:pid} ,function(data,status){ //alert("回来数据了:"+data); //先清空以前的值: $("#city").html("<option value='' >-请选择-") //遍历: //从data数据里面找出所有的city , 然后遍历所有的city。 //遍历一个city,就执行一次function方法 $(data).find("city").each(function() { //遍历出来的每一个city,取它的孩子。 id , cname var id = $(this).children("id").text(); var cname = $(this).children("cname").text(); $("#city").append("<option value='"+id+"' >"+cname) }); } ); });}); 服务器和客户端数据传输的方式 1234567891011121314151617181920212223242526xml<list> <city> <id>1<id> <pid>1</pid> <cname>深圳</cname> </city> <city > <id>2<id> <pid>1</pid> <cname>东莞</cname> </city> </list> json阅读性更好 、 容量更小。{"name":"aaa" , "age":19}把javaBean 转化成 json数据 //3. 把list ---> json数据 //JSONArray ---> 变成数组 , 集合 [] //JSONObject ---> 变成简单的数据 { name : zhangsan , age:18} JSONArray jsonArray = JSONArray.fromObject(list); String json = jsonArray.toString(); 使用json格式数据显示省市联动效果 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960serlvet代码:protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { //1. 获取参数 int pid = Integer.parseInt(request.getParameter("pid")); //2 找出所有的城市 CityDao dao = new CityDaoImpl(); List<CityBean> list = dao.findCity(pid); //3. 把list ---> json数据 //JSONArray ---> 变成数组 , 集合 [] //JSONObject ---> 变成简单的数据 { name : zhangsan , age:18} JSONArray jsonArray = JSONArray.fromObject(list); String json = jsonArray.toString(); response.setContentType("text/html;charset=utf-8"); response.getWriter().write(json); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); };}js代码$(function() { //1。找到省份的元素 $("#province").change(function() { //2. 一旦里面的值发生了改变,那么就去请求该省份的城市数据 //$("#province").varl(); var pid = $(this).val(); /*[ { "cname": "深圳", "id": 1, "pid": 1 }, { "cname": "东莞", "id": 2, "pid": 1 } ... ]*/ $.post( "CityServlet02",{pid:pid} ,function(data,status){ //先清空 $("#city").html("<option value='' >-请选择-"); //再遍历,追加 $(data).each(function(index , c) { $("#city").append("<option value='"+c.id+"' >"+c.cname) }); },"json" ); }); }); ##总结 ###Ajax 发送get请求 发送post请求 都要求带数据 + 获取数据+ 放置到元素上。 JQuery 发送get请求 发送post请求 都要求带数据 + 获取数据+ 放置到元素上。 服务器返回xml数据 服务器返回json数据]]></content>
<categories>
<category>JavaEE</category>
</categories>
<tags>
<tag>JavaEE</tag>
</tags>
</entry>
</search>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。