2020Java面试题整理--更新中

数据结构

二叉树的遍历方式,前序、中序、后序和层序

Java基础

java访问修饰符资源权限的区别

public 没有访问限制

private 私有的,只有当前类能访问

protect 受保护的 继承,同包能访问

默认的 只允许包内访问

String 是否可以继承, “+” 怎样实现?

String被final修饰无法继承,+ 是通过StringBuffer类的append方法实现

String,StringBuffer,StringBuilder,区别

String 字符串长度不可变

StringBuffer 长度可变,线程安全,效率低

StringBuilder 长度可变,线程不安全,效率高

java异常

java中所有的异常都是Throwable的子类,他的直接子类有两个Error,Exception两种

Error表示JVM出现异常,比如栈溢出或OOM

Exception中异常分为两类

​ 一类是RuntimeException表示运行期间出现的错误

​ 一类是编译时异常

HashMap

特性

HashMap存储键值已实现快速存储,允许null作为key,key值不能重复,如果重复则覆盖

非同步,不是线程安全

底层是hash表,不保证有序性

底层原理

底层使用了数组+链表+红黑树的数据结构,通过无参构造函数new对象时,默认创建一个长度为16的数据

在Put数据的时候通过hash算法计算hash值,然后算出在数据中的坐标

如果该坐标为null则存在数组中,如果坐标有值,则判断当前节点的key是否相同,如果相同则根据参数决定是否替换

如果key不相同,则判断当前节点的属性,如果是链表放到链表尾部,并判断是否超过链表长度8,如果超过则链表转红黑树,如果当前节点是红黑树,则存在树中

完毕后会判断是否扩容,如果map中元素个数大于一定数量则会进行扩容操作

put的实现

1.计算hashcode值(key.hashcode ^ key.hashcode>>>16,讲hashcode值与高16位进行异或运算,从而增加了随机性,减少了hash碰撞)

2.通过计算在数组中的索引,判断数组该位置是否为null,如果为null则创建Node插入,否则判断当前节点类型,如果是链表,则添加到尾部,添加后判断是否链表长度大于8,如果大于则转成红黑树,如果节点类型是红黑树,则直接添加。

3.添加完毕后判断当前size大小,如果超过threshold则进行扩容操作

img

resize
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果到了最大值,则不能进行扩容了
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //双倍扩容
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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;
                    else if (e instanceof TreeNode)
                        //红黑树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //low低头结点,低尾结点(临时存放)
                        Node<K,V> loHead = null, loTail = null;
                        //高头结点,高尾结点(临时存放)
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //判断数组坐标是否发生变化
                            //如果变化多一个bit位,坐标位置变成 当前位置+oldCap
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

//讲述一下,链表的迁移,计算索引是否发生变化,如果变化,则第一个元素设置为头结点,尾结点
//第二个元素进来,头结点的next指向第二个节点,第二个节点变为尾结点,
//第三个元素进来,尾结点的next指向第三个节点(第二个元素),第三个元素变为节点

//下面是赋值操作,如果Lotail不为null则,lohead组成了一个链表,直接赋值即可
长度为什么是2的倍数

在HashMap的操作流程中,首先会对key进行hash算法得到一个索引值,这个索引值就是对应哈希桶数组的索引。为了得到这个索引值必须对扰动后的数跟数组长度进行取余运算。即 hash % n (n为hashmap的长度),又因为&比%运算快。n如果为2的倍数,就可以将%转换为&,结果就是 hash & (n-1)。所以这就解释了为什么HashMap长度是2的倍数

Jdk1.8中满足什么条件后将链表转化成红黑树

很显然在putVal方法中是判断桶内的节点个数是否大于8,之后通过treeifyBin方法中判断长度是否大于最小红黑树容量64,小于则继续扩容,大于则转为红黑树。

//putVal方法判断桶内元素是是否大于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
break;

//treeifyBin方法中判断长度是否大于最小红黑树容量64,小于则继续扩容,大于则转为红黑树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();
hashMap中hash函数是怎么实现的?

通过计算 key.hashcode() ^ (key.hashcode() >>> 16)

hashMap计算下标方式

(n-1) & hash

知道 LRU 吗,20分钟基于 HashMap 实现一个 LRU 算法
public class LRUCache {
    class Node {
        Node pre;
        Node next;
        Integer key;
        Integer val;

