10、并发容器,ConcurrentHashMap

Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低

另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:

  • 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList(之前文章有介绍)。

  • 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。

  • 各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于锁分离实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

 

 

Hashtable 本身比较低效,因为它的实现基本就是将 put、get、size 等各种方法加上“synchronized”。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。

Collections.synchronizedMap同步包装器只是利用输入 Map 构造了另一个同步版本,所有操作虽然不再声明成为 synchronized 方法,但是还是利用了“this”作为互斥的 mutex,没有真正意义上的改进。

private static class SynchronizedMap<K,V>implements Map<K,V>, Serializable {private final Map<K,V> m;     // Backing Mapfinal Object      mutex;        // Object on which to synchronize//public int size() {synchronized (mutex) {return m.size();}}//
}

所以,Hashtable 或者同步包装版本,都只是适合在非高度并发的场景下。

 

 

 

ConcurrentHashMap 分析

早期 ConcurrentHashMap,JDK1.7,其实现是基于:

  • 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。

  • HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。

你可以参考下面这个早期 ConcurrentHashMap 内部结构的示意图,其核心是利用分段设计,在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似 Hashtable 整体同步的问题,大大提高了性能。

 

 

在构造的时候,Segment 的数量由所谓的 concurrentcyLevel 决定,默认是 16,也可以在相应构造函数直接指定。注意,Java 需要它是 2 的幂数值,如果输入是类似 15 这种非幂值,会被自动调整到 16 之类 2 的幂数值。get 操作需要保证的是可见性,所以并没有什么同步逻辑。

public V get(Object key) {Segment<K,V> s; // manually integrate access methods to reduce overheadHashEntry<K,V>[] tab;int h = hash(key.hashCode());// 利用位操作替换普通数学运算long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;// 以 Segment 为单位,进行定位// 利用 Unsafe 直接进行 volatile accessif ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {// 省略
          }return null;}

对于 put 操作,首先是通过二次哈希避免哈希冲突,然后以 Unsafe 调用方式,直接获取相应的 Segment,然后进行线程安全的 put 操作:

 public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();// 二次哈希,以保证数据的分散性,避免哈希冲突int hash = hash(key.hashCode());int j = (hash >>> segmentShift) & segmentMask;if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegments = ensureSegment(j);return s.put(key, hash, value, false);}

其核心逻辑实现在下面的内部方法中:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {// scanAndLockForPut 会去查找是否有 key 相同 Node// 无论如何,确保获取锁HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);V oldValue;try {HashEntry<K,V>[] tab = table;int index = (tab.length - 1) & hash;HashEntry<K,V> first = entryAt(tab, index);for (HashEntry<K,V> e = first;;) {if (e != null) {K k;// 更新已有 value...
                    }else {// 放置 HashEntry 到特定位置,如果超过阈值,进行 rehash// ...
                    }}} finally {unlock();}return oldValue;}

 所以,从上面的源码清晰的看出,在进行并发写操作时:

  • ConcurrentHashMap 会获取可重入锁,以保证数据一致性,Segment 本身就是基于 ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的。

  • 在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是更新还是放置操作。重复扫描、检测冲突是 ConcurrentHashMap 的常见技巧。

  •  ConcurrentHashMap 中同样存在扩容。不过与HashMap有一个明显区别,就是它进行的不是整体的扩容,而是单独对 某个Segment中的数组 进行扩容。

 

另外一个 Map 的 size 方法同样需要关注,它的实现涉及分离锁的一个副作用。

试想,如果不进行同步,简单的计算所有 Segment 的总值,可能会因为并发 put,导致结果不准确,但是直接锁定所有 Segment 进行计算,就会变得非常昂贵。其实,分离锁也限制了 Map 的初始化等操作。

所以,ConcurrentHashMap 的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重试次数 2)来试图获得可靠值。如果没有监控到发生变化(通过对比 Segment.modCount),就直接返回,否则获取锁进行操作。

 

 JDK1.7对ConcurrentHashMap的详细分析:   

