java基础相关

6 篇文章 0 订阅
2 篇文章 0 订阅

1、JDK对象与数据结构

1.1 HashMap:

  • 数组+链表:链表在增删方面的高效和数组在寻址上的优势。
  • HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entrynext指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
  • initialCapacity默认为16,loadFactory默认为0.75,大于16*0.75=12的时候,扩容为之前的2倍,所以扩容相对来说是个耗资源的操作。
  • 在JDK1.8及以后的版本中引入了红黑树结构,HashMap的实现就变成了数组+链表或数组+红黑树。添加元素时,若桶中链表个数超过8(如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点,而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小),链表会转换成红黑树;删除元素、扩容时,若桶中结构为红黑树并且树中元素个数较少时会进行修剪或直接还原成链表结构,以提高后续操作性能;遍历、查找时,由于使用红黑树结构,红黑树遍历的时间复杂度为 O(logn),所以性能得到提升。
  • TreeMap:基于红黑树实现,特点有些是源于红黑树的一些特点,比如TreeMap是有序的,TreeMap不允许key-value为空等

     
    HashMap的put操作
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node<K,V>[] tab;
        ......
        //数组对应位置没有存放过元素,直接占坑
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //数组对应位置已经存放过元素,需要继续判断:
        else {
            HashMap.Node<K, V> e;
            K k;
            //1、如果当前put的key的hash值与当前占坑的元素一样且key也相等,覆盖即可
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
                //2、否则需要重新找坑
                //2.1 如果采用了红黑树,则把当前put的元素插入到红黑树中
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                //2.2 如果没用红黑树,即老的链表结构,往后插入一个Node即可,
            else {
                for (int binCount = 0; ; ++binCount) {
                    //3.1 找到末尾位置插入node
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //3.2 如果单链表中节点数,达到触发链表转红黑树的阈值,则转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }......} }......}
    }

1.3 finalize方法

finalize方法在垃圾回收器将对象从内存中清除出去之前做必要的清理工作,它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。例如在一些必须要做清楚的操作中,比如说InputStream,我们都知道执行完了得把流关掉为了避免用户的误操作,可以在InputStream的finalize方法中关闭流,这样为资源的释放增加了一层保护网,虽然不保证一定能够执行。看下FileInputStream的源码,这个对象重写了finalize方法:

   /**
     * Ensures that the <code>close</code> method of this file input stream is
     * called when there are no more references to it.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FileInputStream#close()
     */
    protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }

finalize方法执行细节,请移步:Java的Finalizer引发的内存溢出 - 南极山 - 博客园

1.4 红黑树

红黑树 R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:
1)每个节点或者是黑色,或者是红色。
2)根节点是黑色。
3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NILNULL)的叶子节点!]
4)如果一个节点是红色的,则它的子节点必须是黑色的。
5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意:

  • 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
  • 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

上述的性质约束了红黑树的关键:从根到叶子的最长可能路径不多于最短可能路径的两倍长。得到这个结论的理由是:

  1. 红黑树中最短的可能路径是全部为黑色节点的路径
  2. 红黑树中最长的可能路径是红黑相间的路径

优点:红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!

1.5 B-树(B树,不能读B减树)

B-树(Balance Tree),一个m阶的B树具有如下几个特征:

  • 1.根结点至少有两个子女。
  • 2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
  • 3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  • 4.所有的叶子结点都位于同一层。
  • 5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

1.B+

B+树,一个m阶的B+树具有如下几个特征:

  • k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

由于B+树中间节点无数据,所以同样大小的磁盘页可以容纳更多的节点元素;

B-树,由于每个节点都有数据,所以每次查询是不稳定的;

B+树,由于数据都在叶子节点,所以每次查询都是稳定的,且由于B+树在叶子节点,数据间通过链表链接,方便查询一个范围内的数据,效率比B-树高很多

总结:

B+树比B-树优点:

  • 单一节点存储更多的元素,使得查询的IO次数更少。
  • 所有查询都要查找到叶子节点,查询性能稳定。
  • 所有叶子节点形成有序链表,便于范围查询。

1.7 transient 关键字

  • 1)transient修饰的变量不能被序列化;
  • 2)transient只作用于实现 Serializable 接口;
  • 3)transient只能用来修饰普通成员变量字段;
  • 4)不管有没有 transient 修饰,静态变量都不能被序列化;

1.8 threadlocal关键字

ThreadLocal的实现是这样的:每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。

结论:同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路

           同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想

1.9 fail-fast 解决方案

  • 在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。
  • 使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap。
    比如使用 CopyOnWriterArrayList代替 ArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
    对于HashMap,可以使用ConcurrentHashMap, ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。

