1变量的自增与自减
变量的自增自减相信大家都会,一般情况下直接
或--
就可以了。但是实际情况我们可能需要考虑并发问题,多线程情况下,如果我们直接计算。计算结果可能就会不准确。
public static int num = 0;
public static void increase() {
num ;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
//increaseWithLock();
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
我们直接这么操作,结果就会不准确。上述代码运算结果为:
代码语言:javascript复制9589
并不是我们预算的10000。
2加锁的自增与自减
这时我们就会给运算方法加锁,synchronized
或者lock
都行
public static synchronized void increaseWithSync() {
num ;
}
//或者
public static void increaseWithLock() {
try {
lock.lock();
num ;
} finally {
lock.unlock();
}
}
运行结果:
代码语言:javascript复制10000
但是用到了锁,这个东西可以说偏重量级的了,会引起线程上下文的切换和调用,线程之间的切换也会有性能成本的。这是我们就要使用JDK自带的原子类了。
3原子自增与自减
我们来看看java.util.concurrent.atomic
包下面的原子类AtomicInteger
。看下面的代码:
AtomicInteger atomicInteger = new AtomicInteger();
@Test
public void test() throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
num = atomicInteger.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
代码运行结果:
代码语言:javascript复制10000
符合预期。
Java的原子类主要采用CAS 自旋实现,但是在高并发情况下,还是存在一些性能问题的:
高并发量的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间,并且浪费线程资源。 由于需要保证变量真正的共享,**「缓存一致性」**开销变大。
之前我写了一篇关于如何手写Atomic原子类的文章,有兴趣的同学可以看看:
没用过Java原子类?我来手写一个
AtomicInteger
实际上Java还提供了性能更优越的LongAdder
。我们来看看LongAdder
怎么使用。
private static volatile LongAdder longAdder = new LongAdder();
@Test
public void test() throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i ) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j ) {
//num = atomicInteger.incrementAndGet();
longAdder.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(longAdder);
}
运行结果同样符合预期
代码语言:javascript复制10000
那么LongAdder
性能为什么高呢?
Benchmark Mode Cnt Score Error Units
AtomicTest.atomicLongAdd thrpt 200 52860.651 ± 1337.731 ops/ms
AtomicTest.longAdderAdd thrpt 200 486609.475 ± 5204.630 ops/ms
采用JMH做Benchmark基准测试,分别使用10个线程测试两个方法的吞吐量。测试的性能结果如上。我们发现LongAdder
吞吐量明显要高。
唯一会制约AtomicXXX
高效的原因是高并发,高并发意味着CAS的失败几率更高, 重试次数更多,越多线程重试,CAS失败几率又越高,变成恶性循环,AtomicXXX
效率降低。那怎么解决?
LongAdder
的解决方案是:减少并发,将单一value的更新压力分担到多个value中去,降低单个value的 “热度”,分段更新。这样,线程数再多也会分担到多个value上去更新,只需要增加value就可以降低 value的 “热度” 。
简而言之,LongAdder
采用空间换时间。
4分布式系统中的自增与自减
我们来看这样一个需求:
用户注册就会给用户分配一个编号,编号规则按用户先后注册顺序递增,比如第一位注册的用户编号为100,第二位就为101,依次类推。
这里我们就要考虑并发,不能创建重复的编号。你可能会说,这个简单,我就用上面的LongAdder
,性能好,线程安全,不会出现重复编号的情况。
但是实际上我们的系统可能有多个实列,上面的LongAdder
只是JVM级别的,在自己的实列中获取可以实现安全的自增。在有多个实例的系统中就不行了,为了实现上面的需求,我们可以使用数据库的特性来生成编号。
一般的数据库如MySQL可能会有性能问题。这里我推荐使用Redis
来生成。由于Redis
的主计算线程属于单线程,使用Redis
安全又高效。
Java有个Redis
的API RedissonClient
可以用来实现原子自增与自减。
首先我们需要创建一个RedissonClient
实例:
private RedissonClient getRedissonClient() {
Config config = new Config();
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://127.0.0.1:6379");
singleServerConfig.setPassword("lvshen");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
然后我们就用这个实例做自增计算
代码语言:javascript复制public long getCode(String key) {
RedissonClient redissonClient = getRedissonClient();
RAtomicLong atomicVar = redissonClient.getAtomicLong(key);
if (!atomicVar.isExists()) {
atomicVar.set(100);
}
long value = atomicVar.incrementAndGet(); // 多线程调用该方法,不会造成数据丢失
return value;
}
上面的代码就实现了在分布式系统中的原子自增。
以上就是今天的全部内容啦,如果对你有用,欢迎点赞 转发。