首页 文章 精选 留言 我的

精选列表

搜索[面试],共4913篇文章
优秀的个人博客,低调大师

彻底搞定HashMap面试问题!!!

本文目录 HashMap的设计思想 HashMap的底层结构 为什么不一开始就使用HashMap结构 为什么Map中的节点超过8个时才转换成红黑树 为什么HashMap不是线程安全的 同时put碰撞导致数据丢失 扩容期间取出的值不准确 HashMap在java7和java8中的区别 底层数据结构对比 插入方式对比 扩容方式对比 ConcurrentHashMap在java7和java8中的区别 数据结构 并发程度 遇到Hash碰撞 HashMap的设计思想 HashMap的底层结构 本文主要是讲解jdk1.8中的HashMap源码,会对jdk1.7中的HashMap做一些简单的讲解用来和jdk1.8中的HashMap进行对比。 我们先通过下图来理解HashMap的底层结构: 首先我们通过上面的内容我们可以看到HashMap结构都是这样一个特点:最开始Map时空的,然后往里面放元素时会计算hash值,计算hash值之后,第一个value值会占用一个桶(也就是槽点),以后再来相同hash值的value那么便会用链表的形式在该槽点后继续延伸这就是拉链法。 当链表的长度大于或者等于阙值的(默认是8)的时候,并且同时还满足当前HashMap的容量大于或等于MIN_TREEIFY_CAPACITY(默认64)的要求,就会把链表转成红黑树结构,如果后续由于删除或者其它原因调整了大小,当红黑树的节点小于或等于6个以后,又会恢复链表结构。 为什么不一开始就使用HashMap结构 官方给出的解释是: Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. 翻译就是:因为TreeNodes的大小大约是普通Node节点的两倍,所以只有在节点足够多的情况下才会把Nodes节点转换成TreeNodes节点,是否足够多又由TREEIFY_THRESHOLD决定,而当桶中的节点的数量由于移除或者调整大小变少后,它们又会被转换回普通的链表结构以节省空间。 通过源码中得知,当链表长度达到8就转成红黑树结构,当树节点小于等于6时就转换回去,此处体现了时间和空间的平衡思想。 为什么Map中的节点超过8个时才转换成红黑树 这个问题官方给的解释是: In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are: 0: 0.60653066 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006 more: less than 1 in ten million 上面的意思是:在使用分布良好的用户的hashCodes时,很少使用红黑树结构,因为在理想情况下,链表的节点频率遵循泊松分布(意思就是链表各个长度的命中率依次递减),当命中第8次的时候,链表的长度是8,此时的概率仅为0.00000006,小于千万分之一。 但是,HashMap中决定某一个元素落到哪一个桶中,是和某个对象的hashCode有关的,如果我们自己定义一个极其不均匀的hashCode,例如: public int hashCode(){ return 1; } 由于上述的hashCode方法返回的hash值全部都是1,那么就会导致HashMap中的链表比那的很长,如果数据此时我们向HashMap中放很多节点的话,HashMap就会转换成红黑树结构,所以链表长度超过8就转换成红黑树的设计更多的是为了防止用户自己实现了不好的哈希算法而导致链表过长,影响查询效率,而此时通过转换红黑树结构用来保证极端情况下的查询效率。 HashMap初始化 HashMap的默认初始化大小是16,加载因子是0.75,扩容的阙值就是12(16*0.75),如果进行HashMap初始化的时候传入了自定义容量大小参数size,那么初始化的大小就是正好大于size的2的整数次方,比如传入10,大小就是16,传入30大小就是32,源码如下: static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1;//n>>>1表示n的二进制向右移动1位,以下同理,然后跟移动前的n进行异或操作 n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 上述源码中,通过将n的高位第一个1不断的右移,然后把高位1后面的全变为1,在最后return的时候返回n+1,就符合HashMap容量都是2的整数次幂了。例如下图: 为什么HashMap初始化的容量一定是2的整数次幂 不管我们传入的参数是怎么样的数值,HashMap内部都会通过tableSizeFor方法计算出一个正好大于我们传入的参数的2的整数次幂的数值,那么为什么一定要是2的整数次幂呢?我们先来看看计算key的hash方法如下: //计算key的hash值,hash值是32位int值,通过高16位和低16进行&操作计算。 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 得到了key的hash值后,在计算key在table中的位置索引的时候,代码如下: if ((p = tab[i = (n - 1) & hash]) == null) 正是因为n是2的整数次幂,比如当n是2时,它的二进制是10,4时是100,8时是1000,16时是10000....,那么(n-1)的二进制与之对应就是(2-1)是1,(4-1)是11,(8-1)是111,(16-1)是1111,为什么要用(n - 1) & hash来计算数组的位置索引呢,正是因为(n - 1) & hash的索引一定是落在0~(n-1)之间的位置,而不用管hash值是多少,因为“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15,2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。 为什么HashMap不是线程安全的 我们先来看HashMap中的put方法的源码,如下: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果table容量为空,则进行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //计算hash值在表table中的位置,这里采用&操作是为了更快的计算出位置索引, //而不是取模运算,如果该位置为空,则直接将元素插入到这个位置 //此处也会发生多线程put,值覆盖问题。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //判断tab表中存在相同的key。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判断是否是红黑树节点,如果是则插入到红黑树中 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //遍历链表寻找链表尾部插入新值,如果发现存在相同的key,则停止遍历此时e指向重复的key for (int binCount = 0; ; ++binCount) { //jdk1.7采用的是头差法,jdk1.8采用的是尾差法 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断链表的长度是否大于TREEIFY_THRESHOLD,如果大于则转换红黑树结构 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //发现了重复的key,判断是否覆盖,如果覆盖返回旧值, if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //继续更新次数,不是原子操作,多线程put存在并发安全问题 ++modCount; //如果大于阙值(这个阙值和上面那个不一样,这个等于当前容量*加载因子,默认是16*0.75 = 12),进行扩容。 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 上图部分源码中可以看出HashMap的put方法中有一行代码是++modCount,我们都知道这段代码并不是一个原子操作,它实际上是三个操作,执行步骤分为三步:读取、增加、保存,而且每步操作之间可以穿插其它线程的执行,所以导致线程不安全。 另外还有导致线程不安全的情况还有: 同时put碰撞导致数据丢失 //put方法中部分源码 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 比如上面的源码中,假如现在有两个线程A和B,它们的key的hash值正好一样,然后它们同时执行到了这个if判断,并且都发现tab中i的位置是空,那么线程A就将自己的元素放到该位置,但是线程B之前也是判断到这个位置为空,所以他在A之后也将自己的元素放到了该位置而覆盖了之前线程A的元素,这就是多线程同时put碰撞导致数据丢失的场景,所以HashMap是非线程安全的 扩容期间取出的值不准确 我们先来看看HashMap的取值方法get的源码,源码如下: public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //如果数组时空的,或者当前槽点是空的,说明key所对应的value不存在,直接返回null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //判断第一个节点是否是我们需要的节点,如果是则直接返回 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //判断是否是红黑树节点,如果是的话,就从红黑树中查找 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //遍历链表,查找key do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 上面的get方法的源码中可以看出get方法主要是以下步骤: 计算Hash值,并由此值找到对应的槽点。 如果数组是空的或者该位置为null,那么直接返回null就可以了。 如果该位置处的节点刚好就是我们需要的,直接返回该节点的值。 如果该位置节点是红黑树或者正在扩容,就用find方法继续查找。 否则那就是链表,就进行遍历链表查找。 首先HashMap的get方法是从table中查询我们要查找的key是否存在,如果存在则返回,不存在则直接返回null,那么如果是在扩容期间,为什么获取的结果不准确呢?我们再来看看HashMap的扩容方法resize(),部分源码如下: if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { //重点在这里 oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; . . .//此处忽略 } } } 上面的源码是HashMap的resize方法的一小部分,首先我们知道HashMap的扩容会把旧数组的数据迁移到新数组中(怎么迁移的我们后面再说),在搬迁的过程中会把旧数组正在迁移的桶置为空比如,正如上面代码oldTab[j] = null这一行代码,正是把索引j的桶(或者槽点)置为空了,但是如果此时还没有完成所有的数据的迁移,那么HashMap中仍然是使用的旧数组,此时我们通过get方法查询的key的所以正好在这个旧数组中索引位置是oldTab[j]的位置,因为这个位置已经置空了,所以就会返回null,所以发生了扩容期间读取数据不准确。 HashMap在java7和java8中的区别 底层数据结构对比 java7的HashMap的结构示意图如下: 上图中可以看出jdk1.7中HashMap采用的数据结构是数组+链表的结构。 java8中的HashMap结构示意图如下: 从图中我们可以看出,java8中的HashMap有三种数据结构: 第一种结构是就是数组,数组中空着的位置(槽)代表当前没有数据来填充 第二种是和jdk1.7HashMap类似的拉链结构,在每一个槽中会首先填入第一个节点,后续如果计算出相同的Hash值,就用链表的形式往后延伸 第三种是红黑树结构,这个是java7中HashMap中没有的数据结构,当第二种情况的链表长度大于某一个阙值(默认为8)的时候,HashMap便会把这个链表从链表结构转化为红黑树的形式,目的是为了保证查找效率, 这里简单介绍一下红黑树,红黑树是一种不严格的平衡二叉查找树,主要解决二叉查找树因为动态更新导致的性能退化,其中的平衡的意思代表着近似平衡,等价于性能不会退化.红黑树中的节点分为两类:黑色节点和红色节点,一颗红黑树需要满足: 根节点是黑色。 每个叶子节点都是黑色的空节点(null)。 任何相邻的节点都不能同时为红色,也就是说红色节点是被黑色分开的。 每个节点,从该节点到达其可达到的叶子节点的所有路径,都包含相同数目的黑色节点。 由于红黑树的自平衡特点,所以其查找性能近似于二分查找,时间复杂度是O(log(n)),相比于链表结构,如果发生了最坏的情况,可能需要遍历整个链表才能找到目标元素,时间复杂度为O(n),远远大于红黑树的O(log(n)),尤其是在节点越来越多的情况下,O(log(n)) 插入方式对比 jdk1.7中插入新节点采用的是头查法,就是如果来了新节点,将新节点插入到数组中,原数组中的原始节点作为新节点的后继节点,而且是先判断是否需要扩容,在插入。 jdk1.8中插入新节点的方式是尾查法,就是将新来的节点插入到数组中对应槽点的尾部,插入时先插入,在判断是否需要扩容。 扩容方式对比 jdk1.8中采用的是原位置不变或者位置变为索引+旧容量大小,resize()方法部分源码如下: //jdk1.8代码扩容方式 //其中的loHead开头的表示低位链表开头节点,loTail表示低位链表尾部节点,hiHead开头的表示高位链表开头节点,hiTail表示高位链表尾部节点 //因为扩容时,容量为之前的两倍,而扩容的方法又是原位置不变或者位置变为索引+旧容量大小,所以这里把扩容的容量分为两部分 //一部分是原容量大小,用loHead和loTail表示首尾位置节点,一部分是新扩容的容量大小,用hiHead和hiTail表示首尾位置节点 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { //该方法是扩容时采用的尾查法 next = e.next; //如果判断等于0,则节点在下面插入新数组中的位置索引等于其在旧数组中的位置索引 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //如果不能于0,节点在下面插入新数组中的位置索引等于在旧数组中的位置索引+旧数组容量大小 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //插入位置不变 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //插入位置变为旧数组容量大小+旧数组中的索引 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } jdk1.7中的HashMap扩容的时候需要对原数组中的元素进行重新Hash定位在新数组中的位置,transfer方法的源码如下:。 //jdk1.7HashMap的扩容方法 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; // transfer()方法把原数组中的值放到新数组中,同时依据initHashSeedAsNeeded(newCapacity)返回的boolean值决定是否重新hash transfer(newTable, initHashSeedAsNeeded(newCapacity)); //设置hashmap扩容后为新的数组引用 table = newTable; //设置hashmap扩容新的阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { //保存e的后继节点。 Entry<K,V> next = e.next; //重新hash在新数组中的位置 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } //计算节点e在新数组中的位置索引 int i = indexFor(e.hash, newCapacity); //将原节点e的后继节点指向newTable[i],采用的是头插法 e.next = newTable[i]; //将节点e放到newTable[i]数组中, newTable[i] = e; //将next付给e,开始下一次循环 e = next; } } } 同时因为jdk1.7中HashMap的扩容方法中调用的transfer方法内采用的是头插法,头插法会使链表发生反转,多线程环境下,会产生循环链表,上面代码我们结合图来理解头插法和为什么多线程环境下会产生循环链表:首先假设此时有原HashMap表的内部数据存储如下图: 此时达到了扩容要求,然后线程1和线程2此时同时进行扩容,线程1和线程2扩容时的新数组(newTable)如下图: 假设线程2先执行transfer方法,并且当它正好执行完了Entry<K,V> next = e.next;这行代码后,cpu时间片切换到了线程1来执行,并且线程1执行完了transfer方法,如下图: 线程1插入A节点到自己的新表中 线程1插入B节点到自己的新表中 线程1插入C节点到自己的新表中 当线程1执行完了transfer方法后,还没有执行table = newTable这行代码,此时cpu时间片有切换到了线程2,那么线程2继续接着之前的位置执行,此处需要注意由于由于之前线程2切换线程1之前已经执行完了Entry<K,V> next = e.next这行代码,因此在线程2中变量e存的是A节点了,变量next存的是B节点,而且又由于线程1执行完了transfer方法后,原数组的节点指向如上图可以看出是C指向B,B是指向A的,所以切换到线程2的时候,线程2中的节点指向如下图: 线程2插入A节点到自己的新表中 线程2插入B节点到自己的新表中 由于B节点指向A节点,所以下次插入产生了链表循环,如下图: 综上就是HashMap采用头插法的时候产生链表循环的场景。 jdk1.8中在对HashMap进行扩容的时候放弃头插法而改为尾插法了,扩容代码我已经在上面的扩容方式代码中标出,通过尾插法就避免了因为链表反转导致多线程环境下产生链表循环的情况。 ConcurrentHashMap在java7和java8中的区别 数据结构 我们先看一下jdk1.7中ConcurrentHashMap的底层数据结构,如下图: 图中我们可以看出concurrentHashMap内部进行了Segment分段,Segment继承了ReentrantLock,可以理解为一把锁,各个Segment之间相互独立上锁,互不影响,相比HashTable每次操作都需要锁住整个对象而言,效率大大提升,正是因为concurrentHashMap的锁粒度是针对每个Segment而不是整个对象。 每个Segment的底层数据结构又和HashMap类似,还是数组和链表组成的拉链结构,默认有0~15共16个Segment,所以最多可以同时支持16个线程并发操作(每个线程分别分布在不同的Segment上)。16这个默认值可以在初始化的时候设置为其他值,一旦设置确认初始化之后,是不可以扩容的。 jdk1.8中的ConcurrentHashMap的底层数据结构,如下图: 通过上面两个图中可以看出,jdk1.8中的ConcurrentHashMap在数据结构上比jdk1.7中多了红黑树结构,引入红黑树结构是为了防止查询效率降低。 并发程度 这里我们简单通过java7和java8的ConcurrentHashMap的含参构造函数看一下对比,首先java7的ConcurrentHashMap的构造函数代码如下: //通过指定的容量,加载因子和并发等级创建一个新的ConcurrentHashMap public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { //对构造参数做判断 if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); //限制并发等级不可以大于最大等级 if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; //sshift用来记录向左按位移动的次数 int sshift = 0; //ssize用来记录Segment数组的大小 int ssize = 1; //Segment的大小为大于等于concurrencyLevel的第一个2的n次方的数 while (ssize < concurrencyLevel) { ++sshift; //2的幂次方,2^1,2^2.... ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; //限制最大初始化容量 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //c统计每个Segment内有多少元素 int c = initialCapacity / ssize; //如果有余数,则Segment数量加1 if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; //创建第一个Segment,并放入Segment[]数组中,作为第一个Segment Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); //初始化Segment数组大小 Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; } jdk1.7中的并发等级(concurrencyLevel)用Segment的数量来确定,Segment的个数为大于等于concurrencyLevel的第一个2的n次方的整数,例如当concurrencyLevel为12,13,14,15,16时,此时的Segment的数量为16 java8中的源码汇总保留了Segment片段,但是并没有使用,构造函数如下: public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); //如果并发等级大于初始化容量,则限制初始化容量, //这样的话就是一个槽点对应一个线程,那么理想情况下,最大的并发程度就是数组长度 if (initialCapacity < concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor); int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap; } 通过上面的代码的对比可以得出一下结论: java7中采用Segment分段锁来保证安全,每个Segment独立加锁,最大并发数就是Segment的个数,默认是16。 java8中放弃了Segment设计,采用Node+CAS+synchronized保证线程安全,锁粒度更新,理想情况下table数组元素的个数(也就是数组长度)就是支持并发的最大个数。 遇到Hash碰撞 java7中遇到Hash冲突,采用拉链法,查找时间复杂度是O(n),n是链表的长度。 java8中遇到Hash冲突,先采用拉链法,查找时间复杂度是O(n),当链表长度超过一定阙值时,将链表转换为红黑树结构,来保证查找效率,查找时间复杂度降低为O(log(n)),n为树中的节点个数。

