文档章节

面试总结--未完待续

老虎是个蛋蛋
 老虎是个蛋蛋
发布于 02/20 20:00
字数 18930
阅读 327
收藏 2

面试总结

spring的事务隔离

  1. DEFAULT,这个是PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.
  2. 未提交读(read uncommited) :脏读,不可重复读,幻读都有可能发生
  3. 已提交读 (read commited):避免脏读。但是不可重复读和幻读有可能发生
  4. 可重复读 (repeatable read) :避免脏读和不可重复读.但是幻读有可能发生.
  5. 串行化的 (serializable) :避免以上所有读问题.

数据库的事务隔离

  1. READ_UNCOMMITTED(未提交读):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  2. READ_COMMITTED(提交读):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  3. REPEATABLE_READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  4. SERIALIZABLE(串行):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.

mysql调优

1、使用短索引

对串列进行索引,如果可以就应该指定一个前缀长度。例如,如果有一个char(255)的列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

2、like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,注意正确的使用方式。like ‘%aaa%'不会使用索引,而like ‘aaa%'可以使用索引。

3、索引列排序

mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作,尽量不要包含多个列的排序,如果需要最好给这些列建复合索引。

4、不要在列上进行运算

5、不使用NOT IN 、<>、!=操作,但<,<=,=,>,>=,BETWEEN,IN是可以用到索引的

6、索引要建立在值比较唯一的字段上。

7、对于那些定义为text、image和bit数据类型的列不应该增加索引。因为这些列的数据量要么相当大,要么取值很少。

8、在where和join中出现的列需要建立索引。

9、where的查询条件里有不等号(where column != …),mysql将无法使用索引。

10、如果where字句的查询条件里使用了函数(如:where DAY(column)=…),mysql将无法使用索引。

11、在join操作中(需要从多个数据表提取数据时),mysql只有在主键和外键的数据类型相同时才能使用索引,否则及时建立了索引也不会使用。


mysql锁和索引

引用 https://juejin.im/post/5b55b842f265da0f9e589e79#comment

索引最左匹配原则

索引可以简单如一个列(a),也可以复杂如多个列(a, b, c, d),即联合索引。 如果是联合索引,那么key也由多个列组成,同时,索引只能用于查找key是否存在(相等),遇到范围查询(>、<、between、like左匹配)等就不能进一步匹配了,后续退化为线性查找。 因此,列的排列顺序决定了可命中索引的列数。

例子:

如有索引(a, b, c, d),查询条件a = 1 and b = 2 and c > 3 and d = 4,则会在每个节点依次命中a、b、c,无法命中d。(很简单:索引命中只能是相等的情况,不能是范围匹配)

=、in自动优化顺序

不需要考虑=、in等的顺序,mysql会自动优化这些条件的顺序,以匹配尽可能多的索引列。

例子:

如有索引(a, b, c, d),查询条件c > 3 and b = 2 and a = 1 and d < 4与a = 1 and c > 3 and b = 2 and d < 4等顺序都是可以的,MySQL会自动优化为a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。

索引总结

索引在数据库中是一个非常重要的知识点!上面谈的其实就是索引最基本的东西,要创建出好的索引要顾及到很多的方面:

  1. 最左前缀匹配原则。这是非常重要、非常重要、非常重要(重要的事情说三遍)的原则,MySQL会一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)就停止匹配。
  2. 尽量选择区分度高的列作为索引,区分度的公式是 COUNT(DISTINCT col) / COUNT(*)。表示字段不重复的比率,比率越大我们扫描的记录数就越少。
  3. 索引列不能参与计算,尽量保持列“干净”。比如,FROM_UNIXTIME(create_time) = '2016-06-06' 就不能使用索引,原因很简单,B+树中存储的都是数据表中的字段值,但是进行检索时,需要把所有元素都应用函数才能比较,显然这样的代价太大。所以语句要写成 : create_time = UNIX_TIMESTAMP('2016-06-06')。
  4. 尽可能的扩展索引,不要新建立索引。比如表中已经有了a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
  5. 单个多列组合索引和多个单列索引的检索查询效果不同,因为在执行SQL时,MySQL只能使用一个索引,会从多个单列索引中选择一个限制最为严格的索引。

行级锁

InnoDB实现了以下两种类型的行锁。

  • 共享锁(S锁):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。

    也叫做读锁:读锁是共享的,多个客户可以同时读取同一个资源,但不允许其他客户修改。

  • 排他锁(X锁):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

    也叫做写锁:写锁是排他的,写锁会阻塞其他的写锁和读锁。

数据库事务隔离

1、Read uncommitted(未提交读):会出现的现象--->脏读:一个事务读取到另外一个事务未提交的数据。

  • 例子:A向B转账,A执行了转账语句,但A还没有提交事务,B读取数据,发现自己账户钱变多了!B跟A说,我已经收到钱了。A回滚事务【rollback】,等B再查看账户的钱时,发现钱并没有多。
  • 出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据。

2、Read committed(提交读)避免脏读的做法其实很简单:

  • 就是把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作。
  • 但Read committed出现的现象--->不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改。
  • 注:A查询数据库得到数据,B去修改数据库的数据,导致A多次查询数据库的结果都不一样【危害:A每次查询的结果都是受B的影响的,那么A查询出来的信息就没有意思了】

3、Repeatable read(可重复读)

