java高并发系列 - 第23天:JUC中原子类,一篇就够了
这是java高并发系列第23篇文章,环境:jdk1.8。
本文主要内容
- JUC中的原子类介绍
- 介绍基本类型原子类
- 介绍数组类型原子类
- 介绍引用类型原子类
- 介绍对象属性修改相关原子类
预备知识
JUC中的原子类都是都是依靠volatile、CAS、Unsafe类配合来实现的,需要了解的请移步:
JUC中原子类介绍
什么是原子操作?
atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰,所以,所谓原子类说简单点就是具有原子操作特征的类,原子操作类提供了一些修改数据的方法,这些方法都是原子操作的,在多线程情况下可以确保被修改数据的正确性。
JUC中对原子操作提供了强大的支持,这些类位于java.util.concurrent.atomic包中,如下图:
JUC中原子类思维导图
基本类型原子类
使用原子的方式更新基本类型
- AtomicInteger:int类型原子类
- AtomicLong:long类型原子类
- AtomicBoolean :boolean类型原子类
上面三个类提供的方法几乎相同,这里以 AtomicInteger 为例子来介绍。
AtomicInteger 类常用方法
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
部分源码
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
2个关键字段说明:
value:使用volatile修饰,可以确保value在多线程中的可见性。
valueOffset:value属性在AtomicInteger中的偏移量,通过这个偏移量可以快速定位到value字段,这个是实现AtomicInteger的关键。
getAndIncrement源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
内部调用的是Unsafe类中的getAndAddInt方法,我们看一下getAndAddInt源码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
说明:
this.getIntVolatile:可以确保从主内存中获取变量最新的值。compareAndSwapInt:CAS操作,CAS的原理是拿期望的值和原本的值作比较,如果相同则更新成新的值,可以确保在多线程情况下只有一个线程会操作成功,不成功的返回false。
上面有个do-while循环,compareAndSwapInt返回false之后,会再次从主内存中获取变量的值,继续做CAS操作,直到成功为止。
getAndAddInt操作相当于线程安全的count++操作,如同:
synchronize(lock){
count++;
}
count++操作实际上是被拆分为3步骤执行:
- 获取count的值,记做A:A=count
- 将A的值+1,得到B:B = A+1
- 让B赋值给count:count = B
多线程情况下会出现线程安全的问题,导致数据不准确。synchronize的方式会导致占时无法获取锁的线程处于阻塞状态,性能比较低。CAS的性能比synchronize要快很多。
示例
使用AtomicInteger实现网站访问量计数器功能,模拟100人同时访问网站,每个人访问10次,代码如下:
package com.itsoku.chat23;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 跟着阿里p7学并发,微信公众号:javacode2018
*/
public class Demo1 {
//访问次数
static AtomicInteger count = new AtomicInteger();
//模拟访问一次
public static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//对count原子+1
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
}
}
输出:
main,耗时:158,count=1000
通过输出中可以看出incrementAndGet
在多线程情况下能确保数据的正确性。
数组类型原子类介绍
使用原子的方式更新数组里的某个元素,可以确保修改数组中数据的线程安全性。
- AtomicIntegerArray:整形数组原子操作类
- AtomicLongArray:长整形数组原子操作类
- AtomicReferenceArray :引用类型数组原子操作类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。
AtomicIntegerArray 类常用方法
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
示例
统计网站页面访问量,假设网站有10个页面,现在模拟100个人并行访问每个页面10次,然后将每个页面访问量输出,应该每个页面都是1000次,代码如下:
package com.itsoku.chat23;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* 跟着阿里p7学并发,微信公众号:javacode2018
*/
public class Demo2 {
static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]);
/**
* 模拟访问一次
*
* @param page 访问第几个页面
* @throws InterruptedException
*/
public static void request(int page) throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//pageCountIndex为pageCount数组的下标,表示页面对应数组中的位置
int pageCountIndex = page - 1;
pageRequest.incrementAndGet(pageCountIndex);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int page = 1; page <= 10; page++) {
for (int j = 0; j < 10; j++) {
request(page);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime));
for (int pageIndex = 0; pageIndex < 10; pageIndex++) {
System.out.println("第" + (pageIndex + 1) + "个页面访问次数为" + pageRequest.get(pageIndex));
}
}
}
输出:
main,耗时:635
第1个页面访问次数为1000
第2个页面访问次数为1000
第3个页面访问次数为1000
第4个页面访问次数为1000
第5个页面访问次数为1000
第6个页面访问次数为1000
第7个页面访问次数为1000
第8个页面访问次数为1000
第9个页面访问次数为1000
第10个页面访问次数为1000
说明:
代码中将10个面的访问量放在了一个int类型的数组中,数组大小为10,然后通过
AtomicIntegerArray
来操作数组中的每个元素,可以确保操作数据的原子性,每次访问会调用incrementAndGet
,此方法需要传入数组的下标,然后对指定的元素做原子+1操作。输出结果都是1000,可以看出对于数组中元素的并发修改是线程安全的。如果线程不安全,则部分数据可能会小于1000。
其他的一些方法可以自行操作一下,都非常简单。
引用类型原子类介绍
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
- AtomicReference:引用类型原子类
- AtomicStampedRerence:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
AtomicReference 和 AtomicInteger 非常类似,不同之处在于 AtomicInteger是对整数的封装,而AtomicReference则是对应普通的对象引用,它可以确保你在修改对象引用时的线程安全性。在介绍AtomicReference的同时,我们先来了解一个有关原子操作逻辑上的不足。
ABA问题
之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的,但是可能出现一个小小的例外,就是当你获得当前数据后,在准备修改为新值钱,对象的值被其他线程连续修改了两次,而经过这2次修改后,对象的值又恢复为旧值,这样,当前线程就无法正确判断这个对象究竟是否被修改过,这就是所谓的ABA问题,可能会引发一些问题。
举个例子
有一家蛋糕店,为了挽留客户,决定为贵宾卡客户一次性赠送20元,刺激客户充值和消费,但条件是,每一位客户只能被赠送一次,现在我们用AtomicReference
来实现这个功能,代码如下:
package com.itsoku.chat22;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 跟着阿里p7学并发,微信公众号:javacode2018
*/
public class Demo3 {
//账户原始余额
static int accountMoney = 19;
//用于对账户余额做原子操作
static AtomicReference<Integer> money = new AtomicReference<>(accountMoney);
/**
* 模拟2个线程同时更新后台数据库,为用户充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
Integer m = money.get();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("当前余额:" + m + ",小于20,充值20元成功,余额:" + money.get() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模拟用户消费
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Integer m = money.get();
if (m > 20) {
if (money.compareAndSet(m, m - 20)) {
System.out.println("当前余额:" + m + ",大于10,成功消费10元,余额:" + money.get() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
输出:
当前余额:19,小于20,充值20元成功,余额:39元
当前余额:39,大于10,成功消费10元,余额:19元
当前余额:19,小于20,充值20元成功,余额:39元
当前余额:39,大于10,成功消费10元,余额:19元
当前余额:19,小于20,充值20元成功,余额:39元
当前余额:39,大于10,成功消费10元,余额:19元
当前余额:19,小于20,充值20元成功,余额:39元
从输出中可以看到,这个账户被先后反复多次充值。其原因是账户余额被反复修改,修改后的值和原有的数值19一样,使得CAS操作无法正确判断当前数据是否被修改过(是否被加过20)。虽然这种情况出现的概率不大,但是依然是有可能出现的,因此,当业务上确实可能出现这种情况时,我们必须多加防范。JDK也为我们考虑到了这种情况,使用
AtomicStampedReference
可以很好地解决这个问题。
使用AtomicStampedRerence解决ABA的问题
AtomicReference
无法解决上述问题的根本原因是,对象在被修改过程中丢失了状态信息,比如充值20元的时候,需要同时标记一个状态,用来标注用户被充值过。因此我们只要能够记录对象在修改过程中的状态值,就可以很好地解决对象被反复修改导致线程无法正确判断对象状态的问题。
AtomicStampedRerence
正是这么做的,他内部不仅维护了对象的值,还维护了一个时间戳(我们这里把他称为时间戳,实际上它可以使用任何一个整形来表示状态值),当AtomicStampedRerence对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedRerence设置对象值时,对象值及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变量,就能防止不恰当的写入。
AtomicStampedRerence
的几个Api在AtomicReference
的基础上新增了有关时间戳的信息。
//比较设置,参数依次为:期望值、写入新值、期望时间戳、新时间戳
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
//获得当前对象引用
public V getReference();
//获得当前时间戳
public int getStamp();
//设置当前对象引用和时间戳
public void set(V newReference, int newStamp);
现在我们使用AtomicStampedRerence
来修改一下上面充值的问题,代码如下:
package com.itsoku.chat22;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 跟着阿里p7学并发,微信公众号:javacode2018
*/
public class Demo4 {
//账户原始余额
static int accountMoney = 19;
//用于对账户余额做原子操作
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(accountMoney, 0);
/**
* 模拟2个线程同时更新后台数据库,为用户充值
*/
static void recharge() {
for (int i = 0; i < 2; i++) {
int stamp = money.getStamp();
new Thread(() -> {
for (int j = 0; j < 50; j++) {
Integer m = money.getReference();
if (m == accountMoney) {
if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
System.out.println("当前时间戳:" + money.getStamp() + ",当前余额:" + m + ",小于20,充值20元成功,余额:" + money.getReference() + "元");
}
}
//休眠100ms
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 模拟用户消费
*/
static void consume() throws InterruptedException {
for (int i = 0; i < 50; i++) {
Integer m = money.getReference();
int stamp = money.getStamp();
if (m > 20) {
if (money.compareAndSet(m, m - 20, stamp, stamp + 1)) {
System.out.println("当前时间戳:" + money.getStamp() + ",当前余额:" + m + ",大于10,成功消费10元,余额:" + money.getReference() + "元");
}
}
//休眠50ms
TimeUnit.MILLISECONDS.sleep(50);
}
}
public static void main(String[] args) throws InterruptedException {
recharge();
consume();
}
}
输出:
当前时间戳:1,当前余额:19,小于20,充值20元成功,余额:39元
当前时间戳:2,当前余额:39,大于10,成功消费10元,余额:19元
结果正常了。
关于这个时间戳的,在数据库修改数据中也有类似的用法,比如2个编辑同时编辑一篇文章,同时提交,只允许一个用户提交成功,提示另外一个用户:博客已被其他人修改,如何实现呢?
博客表:t_blog(id,content,stamp),stamp默认值为0,每次更新+1
A、B 二个编辑同时对一篇文章进行编辑,stamp都为0,当点击提交的时候,将stamp和id作为条件更新博客内容,执行的sql如下:
update t_blog set content = 更新的内容,stamp = stamp+1 where id = 博客id and stamp = 0;
这条update会返回影响的行数,只有一个会返回1,表示更新成功,另外一个提交者返回0,表示需要修改的数据已经不满足条件了,被其他用户给修改了。这种修改数据的方式也叫乐观锁。
对象的属性修改原子类介绍
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改原子类。
- AtomicIntegerFieldUpdater:原子更新整形字段的值
- AtomicLongFieldUpdater:原子更新长整形字段的值
- AtomicReferenceFieldUpdater :原子更新应用类型字段的值
要想原子地更新对象的属性需要两步:
第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
第二步,更新的对象属性必须使用 public volatile 修饰符。
上面三个类提供的方法几乎相同,所以我们这里以AtomicReferenceFieldUpdater
为例子来介绍。
调用AtomicReferenceFieldUpdater
静态方法newUpdater
创建AtomicReferenceFieldUpdater
对象
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
Class<W> vclass,
String fieldName)
说明:
三个参数
tclass:需要操作的字段所在的类
vclass:操作字段的类型
fieldName:字段名称
示例
多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
代码如下:
package com.itsoku.chat22;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 跟着阿里p7学并发,微信公众号:javacode2018
*/
public class Demo5 {
static Demo5 demo5 = new Demo5();
//isInit用来标注是否被初始化过
volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<Demo5, Boolean> updater = AtomicReferenceFieldUpdater.newUpdater(Demo5.class, Boolean.class, "isInit");
/**
* 模拟初始化工作
*
* @throws InterruptedException
*/
public void init() throws InterruptedException {
//isInit为false的时候,才进行初始化,并将isInit采用原子操作置为true
if (updater.compareAndSet(demo5, Boolean.FALSE, Boolean.TRUE)) {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",开始初始化!");
//模拟休眠3秒
TimeUnit.SECONDS.sleep(3);
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完毕!");
} else {
System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其他线程已经执行了初始化!");
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
demo5.init();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
输出:
1565159962098,Thread-0,开始初始化!
1565159962098,Thread-3,有其他线程已经执行了初始化!
1565159962098,Thread-4,有其他线程已经执行了初始化!
1565159962098,Thread-2,有其他线程已经执行了初始化!
1565159962098,Thread-1,有其他线程已经执行了初始化!
1565159965100,Thread-0,初始化完毕!
说明:
- isInit属性必须要volatille修饰,可以确保变量的可见性
- 可以看出多线程同时执行
init()
方法,只有一个线程执行了初始化的操作,其他线程跳过了。多个线程同时到达updater.compareAndSet
,只有一个会成功。
java高并发系列目录
- 第1天:必须知道的几个概念
- 第2天:并发级别
- 第3天:有关并行的两个重要定律
- 第4天:JMM相关的一些概念
- 第5天:深入理解进程和线程
- 第6天:线程的基本操作
- 第7天:volatile与Java内存模型
- 第8天:线程组
- 第9天:用户线程和守护线程
- 第10天:线程安全和synchronized关键字
- 第11天:线程中断的几种方式
- 第12天JUC:ReentrantLock重入锁
- 第13天:JUC中的Condition对象
- 第14天:JUC中的LockSupport工具类,必备技能
- 第15天:JUC中的Semaphore(信号量)
- 第16天:JUC中等待多线程完成的工具类CountDownLatch,必备技能
- 第17天:JUC中的循环栅栏CyclicBarrier的6种使用场景
- 第18天:JAVA线程池,这一篇就够了
- 第19天:JUC中的Executor框架详解1
- 第20天:JUC中的Executor框架详解2
- 第21天:java中的CAS,你需要知道的东西
- 第22天:JUC底层工具类Unsafe,高手必须要了解
java高并发系列连载中,总计估计会有四五十篇文章。
阿里p7一起学并发,公众号:路人甲java,每天获取最新文章!
java高并发系列 - 第23天:JUC中原子类,一篇就够了的更多相关文章
- java高并发系列 - 第12天JUC:ReentrantLock重入锁
java高并发系列 - 第12天JUC:ReentrantLock重入锁 本篇文章开始将juc中常用的一些类,估计会有十来篇. synchronized的局限性 synchronized是java内置 ...
- java高并发系列 - 第13天:JUC中的Condition对象
本文目标: synchronized中实现线程等待和唤醒 Condition简介及常用方法介绍及相关示例 使用Condition实现生产者消费者 使用Condition实现同步阻塞队列 Object对 ...
- java高并发系列 - 第15天:JUC中的Semaphore,最简单的限流工具类,必备技能
这是java高并发系列第15篇文章 Semaphore(信号量)为多线程协作提供了更为强大的控制方法,前面的文章中我们学了synchronized和重入锁ReentrantLock,这2种锁一次都只能 ...
- java高并发系列 - 第14天:JUC中的LockSupport工具类,必备技能
这是java高并发系列第14篇文章. 本文主要内容: 讲解3种让线程等待和唤醒的方法,每种方法配合具体的示例 介绍LockSupport主要用法 对比3种方式,了解他们之间的区别 LockSuppor ...
- java高并发系列 - 第16天:JUC中等待多线程完成的工具类CountDownLatch,必备技能
这是java高并发系列第16篇文章. 本篇内容 介绍CountDownLatch及使用场景 提供几个示例介绍CountDownLatch的使用 手写一个并行处理任务的工具类 假如有这样一个需求,当我们 ...
- java高并发系列 - 第17天:JUC中的循环栅栏CyclicBarrier常见的6种使用场景及代码示例
这是java高并发系列第17篇. 本文主要内容: 介绍CyclicBarrier 6个示例介绍CyclicBarrier的使用 对比CyclicBarrier和CountDownLatch Cycli ...
- java高并发系列 - 第20天:JUC中的Executor框架详解2之ExecutorCompletionService
这是java高并发系列第20篇文章. 本文内容 ExecutorCompletionService出现的背景 介绍CompletionService接口及常用的方法 介绍ExecutorComplet ...
- java高并发系列-第1天:必须知道的几个概念
java高并发系列-第1天:必须知道的几个概念 同步(Synchronous)和异步(Asynchronous) 同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后, ...
- java高并发系列 - 第6天:线程的基本操作
新建线程 新建线程很简单.只需要使用new关键字创建一个线程对象,然后调用它的start()启动线程即可. Thread thread1 = new Thread1(); t1.start(); 那么 ...
随机推荐
- 授权公钥登录,sudo权限脚本
#!/bin/bash############################################################### File Name: key_auth.sh# V ...
- 🔨揭秘vue-sfc-cli: 组件研发利器
前言 本文将揭示vue单文件组件的工具 vue-sfc-cli 的内涵,说明它是如何在整个组件研发流程中提升效率的. 本文可以看成是
- 模型model
django的ORM系统 ORM概念:对象关系映射(Object Relational Mapping,简称ORM)ORM的优势:不用直接编写SQL代码,只需像操作对象一样从数据库操作数据. 模型类必 ...
- Java 中的字符串(String)与C# 中字符串(string)的异同
1. C# 中比较两个字符串字面量是否相等,可以使用 “==”比较运算符,是因为string 类型重写(override)了“==” 和 “!=” 运算符,在使用“==” 和 “!=” 进行字符串比较 ...
- VUE-CLI3.0脚手架安装
文档:https://cli.vuejs.org/zh/guide/ 条件: npm 更至最新 node >=8.9 1.全局安装 npm install -g @vue/cli 或 yarn ...
- 后端访问sso后,如何返回前端vue页面(后端redirect跳转,vue代理,axios带参)
由于项目要加上公司的sso,出现的一系列问题,找到解决办法,在此记录一下.可能并不适合其他项目,给个参考. 前提: 前端是vue.js,后端springboot sso配置需要增加公司自己的maven ...
- Bzoj 1040 [ZJOI2008]骑士 题解
1040: [ZJOI2008]骑士 Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 5368 Solved: 2044[Submit][Status ...
- JDK1.8--体验Stream表达式,从一个对象集合中获取每一个对象的某一个值返回新集合
xl_echo编辑整理,欢迎转载,转载请声明文章来源.更多IT.编程案例.资料请联系QQ:1280023003 百战不败,依不自称常胜,百败不颓,依能奋力前行.——这才是真正的堪称强大!! --- 开 ...
- Python入门基础(9)__面向对象编程_1
定义一个只包含方法的类 class 类名: def 方法1(self,参数列表): pass def 方法2(self,参数列表): pass 当一个类定义之后,要使用这个类来创键对象.语法如下: 对 ...
- 网页学习:day1
初始准备: Write some function Write a titie Write a article Write some button Button function写法: functio ...