优秀的个人博客,低调大师

Java面试系列-线程相关(一)

实现多线程的方式 继承Thread类,重写run方法,调用start方法启动线程 实现Runnable接口,重写run方法,调用start方法启动线程 实现Callable接口,重写call方法,并用FutureTask包装,在new Thread中传入FutureTask,然后调用start方法启动线程 使用线程池 保证线程安全的方式 synchronized关键字实现的同步方法或者同步代码块 ReentrantLock等实现的锁机制 volatile关键字实现的变量线程安全 使用AtomicInteger等原子类 使用ConcurrentHashMap等线程安全容器 线程有哪些状态? 五个状态:初始化(New)、可运行(Runnable)、运行中(Running)、阻塞(Blocked)、死亡(Dead)。 线程状态图 线程池的7个参数 publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler) corePoolSize 核心线程数:一直存活的核心线程,不会销毁。 maximumPoolSize 最大线程数:提交一个任务,会先进入工作队列,如果队列无法加入,会创建新线程,然后从工作队列中取出一个任务交给新线程来处理,而刚提交的任务会进入工作队列。如果创建新线程导致线程数量超过最大线程,则会执行拒绝策略。 keepAliveTime 空闲线程存活时间:当线程数大于核心线程数时,空余线程等待新任务的最长时间。 unit 空闲线程存活时间单位 workQueue 工作队列 threadFactory 线程工厂:创建线程使用的工厂,可以用来指定线程名字。 handler 拒绝策略 工作队列:有四种 ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO。 LinkedBlockingQuene:基于链表的无界阻塞队列,FIFO。如果指定长度可以充当有界队列,不指定长度则默认Interger.MAX,相当于是无界的。 SynchronousQuene:不缓存任务的阻塞队列,相当于没有队列。 PriorityBlockingQueue:具有优先级的无界阻塞队列。 拒绝策略:工作队列无法加入新任务,且线程数量达到最大线程数,则采用拒绝策略。也有四种 AbortPolicy:默认策略,直接丢弃任务,并且抛出异常。 CallerRunsPolicy:调用者直接执行拒绝任务的run方法。 DiscardPolicy:直接丢弃任务,啥都不做。 DiscardOldestPolicy:抛弃队列中最早的任务,并将当前任务放入队列。 线程池最多能同时处理多少个任务? 如果工作队列是有界队列,则最多:工作队列长度+最大线程数;如果工作队列是无界队列,则最多是无限个。 Executors工具类有哪几种构造线程池的方法? newFixedThreadPool:创建固定大小的线程池。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 JVM中哪些区域线程共享,哪些线程私有? 线程共享:方法区、堆 线程私有:Java栈、本地方法栈、程序计数器 Java到底是引用传递还是值传递 2020-08-07 数据库索引 2020-08-02 事务:不好意思,你被隔离了! 2020-07-23 spring事务咋和新冠病毒一样,还会传染? 2020-07-05 数据是怎么一步一步到服务器的 2020-06-18 本文分享自微信公众号 - pipi蛋(pipidan_fuyun)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