避免不可重复读是事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。

至于虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。

  • 注:和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致
  • MySQL的Repeatable read隔离级别加上GAP间隙锁已经处理了幻读了。

多事务并行问题

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

为什么使用B+Tree,其数据结构

为什么使用B+Tree

  1. 文件很大,不可能全部存储在内存中,故要存储到磁盘上
  2. 索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。)
  3. 局部性原理与磁盘预读,预读的长度一般为页(page)的整倍数,(在许多操作系统中,页得大小通常为4k)
  4. 数据库系统巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,(由于节点中有两个数组,所以地址连续)。而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性

B+Tree数据结构

B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

B+Tree相对于B-Tree有几点不同:

  • 非叶子节点只存储键值信息。
  • 所有叶子节点之间都有一个链指针。
  • 数据记录都存放在叶子节点中。

通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为〖10〗^3)。也就是说一个深度为3的B+Tree索引可以维护10^3 * 10^3 * 10^3 = 10亿 条记录。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。mysql的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。

Mysql调优

索引类型

  1. 唯一索引
  2. 主索引
  3. 普通索引
  4. 联合索引
  5. 全文索引

mysql调优

表结构及索引优化
  • 分库分表,读写分离
  • 为字段选择合理的数据类型,在保留扩展能力的前提下优先使用较小的数据结构,例如保存年龄的字段,要使用TINY INT而不要使用INT
  • 将字段多的表拆分成多个表,增加中间表
  • 混用范式和返范式,适当冗余
  • 未查询创建必要索引,避免滥用索引
  • 列字段尽可能设置NOT NULL
SQL语句优化
  • 寻找需要优化的语句,分析慢查询日志
  • 利用分析工:explain、profile
  • 避免使用select *
  • 尽量使用prepared statements
  • 用索引扫描来排序,也就是尽量在由索引的字段上进行排序
SQL多索引在查询时是如何使用索引的

