更多更新的Java多线程的可扩展性问题
,与所选答案解释,问题出在JVM的垃圾回收算法。 JVM使用卡片标记算法跟踪对象字段中的修改引用。对于每个字段的引用分配,它将卡中的相关位标记为真 - 这会导致错误共享,从而阻止缩放。细节在本文中有详细描述:https://blogs.oracle.com/dave/entry/false_sharing_induced_by_card
选项-XX:+ UseCondCardMark(在Java 1.7u40及更高版本中)缓解了问题并使其几乎完美地缩放。
更新
我发现了(从公园Eung菊暗示),该分配对象插入字段变量产生变化。如果我删除了作业,它可以完美地缩放。 我认为它可能与Java内存模型有关 - 例如,对象引用必须在可见之前指向有效地址,但我不完全确定。 double和Object引用(很可能)在64位机器上有8个字节大小,所以在我看来,分配一个double值和一个Object引用在同步方面应该是相同的。
任何人都有合理的解释?
这里我有一个奇怪的Java多线程可伸缩性问题。
我的代码只是遍历一个数组(使用visitor模式)来计算简单的浮点运算并将结果赋值给另一个数组。没有数据依赖性,也没有同步,因此它应该线性缩放(2线程更快,4线程更快4倍)。
使用原始(double)数组时,它可以非常好地缩放。当(例如字符串)阵列用于对象类型,它完全不缩放(即使字符串数组的值不是在所有使用...)
这里就是整个源代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CyclicBarrier;
class Table1 {
public static final int SIZE1=200000000;
public static final boolean OBJ_PARAM;
static {
String type=System.getProperty("arg.type");
if ("double".equalsIgnoreCase(type)) {
System.out.println("Using primitive (double) type arg");
OBJ_PARAM = false;
} else {
System.out.println("Using object type arg");
OBJ_PARAM = true;
}
}
byte[] filled;
int[] ivals;
String[] strs;
Table1(int size) {
filled = new byte[size];
ivals = new int[size];
strs = new String[size];
Arrays.fill(filled, (byte)1);
Arrays.fill(ivals, 42);
Arrays.fill(strs, "Strs");
}
public boolean iterate_range(int from, int to, MyVisitor v) {
for (int i=from; i<to; i++) {
if (filled[i]==1) {
// XXX: Here we are passing double or String argument
if (OBJ_PARAM) v.visit_obj(i, strs[i]);
else v.visit(i, ivals[i]);
}
}
return true;
}
}
class HeadTable {
byte[] filled;
double[] dvals;
boolean isEmpty;
HeadTable(int size) {
filled = new byte[size];
dvals = new double[size];
Arrays.fill(filled, (byte)0);
isEmpty = true;
}
public boolean contains(int i, double d) {
if (filled[i]==0) return false;
if (dvals[i]==d) return true;
return false;
}
public boolean contains(int i) {
if (filled[i]==0) return false;
return true;
}
public double groupby(int i) {
assert filled[i]==1;
return dvals[i];
}
public boolean insert(int i, double d) {
if (filled[i]==1 && contains(i,d)) return false;
if (isEmpty) isEmpty=false;
filled[i]=1;
dvals[i] = d;
return true;
}
public boolean update(int i, double d) {
assert filled[i]==1;
dvals[i]=d;
return true;
}
}
class MyVisitor {
public static final int NUM=128;
int[] range = new int[2];
Table1 table1;
HeadTable head;
double diff=0;
int i;
int iv;
String sv;
MyVisitor(Table1 _table1, HeadTable _head, int id) {
table1 = _table1;
head = _head;
int elems=Table1.SIZE1/NUM;
range[0] = elems*id;
range[1] = elems*(id+1);
}
public void run() {
table1.iterate_range(range[0], range[1], this);
}
//YYY 1: with double argument, this function is called
public boolean visit(int _i, int _v) {
i = _i;
iv = _v;
insertDiff();
return true;
}
//YYY 2: with String argument, this function is called
public boolean visit_obj(int _i, Object _v) {
i = _i;
iv = 42;
sv = (String)_v;
insertDiff();
return true;
}
public boolean insertDiff() {
if (!head.contains(i)) {
head.insert(i, diff);
return true;
}
double old = head.groupby(i);
double newval=Math.min(old, diff);
head.update(i, newval);
head.insert(i, diff);
return true;
}
}
public class ParTest1 {
public static int THREAD_NUM=4;
public static void main(String[] args) throws Exception {
if (args.length>0) {
THREAD_NUM = Integer.parseInt(args[0]);
System.out.println("Setting THREAD_NUM:"+THREAD_NUM);
}
Table1 table1 = new Table1(Table1.SIZE1);
HeadTable head = new HeadTable(Table1.SIZE1);
MyVisitor[] visitors = new MyVisitor[MyVisitor.NUM];
for (int i=0; i<visitors.length; i++) {
visitors[i] = new MyVisitor(table1, head, i);
}
int taskPerThread = visitors.length/THREAD_NUM;
MyThread[] threads = new MyThread[THREAD_NUM];
CyclicBarrier barrier = new CyclicBarrier(THREAD_NUM+1);
for (int i=0; i<THREAD_NUM; i++) {
threads[i] = new MyThread(barrier);
for (int j=taskPerThread*i; j<taskPerThread*(i+1); j++) {
if (j>=visitors.length) break;
threads[i].addVisitors(visitors[j]);
}
}
Runtime r=Runtime.getRuntime();
System.out.println("Force running gc");
r.gc(); // running GC here (excluding GC effect)
System.out.println("Running gc done");
// not measuring 1st run (excluding JIT compilation effect)
for (int i=0; i<THREAD_NUM; i++) {
threads[i].start();
}
barrier.await();
for (int i=0; i<10; i++) {
MyThread.start = true;
long s=System.currentTimeMillis();
barrier.await();
long e=System.currentTimeMillis();
System.out.println("Iter "+i+" Exec time:"+(e-s)/1000.0+"s");
}
}
}
class MyThread extends Thread {
static volatile boolean start=true;
static int tid=0;
int id=0;
ArrayList<MyVisitor> tasks;
CyclicBarrier barrier;
public MyThread(CyclicBarrier _barrier) {
super("MyThread"+(tid++));
barrier = _barrier;
id=tid;
tasks = new ArrayList(256);
}
void addVisitors(MyVisitor v) {
tasks.add(v);
}
public void run() {
while (true) {
while (!start) { ; }
for (int i=0; i<tasks.size(); i++) {
MyVisitor v=tasks.get(i);
v.run();
}
start = false;
try { barrier.await();}
catch (InterruptedException e) { break; }
catch (Exception e) { throw new RuntimeException(e); }
}
}
}
Java代码可以不依赖被编译,你可以用下面的命令来运行它:
的Java -Darg.type =双-server ParTest1 2
您通过工人的数量线程作为参数(t他上面使用2个线程)。 设置数组后(不包括在测量时间内),它执行相同的操作10次,在每次迭代时打印出执行时间。 使用上述选项,它使用双数组,并且使用1,2,4个线程缩放非常好(即执行时间缩短为1/2和1/4),但是
java -Darg.type = Object -server ParTest1 2
使用此选项,它使用Object(String)数组,并且它根本不缩放!我测量了GC时间,但它并不重要(在测量时间之前我也强制GC运行)。我已经测试过Java 6(更新版本43)和Java 7(更新版本51),但它们是一样的。
当使用arg.type = double或arg.type = Object选项时,代码对使用XXX和YYY的注释进行了描述。
你能弄清楚在这里传递的字符串类型参数是怎么回事?
请在您的问题中包含代码;外部链接可能会被删除。 – GreySwordz
你能否执行一个配置文件来查看是否有相当多的GC活动?第126行的'(String)'似乎也是一个潜在的问题。你可以尝试直接接受'String'来检查吗? – hexafraction
我用GarbageCollectorMXBean测量了GC时间;这并不重要(在测量时间之前,我也强制运行GC)。类型转换不是一个问题 - 我没有进行铸造测试,情况也是如此。 对于包含代码,它包含的时间太长。一旦我有足够数量的答案,我会尝试包括它。 – Jiwon