蚂蚁中间件面试指南

写过代码的技术同学都知道,中间件在整个技术体系里的重要性。在过去的十多年里,蚂蚁金服自主研发出了金融级的分布式中间件——SOFAStack,并多次在极为复杂的场景下得到验证,比如每年的双11。在蚂蚁金服,中间件团队是一个带着“光环”的队伍,CTO程立、副CTO胡喜都出自这个组织…… 文/图 无暮 配置千万条,集群第一条,环境不匹配,战友两行泪。——《流浪程序猿》 为什么选择蚂蚁中间件 2年前的这个时候,作为南哪大学实习求职大潮的一名小白,一开学各种互联网大小厂学长学姐的内推邮件就塞满了邮箱,特别是阿里系的内推邮件各种部门玲琅满目。要说为什么在众多内推中对蚂蚁中间件情有独钟,说起来有表里两个原因:先说里原因,一直觉得程序员的核心是用抽象和自动化来低成本和快速地实现更多的价值,而中间件则可以抽象出通用的能力为业务同学赋能,让业务同学专心于业务

优秀的个人博客,低调大师

python高频面试问题(二)

1. 解释什么是栈溢出,在什么情况下可能出现。 栈溢出是由于C语言系列没有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。 在Python中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。 以上内容来自百度百科。https://baike.baidu.com/item/%E6%A0%88%E6%BA%A2%E5%87%BA/8538051?fr=aladdin 栈溢出的几种情况: 局部数组过大,当函数内部的数组过大时,有可能导致堆栈溢出。 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。 以上内容来自,https://blog.csdn.net/u010590166/article/details/22294291/ 2.简述Cpython的内存管理机制 Python和其他高级编程语言,如Java、Ruby或JavaScript等一样,有自动内存管理机制。所以许多程序开发人员没有过多地关注内存管理,但是这可能会导致更多的内存开销和内存泄漏。 引用计数 每一个Python对象都有一个引用计数器----用于记录有多少其他对象指向(引用)这个对象。它存储在变量 refcnt 中,并通过调用C宏Py_INCREF实现引用计数增加和Py_DECREF实现引用计数减少的操作。 Py_DECREF更复杂点,当引用计数器到零时,它会运行该对象的释放函数,回收该类型的对象。 通常以下两种情况你需要考虑这个宏定义:实现自己创建数据结构,或者修改已经存在的Python C API。如果你使用Python内置的数据结构,那么不需要任何操作。 如果想不增加引用计数,可以使用弱引用或 weakrefs 引用对象。 Weakrefs对于实现缓存和代理非常有用。 垃圾回收(GC) 引用计数是在Python 2.0之前管理对象生命周期的唯一方法。它有一个弱点,它不能删除循环引用的对象。 循环引用的最简单的例子是对象引用自身。 通常情况下,可以避免使用循环引用对象,但是有时是不可避免的(例如:长时间运行的程序)。 为了解决这个问题,Python 2.0引入了新的垃圾回收机制。 新GC与其他语言运行时(如JVM和CLR)的GC的主要区别在于,它仅用于寻找存在引用计数的循环引用。 循环引用只能由容器对象创建,因此Python GC不会跟踪整数,字符串等类型。 GC将对象分为3代,每一代对象都有一个计数器和一个阈值。当对象被创建时,阈值会被自动地指派为0,也就是第0代对象。当计数器大于某个阀值,GC就会运行在当前对象代上,回收该对象。没被回收的对象会被移至下一代,并且将相应的计数器复位。下一代的对象保留在下一代。 在Python 3.4之前,GC有一个致命缺点----每个对象重载了del__()方法,因为每个对象都可以相互引用,所以GC不知道该调用那个对象的__del()方法,这会导致GC直接跳过这些对象。具体详细信息可以参考 gc.garbage并且循环引用需要编程人员手动打破。 Python3.4介绍了一种最终的解决方法finalization approach ,现在的GC可以打破对象的循环引用,而不在使用gc.garbage介绍的方法去回收对象。 此外,值得一提的是,如果你确定你的代码没有创建循环引用(或者你不关心内存管理),那么你可以只依赖引用计数器自动管理内存,而不使用GC去管理内存。 以上内容来自:,https://python.freelycode.com/contribution/detail/511 英文原文:https://medium.com/@nvdv/cpython-memory-management-479e6cd86c9#.sbvb0py87 3.请列举你知道的Python的魔法方法及用途。 __init__ 构造器,当一个实例被创建的时候调用的初始化方法 __new__ __new__ 是在一个对象实例化的时候所调用的第一个方法 它的第一个参数是这个类,其他的参数是用来直接传递给 ####__init__ 方法 __new__ 决定是否要使用该 ####__init__ 方法,因为 ####__new__ 可以调用其他类的构造方法或者直接返回别的实例对象来作为本类的实例,如果 ####__new__ 没有返回实例对象,则 ####__init__ 不会被调用 __new__ 主要是用于继承一个不可变的类型比如一个 tuple 或者 string __call__ 允许一个类的实例像函数一样被调用 __del__ 析构器,当一个实例被销毁的时候调用的方法 __len__(self): 定义当被 len() 调用时的行为 __repr__(self): 定义当被 repr() 调用时的行为 __str__(self): 定义当被 str() 调用时的行为 __bytes__(self):定义当被 bytes() 调用时的行为 __hash__(self): 定义当被 hash() 调用时的行为 __bool__(self): 定义当被 bool() 调用时的行为,应该返回 True 或 False 4. 已知以下list: list1 = [ { "mm": 2, },{ "mm": 1, },{ "mm": 4, },{ "mm": 3, },{ "mm": 3, } ] 4.1 把list1中的元素按mm的值排序。 首先说函数sorted的具体用法: (1).Python2.x:sorted(iterable, cmp=None, key=None, reverse=False) ,Python3.x:sorted(iterable, /, *, key=None, reverse=False),Python3.x和Python2.x的sorted函数有点不太一样,少了cmp参数。 key接受一个函数,这个函数只接受一个元素,默认为None reverse是一个布尔值。如果设置为True,列表元素将被倒序排列,默认为False 着重介绍key的作用原理: key指定一个接收一个参数的函数,这个函数用于从每个元素中提取一个用于比较的关键字。默认值为None 。 (2).sorted() 函数对所有可迭代的对象进行排序操作。 (3).对于 元组类型的列表比如 list2=[('b',4),('a',0),('c',2),('d',3)] 排序的方法是使用lambda然后获取需要排的元素的下标即可。 print(sorted(list2,key=lambdax:x[0])) 而本题有点麻烦 里面是字典类型所以我们需要通过以下方式获取 答案 : sorted(list1,key=lambdax:x['mm'])) 或者用 operator 函数来加快速度, 上面排序等价于 fromoperatorimportitemgetterprint(sorted(list1,key=itemgetter('mm'))) 4.2 获取list1中第一个mm值等于x的元素。 x=1forindex,iteminenumerate(list1): ifxinitem.values(): print(list1[index]) break 4.3 删除list1中所有mm等于x的元素,且不对list重新赋值。 x=3foriteminlist1[:]: ifxinitem.values(): list1.remove(item)print(list1) 4.4 取出list1中mm最大的元素,不能排序。 max_num=0foriteminlist1[:]: ifitem['mm']>max_num: max_num=item['mm']print(max_num) 5. 以下操作的时间复杂度是多少? 5.1 list.index 时间复杂度: 5.2 dict.get 时间复杂度: 5.3 x in set(…..) 时间复杂度: 首先了解什么是时间复杂度: 时间复杂度是指执行算法所需要的计算工作量 如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。 大概是这个意思,取n的最大次幂,去除低级次幂和常数以及系数项 简单的理解的话比如for循环,对于一个循环,假设循环体的时间复杂度为 O(n) foriinrange(1,1000): print(i) 2个循环就是 O(n^2) foriinrange(1,1000): forjinrang(1,1000): print(i*j) 依次类推 再举一个例子 n=64whilen>1: print(n) n=n//2 这个 n = n // 2,每次循环之后都会减半,时间复杂度为O(log2n),而常数项往往可以省略所以这个时间复杂度是O(logn)。 常见python内置方法的时间复杂度参考:https://www.cnblogs.com/harvey888/p/6659061.html 5.1 list.index 时间复杂度: 列表是以数组(Array)实现的。最大的开销发生在超过当前分配大小的增长,这种情况下所有元素都需要移动;或者是在起始位置附近插入或者删除元素,这种情况下所有在该位置后面的元素都需要移动。 点击这里查看列表的内部实现原理http://python.jobbole.com/82549/ 答案: O(1) 5.2 dict.get 时间复杂度: 首先先看dict的内部实现原理:http://python.jobbole.com/85040/ 答案: 平均复杂度:O(1)最坏复杂度:O(n) 5.3 x in set(…..) 时间复杂度: 答案: 平均复杂度:O(1)最坏复杂度:O(n) 6. 解释以下输出的原因: In[1]:'{:0.2f}'.format(0.135) Out[1]:'0.14' In[2]:'{:0.2f}'.format(0.145) Out[2]:'0.14' 答案: 官方文档:6.1. string - Common string operations - Python 3.4.8 documentationhttps://docs.python.org/3.4/library/string.html 涉及知识点:Fixed Point 定点数表示法然后看看这两个数的定点数表示法 3.145:3.1450000000000000177635683940 3.135:3.1349999999999997868371792719699442386627197265625 所以就是一个简单的四舍五入: 3.1349显然舍了4 3.1450显然入了5 参考链接:https://www.zhihu.com/question/270543447/answer/355068323 7 简述代码跑出以下异常的原因是什么: IndexError 序列中没有此索引(index) AttributeError 对象没有这个属性 AssertionError 断言语句失败 NotImplementedError 尚未实现的方法 StopIteration 迭代器没有更多的值 TypeError 对类型无效的操作 IndentationError 缩进错误 8. 简述你对GIL的理解 GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。 每个CPU在同一时间只能执行一个线程 在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。 但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。 在Python多线程下,每个线程的执行方式: 1、获取GIL 2、执行代码直到sleep或者是python虚拟机将其挂起。 3、释放GIL 可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。 拿不到通行证的线程,就不允许进入CPU执行。 在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器, 专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。 而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在, python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行)。 IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待, 造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B, 可以不浪费CPU的资源,从而能提升程序执行效率),所以多线程对IO密集型代码比较友好。 更多参考资料:http://cenalulu.github.io/python/gil-in-python/ 9.简述以下内置函数的用法 reduce map all any 答案: reduce: 在Python 3里,reduce()函数已经被从全局名字空间里移除了,它现在被放置在fucntools模块里 用的话要 先引入 from functools import reduce reduce的用法 reduce(function, sequence[, initial]) -> value fromfunctoolsimportreduce reduce(lambdax,y:x+y,[1,2,3]) 输出6 reduce(lambdax,y:x+y,[1,2,3],9) 输出15 reduce(lambdax,y:x+y,[1,2,3],7) 输出13 map: map()是 Python 内置的高阶函数,它接收一个函数 f 和一个 list,并通过把函数 f 依次作用在 list 的每个元素上,得到一个新的 list 并返回。 例如,对于list[1,2,3,4,5,6,7,8,9] 如果希望把list的每个元素都作平方,就可以用map()函数: 因此,我们只需要传入函数f(x)=x*x,就可以利用map()函数完成这个计算: deff(x): returnx*x printmap(f,[1,2,3,4,5,6,7,8,9]) 输出结果: [1,4,9,10,25,36,49,64,81] 注意:map()函数不改变原有的list,而是返回一个新的list。 all: all(x)如果all(x)参数x对象的所有元素不为0、''、False或者x为空对象,则返回True,否则返回False all([0, 1,2, 3]) #列表list,存在一个为0的元素 输出Flase all(('a', 'b', '', 'd')) #元组tuple,存在一个为空的元素 输出Flase all(('', '', '', '')) #元组tuple,全部为空的元素 输出Flase all([]) # 空列表 输出True all(()) # 空元组 输出True all('') # 空字串 输出True 注意:空元组、空列表,空字串返回值为True,这里要特别注意 any: any(x)判断x对象是否为空对象,如果都为空、0、false,则返回false,如果不都为空、0、false,则返回true any('123') 输出True any([0,1]) 输出True any([0,'0','']) 输出True any([0,'']) 输出False any([0,'','false']) 输出True any([0,'',bool('false')]) 输出True any([0,'',False]) 输出False any(('a','b','c')) 输出True any(('a','b','')) 输出True any((0,False,'')) 输出False any([]) 输出False any(()) 输出False 10.copy和deepcopy的区别是什么? —–我们寻常意义的复制就是深复制,即将被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出来的新对象产生影响。 —–而浅复制并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。这就和我们寻常意义上的复制有所不同了。 对于简单的 object,用 shallow copy 和 deep copy 没区别 复杂的 object, 如 list 中套着 list 的情况,shallow copy 中的 子list,并未从原 object 真的「独立」出来。也就是说,如果你改变原 object 的子 list 中的一个元素,你的 copy 就会跟着一起变。这跟我们直觉上对「复制」的理解不同。 代码解释 >>>importcopy>>>origin=[1,2,[3,4]]#origin里边有三个元素:1,2,[3,4]>>>cop1=copy.copy(origin)>>>cop2=copy.deepcopy(origin)>>>cop1==cop2 True>>>cop1iscop2 False#cop1和cop2看上去相同,但已不再是同一个object>>>origin[2][0]="hey!">>>origin [1,2,['hey!',4]]>>>cop1 [1,2,['hey!',4]]>>>cop2 [1,2,[3,4]]#把origin内的子list[3,4]改掉了一个元素,观察cop1和cop2 该部分内容来自:https://blog.csdn.net/qq_32907349/article/details/52190796 11. 简述多线程、多进程、协程之间的区别与联系。 概念: 进程: 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动, 进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间, 不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存, 所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 线程: 线程是进程的一个实体,是CPU调度和分派的基本单位, 它是比进程更小的能独立运行的基本单位. 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈), 但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 协程: 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。 协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈, 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 区别: 进程与线程比较: 线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别: 1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间, 而进程有自己独立的地址空间 2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源 3) 线程是处理器调度的基本单位,但进程不是 4) 二者均可并发执行 5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口, 但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 协程与线程进行比较: 1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。 2) 线程进程都是同步机制,而协程则是异步 3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态 12. 代码中经常遇到的*args, **kwargs含义及用法。 在函数定义中使用args和*kwargs传递可变长参数 *args用来将参数打包成tuple给函数体调用 **kwargs 打包关键字参数成dict给函数体调用 13. 列举一些你知道的HTTP Header 及其功能。 Accept 作用: 浏览器端可以接受的媒体类型, 例如: Accept: text/html 代表浏览器可以接受服务器回发的类型为 text/html 也就是我们常说的html文档, 如果服务器无法返回text/html类型的数据,服务器应该返回一个406错误(non acceptable) 通配符 * 代表任意类型 例如 Accept:/代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个) Accept-Encoding: 作用: 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码); 例如: Accept-Encoding: zh-CN,zh;q=0.8 Accept-Language 作用: 浏览器申明自己接收的语言。 语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等; 例如: Accept-Language: en-us Connection 例如: Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接 例如: Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。 Host(发送请求时,该报头域是必需的) 作用: 请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的 例如: 我们在浏览器中输入:http://www.hzau.edu.cn 浏览器发送的请求消息中,就会包含Host请求报头域,如下: Host:www.hzau.edu.cn 此处使用缺省端口号80,若指定了端口号,则变成:Host:指定端口号 Referer 当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。比如从我主页上链接到一个朋友那里,他的服务器就能够从HTTP Referer中统计出每天有多少用户点击我主页上的链接访问他的网站。可用于防盗链 User-Agent 作用:告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本. 我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。 例如: User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; CIBA; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; InfoPath.2; .NET4.0E) 更多参考资料:https://blog.csdn.net/u014175572/article/details/54861813 14 简述Cookie和Session的区别与联系。 cookie数据存放在客户的浏览器上,session数据放在服务器上 cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。 session会在一定时间内保存在服务器上。当访问增多,会比较占用服务器的性能考虑到减轻服务器性能方面,应当使用COOKIE。 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。 建议: 将登陆信息等重要信息存放为SESSION 其他信息如果需要保留,可以放在COOKIE中 15. 简述什么是浏览器的同源策略。 同源策略是浏览器上为安全性考虑实施的非常重要的安全策略。 何谓同源: URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示他们同源。 同源策略: 浏览器的同源策略,限制了来自不同源的"document"或脚本,对当前"document"读取或设置某些属性。 从一个域上加载的脚本不允许访问另外一个域的文档属性。 举个例子: 比如一个恶意网站的页面通过iframe嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的javascript脚本就可以在用户登录银行的时候获取用户名和密码。 在浏览器中<script>、<img>、<iframe>、<link>等标签都可以加载跨域资源, 而不受同源限制,但浏览器限制了JavaScript的权限使其不能读、写加载的内容。 另外同源策略只对网页的HTML文档做了限制,对加载的其他静态资源如javascript、css、图片等仍然认为属于同源。 16. git commit --amend 有和用处 git commit --amend命令是修复最新提交的便捷方式。它允许你将缓存的修改和之前的提交合并到一起,而不是提交一个全新的快照。它还可以用来简单地编辑上一次提交的信息而不改变快照。 但是,amend不只是修改了最新的提交——它进行了一次替换。对于Git来说,这看上去像一个全新的提交,即上图中用星号表示的那一个。在公共仓库工作时一定要牢记这一点。 17. git如何查看某次提交修改的内容 首先我们可以使用git log 显示历史的提交列表 git show 便可以显示某次提交的修改内容 git show filename 可以显示某次提交的某个内容的修改信息。 git log -p 查看某个文件的修改历史 git log -p -2 查看最近2次的更新内容 18. git如何比较两个commit的区别? git diff commit-id-1 commit-id-2 19. git如何把分支A上某个commit应用到分支B上。 场景A分支上部分文件需要合并到B分支,然而这些文件又是多次commit,并不能直接使用cherry-pick。 然而需要合并的文件并不是太多,所以果断的选择了merge的部分文件合并。 1 首先切换到B分支 , git checkout branchB 2 整理好需要合并的文件列表, git checkout branchA file1 file2 …… 20. 如何查看Linux系统的启动时间,磁盘使用量,内存使用量。 查看启动时间: uptime 查看磁盘使用情况:df -lh 查看内存使用量:top 本次题目内容较多,难度也偏大,大部分内容来自网络的整理,如有意见和疑问大家可以在下面留言,我们一起探讨。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。