http://www.cnblogs.com/ITtangtang/p/3948786.html    (jdk1.7  比较详细的介绍)

https://blog.csdn.net/zlfprogram/article/details/77524326

 

获取锁时,并不直接使用lock来获取,因为该方法获取锁失败时会挂起(可重入锁)。事实上,它使用了自旋锁(CAS),如果tryLock获取锁失败,说明锁被其它线程占用,此时通过循环再次以tryLock的方式申请锁。如果在循环过程中该Key所对应的链表头被修改,则重置retry次数。如果retry次数超过一定值,则使用lock方法申请锁。

这里使用自旋锁(CAS)是因为自旋锁的效率比较高,但是它消耗CPU资源比较多,因此在自旋次数超过阈值时切换为互斥锁。

 

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:
 static final class HashEntry<K,V> {  final K key;  final int hash;  volatile V value;  final HashEntry<K,V> next;  } 
可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next 引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍(复制后的节点顺序会变,因为是从旧节点头部开始往后复制,并且是头插法),最后一个节点指向要删除结点的下一个结点,删除节点后面的结点不需要复制,它们可以重用。为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。

 

ConcurrentHashMap的一些特点:

1、public V get(Object key)不涉及到锁,也就是说获得对象时没有使用锁;(CopyOnWrite读的时候也是不需要加锁的)

2、put、remove方法要使用锁,但并不一定有锁争用,原因在于ConcurrentHashMap将缓存的变量分到多个Segment,每个Segment上有一个锁,只要多个线程访问的不是一个Segment就没有锁争用,就没有堵塞,各线程用各自的锁,ConcurrentHashMap缺省情况下生成16个Segment,也就是允许16个线程并发的更新而尽量没有锁争用;(CopyOnWrite写的时候需要加可重入锁)

3、Iterator对象的使用,不一定是和其它更新线程同步,获得的对象可能是更新前的对象,ConcurrentHashMap允许一边更新、一边遍历,也就是说在Iterator对象遍历的时候,ConcurrentHashMap也可以进行remove,put操作,且遍历的数据会随着remove,put操作产出变化,所以希望遍历到当前全部数据的话,要么以ConcurrentHashMap变量为锁进行同步(synchronized该变量),以整个ConcurrentHashMap为获取锁的对象,要么使用CopiedIterator包装iterator,使其拷贝当前集合的全部数据,但是这样生成的iterator不可以进行remove操作。

 

 

 

 

 

 

Java 8 中,ConcurrentHashMap 的变化:

1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现

  • 总体结构上,它的内部存储变得和HashMap 结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin)同步的粒度要更细致一些1.8以后的锁的颗粒度,是加在链表头上的,这个是个思路上的突破。

  • 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。

  • 因为不再使用 Segment,初始化操作大大简化,修改为 lazy-load(延迟初始化) 形式,这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。

  • 数据存储利用 volatile 来保证可见性。

  • 使用 CAS 等操作,在特定场景进行无锁并发操作

  • 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。

 先看看现在的数据存储内部实现,我们可以发现 Key 是 final 的,因为在生命周期中,一个条目的 Key 发生变化是不可能的;与此同时 val,则声明为 volatile,以保证可见性

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;  // key基本不会变volatile V val;  // val需要保证可见性volatile Node<K,V> next;//}

并发的 put :

当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置

1、如果相应位置的Node未初始化,则通过CAS插入相应的数据;

2、如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该头节点加synchronized,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

3、如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;

4、如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;

5、如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount

final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0; // 链表长度for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh; K fk; V fv;if (tab == null || (n = tab.length) == 0)tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // f为链表第一个节点,即在数组中的元素// 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))break; }else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else if (onlyIfAbsent // 不加锁,进行检查&& fh == hash&& ((fk = f.key) == key || (fk != null && key.equals(fk)))&& (fv = f.val) != null)return fv;else {V oldVal = null;synchronized (f) { // 对链表头节点加锁,即数组中的那个元素加锁// 细粒度的同步修改操作... 
                }}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD) //Bin链表超过阀值,树化treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;
}