2、序列化

序列化,你们用了哪些?在选择序列化工具时,你注重是什么,在乎什么?

性能:包括两个方面,时间复杂度和空间复杂度: 第一、空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布式存储系统,数据量往往以TB为单位,巨大的的额外空间开销意味着高昂的成本。 第二、时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反序列化阶段成为整个系统的瓶颈。

当下比较流行的序列化协议,包括XML、JSON、Protobuf:

协议效率编码复杂度可读性适用场景
xml本地存储配置文件
json进程间消息通讯或网络间消息通讯
protobuf低 (二进制格式)进程间消息通讯或网络间消息通讯

3、设计模式

  1. 静态代理、动态代理:Java 动态代理
  2. 动态代理与CGLIB:动态代理proxy与CGLib的区别
  3. 单例模式:单例模式集锦

4、算法题

5、JUC

5.1 CountDownLatch、CyclicBarrier、Semaphore

CountDownLatchCountDownLatch latch = new CountDownLatch(4);比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行;A调用await(),等其他四个任务都执行完countDown()方法后,才能继续执行A被阻塞的逻辑;

CyclicBarrierCyclicBarrier barrier  = new CyclicBarrier(4);有4干个线程都要进行写数据操作,并且只有所有4个线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了;只有await方法,4个线程都执行完await方法后,4个线程才能继续自己后面的任务;

Semaphore:信号量:Semaphore semaphore = new Semaphore(5),默认非公平模式;可以控同时访问某个资源的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

总结:

  1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

5.AQS(AbstractQueuedSynchronizer):Java并发之AQS详解

5.3 线程池

如何设置合理的线程数

任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池

CPU密集型:

  • 尽量使用较小的线程池,一般Cpu核心数+1;因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销

IO密集型:

  • 方法一:可以使用较大的线程池,一般CPU核心数 * 2;IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间;
  • 方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。举个例子:比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

如何设置合理的队列大小

  • 基于空间 :比如队列可以占用10M内存,每个请求大小10K ,那么workQueue队列长度为1000合适
  • 基于时间 :对于单个线程,如果请求超时时间为1s,单个请求平均处理时间10ms,那么队列长度为100合适

12、系统安全设计

12.1 XSS:跨站脚本攻击

XSS危害

  1. 通过document.cookie盗取cookie
  2. 使用js或css破坏页面正常的结构与样式
  3. 流量劫持(通过访问某段具有window.location.href定位到其他页面)
  4. Dos攻击:利用合理的客户端请求来占用过多的服务器资源,从而使合法用户无法得到服务器响应。
  5. 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  6. 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。

XSS防御

从以上的反射型和DOM XSS攻击可以看出,我们不能原样的将用户输入的数据直接存到服务器,需要对数据进行一些处理。以上的代码出现的一些问题如下

  1. 没有过滤危险的DOM节点。如具有执行脚本能力的script, 具有显示广告和色情图片的img, 具有改变样式的link, style, 具有内嵌页面的iframe, frame等元素节点。
  2. 没有过滤危险的属性节点。如事件, style, src, href等
  3. 没有对cookie设置httpOnly。

如果将以上三点都在渲染过程中过滤,那么出现的XSS攻击的概率也就小很多。

12.2 CSRF(Cross-site request forgery):跨站请求伪造

要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1.登录受信任网站A,并在本地生成Cookie。

  2.在不登出A的情况下,访问危险网站B。

理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。
预防措施:

服务端: 1、验证HTTP Referer字段;  2、 校验token

客户端:在请求地址中添加token并验证

HttpServletRequest request = (HttpServletRequest) servletRequest;
String origin = request.getHeader("Origin");
//我们假设只有这个origins列表里面的才是合法的
List<String> origins = Arrays.asList("www.baidu.com", "www.nba.com");
for (String originTemp : origins) {
    if (originTemp.equals(origin)) {
        //安全的
    }
}
//TODO  非安全的 

// 从 HTTP 头中取得 Referer 值
 String referer=request.getHeader("Referer");
 // 判断 Referer 是否以 bank.example 开头
 if((referer!=null) &&(referer.trim().startsWith(“bank.example”))){
    chain.doFilter(request, response);
 }else{
request.getRequestDispatcher(“error.jsp”).forward(request,response);
 } 

12.3 https