        Node(Integer k, Integer v) {
            key = k;
            val = v;
        }
    }

    Map<Integer, Node> map = new HashMap<Integer, Node>();
    // The head (eldest) of the doubly linked list.
    Node head;
    // The tail (youngest) of the doubly linked list.
    Node tail;
    int cap;
    public LRUCache(int capacity) {
        cap = capacity;
        head = new Node(null, null);
        tail = new Node(null, null);
        head.next = tail;
        tail.pre = head;
    }

    public int get(int key) {
        Node n = map.get(key);
        if(n!=null) {
            n.pre.next = n.next;
            n.next.pre = n.pre;
            appendTail(n);
            return n.val;
        }
        return -1;
    }

    public void set(int key, int value) {
        Node n = map.get(key);
        // existed
        if(n!=null) {
            n.val = value;
            map.put(key, n);
            n.pre.next = n.next;
            n.next.pre = n.pre;
            appendTail(n);
            return;
        }
        // else {
        if(map.size() == cap) {
            Node tmp = head.next;
            head.next = head.next.next;
            head.next.pre = head;
            map.remove(tmp.key);
        }
        n = new Node(key, value);
        // youngest node append taill
        appendTail(n);
        map.put(key, n);
    }

    private void appendTail(Node n) {
        n.next = tail;
        n.pre = tail.pre;
        tail.pre.next = n;
        tail.pre = n;
    }
}

基于LinkedHashMap的实现
HashMap+双向链表?这不就是LinkedHashMap吗!

public class LRUCache {

    private int capacity;
    private Map<Integer, Integer> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new java.util.LinkedHashMap<Integer, Integer> (capacity, 0.75f, true) {
            // 定义put后的移除规则,大于容量就删除eldest
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                return size() > capacity;
            }
        };
    }

    public int get(int key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        } else
            return -1;
    }

    public void set(int key, int value) {
        cache.put(key, value);
    }
}

ArrayList

数据结构

底层基于数组实现的,通过更改数组大小,来实现元素的添加

add(E e)
/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //确认内部容量
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //将元素放在数组里
        elementData[size++] = e;
        return true;
    }

	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果是默认空数组,则返回最大值
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //size+1
        return minCapacity;
    }

	private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
		//说明需要的长度已经大于现在的容量,需要扩容
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        //当前数组长度
        int oldCapacity = elementData.length;
        //扩容后的长度(扩容当前长度的1.5倍)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //第一次添加的时候会触发,  minCapacity = size + 1 -->(0 + 1)
        //newCapacity = 0 + 0 * 0.75,newCapacity<minCapacity(1)
        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);
    }

	//超过最大数组长度,按需扩容
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
add(int index, E e)
    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //数组中的元素移位置,将需要放在元素的位置,往后移
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //设置元素
        elementData[index] = element;
        size++;
    }
