内容简介: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
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- kafka高吞吐量之消息压缩
- 如何找到 Kafka 集群的吞吐量极限?
- 高吞吐量的Java事件总线:MBassador
- 再次提高 Kafka 吞吐量,原来还有这么多细节?
- 软件交付效能度量:从吞吐量和稳定性开始
- 惊:FastThreadLocal吞吐量居然是ThreadLocal的3倍!!!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。