我们以简单的user表为例讲解,表结构如下:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `age` tinyint(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name_age` (`name`,`age`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们创建了两个索引,顺序为:idx_name_age、idx_name 然后我们看一下执行计划: explain select * from user where name='11' and age =10结果如下:

我们会发现,虽然我们创建了两个索引,并都包含name字段,但实际索引只使用了idx_name_age这个索引。 如果我么将这个两个索引的创建顺序颠倒一下会怎么样呢? 修改后的建表语句如下:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `age` tinyint(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`),
  KEY `idx_name_age` (`name`,`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

索引顺序为:idx_name、idx_name_age 我们执行计划看一下结果:

看出区别了嘛?是的索引只走了idx_name,针对于第一次创建时添加的索引,索引是按照创建顺序执行的。

JVM相关

JVM内存模型

1、栈

是线程私有的,线程在执行每个方法时都会创建一个栈帧,用来存储局部变量表,操作栈,动态链接,方法出口等信息,调用方法时执行入栈,方法返回时执行出栈

2、本地方法栈

本地方法栈与栈类似,也是用来执行线程执行方法时的信息,不同的是执行java方法时使用的是栈,执行native方法时使用本地方法栈

3、程序计数栈

保存了当前线程所执行的字节码位置,每个线程执行时都有一个独立的计数器,程序技术器只为java方法服务,执行native方法时,程序计数器为空

4、堆

堆被所有线程共享,目的是存放对象的实例,几乎所有的对象实例都被存放在这里,当堆内存没有可用空间时会抛出OOM异常,堆内存被分代管理

5、方法区

用于存储已经被虚拟机加载的类信息、常量、静态变量,永久带就是方法区的一种实现

Java内存模型

JMM是Java内存模型,JMM主要目标是定义程序中变量的访问规则,如图,所有的共享变量都存储在主内存中共享,每个线程都有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程对变量读写等操作必须在自己的工作内存中进行,而不能直接读写主内存中的变量,在多线程进行数据交互时,例如线程A给一个共享变量赋值,然后由线程B来读取这个值,A修改的变量,是修改的自己工作内存区中,B是不可见的,只有当从A的工作内存区写回到住内存,B再从主内存读取到自己的工作内存区,才能进行进一步的操作。由于指令重排序的存在,这个写和读的顺有可能被打乱,因此JMM需要提供原子性、可见性、有序性的保证

原子性
  • 基本数据类型读或写(long,double除外)
  • synchornized
可见性
  • synchornized
  • volatile

volatile:

  • 作用1:强制变量的赋值,会同步刷新到主内存,强制变量的读取会从主内存中重新加载,保证不同的线程,总是能够看到该变量的最新值
  • 作用2:阻止指令重排序
有序性
  • volatile
  • happens-before原则

java对象创建过程、jvm做了哪些工作

一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化

首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。

加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。

第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:

  • 验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
  • 准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
  • 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java 虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。

最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

类加载

  1. 加载是文件到内存的过程,通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个class对象

  2. 验证是对类文件内容的验证,目的在于确保class文件符合当前虚拟机的要求,不会危害到虚拟机大安全,主要包括四种:文件格式验证、原数据验证、字节码验证、符号引用验证

  3. 准备阶段是进行内存分配,为static修饰的变量进行分配内存并设置初始值,这里要注意初始值是0或者null而不是代码中设置的具体值,代码中设置的值在初始化阶段完成,另外这里也不包含final修饰的静态变量,因为final变量在编译时就已经分配了

  4. 解析主要是将常量池中的符号引用替换为直接引用的过程,直接引用就是直接指向目标的指针或者相对偏移量等

  5. 初始化主要完成静态块和静态变量的赋值,这是类加载的最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化,只有对类的主动使用时才会初始化。初始化的出发条件包括,创建类的实例的时候、访问类的静态方法或者静态变量的时候、使用classForName反射类的时候、某个子类被初始化的时候

双亲委派

类加载器分为以下几种:

  • Bootstrap类加载器(Bootstrap ClassLoader)
  • Extension类加载器(ExtClassLoader)
  • System类加载器(AppClassLoader)
  • 自定义类加载器(Custom ClassLoader)

一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类器就继续向上委托,直到顶层的启动类加载器,如图中蓝色向上的箭头。如果父类加载器能够完成加载就成功返回,如果父类加载器无法完成加载自加载器才会尝试自己加载,如图中黄色向下的箭头。 这种双亲委派的好处,一是防止类的重复加载,另外也避免了java的核心API被篡改


分布式锁

分为以下几种:

基于数据库的锁,创建一张表,对于需要锁的字段加唯一性约束,获取锁的过程就是插入数据的过程,如果插入数据成功则认为获取锁成功,反之失败则认为已经被其他线程占有锁了

基于数据库的for update排他锁

基于redis的setNx、expire()

public static boolean acquireLock(String lock) {
    // 1. 通过SETNX试图获取一个lock
    boolean success = false;
    Jedis jedis = pool.getResource();       
    long value = System.currentTimeMillis() + expired + 1;     
    System.out.println(value);    
    long acquired = jedis.setnx(lock, String.valueOf(value));
    //SETNX成功,则成功获取一个锁
    if (acquired == 1)      
        success = true;
    //SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时
    else {
        long oldValue = Long.valueOf(jedis.get(lock));
 
        //超时
        if (oldValue < System.currentTimeMillis()) {
            String getValue = jedis.getSet(lock, String.valueOf(value));               
            // 获取锁成功
            if (Long.valueOf(getValue) == oldValue) 
                success = true;
            // 已被其他进程捷足先登了
            else 
                success = false;
        }
        //未超时,则直接返回失败
        else             
            success = false;
    }        
    pool.returnResource(jedis);
    return success;      
}
注意:
Redis 加锁新方法 - Jediscluster.Set(Key,Value,"Nx","Ex",Expireseconds);

基于 ZooKeeper 做分布式锁,利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。

基于redis的可重入锁

最近在面试的时候被问到如何用redis设计一个可重入锁?可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,如果没有可重入锁的支持,在第二次尝试获得锁时将会进入死锁状态。

具体是否可重入,我们可以使用token的方式进行验证,代码如下:

具体代码可以访问:https://gitee.com/wangGet/study8/tree/master/src/main/java/com/wangxin/study8/lock

@Component
public class ReentrantRedisLock implements ReentrantRedisLockService{
    // TODO: 2019-05-29 注入redis的service类,此处只做了一个假的service
    @Resource
    private RedisService redisService;

    private static ThreadLocal<String> TOKEN_MAP = new ThreadLocal<>();

    /**
     * 锁前缀
     */
    private static final String LOCK_PREFIX = "LOCK_";

    /**
     * 本机ip
     */
    private static String IP;

    private static final long MAX_TIME_OUT = Long.MAX_VALUE;

    static {
        try {
            IP = InetAddress.getLocalHost().getHostAddress();
        }catch (Exception e){
            IP = "UNKNOWN";
        }
    }


    @Override
    public boolean tryLock(String key, long timeOut, TimeUnit timeUnit, long expire) {
        if (tryLock(key,expire)){
            return true;
        }
        boolean temp = false;
        long startTime = System.nanoTime();

        /**
         * 自旋判断
         * 是否获取到了锁 && 是否超时
         */
        while (!temp && (System.nanoTime()-startTime)<timeUnit.toNanos(timeOut)){
            temp = getLock(key,expire);
        }
        return temp;
    }

    @Override
    public boolean tryLock(String key, long expire) {
        if (getLock(key,expire)){
            return true;
        }
        if (getToken().equals(getLockToken(key))){
            return true;
        }
        return false;
    }

    @Override
    public void lock(String key, long expireSecs) {
        tryLock(key,MAX_TIME_OUT,TimeUnit.SECONDS,expireSecs);
    }

    @Override
    public boolean unlock(String key) {
        if (getToken().equals(getLockToken(key))){
            List<String> keys = new ArrayList<>(1);
            keys.add(key(key));
            List<String> args = new ArrayList<>(1);
            args.add(TOKEN_MAP.get());

            boolean result = redisService.del(keys, args);
            if (result){
                TOKEN_MAP.remove();
            }
            return result;
        }
        return false;
    }

    private String getToken(){
        String token = TOKEN_MAP.get();
        if (null == token ){
            TOKEN_MAP.set(token=IP+"_"+ UUID.randomUUID().toString().replaceAll("-",""));
        }
        return token;
    }

    private String getLockToken(String key){
        return redisService.get(key);
    }

    private boolean getLock(String key, long expire){
        return "OK".equals(redisService.set(key(key),getToken(),"NX", "EX", expire));
    }

    private String key(String key){
        return LOCK_PREFIX+key;
    }
}

synchronized和ReenTrantLock

synchronized的使用方法

  • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!

双检锁实现单例模式

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

采用 volatile 关键字修饰也是很有必要的,由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

ps:最近在面试的时候被问到过单例模式,随即我写了上面的双检锁模式的单例,针对于双检锁是否需要加volatile产生了分歧,面试官坚持说因为加了synchronized所以不用加volatile,我说“在单线程的环境下,不加volatile是没有问题的,但是在多线程的环境下,因为编译器重排序的情况存在,new Singleton()的初始化可能会发生重排,实例化对象的时候大概有这三个步骤:1、分配内存空间;2、初始化对象;3、将对象指向刚分配的内存空间;;但是某些编译器因为性能的原因会把2和3进行重排序。这就可能出现在A线程在实例化对象的过程中B线程uniqueInstance == null的时候发现对象已经指向的分配的内存就直接返回Singleton但是这时候可能Singleton还没有真正实例化。使用了volatile可以防止这种重排,保证Singleton正常实例化”;;最后僵持无果,只能跳过这个问题,回来后我查了一些资料,也证实了,我说的大致没错的,volatile是必须加的,也查到到了一些资料:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html 文中提交的Symantec JIT大家可以自行安装进行分析 https://github.com/AdoptOpenJDK/jitwatch

时间 线程A 线程B
t1 A1 检查到instance为空
t2 A2 获取锁
t3 A3 再次检查到instance为空
t4 A4 为instance分配内存空间
t5 A5 将instance指向内存空间
t6 B1 检查到instance不为空
t7 B2 访问instance(对象还未初始化)
t8 A6 初始化instance

synchronized 关键字底层原理总结

  • synchronized 同步语句块的情况
public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置.当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

Java对象头、monitor

Java对象头和monitor是实现synchronized的基础!

synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

synchronized中的锁优化

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;

适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

在大多数的情况下,上述观点是正确的,LZ也一直坚持着这个观点。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。

锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如下面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

public void vectorTest(){
        Vector<String> vector = new Vector<String>();
        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }
        System.out.println(vector);
    }
偏向锁

引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

获取锁
  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  3. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块
释放锁

偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
  2. 撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态;
轻量级锁

引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

获取锁
  1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
  3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
释放锁

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。


并发容器

  • ConcurrentHashMap: 线程安全的HashMap
  • CopyOnWriteArrayList: 线程安全的List,在读多写少的场合性能非常好,远远好于Vector.
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
  • BlockingQueue: 这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。

两个整数数组,合并为一个排序并去重

public static void main(String[] args) {
        int [] a = {1,2,3,4,66};
        int [] b = {1,2,3,7,66};
        System.out.println(paixu(a,b).toString());
    }
    public static  List paixu(int[] a,int[] b){
        Set set = new HashSet();
        int temp[] = new int[a.length+b.length];
        System.arraycopy(a,0,temp,0,a.length);
        System.arraycopy(b,0,temp,a.length,b.length);
        for (int i = 0; i < temp.length; i++) {
            set.add(temp[i]);
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort((s,f)->s-f);
        return list;
    }

实现一个有界队列的put和take

当时脑袋有点懵,一直在纠结队列头上一直保持有数据,但是其实完全不用纠结这个,只要put和take的角标值,当put或者take到了最后一个角标,重置角标就可以了。以下是多线程版本的demo

public class ReentrantLockTest {
    final ReentrantLock lock = new ReentrantLock();//锁对象
    final Condition notFull  = lock.newCondition();//写线程条件
    final Condition notEmpty = lock.newCondition();//读线程条件
    final Object[] items = new Object[10];//缓存队列
    private int count = 0;
    private int putptr=0;
    private int takeptr=0;
    public void put(Object x) throws InterruptedException {
        lock.lock();
        System.out.println("put");
        try {
            while (count == items.length){
                System.out.println("阻塞了写线程");
                //如果队列满了
                notFull.await();//阻塞写线程
            }
            items[putptr] = x;//赋值
            if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
            ++count;//个数++
            notEmpty.signal();//唤醒读线程
        } finally {
            lock.unlock();
        }
    }
    public Object take() throws InterruptedException {
        System.out.println("take");
        lock.lock();
        try {
            while (count == 0){
                System.out.println("阻塞了读线程");
                //如果队列为空
                notEmpty.await();//阻塞读线程
            }
            Object x = items[takeptr];//取值
            if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
            --count;//个数--
            notFull.signal();//唤醒写线程
            return x;
        } finally {
            lock.unlock();
        }
    }
}


JVM的内存分配和垃圾回收

分代回收

CMS垃圾回收算法

是1.7以前最主流的垃圾回收算法,它使用了标记清除算法,优点是并发收集,停顿小。第一个阶段是初始标记,这个阶段会“stop the world”,标记的对象只是从Root级最直接可达的对象。第二个阶段是并发标记,这时GC线程和应用线程并发执行,主要是标记可达的对象。第三个阶段是重新标记阶段,这个阶段是第二个“stop the world”阶段,停顿时间比并发标记要小很多,但比初始标记稍长,主要是对对象进行重新扫描并标记。第四个阶段是并发清理阶段,进行并发的垃圾清理。最后一个阶段是并发重置阶段,为下一次GC重置相关数据结构。

G1垃圾回收算法

ZGC垃圾回收算法

初始状态是堆空间被划分为大小不对等的Region。开始进行回收时,ZGC首先会进行一个短暂的“stop the world”来进行roots根对象的标记,然后进行并发标记,通过对对象指针进行着色进行标记,结合读屏障,解决单个对象的并发问题。下一个阶段是清理阶段,这个阶段会把标记为不可用的对象进行回收,如图把橘色的不在使用的对象进行回收。最后一个阶段是重定位,重定位就是对GC后存活的对象进行移动,来腾出大块的内存空间,解决碎片问题,在重定位最开始会有一个短暂的“stop the world” ,用来重定义集合中的root对象。最后是并发重定位,这个过程也是通过读屏障与应用线程并发进行的

  • 着色指针:

64位平台上,一个指针可用位是64位,ZGC限制支持最大4GB的堆,这样寻址就只使用42位,那么会剩下22位,就可以用来保存额外的信息,着色指针技术就是利用指针的额外信息位,在指针上对对象进行着色标记

  • 读屏障:

用来解决GC线程和应用线程,可能并发修改对象状态的问题,而不是通过简单粗暴的“stop the world”来做全局的锁定,使用读屏障只会对单个对象的处理上有概率被减速,由于读屏障的使用,进行垃圾回收的大部分时候都是不需要“stop the world”,因此ZGC的大部分时间都是并发处理

  • 基于Region:

没有进行分代,Region是动态大小,Region可以动态创建和销毁

  • 内存压缩(整理):

垃圾回收时会对内存进行移动合并,解决了碎片问题


java内存模型和volatile的实现原理


多线程相关

1.线程的状态转换

当创建一个线程时,线程处于NEW状态,运行Thread.start方法后,线程进入RUNNABLE可运行状态,这个时候所有可运行状态的线程并不能马上运行,而是需要先进入就绪状态,等待线程调度,就是图中间的READY状态,在获取到CPU后,才能进入运行状态,就是图中的RUNNING状态,运行状态可随着不同条件转换成除NEW以外的其他状态。看左边,在运行态中的线程,进入synchronized同步块或者同步方法时,如果获取锁失败,就会进入到BLOCKED的状态,当获取到锁时,会从BLOCKED状态恢复到就绪状态。看右边,运行中的线程还会进入等待状态,这两个等待状态,一个是有超时时间的等待,例如调用Object.wait(long)方法、Thread.join(long)方法。另外一个是无超时的等待,例如调用Thread.join()方法或者LockSupport. park()方法。这两种等待都可以通过notify或者unpark结束等待状态,恢复到就绪状态。最后是线程运行完成结束时,如图下方,线程状态就变成了TERMINATED。

2.JUC常用工具

3.线程池

corePoolSize核心线程数,默认情况下核心线程一直存活,maximumPoolSize设置最大线程数,决定线程池最多可以创建多少个线程,keepAliveTime和unit用来设置线程的空闲时间和空闲时间的单位,当线程闲置超过空闲时间时,就会被销毁。workQueue设置缓冲队列,图中左下方的三个队列是设置线程池缓冲队列最常用的三个, 其中ArrayBlockingQueue是一个有界队列,LinkedBlockingQueue是无界队列,synchronousQueue是一个同步队列,内部没有缓冲区。threadFactory设置线程池工厂方法,线程工厂用来创建新的线程,可以用来对线程的一些属性进行定制,一般使用默认工厂类即可。handler设置线程池满时的拒绝策略,如右下角所示,Abort线程池满后抛出异常,Discard会在提交失败时直接对任务进行丢弃。CallerRuns策略,会在提交失败时,由提交任务的线程直接执行提交的任务。DiscardOldest会丢弃最早提交的任务,

4.同步与互斥

  • synchronizd实现原理

synchronized是对对象进行加锁,在JVM中,对象在内存中分为三块区域,对象头、实例数据、和对其填充。在对象头中保存了锁标志位和指向Monitor对象的起始地址,如图所有,右边的就是一个对象对应的Monitor对象,当Monitor被某个线程占用后,就会处于锁定状态,如图中的Owner部分,会指向持有Monitor对象的线程,另外Monitor还有两个队列,用来存放进入和等待获取锁的线程。synchronized应用在方法时,是通过方法的ACC_SYNCHRONIZED标志来实现的,synchronized应用在同步块时,是通过 monitorenter和monitorexit来实现的。针对synchronized获取锁的方式JVM使用了锁升级的优化方式,先使用偏向锁,优先同一线程再次获取锁,如果失败就升级为CAS轻量级锁,如果再失败就会进行短暂的自旋,防止线程被线程挂起,最后如果以上都失败,就升级为重量级锁,

  • AQS与Lock

左图是AQS的结构图,从图中可以看出,AQS有一个state标志位,值为1时表示有线程占用,其他线程需要进入同步队列等待,同步队列是一个双向链表,当获得锁的线程需要等待某个条件时,会进入等待队列,等待队列可以有多个,当条件满足时,线程从等待队列重新进入到同步队列进行获取锁的竞争,ReentrantLock就是基于AQS实现的。看右边的图,ReentrantLock内部有公平锁和非公平锁两种实现。和ReentrantLock类似Semaphore也是基于AQS,差别在于ReentrantLock是独占锁,Semaphore是共享锁。

AQS原理概述

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock

CountDownLatch使用

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在Java并发中,countdownlatch的概念是一个常见的面试题,所以一定要确保你很好的理解了它。

CountDownLatch 的三种典型用法:

①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1) ,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

③死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。

CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

public class CountDownLatchTestA {
    public static void main(String[] args) throws Exception{
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new FirstThread(countDownLatch));
            thread.start();
        }
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new SecondThread(countDownLatch));
            thread.start();
        }
        while (countDownLatch.getCount()!=1){
            Thread.sleep(100L);
        }
        System.out.println("wait for first is finish!!");
        countDownLatch.countDown();
    }
}
class FirstThread implements Runnable{
    private CountDownLatch countDownLatch;
    public FirstThread(CountDownLatch countDownLatch){
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        System.out.println("First executed!!");
        countDownLatch.countDown();
    }
}
class SecondThread implements Runnable{
    private CountDownLatch countDownLatch;
    public SecondThread (CountDownLatch countDownLatch){
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        try {
            countDownLatch.await();
            System.out.println("second executed!!");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

结果:

First executed!!
First executed!!
First executed!!
First executed!!
First executed!!
wait for first is finish!!
second executed!!
second executed!!
second executed!!
second executed!!
second executed!!

CyclicBarrier

CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

public class CyclicBarrierActionTest {
    public static volatile CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
            System.out.println(copyOnWriteArrayList.toString());
        });
        for (int i = 0; i < 3; i++) {
            Thread thread1 = new Thread(new Thread1(cyclicBarrier,i));
            Thread thread2 = new Thread(new Thread2(cyclicBarrier,i));
            thread1.start();
            thread2.start();
        }
    }


}
class Thread1 implements Runnable{
    private CyclicBarrier cyclicBarrier;
    private int num;
    public Thread1(CyclicBarrier cyclicBarrier,int num){
        this.cyclicBarrier = cyclicBarrier;
        this.num = num;
    }
    @Override
    public void run() {
        System.out.println("thread1 threadNum =="+num+"is ready!");
        try {
            CyclicBarrierActionTest.copyOnWriteArrayList.add("AA");
            cyclicBarrier.await();
        }catch (Exception e){
        }
        System.out.println("thread1 threadNum:"+num+"is finish!!!");
    }
}

class Thread2 implements Runnable{
    private CyclicBarrier cyclicBarrier;
    private int num;
    public Thread2(CyclicBarrier cyclicBarrier,int num){
        this.cyclicBarrier = cyclicBarrier;
        this.num = num;
    }
    @Override
    public void run() {
        System.out.println("thread2 threadNum =="+num+"is ready!");
        try {
            CyclicBarrierActionTest.copyOnWriteArrayList.add("BB");
            cyclicBarrier.await();
        }catch (Exception e){

        }
        System.out.println("thread2 threadNum:"+num+"is finish!!!");
    }
}

结果

thread1 threadNum ==0is ready!
thread1 threadNum ==1is ready!
thread2 threadNum ==0is ready!
thread2 threadNum ==1is ready!
thread1 threadNum ==2is ready!
thread2 threadNum ==2is ready!
action:[AA, AA, BB, BB, AA, BB]
thread2 threadNum:2is finish!!!
thread1 threadNum:1is finish!!!
thread2 threadNum:1is finish!!!
thread1 threadNum:0is finish!!!
thread1 threadNum:2is finish!!!
thread2 threadNum:0is finish!!!

Semaphore

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

执行 acquire 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。 Semaphore经常用于限制获取某种资源的线程数量。

public class SemaphoreTestA {
    private static final int count = 50;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(300);
        final Semaphore semaphore = new Semaphore(20);
        for (int i = 0; i < count; i++) {
            final int num = i;
            try {
                SemaphoreWorker semaphoreWorker = new SemaphoreWorker(semaphore);
                executorService.submit(semaphoreWorker);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        executorService.shutdown();
        System.out.println("finish");
    }

}
class SemaphoreWorker implements Runnable{
    private Semaphore semaphore;
    public SemaphoreWorker (Semaphore semaphore){
        this.semaphore = semaphore;
    }
    @Override
    public void run() {
        try {
            log("is waiting for a permit!");
            semaphore.acquire();
            log("acquire a permit!!");
            log("executed!!");
            Thread.sleep(1000L);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            log("release!");
            semaphore.release();
        }
    }
    private void log(String msg){
        String name = "";
        if (null==msg){
            name = Thread.currentThread().getName();
        }
        System.out.println(name+"--"+msg);
    }
}

结果

--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--is waiting for a permit!
--executed!!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--is waiting for a permit!
--executed!!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--acquire a permit!!
--executed!!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
--is waiting for a permit!
finish
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--release!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--release!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--release!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--acquire a permit!!
--executed!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--release!
--release!
--release!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--release!
--acquire a permit!!
--executed!!
--release!
--acquire a permit!!
--release!
--executed!!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
--release!
  • CAS与ABA问题

CAS是一种乐观锁的实现方式,是轻量级锁,JUC中很多工具类的实现就是基于CAS,CAS的操作流程如左图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程,这是一种乐观策略,认为并发操作并不总会发生,比较并写回的操作时通过操作系统的原语实现的,保证执行过程中不会被中断。CAS容易出现ABA问题,比如按右图所示的时序,线程T1在读取完值A后,发生过2次写入,先有线程T2写回了B,又有线程T3写回了A,此时T1在写回时进行比较,发现值还是A,就无法判断是否发生过修改。ABA问题不一定会影响到结果,但还是需要防范,解决的办法可以增加额外的标志位或者时间戳。JUC工具包中提供了这样的类。

5.死锁

死锁的产生条件:

  • 互斥
  • 请求并持有
  • 非剥夺
  • 循环等待
认识死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

死锁的例子

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

学过操作系统的朋友都知道产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

1.破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

2.破坏请求与保持条件

一次性申请所有的资源。

3.破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

4.破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();

输出

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

Mybatis

主要对象

对象 说明
SqlSessionFactory 用来创建SqlSession的工厂类
SqlSession 对数据库的操作在SqlSession中进行,SqlSession非线程安全,每一次操作完数据库后,都要调用close对其关闭
Executor SqlSession通过内部的Executor来执行crud操作,
StatementHandler 用来处理sql语句的预编译,设置参数等等,
ParameterHandler 用来设置预编译参数,
ResultSetHandler 用来处理结果集
TypeHandler 对数据库类型和java数据类型进行映射

Mybatis处理流程

在执行Sql时首先会从SqlSessionFactory中创建一个新的SqlSession,Sql语句是通过SqlSession中的Executor来执行的,Executor根据SqlSession传递的参数,执行Query方法,然后创建StatementHandler对象,将必要的参数传递给StatementHandler,由StatementHandler完成对数据库的查询,StatementHandler调用ParameterHandler的setParameters方法,把用户传递的参数转换成jdbcStatement所需要的参数,调用原生的JDBC 来执行语句,最后由ResultSetHandler的handlerResultSet方法对JDBC返回的ResultSet结果集转换成对象集,并逐级返回结果,完成一次sql语句的执行,

缓存

一级缓存:

  • 作用域是session
  • HashMap实现
  • 默认开启

二级缓存:

  • 作用域是Mapper(namespace)
  • 支持ehcache等缓存实现
  • 可配置剔除策略,刷新间隔,缓存数量等

spring相关

Spring Context初始化流程

Bean的声明周期

redis相关

数据结构

redis内部使用字典存储不同类型的数据,如图中的dictht,字典由一组dictEntry组成,其中包括了指向key和value的指针,以及指向下一个dictEntry的指针,在redis中,所有的对象都被封装成了redisObject对象,redisObject包括了对象的类型,另外还存放了具体对象的存储方式。string是redis中最常使用的数据类型,内部的实现是通过SDS来实现的,SDS类似Java中的ArrayList,可以通过预分配冗余空间的方式,来减小内存的频繁分配,对于list类型,有ziplist压缩列表和linkedList双链表实现,ziplist是存储在一段连续的内存上,存储效率高,但不利于修改操作,适用于数据较少的情况。linkedlist在插入节点上复杂度很低但它的内存开销很大,每个节点的地址不连续,容易产生内存碎片。hash类型在redis中,有ziplist和hashtable两种实现,当hash中所有的key和value字符串长度都小于64字节,且键值对的数量小于512个时,使用压缩表来节省空间,超过时转为使用hashtable。set类型的内部实现,可以是intset或者是hashtable,当集合中元素小于512且所有的数据都是数值类型时,才会使用intset,否则会使用hashtable。sorted set是有序集合,有序集合的实现是ziplist或者是skiplist跳表,有序集合的编码转换条件,与hash和list不同,当有序集合中元素数量小于128个时并且所有元素长度都64字节时会使用ziplist否则会转换成skiplist跳表。

缓存常见问题

  1. 缓存更新方式:

缓存的数据在数据源发生变更时,需要对缓存进行更新,数据源可能是DB也可能是远程服务。更新的方式可以是主动更新,例如数据源是DB时,可以在更新完DB后直接更新缓存,当数据源不是DB而是其他远程服务,可能无法及时主动的感知DB变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间,这种场景下可以选择失效更新,key不存在或者失效时,先请求数据源获取最新的数据,然后再次缓存并更新失效期,但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用,改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步的线程执行更新任务,这样就避免了失效瞬间的空窗期,另外还有一种纯异步的更新方式,定时对数据进行分批更新。

  1. 缓存不一致

缓存不一致产生的原因一般是主动更新失败,例如更新DB后更新reids时,因为网络原因请求超时,或者异步更新失败导致。解决的办法,如果服务对耗时不是特别敏感,可以增加重试;如果服务对耗时敏感,可以通过异步补偿任务来处理失败的更新。或者短期的数据不一致不会影响到业务,那么只要下次更新时能够成功,保证最终一致性就可以了。

  1. 缓存穿透

产生这个的原因可能是外部的恶意攻击,例如对用户信息进行的缓存,恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透DB查询依然不命中,这时候会有大量的请求穿透缓存访问到DB。解决的办法,一是对不存在的用户在缓存中保存一个空对象进行标记,防止相同id再次访问DB,不过有时这个方法并不能很好的解决问题,可能会导致缓存存储大量的无用数据,另一个方法就是用bloomfilter过滤器,bloomfilter的特点是存在性检测,如果bloomfilter中不存在,那么数据一定不存在,如果bloomfilter中存在,实际数据可能存在也有可能不存在,这非常适合解决这类问题

  1. 缓存击穿

某个热点数据失效时,大量针对这个数据的请求会穿透到数据源,问了解决这个问题,可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求DB,减小DB的压力;另一个方法就是使用随机退避方式,失效时随机sleep一个很短的时间,再次查询,如果失败再执行更新;还有一个方法是针对多个热点key同时失效的问题,可以在缓存时使用固定时间加上一个很小的随机数避免同一时间大量热点key同一时刻失效

  1. 缓存雪崩

产生原因是缓存挂掉了,这时候所有的请求都会穿透到DB,解决的办法是一个是使用快速失败的熔断策略,减少DB的压力,另一个就是使用主从模式和集群模式,来尽可能保证集群的高可用,实际场景中通常用把这两种方式结合使用,

redis数据类型和常用操作

  • String常用命令: set,get,decr,incr,mget 等。

  • Hash 常用命令: hget,hset,hgetall 等。

    Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:

    	key=JavaUser293847
    	value={
    	  “id”: 1,
    	  “name”: “SnailClimb”,
    	  “age”: 22,
    	  “location”: “Wuhan, Hubei”
    	}
    
  • List常用命令: lpush,rpush,lpop,rpop,lrange等

    list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。

    Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

    另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

  • Set 常用命令: sadd,spop,smembers,sunion 等

    set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

    当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

    比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:

sinterstore key1 key2 key3     将交集存在key1内
  • Sorted Set 常用命令: zadd,zrange,zrem,zcard等

    和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

    举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。


redis击穿

简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。


redis雪崩

简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法:

  • 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
  • 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
  • 事后:利用 redis 持久化机制保存的数据尽快恢复缓存

几种单例模式

  1. 饿汉式
public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}
  1. 双检锁
public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}
  1. 静态内部类法
public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}
  1. 枚举写法
public enum Singleton {
    INSTANCE ;
 
    public void show(){
        // Do you need to do things
    }
}

//使用
System.out.println( Singleton.INSTANCE.hashCode() ) ;

© 著作权归作者所有

老虎是个蛋蛋
粉丝 173
博文 31
码字总数 40085
作品 0
朝阳
高级程序员
私信 提问
好文收集之Spring :Spring Bean的生命周期

这个问题在Spring的面试的,我感觉是被问到最多的。因为流程比较长,有点绕,可问的点也比较多,还有常用Spring的,如果这个过程都不了解,那真的是不会学习的了。 Spring Bean的生命周期(非...

ol_O_O_lo
02/28
124
0
2018 前端面试题(不定期更新)

前端基础面试题 以下更多的题目,希望大家能掌握更多的前端知识,发现自身的不足。不单单是看题目,背答案。 面试题应该反映出的只是你掌握前端知识的冰山一角。别把冰山全貌给展现出来咯 HT...

青丘
2018/08/02
0
0
IT连创业系列:App产品上线后,运营怎么搞?(中)

等运营篇写完,计划是想写一个IOS系列,把IT连App里用到和遇到的坑都完整的和大伙分享。 不过写IOS系列前,还是要认真把这个运营篇写完,接下来好好码字!!! 上篇说到,我们计划去一次富士...

路过秋天
2017/11/30
0
0
asp.net面试题总结1(未完待续。。。。)

1、MVC中的TempDataViewBagViewData区别? 答:页面对象传值,有这三种对象可以传。 (1) TempData 保存在Session中,Controller每次执行请求的时候,会从Session中先获取 TempData,而后清...

拭不去の泪痕
07/31
0
0
python爬虫知识储备

在开始制作爬虫之前,必要的知识储备是必须的。下面就对基本的知识和工具做些总结. 推荐网页: https://www.crifan.com/howtousesomelanguagepythoncsharptoimplementcrawlwebsiteextractdyn...

youngbit007
2017/12/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

采坑指南——k8s域名解析coredns问题排查过程

正文 前几天,在ucloud上搭建的k8s集群(搭建教程后续会发出)。今天发现域名解析不了。 组件版本:k8s 1.15.0,coredns:1.3.1 过程是这样的: 首先用以下yaml文件创建了一个nginx服务 apiV...

码农实战
14分钟前
1
0
【2019年8月版本】OCP 071认证考试最新版本的考试原题-第6题

choose three Which three statements are true about indexes and their administration in an Orade database? A) An INVISIBLE index is not maintained when Data Manipulation Language......

oschina_5359
16分钟前
1
0
阿里巴巴开源 Dragonwell JDK 最新版本 8.1.1-GA 发布

导读:新版本主要有三大变化:同步了 OpenJDK 上游社区 jdk8u222-ga 的最新更新;带来了正式的 feature:G1ElasticHeap;发布了用户期待的 Windows 实验版本 Experimental Windows version。...

阿里巴巴云原生
21分钟前
1
0
教你玩转Linux—磁盘管理

Linux磁盘管理好坏直接关系到整个系统的性能问题,Linux磁盘管理常用三个命令为df、du和fdisk。 df df命令参数功能:检查文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少...

xiangyunyan
24分钟前
3
0
js 让textarea的高度自适应父元素的高度

textarea按照普通元素设置height是没有作用的,可以这么来设置, 下面给上一段项目代码 JS代码: $.fn.extend({ txtaAutoHeight: function () { return this.each(function () {...

文文1
25分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部