remove(int index)
    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);
		//计算需要移位的数组长度
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
remove(Object o)
    /**
     * Removes the first occurrence of the specified element from this list,
     * if it is present.  If the list does not contain the element, it is
     * unchanged.  More formally, removes the element with the lowest index
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

多线程原理

线程的状态

NEW,RUNNABLE,WAITING,TIME_WAITING,BLOCKED,TERMINATED

img

Sleep和Wait的区别

Sleep是休眠线程,wait是等待,sleep是thread的静态方法,wait是object的方法

Sleep依旧持有锁,并在指定时间自动换下,wait则释放锁

synchronized

原理是什么

synchronized底层是通过监视器的enter和exit实现的,每个对象都有一个监视器锁,当线程执行被synchronized修饰的方法时会尝试通过monitorenter来获取监视器的所有权,如果获取成功进入数加1,如果该线程已获取监视器,则可以冲入并且进入数加1,通过monitorexit来释放监视器,如果调用则进入数减1,当然执行monitor的线程必须是当前获取monitorenter的线程

https://my.oschina.net/cnarthurs/blog/847801

锁升级

img

在这里插入图片描述

https://upload-images.jianshu.io/upload_images/999329-b6563d61ee7088bd.jpeg

锁的状态总共有四种:无锁状态,偏向锁,轻量级锁,重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,在升级到重量级锁(锁升级是单向的,不会出现降级的现象)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-isa9S3mJ-1586425892219)(C:\Users\xinyu\AppData\Roaming\Typora\typora-user-images\image-20200331173639778.png)]

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier)

偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

偏向锁的设置

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

自旋锁

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。

所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。

自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。

轻量级锁
加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

解锁

**轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。**如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。

重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行速度较长。

Lock和Synchronized的区别

首先两者都保持了高并发场景下的原子性和可见性,区别是synchronized的释放锁机制是由其自身控制的,且互斥性在某些场景下不符合逻辑,无法进行干预,不可人为中断

而Lock常用的有ReentrantLock和Readwritelock两者,添加了类似锁投票,定时锁等候和可中断锁等候的一些特性,此外他还提供了激烈争用情况下更佳的性能

首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

volite实现原理

volite 可以保证内存的可见性,在汇编层面,volite修饰的变量会增加一个lock指令,local前缀的指令在多核心浏览器下 会发生两件事情:

将当前处理器缓存行的数据写回到主内存中

写会主内存的操作会使其他CPU缓存了该地址的数据无效

解释什么是 MESI 协议(缓存一致性)。

CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示):

M: 被修改(Modified)

该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。

当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

E: 独享的(Exclusive)

该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。

同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

S: 共享的(Shared)

该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

I: 无效的(Invalid)

ThreadLocal

ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间互不干扰。

每个Thread里面都存放一个ThreadLocal.ThreadLocalMap 用来存放本地对象(Key)及Value(保存的副本)

public T get()
public void set(T value)
public void remove()
  • get()方法用来获取当前线程的副本变量值
  • set()方法用来保存当前线程的副本变量值
  • remove()方法用来移除当前线程的副本变量值
get()方法
/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

步骤:
 1.获取当前线程的ThreadLocalMap对象threadLocals
 2.从map中获取线程存储的K-V Entry节点。
 3.从Entry节点获取存储的Value副本值返回。
 4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
set()
/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

步骤:
1.获取当前线程的成员变量map
2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中
remove()
/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * <tt>initialValue</tt> method in the current thread.
 *
 * @since 1.5
 */
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
Hash冲突怎么解决

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

ThreadLocal和ThreadPool的冲突

由于ThreadLocal是保存在线程中的,而线程池是会保存活动线程一直在持续运行,这就导致了,部分线程虽然没有设置副本值,但是却存在,解决办法就是在保证不会调用get()的前提下,调用remove()清除数据

ReentrantLock

指可重入锁,指的是一个线程能够对一个临界资源重复加锁。

img

// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
	synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
	// 1.初始化选择公平锁、非公平锁
	ReentrantLock lock = new ReentrantLock(true);
	// 2.可用于代码块
	lock.lock();
	try {
		try {
			// 3.支持多种加锁方式,比较灵活; 具有可重入特性
			if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
		} finally {
			// 4.手动释放锁
			lock.unlock()
		}
	} finally {
		lock.unlock();
	}
}

reentrantlock主要通过判断status的值来进行争抢锁的逻辑

通过lock()获取锁,通过unlock()释放锁

lock()流程

主要讲非公平锁的逻辑

调用lock.lock()

    public void lock() {
        sync.lock();
    }

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

然后调用非公平锁的lock方法,lock方法首先会通过CAS修改state的值,如果修改成功则获取锁,否则判断当前线程是否已经获得锁,如果没有,则将当前线程包装成node放在队列尾部(队列是改良的FIFO,成为CLH)

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

然后进入队列,进行自旋,首先判断当前节点前一个节点是否是头结点,如果是,尝试获取锁,如果获取成功,吧当前节点设置为头结点,然后头结点的next只为null,方便头结点被gc回收,如果不是头结点或者没有抢占锁成功,则进入,shouldParkAfterFailedAcquire()方法

    private 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;
    }

如果前一个节点的watiStatus = -1,则进入挂起状态,

如果前一个节点的waitStatus大于1,说明当前节点被取消了(无效),则删除队列中无效节点,否则,讲前一个节点的waitStatus设置为-1 NODE.SIGNAL

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

