更多更新的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