HTTP(超文本传输协议)被用于在Web浏览器和网站服务器之间,以明文方式传递信息,不提供任何方式的数据加密,因此使用HTTP协议传输隐私信息(如:银行卡号、密码等支付信息)非常不安全。为了解决这一安全缺陷,网景公司设计了SSL(Secure Sockets Layer)协议,在HTTP的基础上加入了SSL(Secure Sockets Layer)协议,SSL依靠SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。从而诞生了HTTPS(安全套接字层超文本传输协议)。简单来说,HTTPS协议="SSL+HTTP协议"构建的可进行加密传输、身份认证的网络协议,是HTTP的安全版。
SSL/TLS协议运行机制的概述
HTTPS 如何保证数据传输的安全性

SSL证书作用

  • 实现加密传输:网站安装SSL证书后,使用HTTPS加密协议访问网站,可激活客户端浏览器到网站服务器之间的SSL加密通道,实现高强度双向加密传输。
  • 认证服务器真实身份:在网站使用HTTPS协议后,通过SSL证书,浏览器内置安全机制,实时查验证书状态,通过浏览器向用户展示网站认证信息,让用户轻松识别网站真实身份。
  • 保障信息不被窃取:使用了SSL证书后,数据实现了加密传输,保护数据不被泄露或者恶意篡改。

虽然HTTPS可以保证你与网站的通信是加密的,是安全的,但是不能阻止恶意网站,所以浏览网页时千万不要放松警惕,特别是涉及资金或者个人信息等操作时。目前要保护上网安全的话,唯一的方法还是使用一些第三方安全公司的安全软件,比如浏览器。这些公司一般都有丰富的钓鱼网站数据库,如果你访问了钓鱼网站的话,都是可以识别出来并发出警告的。HTTPS有两种,上文提到的就是绿色的HTTPS,绿色的HTTPS表示网站有证书,并且与网站服务器进行通信时数据是加密的,另外还有红色HTTPS,红色HTTPS表示通信时依然是加密的,并且网站有证书,但是证书可能过期或者没有验证过,所以如果在浏览网页时看到有网站的网址前面有红色HTTPS的话,最好不要在这种网站中输入任何账号密码。

12.4  加解密

  • AES加密(对称密钥加密):AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;AES加密为对称密钥加密,加密和解密都是用同一个解密规则,AES加密过程是在一个4×4的字节矩阵上运作,这个矩阵又称为"状态(state)",因为密钥和加密块要在矩阵上多次的迭代,置换,组合,所以对加密快和密钥的字节数都有一定的要求,AES密钥长度的最少支持为128、192、256,加密块分组长度128位。这种加密模式有一个最大弱点:甲方必须把加密规则告诉乙方,否则无法解密。保存和传递密钥,就成了最头疼的问题。 

  • RSA加密(公钥加密,私钥解密): 它是目前最重要的加密算法!计算机通信安全的基石,保证了加密数据不会被破解。你可以想象一下,信用卡交易被破解的后果。甲乙双方通讯,乙方生成公钥和私钥,甲方获取公钥,并对信息加密(公钥是公开的,任何人都可以获取),甲方用公钥对信息进行加密,此时加密后的信息只有私钥才可以破解,所以只要私钥不泄漏,就能保证信息的安全性。

14、锁类型:锁类型

  • 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  • 1、偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 2、轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 3、重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  • 自旋锁:在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。 
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。 
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

synchronized的执行过程: 
检测Mark Word里面是不是当前线程的ID:

    如果是,表示当前线程处于偏向锁 
    如果不是,则使用CAS将当前线程的ID替换Mark Word,
        如果成功则表示当前线程获得偏向锁,置偏向标志位1,当前线程使用CAS将对象头的Mark Word替换为锁记录指针:

             如果成功,当前线程获得锁 
             如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,如果自旋成功则依然处于轻量级状态,如果自旋失败,则升级为重量级锁。

偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

如果线程争用激烈,那么应该禁用偏向锁。

java锁:Java锁---偏向锁、轻量级锁、自旋锁、重量级锁 - linghu_java - 博客园


 

16、 单点登录

Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

  • cookie数据存放在客户的浏览器上,session数据放在服务器上。
  • cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。
  • session会在一定时间内保存在服务器上。当访问增多会占用你服务器的性能,考虑到减轻服务器性能方面应当使用cookie。
  • 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
  • 可以考虑将登陆信息等重要信息存放为session,其他信息如果需要保留,可以放在cookie中。

16.2 Session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录,在服务器上,这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。每个用户访问服务器都会建立一个session,那服务器是怎么标识用户的唯一身份呢?事实上,用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId。

