HashMap详解-阿里云开发者社区

开发者社区> 李子捌> 正文

HashMap详解

简介: HashMap详解
+关注继续查看
(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅998元/3年,立即抢购>>>:9i0i.cn/aliyun

福利推荐:阿里云、腾讯云、华为云等大品牌云产品全线2折优惠活动来袭,4核8G云服务器899元/3年,新老用户共享优惠,点击这里立即抢购>>>


摘要


散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。


HashMap是Java程序员使用最频繁的的用于键值对(key value)数据处理的容器,在JDK1.7(Java Developmet Kit)时HashMap采取的是数组+链表的形式存储数据,JDK1.8对HashMap进行了存储结构上的优化,引入了红黑树数据结构,极大的增强了HashMap的存取性能!为什么会引入红黑树呢?因为HashMap存在一个问题,即使负载因子和Hash算法设计的再合理,也无法避免出现在链表上拉链过长的问题,如果极端情况下出现严重的Hash冲突,会严重影响HashMap的存取性能,于是HashMap在jdk1.8时,引入了红黑树,利用红黑树快速增删改查的特点来优化了HashMap的性能!


简介


Java为数据结构中的映射(键值对)定义了一个接口java.util.Map,这个接口有四个我们在开发中使用较为频繁的实现类,分别是HashMap、Hashtable、LinkedHashMap、TreeMap

bda8b42f-e9d4-47bb-89ff-4dfcfbbfa061.png


序号

实现类

特点

1

HashMap<K, V>

1、根据键的hashcode存储数据
2、允许一条记录的key为null,允许多条记录的value为null
3、非线程安全

2

Hashtable<K, V>

1、线程安全
2、性能较低,需要使用的场景可以用ConcurrentHashMap替换

3

LinkedHashMap<K,V>

1、访问具有顺序性,保存了插入顺序
2、可以通过构造函数入参来使其按照访问次序排序

4

TreeMap<K,V>

1、有序Map,可以通过排序比较器来自定义存储数据的排序规则,默认按照key的生序排列
2、使用时key需要实现Comparable接口或者通过构造函数传入自定义Comparator


存储结构


jdk1.8以后HashMap采用数组+链表/红黑树的方式来存储数据


b15745c1-5064-4e22-9aa8-a8584ca0c1fa.png


源码分析


主干


// HashMap的主干,也就是上面的绿色部分,是一个Node数组,每个Node包含一个K-V键值对

transient Node[] table;


节点元素


// Node是HashMap的静态内部类,实现了Map接口中的内部Entry接口

static class Node implements Map.Entry {

? ? // 记录当前Node的key的hash值,可以避免重复计算,空间换时间

? ? final int hash;

? ? // 键

? ? final K key;

? ? // 值

? ? V value;

? ? // 存储指向下一个Entry的引用,是单向链表结构

? ? Node next;

? ? // ...

?}


其他重要字段


// 默认的初始容量 16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量,1左移30位

static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认扩容因子 0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表转红黑树的链表长度

static final int TREEIFY_THRESHOLD = 8;

// 红黑树转链表的阈值

static final int UNTREEIFY_THRESHOLD = 6;

// 链表转红黑树的数组长度

static final int MIN_TREEIFY_CAPACITY = 64;

// 实际存储K-V键值对的个数

transient int size;

// 记录HashMap被改动的次数,由于HashMap非线程安全,modCount可用于FailFast机制

transient int modCount;

// 扩容阈值,默认16*0.75=12,当填充到13个元素时,扩容后将会变为32,

int threshold;

// 负载因子 loadFactor=capacity*threshold,HashMap扩容需要参考loadFactor的值

final float loadFactor;


构造函数


// 看一个参数比较全的构造函数,构造函数中并未给table分配内存空间,此构造函数HashMap(Map m)会给table分配内存空间

public HashMap(int initialCapacity, float loadFactor) {

? ? // 判断初始化容量是否合法,如果<0则抛出异常

? ? if (initialCapacity < 0)

? ? ? ? throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

? ? // 判断initialCapacity是否大于 1<<30,如果大于则取 1<<30 = 2^30

? ? if (initialCapacity > MAXIMUM_CAPACITY)

? ? ? ? initialCapacity = MAXIMUM_CAPACITY;

? ? // 判断负载因子是否合法,如果小于等于0或者isNaN,loadFactor!=loadFactor,则抛出异常

? ? if (loadFactor <= 0 || Float.isNaN(loadFactor))

? ? ? ? throw new IllegalArgumentException("Illegal load factor: " +loadFactor);

? ? // 赋值loadFactor

? ? this.loadFactor = loadFactor;

? ? // 通过位运算将threshold设值为最接近initialCapacity的一个2的幂次方(这里非常重要)

? ? this.threshold = tableSizeFor(initialCapacity);

}


hash算法实现


HashMap中的hash算法实现分为三步。其中第二步使用hash值高16位参与位运算,是为了保证在数组table的length比较小的时候,可以保证高低bit都参与到hash运算中,保证分配均匀的同时采用位运算,也不会有太多的性能消耗;其中第三步,当n是2的整数的幂次方是,hash&(n-1),相当于对hash值取模,而位运算比取模运算效率更高;具体流程可以通过图示查看。


第一步:通过key.hashCode()获取key的hashcode;


第二步:通过(h = key.hashCode()) ^ (h >>> 16)进行高16位的位运算;


第三步:通过(n - 1) & hash对计算的hash值取模运算,得到节点插入的数组所在位置。


8542a88f-d5c3-4d11-909a-4ad80d088ffc.png


HashMap之put方法


第一步:判断键值对数组table[i]是否为空/null,是则执行resize()扩容。


第二步:根据键key计算hash值得到插入数组的索引i,如果tab[i]== null则直接插入,执行第六步;如果tab[i] != null,执行第三步。


第三步:判断tab[i]的第一个元素与插入元素key的hashcode&equals是否相等,相等则覆盖,否则执行第四步。


第四步:判断tab[i]是否是红黑树节点TreeNode,是则在红黑树中插入节点,否则执行第五步。


第五步:遍历tab[i]判断链表是否大于8,大于8则可能转成红黑树(数组需要大于64),满足则在红黑树中插入节点;否则在链表中插入;在遍历链表的过程中如果存在key的hashcode&equals相等则替换即可。


第六步:插入成功,判断hashmap的size是否超过threshold的值,超过则扩容。


0cbd6b64-3bbd-4100-859b-074b36d83e8d.png


put(K key, V value)


public V put(K key, V value) {

? ? // hash(key) 根据key计算一个hash值,具体实现如下函数

? ? return putVal(hash(key), key, value, false, true);

}


计算key的hash值


static final int hash(Object key) {

? ? int h;

? ? // 如果key等于null,返回0

? ? // 否则取key的hashcode值h, 将h无符号右移16位也就是获取高16位,将两个值做异或位运算后返回

? ? return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}


putVal(int hash, K key, V value, boolean onlyIfAbsent, ?boolean evict)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

? ? Node[] tab; Node p; int n, i;

? ? // 判断table是否为空,为空则resize()创建

? ? if ((tab = table) == null || (n = tab.length) == 0)

? ? ? ? n = (tab = resize()).length;

? ? // (n - 1) & hash计算插入节点在数组中的索引index,为空则直接插入

? ? if ((p = tab[i = (n - 1) & hash]) == null)

? ? ? ? tab[i] = newNode(hash, key, value, null);

? ? else {

? ? ? ? Node e; K k;

? ? ? ? // 如果节点key存在,则覆盖当前元素

? ? ? ? if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? e = p;

? ? ? ? // 节点不存在且当前的数组节点上的链为RBT红黑树,则按照红黑树的方式插入节点

? ? ? ? else if (p instanceof TreeNode)

? ? ? ? ? ? e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

? ? ? ? // 链为链表

? ? ? ? else {

? ? ? ? ? ? for (int binCount = 0; ; ++binCount) {

? ? ? ? ? ? ? ? // 节点的下一个节点为null,说明遍历到了链表的最后一个节点,将当前遍历到的最后一个节点的next指向新插入节点

? ? ? ? ? ? ? ? if ((e = p.next) == null) {

? ? ? ? ? ? ? ? ? ? p.next = newNode(hash, key, value, null);

? ? ? ? ? ? ? ? ? ? // 链表长度大于8可能需要做红黑树转换,具体要看treeifyBin()中判断数组的长度是否大于64

? ? ? ? ? ? ? ? ? ? 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;

? ? ? ? ? ? ? ? // 不存在且写一个节点不为null,则将下一个节点赋值给当前节点,继续下一次循环

? ? ? ? ? ? ? ? p = e;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? // 节点存在替换后直接返回,不会记录HashmMap结构变化

? ? ? ? if (e != null) { // existing mapping for key

? ? ? ? ? ? V oldValue = e.value;

? ? ? ? ? ? if (!onlyIfAbsent || oldValue == null)

? ? ? ? ? ? ? ? e.value = value;

? ? ? ? ? ? afterNodeAccess(e);

? ? ? ? ? ? return oldValue;

? ? ? ? }

? ? }

? ? // 记录HashMap的结构变化

? ? ++modCount;

? ? // 判断节点插入后是否需要对hashMap的数组进行扩容

? ? if (++size > threshold)

? ? ? ? resize();

? ? afterNodeInsertion(evict);

? ? return null;

}


扩容


final Node[] resize() {

? ? // 将扩容前的数组赋值给oldTab

? ? Node[] 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

? ? }

? ? // 如果数组大小小于等于0且threshold大于0(比如HashMap中的元素被全部删除了),则将oldThr的值赋值给newCap

? ? else if (oldThr > 0)?

? ? ? ? newCap = oldThr;

? ? //首次初始化,给与默认的值

? ? else { ? ? ? ? ? ? ??

? ? ? ? newCap = DEFAULT_INITIAL_CAPACITY;

? ? ? ? newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

? ? }

? ? // 如果新的扩容临界值仍为0,需要给newThr计算一个值

? ? 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数组

? ? Node[] newTab = (Node[])new Node[newCap];

? ? table = newTab;

? ? if (oldTab != null) {

? ? ? ? // 遍历旧数组赋值到新数组中

? ? ? ? for (int j = 0; j < oldCap; ++j) {

? ? ? ? ? ? Node e;

? ? ? ? ? ? // 如果节点不为null,则将节点赋值给e

? ? ? ? ? ? if ((e = oldTab[j]) != null) {

? ? ? ? ? ? ? ? // 将节点引用置空,等待GC回收

? ? ? ? ? ? ? ? oldTab[j] = null;

? ? ? ? ? ? ? ? // 如果节点的next指向为空,则根据节点记录的hash值&扩容的大小-1,重新计算节点在数组中的索引位置

? ? ? ? ? ? ? ? if (e.next == null)

? ? ? ? ? ? ? ? ? ? newTab[e.hash & (newCap - 1)] = e;

? ? ? ? ? ? ? ? // 如果节点是红黑树节点,按照红黑树节点插入方式插入

? ? ? ? ? ? ? ? else if (e instanceof TreeNode)

? ? ? ? ? ? ? ? ? ? ((TreeNode)e).split(this, newTab, j, oldCap);

? ? ? ? ? ? ? ? // 此处表示当前节点上的链为链表,遍历链表,将链表转移到新的数组上

? ? ? ? ? ? ? ? else {

? ? ? ? ? ? ? ? ? ? // 创建两条链lo链在原先数组节点位置

? ? ? ? ? ? ? ? ? ? Node loHead = null, loTail = null;

? ? ? ? ? ? ? ? ? ? // hi链插入原先数组索引+oldCap位置

? ? ? ? ? ? ? ? ? ? Node hiHead = null, hiTail = null;

? ? ? ? ? ? ? ? ? ? Node next;

? ? ? ? ? ? ? ? ? ? do {

? ? ? ? ? ? ? ? ? ? ? ? next = e.next;

? ? ? ? ? ? ? ? ? ? ? ? // 重点

? ? ? ? ? ? ? ? ? ? ? ? // 获取当前节点的hash值,与oldCap做按位与运算,如果运算结果为0,则加入lo链

? ? ? ? ? ? ? ? ? ? ? ? if ((e.hash & oldCap) == 0) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? if (loTail == null)

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? loHead = e;

? ? ? ? ? ? ? ? ? ? ? ? ? ? else

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? loTail.next = e;

? ? ? ? ? ? ? ? ? ? ? ? ? ? loTail = e;

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? ? ? // 否则加入hi链

? ? ? ? ? ? ? ? ? ? ? ? else {

? ? ? ? ? ? ? ? ? ? ? ? ? ? if (hiTail == null)

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? hiHead = e;

? ? ? ? ? ? ? ? ? ? ? ? ? ? else

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? hiTail.next = e;

? ? ? ? ? ? ? ? ? ? ? ? ? ? hiTail = e;

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? } while ((e = next) != null);

? ? ? ? ? ? ? ? ? ? // 新数组的原位置指向lo链的头节点

? ? ? ? ? ? ? ? ? ? if (loTail != null) {

? ? ? ? ? ? ? ? ? ? ? ? loTail.next = null;

? ? ? ? ? ? ? ? ? ? ? ? newTab[j] = loHead;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? // 新数组的j+oldCap指向hi链的头节点

? ? ? ? ? ? ? ? ? ? if (hiTail != null) {

? ? ? ? ? ? ? ? ? ? ? ? hiTail.next = null;

? ? ? ? ? ? ? ? ? ? ? ? newTab[j + oldCap] = hiHead;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? return newTab;

}


图文聊聊jdk1.8中扩容后resize()那些事


在jdk1.8中,当发生HashMap扩容时我们通过源码可以发现,HashMap并未重新通过新的数组长度来确定节点的位置,而是通过(e.hash & oldCap) == 0,原hash值与原数组长度做与位运算来确定节点在新数组中的位置,当(e.hash & oldCap) == 0时,节点在新数组中的位置和老数组位置一致;当(e.hash & oldCap) != 0时,节点在新数组中的位置为j + oldCap,也就是原先的位置加上老数组的长度。


这里抛出两个问题,带着问题来分析


问题一:为什么能这么实现呢?


问题二:以前hash值相同的节点在数组的同一个索引位置上,现在竟然会被随机分配到两个新的索引位置上,这会不会导致get(key)时无法获取到元素呢?


三个前提条件


1、HashMap在做初始化的时候数组的默认大小是16,且如果我们设值的初始值不是2的整数次幂,那么它会自动的去计算一个与初始化值最靠近的2的整数次幂的初始值,具体实现可以查看源码中的tableSizeFor()方法。


static final int tableSizeFor(int cap) {

? ? int n = cap - 1;

? ? n |= n >>> 1;

? ? n |= n >>> 2;

? ? n |= n >>> 4;

? ? n |= n >>> 8;

? ? n |= n >>> 16;

? ? return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}


2、HashMap扩容的方式是oldCap << 1,一定是扩容为原来的2倍,保证扩容后仍然是2的整数次幂


3、HashMap计算数组索引的方式为(n - 1) & hash


数组的初始大小为16,扩容因子为0.75,依次插入三个节点,节点的key分别为key1、key2、key3,假设在插入完成第三个节点后数组中节点的个数到达12个,此时需要进行扩容


fe44018a-039f-4752-ac3d-87df1972dad5.png


此时HashMap中元素分布假设下图所示,size=12,进行扩容


04e3f679-dcea-4385-9ad5-37719649da8f.png


扩容的算法为 e.hash & oldCap,计算后发现key2的值为0,维持在老数组中下标位置,key1和key3需要转移到j + oldCap的位置,也就是1+16=17下标位置上


c6374d5b-dd81-48fc-9cd5-d633b00153d8.png


扩容后节点分布图


e0bb901b-28ac-4fcd-a05d-d0d463695cb9.png


此时我们再来看扩容后根据key获取元素的代码


public V get(Object key) {

? ? Node e;

? ? // 根据当前key计算hash值,这个计算方式上面已经详细介绍过

? ? return (e = getNode(hash(key), key)) == null ? null : e.value;

}


final Node getNode(int hash, Object key) {

? ? Node[] tab; Node first, e; int n; K k;

? ? // 当数组不为空,并且key对应的数组下标的元素存在

? ? if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {

? ? ? ? // 检查获取的节点是否是第一个节点,是则返回

? ? ? ? if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? return first;

? ? ? ? // 不是头结点则判断下一个节点是否存在

? ? ? ? if ((e = first.next) != null) {

? ? ? ? ? ? // 如果节点是红黑树,则在红黑树中获取元素

? ? ? ? ? ? if (first instanceof TreeNode)

? ? ? ? ? ? ? ? return ((TreeNode)first).getTreeNode(hash, key);

? ? ? ? ? ? // 如果是链表则遍历链表中的节点,匹配key则返回,当e不存在下一个节点则结束循环返回null

? ? ? ? ? ? do {

? ? ? ? ? ? ? ? if (e.hash == hash &&

? ? ? ? ? ? ? ? ? ? ((k = e.key) == key || (key != null && key.equals(k))))

? ? ? ? ? ? ? ? ? ? return e;

? ? ? ? ? ? } while ((e = e.next) != null);

? ? ? ? }

? ? }

? ? // 为匹配上则返回null

? ? return null;

}


从上面的源代码中我们可以非常明显的看见,获取节点所在数组中的下标的方式也是(n - 1) & hash,与插入节点时计算数组下标的方式是一抹一样的,那此时为何能准确的找到在key1、key2、key3呢?原因就是n的值变为了原来的两倍,而节点的hash值是不会发生改变的,此时计算出来的节点在数组中的位置,与我们在扩容时,进行的节点移动操作(也可以理解为重新计算数组中的索引)是一致的;我们可以通过get时,计算key1、key2、key3的索引来展示效果,如下


7ddf3a86-99e6-4b43-be91-99daf285bbfb.png


小结


  • HashMap在1.8中做了很大的优化,无论是在计算hash值的算法上,还是底层数据结构从数组+链表变为数组+链表/红黑树,都是对性能有很大的提升
  • HashMap是线程不安全的,在实际生产开发过程中我们如果需要使用的线程安全的key-value存储,可以选择ConcurrentHashMap或者Collections的synchronizedMap是的HashMap具有线程安全的能力,当然推荐的是ConcurrentHashMap
  • 为了保证HashMap的性能,减少扩容带来的性能损耗,我们在使用的过程中,要提前预估存储值的大小,设置合理的初始大小和扩容因子也是有必要的
  • 在使用HashMap时,有必要的情况下我们一定要重写key的hashcode方法和equals方法

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
9473 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
2920 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
11178 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的,?mysql的 3306,?mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建. ? have?fun! ?将编程看作是一门艺术,而不单单是个技术。
10839 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
12029 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
9045 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
13151 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
4619 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
21881 0
+关注
李子捌
简介: CSDN优秀作者、华为云专家 领域: Java框架、并发编程、分布式、微服务、Redis、HarmonyOS、中间件等技术
240
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载


http://www.vxiaotou.com