Java 系统吞吐量起不来?可能是 Java Concurrency 使用不当(一)

栏目: IT技术 · 发布时间: 4年前

内容简介:Java平台从设计之初就对多线程和并发有了很强的支持,从而帮助咱们方便地写出并发的程序。不过,一般来讲,多线程系统很难Debug,排查问题,一些情况下, 也能做扩容。从我自身的经验来看,大多数多数程问题是在并发量大涨后才会出现。为了让咱排查解决多线程问题时方便时,我们有必要理解下多线程的工作原理以及每一种选择的优缺点。从下面这个简单的例子开始:

因为深入,所以使用起来更得心应手、更灵活。

Java平台从设计之初就对多线程和并发有了很强的支持,从而帮助咱们方便地写出并发的程序。不过,一般来讲,多线程系统很难Debug,排查问题,一些情况下, 也能做扩容。从我自身的经验来看,大多数多数程问题是在并发量大涨后才会出现。为了让咱排查解决多线程问题时方便时,我们有必要理解下多线程的工作原理以及每一种选择的优缺点。

从下面这个简单的例子开始:

public class Foo {

private int x;

public int getX() {

return x;

}

public void setX(int x) {

this.x = x;

}

}

很明显上面的代码不是线程安全的。修复的一个方式是使用synchronized关键字来修改setX()和getX()两个方法。

Synchronization的原理怎样?  

一个线程调用synchronized修饰的方法或代码块后,这个线程会尝试着获取一个内部锁。这个锁获取后一直到释放前,其它想获取此锁的线程会一直等待。

这个机制看起来不错,不过细想下, 还有些缺点:

    线程饥饿(Starvation):Synchronization机制并不能确保公平。这也就意味着如果大量线程都竞争着获取同一个锁时,会有不小的概率使某些线程一直得不到锁,这也就是线程饥饿。 

    死锁:如果synchronized修改的代码线程再调用另一个synchronized修改的代码时,有可能会引起死锁发生。 

   低吞吐:使用synchronization机制时, 也就意味着只能有一个线程才能执行。很多情况下, 没必要这样严格的排队,毕竟仔细研究的话,只要限制写入操作就可以了, 没必要保护那些读操作。 

Synchronization机制可以解决线程安全问题, 但一些情况下并不是最好选择。

可从Javadoc里看下liveness问题。

Volatile

针对上面的问题, 另一个解决方案是使用volatile关键字, 代码示例如下:

public class Foo {

private volatile int x;

...

}

Volatile的工作原理怎样?  

JVM中定义, Volatile关键字可以保证:

可见性:如果一个线程修改了某一个变量的值后,此变量的更新会立即同步给读这个变量的线程。使用这个关键字后,编译器或JVM不再将此变量分配到CPU的register中。这样,任何往此变量上写更新,就会立即刷新到主存中, 随后读此变量的线程将直接主存里取值。使用这种机制后,会有些很小的性能损耗,不过从线程安全的角度看,效果很好。 

排序性:某种情况下为了性能优化,JVM会对指令重排序。使用Volatile,修改变量的读取操作, 将不会被重排序。这样,就最终保证非volatile变量周围的写操作会立即对其它线程可变。 

针对上面的原则, 我们看下面的代码例子:

public class Foo {

private int x = -1;

private volatile boolean v = false;

public void setX(int x) {

this.x = x;

v = true;

}

public int getX() {

if (v == true) {

return x;

}

return 0;

}

}

因为上面的第一个原则,如果线程A调用setX(), 线程B调用了getX(), 这种调用场景下,对变量v的更新会立即同步到线程B。因为第二个原则,对x变量的修改也会立即同步给线程B。

不过,volatile关键字对某些场景下的事件不适合,如++、--等。这是因为这样的数据操作会转换成多个读和写指令。例如:

public int increment() {

//x++

int tmp = x;

tmp = tmp + 1;

x = tmp;

return x;

}

在多线程场景下, 上面的操作应该调整成原子性,而volatile并不能保证。Java SE中自带了一些原子性类,如AtomicInteger, AtomicLong和AtomicBoolean, 使用这样的原子性类后,上面的问题可以解决。

原子性类是怎么工作的?  

Java依赖机器的指令和算法来达到原子性。在 Java 8之前,原子类使用了Compare-and-Swap。在Java 8后,原子类中的某些方法改用Fetch-and-Add。

我们看下Java 7中AtomicInteger.getAndIncrement()方法的实现, 如下代码:

public final int getAndIncrement() {

for (;;) {

int current = get();

int next = current + 1;

if (compareAndSet(current, next))

return current;

}

}

Java 8中,同样方法的实现代码如下:

public final int getAndIncrement() {

return unsafe.getAndAddInt(this, valueOffset, 1);

}

第一个实现中, 调用的compareAndSet方法只有在当前值等于实际值时才返回true,这样整个循环在这个条件不满足时会一直执行。

在线程数少时,这个实现方式很不错,不过考虑下这个场景:如果有100个线程调用这个方法会怎样?由于如此高密度的竞争,上面方法实现中的循环会等很长时间才能返回。这样会造成livelock。基于这样的实现,业务设计时需要仔细考虑了。针对这个实现上的不足,一个绕过的思路是使用类似map-reduce方案,将多个线程分到Set(mapper)中,每一个Set共享一个原子类实例,随后一个reducer线程从共享的原子类中收集数据。

Java8中是否解决了上面的问题?

关于这一点,请留意下, 在Java 8中,一些原子类的方法还是使用第一个方式,如IntUnaryOperator类的getAndUpdate。

在Java8下,竞争下的性能问题也存在,不过会有一些发送。这个改进, 可以查看下文章:http://ashkrit.blogspot.nl/2014/02/atomicinteger-java-7-vs-java-8.html


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算机组成原理

计算机组成原理

唐朔飞 / 高等教育出版社 / 2008-1 / 43.00元

《面向21世纪课程教材•普通高等教育"十一五"国家级规划教材:计算机组成原理(第2版)》内容简介:为了紧跟国际上计算机技术的新发展,《面向21世纪课程教材•普通高等教育"十一五"国家级规划教材:计算机组成原理(第2版)》对第1版各章节的内容进行了补充和修改,并增加了例题分析,以加深对各知识点的理解和掌握。《面向21世纪课程教材•普通高等教育"十一五"国家级规划教材:计算机组成原理(第2版)》通过对......一起来看看 《计算机组成原理》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试