初始化操作实现在 initTable 里面,这是一个典型的 CAS 使用场景,利用 volatile 的 sizeCtl 作为互斥手段:如果发现竞争性的初始化,就 spin 在那里,等待条件恢复;否则利用 CAS 设置排他标志。如果成功则进行初始化;否则重试。

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {// 如果发现冲突,进行 spin 等待if ((sc = sizeCtl) < 0)Thread.yield(); // CAS 成功返回 true,则进入真正的初始化逻辑else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}

当 bin 为空时,同样是没有必要锁定,也是以 CAS 操作去放置。

你有没有注意到,在同步逻辑上,它使用的是 synchronized,而不是通常建议的 ReentrantLock 之类,这是为什么呢?现代 JDK 中,synchronized 已经被不断优化,可以不再过分担心性能差异,另外,相比于 ReentrantLock,它可以减少内存消耗,这是个非常大的优势。

与此同时,更多细节实现通过使用 Unsafe 进行了优化,例如 tabAt 就是直接利用 getObjectAcquire,避免间接调用的开销。

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

如何实现 size 操作的,真正的逻辑是在 sumCount 方法中

final long sumCount() {CounterCell[] as = counterCells; CounterCell a;long sum = baseCount;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;
}

我们发现,虽然思路仍然和以前类似,都是分而治之的进行计数,然后求和处理,但实现却基于一个奇怪的 CounterCell。 难道它的数值,就更加准确吗?数据一致性是怎么保证的?

static final class CounterCell {volatile long value;CounterCell(long x) { value = x; }
}

其实,对于 CounterCell 的操作,是基于 java.util.concurrent.atomic.LongAdder 进行的,是一种 JVM 利用空间换取更高效率的方法。这个东西非常小众,大多数情况下,建议还是使用 AtomicLong,足以满足绝大部分应用的性能需求。

 

 

size实现

JDK1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount

1、初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化

2、如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组

3、如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象

所以在1.8中的size实现比1.7简单多,因为元素个数保存baseCount中,部分元素的变化个数保存在CounterCell数组中。通过累加baseCountCounterCell数组中的数量,即可得到元素的总个数

 

 

 

需要注意的一点是,1.8以后的锁的颗粒度,是加在链表头上的,这个是个思路上的突破。

 

ConcurrentHashMap1.7与1.8的不同实现:http://www.importnew.com/23610.html

 

 

自旋锁个人理解的是CAS的一种应用方式。并发包中的原子类是典型的应用。
偏向锁个人理解的是获取锁的优化。在ReentrantLock中用于实现已获取完锁的的线程重入问题。偏向锁,侧重是低竞争场景的优化,去掉可能不必要的同步

 

从1.5有并发包,到1.6对synchronized的改进,到1.7的并发map的分段锁(segment是可重入锁ReentrantLock),再到1.8cas(链表头为空)+synchronized(链表头不为空,对链表头加锁)

 jdk8就相当于把segment分段锁更细粒度了(去掉了segment)每个数组元素(链表头节点)就是原来一个segment,那并发度就由原来segment数变为数组长度了,而且用到了cas乐观锁,所以能支持更高的并发。

 

转载于:https://www.cnblogs.com/xuan5301215/p/9100182.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452516.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员的本质

Computers are useless. They can only give you answers. – Picasso计算机没有什么作用。他们只能告诉你答案。——毕加索很多人&#xff08;包括我岳母&#xff09;认为计算机变得如此智能&#xff0c;所以在不久的未来将不再需要程序员。另外一些人认为程序员是天才&#x…

剖析管理所有大数据组件的可视化利器:Hue

欢迎关注大数据和人工智能技术文章发布的微信公众号&#xff1a;清研学堂&#xff0c;在这里你可以学到夜白&#xff08;作者笔名&#xff09;精心整理的笔记&#xff0c;让我们每天进步一点点&#xff0c;让优秀成为一种习惯&#xff01; 日常的大数据使用都是在服务器命令行中…

Django博客--5.让博客支持 Markdown 语法和代码高亮

文章目录0.前言1.安装 Python Markdown2.在 detail 视图中解析 Markdown3.safe 标签4.代码高亮5.效果展示0.前言 Markdown 是一种 HTML 文本标记语言&#xff0c;只要遵循它约定的语法格式&#xff0c;Markdown 的解析工具就能够把 Markdown 文档转换为标准的 HTML 文档&#…

Diango博客--6.Markdown 文章自动生成目录

文章目录0.思路引导1.在文中插入目录2.在页面的任何地方插入目录3.美化标题的锚点 URL0.思路引导 Markdown 在解析内容的同时还可以自动提取整个内容的目录结构&#xff0c;本文内容将从以下几个方面展开&#xff1a; 1&#xff09;在文中插入目录&#xff1b; 2&#xff09;在…

Java中对象和引用的理解

2019独角兽企业重金招聘Python工程师标准>>> 偶然想起Java中对象和引用的基本概念&#xff0c;为了加深下对此的理解和认识&#xff0c;特地整理一下相关的知识点&#xff0c;通过具体实例从两者的概念和区别两方面去更形象的认识理解&#xff0c;再去记忆。12一、对…

android怎样封装,如何封装属于自己的博客网站安卓APP 源码家园

说实话我今天在写这个文章的时候是我使用易语言(E4A\易安卓)的第一天&#xff0c;我也是易小白&#xff0c;但是的确可以用&#xff01;我为什么写这个文章呢&#xff1f;因为之前我也想封装自己的网站&#xff0c;然后去网上找的在线封装生成APP&#xff0c;果然能封装好了&am…

Diango博客--7.自动生成文章摘要

文章目录0.思路引导1.方法一&#xff1a;覆写 save 方法2.方法二&#xff1a;使用 truncatechars 模板过滤器0.思路引导 博客文章的模型有一个 excerpt 字段&#xff0c;这个字段用于存储文章的摘要。 若在 django admin 后台手动为文章输入摘要&#xff0c;每次手动输入摘要…

特斯拉股价暴跌,疯狂烧钱是否真的能够带来高额回报?

“疯狂烧钱”并不能成为公司持续亏损的理由&#xff0c;反而可能成为公司升级转型的关键所在。 上周三&#xff0c;特斯拉发布第四季度财报&#xff0c;其后特斯拉CEO马斯克在电话会议上表示&#xff0c;特斯拉亏损收窄&#xff0c;营收同比增长88%&#xff0c;但与此同时其首…

Diango博客--8.解锁博客侧栏

文章目录0.思路引导1.[最新文章] 模板标签2.[归档] 模板标签3.[分类] 模板标签4.[标签云] 模板标签5.使用自定义的模板标签0.思路引导 博客侧边栏有四项内容&#xff1a;最新文章、归档、分类和标签云&#xff0c;效果展示如下&#xff1a; 这些内容相对比较固定和独立&…

十五、详述 IntelliJ IDEA 插件的安装及使用方法

正文 首先&#xff0c;进入插件安装界面&#xff1a; Mac&#xff1a;IntelliJ IDEA -> Preferences -> Plugins;Windows&#xff1a;File -> Settings -> Plugins.标注 1&#xff1a;显示 IntelliJ IDEA 的插件分类&#xff0c; All plugins&#xff1a;显示 Inte…

面向数据流的设计方法

面向数据流的设计方法的目标是给出设计软件结构的一个系统化的途径。 在软件工程的需求分析阶段&#xff0c;信息流是一个关键考虑。通常用数据流图描绘信息在系统中加工和流动的 情况。面向数据流的设计方法定义了一些不同的“映射”&#xff0c;利用这些映射可以把数据流图…

AI研究的盲点:无解的神经网络内在逻辑

论人工神经网络内在逻辑的研究历史及现状。 伴随着大数据&#xff0c;人工智能&#xff08;AI&#xff09;在沉寂了多年之后&#xff0c;又迎来了新的高潮。在这场涉及大部分科学的革命中&#xff0c;人工神经网络释放了人工智能&#xff08;AI&#xff09;。但科学家们发现&a…

Diango博客--9.归档、分类和标签页

文章目录0.思路引导1.回顾2.归档页面3.分类页面4.标签页面0.思路引导 侧边栏已经正确地显示了最新文章列表、归档、分类、标签等信息&#xff0c;现在来完善归档、分类和标签功能。 当用户点击归档下的某个日期、分类栏目下的某个分类或者标签栏目下的某个标签时&#xff0c;…

android studio1.2.6,1.2.2 使用Android Studio开发Android APP | 菜鸟教程

写在前面本节将介绍如何使用Android Studio开发Android APP&#xff0c;和前面Eclipse ADT SDK搭建Android开发环境一样&#xff0c;本节也只是介绍一些基本东西&#xff0c;深入的&#xff0c;比如快捷键&#xff0c;小技巧等会再另一篇文章中详细地介绍&#xff01;1.下载A…

UPS开始尝试“货车+无人机”的投递方式,不必再担心快递员离职了

继亚马逊“空中仓库”&#xff0c;无人机送货再现新形式。 作为世界上最大的快递承运商与包裹递送公司&#xff0c;UPS当然也没有放过“送货无人机”这一新颖业务。与亚马逊推出“空中仓库”的理念类似&#xff0c;UPS并没有选择让无人机从仓库直接起飞&#xff0c;而是将之与…

Diango博客--10.交流的桥梁“评论功能”

文章目录0.思路引导1.创建"评论"应用2.设计"评论"的数据库模型3.注册"评论"模型到 admin4.设计“评论”表单5.展示评论表单6.“评论”视图函数7.绑定 URL8.向读者发送是否“评论”成功的状态9.详情页底部显示“评论”内容0.思路引导 本文将创建…

python与android交互,Android客户端与Python服务器端的简单通信

最近在做一个APP&#xff0c;需要与服务器通信&#xff0c;一点一点的尝试&#xff0c;记录一下。本文使用了OkHttp和Flask框架。Android客户端&#xff1a;实现功能输入完点击OK按钮后会toast成功的信息。Python服务端&#xff1a;各部分代码如下&#xff1a;activity_main.xm…

云栖科技评论第48期:前沿科技对世界的改造 我们这代人只完成了1%

1、数字经济版图呈中美双分趋势 日本IT行业为前景担忧 数字经济版图呈中美双分趋势 日本IT行业为前景担忧 【新闻摘要】《日本经济新闻》日前刊文称&#xff0c;数字经济的势力版图呈现中国和美国两强双分的趋势明显&#xff0c;这意味着日本可能不得不使用中美的技术&#xff…

CentOS下添加Root权限用户‘超级用户’方法(xxx is not in the sudoers file.This incident will be reported.的解决方法)

文章目录1.添加普通用户2.添加sudo文件的写权限3.编辑sudoers文件4.撤销sudoers文件写权限1.添加普通用户 [rootserver ~]# useradd fxd //添加一个名为fxd的用户 [rootserver ~]# passwd fxd //修改密码 Changing password for user chenjiafa. New UNIX password: //在这里输…

android打印intent flag,Android flag详解

Android flag详解Android flag详解一.Flag标志位在阅读源码的时候经常发现有一些标志属性使用一些位操作来判断是否具有该标志&#xff0c;增加标志或者去除标志。如&#xff1a;二.addFlags (int flags)和setFlags (int flags)区别Public Intent addFlags (int flags)增加额外…