两个问题:

  • 什么东西可以让你每次请求都把SessionId自动带到服务器呢?显然就是cookie了,如果你想为用户建立一次会话,可以在用户授权成功时给他一个唯一的cookie。当一个用户提交了表单时,浏览器会将用户的SessionId自动附加在HTTP头信息中,(这是浏览器的自动功能,用户不会察觉到),当服务器处理完这个表单后,将结果返回SessionId所对应的用户。试想,如果没有 SessionId,当有两个用户同时进行注册时,服务器怎样才能知道到底是哪个用户提交了哪个表单呢。
  • 储存需要的信息。服务器通过SessionId作为key,读写到对应的value,这就达到了保持会话信息的目的。

16.3 单点登录(SSO):token

相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理,用下图说明;比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来不就可以了。不过这和session id没有本质区别啊, 任何人都可以可以伪造,  所以我得想点儿办法, 让别人伪造不了。那就对数据做一个签名吧, 比如说我用HMAC-SHA256 算法,加上一个只有我才知道的密钥,  对数据做一个签名, 把这个签名和数据一起作为token ,   由于密钥别人不知道, 就无法伪造token了。

图片123455

这个token 我不保存,  当小F把这个token 给我发过来的时候,我再用同样的HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的user id ,  如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,没有认证。

阿斯顿发发
Token 中的数据是明文保存的(虽然我会用Base64做下编码, 但那不是加密), 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息。

一个关于byte数组的解析:

因为是String类型
图形a对应的的十进制是97,二进制是0110 0001

图形1对应的的十进制是49,二进制是0011 0001

图形2对应的的十进制是50,二进制是0011 0010

图形3对应的的十进制是51,二进制是0011 0011

图形4对应的的十进制是52,二进制是0011 0100

图形c对应的的十进制是99,二进制是0110 0011

buffer.get():读取一个字节,是0110 0001,对应97,读取后,buffer的position后移一位

buffer.getShort():读取两个字节,是读取到图形1、2,为二进制:0011 0001 0011 0010,转换成十进制是:2的13次方+2的12次方+2的8次方+2的5次方+2的4次方+2=12594

一个关于ByteBuffer解析:

buffer.putShort((short) 1):因为short是两个字节,当前buffer是:00000000 00000001
buffer.putInt(2):因为int是四个字节,当前buffer是:00000000 00000001 00000000 00000000 00000000 00000010
以此类推
buffer.get()从buffer读取一个字节,即8位,00000000=十进制0,
读取后,当前buffer是:00000001  00000000 00000000 00000000 00000010
buffer.getShort()从buffer读取两个字节,即16位,00000001 00000000=十进制2的8次方=256
以此类推

计算机里面的数据,很难读懂。

一道面试题:

变量a是一个64位有符号的整数,初始值用16进制表示为:0x7FFFFFFFFFFFFFFF
变量b是一个64位有符号的整数,初始值用16进制表示为:0x8000000000000000
则a+b的结果用10进制表示为多少:

答:a+b=0xFFFFFFFFFFFFFFFF,转换为二进制为:11111....111111(64位1),有符号数,第一位是符号位,1开头代表负数,计算机中负数以补码的形式保存,对于负数:将除符号位之外按位取反,末尾加1,即得到原码:11111....111111取反得到100....00000,加1,100....00001,即为10进制-1

ThreadLocal使用:

    /**
     * 原理:
     *   每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。
     *   也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,
     *   通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。
     *   如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,
     *   所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量
     *
     * 总结:
     *   由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
     *   但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
     *   因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
     *
     * ThreadLocal正确的使用方法:
     *   每次使用完ThreadLocal都调用它的remove()方法清除数据
     *   将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,
     *   也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
     *
     *  个人大白话总结:
     *    虽然大家用的是 ThreadLocal.set操作,但实际是往线程私有的ThreadLocal.ThreadLocalMap threadLocals变量里set
     *    所以他是线程安全的,因为每个线程都有自己的threadLocals,不存在共享竞争问题,这是一种空间换时间的做法
     */

    //ThreadLocal是线程安全的,并且没有使用到锁;常用来存放线程独有变量,解决参数传递问题
    //在这个demo中,localStr是共享的,随后在每个线程中给localStr设置值为自己线程的名字,然后再将当前线程的日志输出
    static ThreadLocal<String> localStr = new ThreadLocal<>();

    public static void main(String[] args) {
        List<Thread> list = new LinkedList<>();
        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    localStr.set(Thread.currentThread().getName() + " localStr");
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                    }
                    System.out.println(localStr.get());
                }
            }, "thread" + String.valueOf(i));
            list.add(t);
        }
        for (Thread t : list) {
            t.start();
        }

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值