unlock()流程
    //释放锁
    public void unlock() {
        sync.release(1);
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }


    /**
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    private 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;
        //如果头结点的下一个节点是null或者被取消了,就从尾部开始遍历
        //为什么从尾部开始遍历?
        //如果是null,有可能是尾结点抢到了锁,或者是新增尾部节点,只设置了tail.pre = s,没有到s.next = tail的地方,肯定要从尾部遍历,因为从头遍历的链路已经断开
        //如果s.waitStatus > 0,说明节点是取消状态,取消状态的node.next = node,指向自己,无法获		 //取下一个节点
        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)
            LockSupport.unpark(s.thread);
    }

数据库

数据库优化

1.sql语句与索引优化

2.加缓存,此处注意数据库和缓存的一致性问题

3.主从复制、读写分离

​ 主从的好处

​ 主从的原理

image

主库有一个Log dump thread ,负责将binlog发送给从库,从库有两个线程I/O线程,一个sql线程,I/O线程读取从主库传过来的的binlog内容,并写入到relay log,然后sql线程从relay log读取内容,写入从库的数据库

​ 主从的一致性问题

​ https://www.cnblogs.com/rjzheng/p/9619855.html

4.分库分表

事务的理解

  • 未提交读(Read Uncommitted):允许脏读,可以读取到其他会话未提交的事务修改的数据
  • 已提交度(Read Commit): 只能读取到其他事务提交的数据
  • 可重复读(Repeated Read):在同一个事务内读取到的结果都是一致的
  • 串行读(Serializable):完全串行化的读取,每次读都需要获得表级共享锁,读写都会相互阻塞

默认是可重复读

Mysql存储引擎

  • MyISAM:拥有较高的插入,查询速度,但是不支持事务
  • InnoDB:默认存储引擎,支持事务,支持ACID事务,支持行级锁
  • Memory:所有的数据置于内存,拥有极高的插入、更新、读写效率,但会占用和数据量成正比的内存空间,并且其内容断电会丢失
  • CVS:逻辑上由逗号分割数据的存储引擎,他会在数据库子目录里为每个数据表创建一个.csv文件,这是一种普通文本文件,每个数据行占用一个文本行

a) 事物具有原子性,一致性,持久性,隔离性
b) 原子性:是指在一个事物中,要么全部执行成功,要么全部失败回滚。
c) 一致性:事物执行之前和执行之后都处于一致性状态
d) 持久性:事物多数据的操作是永久性
e) 隔离性:当一个事物正在对数据进行操作时,另一个事物不可以对数据进行操作,也就是多个并发事物之间相互隔离。

高并发下如何安全的修改一行数据

  • 使用悲观锁

    本质上是当前只有一个线程执行操作,排斥外部请求的修改,遇到加锁的状态就必须等待,结束了在唤醒其他线程进行处理,这种方案虽然解决了安全问题,但是却造成效率低下

  • FIFO缓存队列思路

    直接将请求放入队列中,但是在高并发场景下,请求很多,很可能一瞬间将队列内存撑爆,然后系统又陷入到了异常状态。或者设计 一个极大的内存队列也是一种方案,但是系统处理完一个队列的请求的速度根本无法和疯狂涌入队列的数目相比,也就是说队列请求会越积累越多,最终web系统平均响应时间还是会大幅下降,系统陷入异常

  • 使用乐观锁

    乐观锁采用版本号更新,所有的请求都有资格去更新操作,但是只有符合的版本号才能更新成功

ACID

原子性,隔离性,一致性,持久性

原子性:是指事物是一个不可分割的工作单位,事物中的操作要么都发生,要么都不发生

一致性:事物前后数据的完整性必须保持一致

隔离性:事物的隔离性十多个用户并发访问数据库时,数据库为每一个用户开启的事物,不能被其他事物的操作数据所干扰,多个并发事物之间要相互隔离

a) 事物具有原子性,一致性,持久性,隔离性
b) 原子性:是指在一个事物中,要么全部执行成功,要么全部失败回滚。
c) 一致性:事物执行之前和执行之后都处于一致性状态
d) 持久性:事物多数据的操作是永久性
e) 隔离性:当一个事物正在对数据进行操作时,另一个事物不可以对数据进行操作,也就是多个并发事物之间相互隔离。

事务的隔离级别

框架

Tomcat

如何调优,涉及哪些参数

硬件上选择,操作系统选择,版本选择,jdk选择,配置jvm参数,配置connector的线程数量,开启gzip压缩,trimSpaces,集群等
a) 内存优化:主要是对Tomcat启动参数进行优化,我们可以在Tomcat启动脚本中修改它的最大内存数等等。

b) 线程数优化:Tomcat的并发连接参数,主要在Tomcat配置文件中server.xml中配置,比如修改最小空闲连接线程数,用于提高系统处理性能等等。

c) 优化缓存:打开压缩功能,修改参数,比如压缩的输出内容大小默认为2KB,可以适当的修改。
http://blog.csdn.net/lifetragedy/article/details/7708724

Spring

Spring 事务的传播属性

七种传播属性。
事务传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
①TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
②TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
③TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
④TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
⑤TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
⑥TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
⑦TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
https://www.ibm.com/developerworks/cn/education/opensource/os-cn-spring-trans/

Spring如何管理事务

配置事务的方法有两种:
1)、基于XML的事务配置。

<?xml version="1.0" encoding="UTF-8"?>
<!-- from the file 'context.xml' -->  
<beans xmlns="http://www.springframework.org/schema/beans"  
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
     xmlns:aop="http://www.springframework.org/schema/aop"  
     xmlns:tx="http://www.springframework.org/schema/tx"  
     xsi:schemaLocation="  
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd  
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">  
      
  <!-- 数据元信息 -->  
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>  
    <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>  
    <property name="username" value="root"/>  
    <property name="password" value="root"/>  
  </bean>  
  
  <!-- 管理事务的类,指定我们用谁来管理我们的事务-->  
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
    <property name="dataSource" ref="dataSource"/>  
  </bean>   
  
  <!-- 首先我们要把服务对象声明成一个bean  例如HelloService -->  
  <bean id="helloService" class="com.yintong.service.HelloService"/>  
  
  <!-- 然后是声明一个事物建议tx:advice,spring为我们提供了事物的封装,这个就是封装在了<tx:advice/>中 -->
  <!-- <tx:advice/>有一个transaction-manager属性,我们可以用它来指定我们的事物由谁来管理。
      默认:事务传播设置是 REQUIRED,隔离级别是DEFAULT -->
  <tx:advice id="txAdvice" transaction-manager="txManager">  
      <!-- 配置这个事务建议的属性 -->  
      <tx:attributes>  
        <!-- 指定所有get开头的方法执行在只读事务上下文中 -->  
        <tx:method name="get*" read-only="true"/>  
        <!-- 其余方法执行在默认的读写上下文中 -->  
        <tx:method name="*"/>  
      </tx:attributes>  
  </tx:advice>  
    
  <!-- 我们定义一个切面,它匹配FooService接口定义的所有操作 -->  
  <aop:config>  
     <!-- <aop:pointcut/>元素定义AspectJ的切面表示法,这里是表示com.yintong.service.helloService包下的任意方法。 -->
     <aop:pointcut id="helloServiceOperation" expression="execution(* com.yintong.service.helloService.*(..))"/>  
     <!-- 然后我们用一个通知器:<aop:advisor/>把这个切面和tx:advice绑定在一起,表示当这个切面:fooServiceOperation执行时tx:advice定义的通知逻辑将被执行 -->
     <aop:advisor advice-ref="txAdvice" pointcut-ref="helloServiceOperation"/>  
  </aop:config>  
 
</beans>  

2)、基于注解方式的事务配置。
@Transactional:直接在Java源代码中声明事务的做法让事务声明和将受其影响的代码距离更近了,而且一般来说不会有不恰当的耦合的风险,因为,使用事务性的代码几乎总是被部署在事务环境中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"  
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
     xmlns:aop="http://www.springframework.org/schema/aop"  
     xmlns:tx="http://www.springframework.org/schema/tx"  
     xsi:schemaLocation="  
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd  
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">  
    
  <bean id="helloService" class="com.yintong.service.HelloService"/>  
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
     <property name="dataSource" ref="dataSource"/>  
  </bean>
  <!-- 配置注解事务 -->  
  <tx:annotation-driven transaction-manager="txManager"/>  
</beans>

https://www.jianshu.com/p/805d3cd24d51

说说你对 Spring 的理解,非单例注入的原理?它的生命周期?循环注入的原理, aop 的实现原理,说说 aop 中的几个术语,它们是怎么相互工作的

AOP与IOC的概念(即spring的核心)

a) IOC:Spring是开源框架,使用框架可以使我们减少工作量,提高工作效率并且它是分层结构,即相对应的层处理对应的业务逻辑,减少代码的耦合度。而spring的核心是IOC控制反转和AOP面向切面编程。IOC控制反转主要强调的是程序之间的关系是由容器控制的,容器控制对象,控制了对外部资源的获取。而反转即为,在传统的编程中都是由我们创建对象获取依赖对象,而在IOC中是容器帮我们创建对象并注入依赖对象,正是容器帮我们查找和注入对象,对象是被获取,所以叫反转。

b) AOP:面向切面编程,主要是管理系统层的业务,比如日志,权限,事物等。AOP是将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为切面(aspect),切面将那些与业务逻辑无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

核心组件:bean,context,core,单例注入是通过单例beanFactory进行创建,生命周期是在创建的时候通过接口实现开启,循环注入是通过后置处理器,aop其实就是通过反射进行动态代理,pointcut,advice等。
Aop相关:

http://blog.csdn.net/csh624366188/article/details/7651702/

SpringMvc

SpringMVC的执行流程

SpringMVC是由dispatchservlet为核心的分层控制框架。首先客户端发出一个请求web服务器解析请求url并去匹配dispatchservlet的映射url,如果匹配上就将这个请求放入到dispatchservlet,dispatchservlet根据mapping映射配置去寻找相对应的handel,然后把处理权交给找到的handel,handel封装了处理业务逻辑的代码,当handel处理完后会返回一个逻辑视图modelandview给dispatchservlet,此时的modelandview是一个逻辑视图不是一个正式视图,所以dispatchservlet会通过viewresource视图资源去解析modelandview,然后将解析后的参数放到view中返回到客户端并展现。

Dubbo

原理

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案以及SOA服务治理方案

其核心部分包含:

  • 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
  • 集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
  • 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
dubbo能做什么
  • 透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
  • 软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。
  • 服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。
dubbo架构图

img

一次 RPC 请求的流程是什么

1)服务消费方(client)调用以本地调用方式调用服务;
2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3)client stub找到服务地址,并将消息发送到服务端;
4)server stub收到消息后进行解码;
5)server stub根据解码结果调用本地的服务;
6)本地服务执行并将结果返回给server stub;
7)server stub将返回结果打包成消息并发送至消费方;
8)client stub接收到消息,并进行解码;
9)服务消费方得到最终结果。

Zookeeper

Zookeeper 的用途,选举的原理是什么

Mybatis

Mybatis 的底层实现原理

Redis

数据类型

String,list,hash,set,zset

使用场景

String:缓存热数据

List:队列,

Hash:适合存储对象类型的数据

List:消息队列

Set:共同好友列表、

ZSet:需要排序,比如排行榜

持久化方式

有两种持久化方式AOF,RDB

rdb是时间点快照,aof是纪录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据。

淘汰策略

allkeys-lru:所有key通用,优先删除最近最少使用的key

volatile-lru:从设置过期时间的数据集中挑选出最近最少使用的数据淘汰,没有设置过期时间的key不会被淘汰,这样就可以增加内存空间的同时保证需要持久化的数据不会丢失

volatile-ttl:只限于设置了expire的部分,优先删除剩余时间短的key

allkeys-random:所有的key随机删除一部分key

volatile-random:只限制于设置了expire的部分,随机删除一部分key

Redis选举算法

业务处理

秒杀活动的设计

从限流,削峰,异步,可用性,用户体验

一号店

​ 分为三个模块,排队模块,调度模块,服务模块

  • 排队模块

    创建两个队列,分别存储用户的请求和商品库存,

  • 调度模块

    负责调度服务模块,一旦有处理空闲,就把队列上的用户请求交给服务模块去处理

  • 服务模块

    负责业务的处理,并返回处理结果

  • 容错处理

    重试机制,超时机制(配合前端界面展示)

  • 用户交互

    解决用户等待心情

分库分表的设计

实现分布式锁的方案,及其优缺点

有数据库实现,redis实现,zookeeper实现

  • 数据库实现

    新建一个锁表,纪录方法名,key,主机信息,线程信息,每次获取锁的时候先向数据库插入数据,如果插入成功,则获取锁,在释放锁的时候删除纪录,

    数据库也存在一些问题

    ​ 数据库单点—部署两个数据库双向同步

    ​ 失效时间—设置个定时任务,超时删除纪录

    ​ 非阻塞—while循环直到插入

    ​ 非重入—记录主机信息与线程信息

  • redis实现

    通过setnx()实现,

    SET lock_key random_value NX PX 5000
    值得注意的是:
    random_value 是客户端生成的唯一的字符串。
    NX 代表只在键不存在时,才对键进行设置操作。
    PX 5000 设置键的过期时间为5000毫秒。
    

    redis分布式锁框架 redisson

  • zookeeper

    通过临时有序节点来实现,每个客户端对某个方法加锁,在zookeeper上的与该方法对应的指定节点的目录下,生成一个临时有序的节点,这样只需判断是不是子节点下最小的一个,

    第三方库 Curatro

    三种方案的比较

    上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

    从理解的难易程度角度(从低到高)

    数据库 > 缓存 > Zookeeper

    从实现的复杂性角度(从低到高)

    Zookeeper >= 缓存 > 数据库

    从性能角度(从高到低)

    缓存 > Zookeeper >= 数据库

    从可靠性角度(从高到低)

    Zookeeper > 缓存 > 数据库

分布式事务的原理,优缺点,如何使用分布式事务

什么是一致性 hash

一致性hash是一种分布式hash实现算法。满足平衡性 单调性 分散性 和负载。

http://blog.csdn.net/cywosp/article/details/23397179/

如何防止缓存雪崩

缓存雪崩可能是因为数据未加载到缓存中,或者缓存在同一时间大面积失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机

解决思路:

设置热点数据永不过期

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。

如何防止缓存击穿

缓存是指缓存中和数据库中都没有数据,而频繁发起请求,造成数据库压力过大。

解决思路:

如果数据库不存在则设置为null,并且设置过期时间,过期时间可以设置短点,

采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤(布隆过滤器的特点,判定不存在的一定不存在,判断 存在的可能不存在)

什么是 paxos 算法

什么是 zab 协议

zab是zookeeper原子广播协议的简称

整个zab协议主要包括消息广播和崩溃恢复两个过程,进一步可以分为三个阶段,分别是

发现,同步,广播

一个在线文档系统,文档可以被编辑,如何防止多人同时对同一份文档进行编辑更新

点击编辑的时候,利用redis进行加锁,并设置过期时间,其他用户编辑该文件,如果该文件已被加锁,则不能设置,如果没有加锁则可以设置

如何实现负载均衡,有哪些算法可以实现

后台系统怎么防止请求重复提交

可以通过token值进行防止重复提交,存放到redis中,在表单初始化的时候隐藏在表单中,添加的时候在移除。判断这个状态即可防止重复提交。

如何做到接口的幂等性

数据库和缓存双写一致性方案分析

  • 先更新数据库,在更新缓存

    存在的问题:如果更新数据库成功,缓存更新失败的话,就会导致脏数据

    ​ 或者并发场景下,有两个线程A,B分别更新数据库,A先更新,B后更新,因为网络波 动,A更新缓存晚于B,就会导致脏数据

    ​ 业务场景分析:

    ​ 如果是写多读少的场景,就会导致新能浪费

  • 先删缓存,在更新数据库

    也会存在并发问题,删除缓存后,数据库没来得及更新,另外一个线程刷新了缓存

  • 先删缓存在更新数据库再删除缓存

    这么做,可以避免假如发生脏数据,也能及时删除的操作

  • 先更新数据库,再删除缓存

    这样也会发生脏数据的情况,场景如下,缓存失效的同时更新数据库,A读请求发现缓存失效,读取数据库,B线程写数据,删除缓存后更新,然后A在更新。就会导致问题

  • 提供保障的重试机制

    • 方案一

      image

      1.更新数据库

      2.删除缓存

      3.如果删除缓存失败,讲需要删除的key发送到消息队列

      4.消费端读取需要删除的key

      5.重试删除操作

      缺点:

      ​ 业务耦合性太高,增加代码复杂度

    • 方案二

      通过mysql的binlog日志进行相关处理

      image

      1.更新数据库

      2.mysql写入binlog

      3.产生binlog操作日志

      4.分析出需要操作的数据以及key

      5.删除缓存

      6.如果删除缓存失败,发送数据及key到消息队列

      7.消费端读取操作的数据以及key

      8.重试删除操作

算法

10 亿个数字里里面找最小的 10 个

给一个不知道长度的(可能很大)输入字符串,设计一种方案,将重复的字符排重

遍历二叉树

什么是 B+树,B-树,列出实际的使用场景

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值