我有一种情况,我有N个线程做同样的工作,一个线程X做不同的事情。
每个线程N读/写一个静态对象,它是一个类(称为MMLCounter),它是HashMap的包装,每个线程使用该HashMap的不同键/值对,所以所有线程读/写值到HashMap同时出现。 线程X周期性地需要访问所有值,并且在它访问它们时(从访问第一个值到访问最后一个值的那一刻,其他N个线程都不会改变HashMap中的值)。 / p>
在程序执行开始时线程创建过程中,HashMap被初始化并由线程添加了键/值,之后没有添加新的键/值,只有HashMap中的值发生变化。
因为我没有使用ConcurrentHashMap或同步函数,而是我创建了一个围绕HashMap的包装类,它还有一个标志,表示N个线程允许他们更改值,这个标志是由线程X专门更改。
这样所有N个线程都可以并行使用HashMap,但是当线程X开始工作时,只有它可以使用HashMap直到它完成。
我的问题是,我是否需要将任何东西声明为volatile(例如HashMap中的值),如果是,那么什么以及如何?
我想避免的事情(不知道它是否可能发生)是N个线程中的一个在HashMap中改变了一个值,但是这个值的改变只反映在本地缓存的内存中该线程,当线程X从HashMap读取该值时,它将从其本地缓存内存中读取它,该内存与另一个N线程的本地缓存内存不同步,这意味着它将具有旧值。
以下是代码:
public static void main(String[] args) throws ProtocolException {
int NUMBER_OF_THREADS = 400;
List<Future<?>> futureList = new ArrayList<Future<?>>();
ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS+1);
futureList.add(executor.submit(new Runnable() {
@Override
public void run() {
int measureInterval = 10000;
try {
Thread.sleep(measureInterval);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("--> MML rate is : " + MMLGenerator.MML_COUNTER.getMMLRate(measureInterval/1000) + " MML per second.");
}
}));
//create and start sending threads.
for (int threadNmbr = 0; threadNmbr < NUMBER_OF_THREADS; threadNmbr++) {
futureList.add(executor.submit(new Thread(new MMLGenerator(threadNmbr))));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//wait for threads to finish.
for (Future<?> future : futureList) {
try {
future.get();
} catch (InterruptedException e) {
}
catch (ExecutionException e) {
throw (RuntimeException) e.getCause();
}
}
executor.shutdown();
}
class MMLGenerator implements Runnable {
public static volatile MMLCounter MML_COUNTER = new MMLCounter();
private int threadNmbr = 0;
public MMLGenerator(int threadNmbr) {
this.threadNmbr = threadNmbr;
MMLGenerator.MML_COUNTER.put(this.threadNmbr, 0);
}
@Override
public void run() {
while(RUN_ACTIVE) {
MML_COUNTER.increaseCounter(this.threadNmbr);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class MMLCounter {
private Map<Integer,Integer> MMLCounter = new HashMap<Integer, Integer>();
private boolean MMLCounterLocked = false;
public Integer get(Integer key) {
return this.MMLCounter.get(key);
}
public Integer put(Integer key, Integer value) {
while (this.MMLCounterLocked) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return this.MMLCounter.put(key, value);
}
public void increaseCounter(Integer key) {
while (this.MMLCounterLocked) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.MMLCounter.put(key,this.MMLCounter.get(key).intValue() + 1);
}
public int getMMLRate(int measurementTime) {
this.MMLCounterLocked = true;
int MMLCounterSum = 0;
for (Integer counterID : this.MMLCounter.keySet()) {
int counter = this.MMLCounter.get(counterID);
MMLCounterSum += counter;
this.MMLCounter.put(counterID, 0);
}
this.MMLCounterLocked = false;
return MMLCounterSum/measurementTime;
}
}
谢谢大家的帮助。
我刚刚阅读ReentrantReaderWriterLock
描述,这正是我所需要的。以下是修改后的代码。
但是,我还有两个问题:
1)如果我使用ConcurrentHashMap
保护代码的关键部分,为什么还需要使用HashMap
代替ReentrantReaderWriterLock
?
2)ReentrantReaderWriterLock
的这种用法只会替换我以前的实现标志的使用,我现在看到的标签没有正确完成。但是,我仍然遇到HashMap
中的值对象不是易失性的问题,因此不同的线程将各自拥有自己的本地缓存的值副本,该副本与其他线程的值的本地缓存副本不同步?
public static void main(String[] args) throws ProtocolException {
int NUMBER_OF_THREADS = 400;
List<Future<?>> futureList = new ArrayList<Future<?>>();
ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS+1);
futureList.add(executor.submit(new Runnable() {
@Override
public void run() {
int measureInterval = 10000;
try {
Thread.sleep(measureInterval);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("--> MML rate is : " + MMLGenerator.counter.getMMLRate(measureInterval/1000) + " MML per second.");
}
}));
//create and start sending threads.
for (int threadNmbr = 0; threadNmbr < NUMBER_OF_THREADS; threadNmbr++) {
futureList.add(executor.submit(new Thread(new MMLGenerator(threadNmbr))));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//wait for threads to finish.
for (Future<?> future : futureList) {
try {
future.get();
} catch (InterruptedException e) {
}
catch (ExecutionException e) {
throw (RuntimeException) e.getCause();
}
}
executor.shutdown();
}
class MMLGenerator implements Runnable {
public static MMLCounter counter = new MMLCounter();
private int threadNmbr = 0;
public MMLGenerator(int threadNmbr) {
this.threadNmbr = threadNmbr;
MMLCounter.counter.put(this.threadNmbr, 0);
}
@Override
public void run() {
while(RUN_ACTIVE) {
MMLCounter.counter.increaseCounter(this.threadNmbr);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class MMLCounter {
private Map<Integer,Integer> counter = new HashMap<Integer, Integer>();
public static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
public Integer put(Integer key, Integer value) {
lock.readLock().lock();
Integer oldValue = this.counter.put(key, value);
lock.readLock().unlock();
return oldValue;
}
public void increaseCounter(Integer key) {
lock.readLock().lock();
this.counter.put(key,this.counter.get(key).intValue() + 1);
lock.readLock().unlock();
}
public int getMMLRate(int measurementTime) {
lock.writeLock().lock();
int counterSum = 0;
for (Integer counterID : this.counter.keySet()) {
counterSum += this.counter.get(counterID);;
this.counter.put(counterID, 0);
}
lock.writeLock().unlock();
return counterSum/measurementTime;
}
}
我现在发现我需要实现的逻辑要求我从多个线程中操纵多个计数器,而不仅仅是一个,并且每个线程在任何时候都可以更改任何计数器。 下面是我的实现,但我不确定我是否在性能和数据一致性方面做得很好。
我除了有随机数的计数器(计数器的数量将在执行开始时知道),这将由String值标识,并且每个Counter必须计算两个值(第一个值总是增加,第二个值有时只有,但如果他们增加,他们需要同时增加)。当我需要每个计数器的总和时,我需要在原子操作中获取两个计数器值,并且从我获取第一个计数器直到我获取最后一个计数器的时间,其他线程都不会改变计数器。
出于演示目的,作为Counter标识(HashMap中的键),我获取了计数器序号的字符串值,并确定每个线程的每次迭代中哪个Counter需要增加,以及确定是否只有一个或者两个值都需要增加,我使用了随机生成器。
public static void main(String[] args) {
int NUMBER_OF_THREADS = 400;
MMLGenerator.counterNmbr(2);
List<Future<?>> futureList = new ArrayList<Future<?>>();
ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS+1);
futureList.add(executor.submit(new Runnable() {
@Override
public void run() {
while(true)
{
int measureInterval = 10;
try {
TimeUnit.SECONDS.sleep(measureInterval);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
MMLGenerator.lock.writeLock().lock();
for (String counterId : MMLGenerator.counter.keySet()) {
MMLCounterSimple counter = MMLGenerator.counter.get(counterId).getCountAndReset();
System.out.println("--> For counter " + counterId + " total is : " + counter.getTotal() + ", and failed is : " + counter.getFailed());
}
MMLGenerator.lock.writeLock().unlock();
}
}
}));
//create and start sending threads.
for (int threadNmbr = 0; threadNmbr < NUMBER_OF_THREADS; threadNmbr++) {
futureList.add(executor.submit(new Thread(new MMLGenerator())));
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//wait for threads to finish.
for (Future<?> future : futureList) {
try {
future.get();
} catch (InterruptedException e) {
}
catch (ExecutionException e) {
throw (RuntimeException) e.getCause();
}
}
executor.shutdown();
}
class MMLGenerator implements Runnable {
public static volatile HashMap<String, MMLCounter> counter = new HashMap<String, MMLCounter>();
public static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
public static void counterNmbr(int counterNmbr) {
lock.writeLock().lock();
for(int i = 0; i < counterNmbr; i++) {
counter.put(new Integer(i).toString(), new MMLCounter());
}
lock.writeLock().unlock();
}
@Override
public void run() {
while(RUN_PROVISIONING) {
lock.readLock().lock();
String counterID = new Integer(new Random().nextInt(counter.size())).toString();
long failedInc = 0;
if (new Random().nextInt(2) == 0) {
failedInc = 1;
}
counter.get(counterID).increaseCounter(failedInc);
lock.readLock().unlock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class MMLCounter {
private volatile long total = 0;
private volatile long failed = 0;
public static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
public synchronized void increaseCounter(long failedInc) {
lock.writeLock().lock();
total++;;
failed = failed + failedInc;
lock.writeLock().unlock();
}
public synchronized MMLCounterSimple getCountAndReset() {
lock.writeLock().lock();
MMLCounterSimple simpleCounter = new MMLCounterSimple(total, failed);
total = 0;
failed = 0;
lock.writeLock().unlock();
return simpleCounter;
}
}
public class MMLCounterSimple {
private long total = 0;
private long failed = 0;
public MMLCounterSimple(long total, long failed) {
this.total = total;
this.failed = failed;
}
public long getTotal() {
return this.total;
}
public long getFailed() {
return this.failed;
}
}
答案 0 :(得分:1)
如上所述,这并不能保证按预期工作。 N个线程执行的写操作与X线程执行的读操作之间没有同步点。写作者甚至可以忽略MMLCounterLocked
标志。
除了使代码正常工作外,使用ConcurrentMap
等更高级别的并发工具还可以大大简化代码。
由于您只需要总和,LongAccumulator
就足够了,它使代码变得非常简单和安全。
public static void main(String[] args)
throws Exception
{
int NUMBER_OF_THREADS = 400;
ExecutorService executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
LongAccumulator sum = new LongAccumulator(Long::sum, 0);
for (int i = 0; i < NUMBER_OF_THREADS; ++i) {
executor.submit(new MMLGenerator(sum));
TimeUnit.MILLISECONDS.sleep(50); /* Why??? */
}
int interval = 10;
TimeUnit.SECONDS.sleep(interval);
long rate = sum.getThenReset() / interval;
System.out.println("--> MML rate is : " + rate + " MML per second.");
executor.shutdownNow();
}
private static final class MMLGenerator
implements Runnable
{
private final LongAccumulator counter;
MMLGenerator(LongAccumulator counter)
{
this.counter = counter;
}
@Override
public void run()
{
while (true) {
counter.accumulate(1);
try {
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException shutdown) {
break;
}
}
}
}
关于你的两个新问题:
ConcurrentHashMap
保护代码的关键部分,为什么还需要使用HashMap
代替ReentrantReaderWriterLock
?&#34; 您没有使用读写锁保护关键部分。当您写表时,您正在获取读取锁,当读取时,写锁。 HashMap
的行为未在并发修改下定义。您可能导致使用该表的线程挂起。
此外,您应该使用try-finally
构造来确保无论发生任何错误都能解锁。
如果您使用了ConcurrentMap
,则线程可以在不获取整个表的锁定的情况下进行更新,这正是您在正确应用读写锁定时所做的事情。
ReentrantReaderWriterLock
的这种用法只会替换我以前的实现标志的用法,我现在看到的用法没有正确完成。但是,我仍然遇到HashMap
中的值对象不是volatile
的问题,因此[将]不同的线程...每个都有自己的本地缓存的值副本,该副本与本地缓存的副本不同步来自其他线程的价值?&#34; 不,Lock
获取确实与其他线程同步,并且在获取锁之前发生的其他线程的更改将是可见的。如果您修复了锁定,HashMap
将正常工作,但它会通过在更新期间锁定整个表来实现。
答案 1 :(得分:0)
是的,你需要volatile
,没有它,就不能保证其他线程会看到变化; java可能(通常会)在另一个线程中本地缓存该值,因此在本地更改它不会对等待它的其他线程产生任何影响。
例如,这一行:
this.MMLCounterLocked = true;
如果字段为volatile
,则无效。
你为什么要尝试实现自己的锁?只需通过在静态字段或静态方法上同步来使用java锁定。
答案 2 :(得分:0)
您可能希望使用一种锁定方案,允许多个读取器和一个编写器的独占访问权限,例如java.util.concurrent.locks.ReentrantReadWriteLock
,而不是尝试烘焙自己的同步机制。
ReadWriteLock维护一对关联的锁,一个用于 只读操作和写操作。可以保持读锁定 同时由多个读者线程,只要没有 作家。写锁是独占的。
答案 3 :(得分:-1)
结合rob和erickson的说法:
使用ConcurrentHashMap
这是线程安全的。
使用ReentrantReaderWriterLock
,让对地图进行编辑的所有主题都抓取ReadLock
,以便他们可以同时编辑所有主题。锁的读取端是共享锁。然后,需要一致查看整个地图的线程应使用WriteLock
获得对地图的独占访问权。
您需要ConcurrentHashMap
的原因是所有线程都会看到对地图的所有编辑,包括新条目。在这里使用ReentrantReaderWriterLock
的读取侧是不够的。
你应该使用ReentrantReaderWriterLock
的原因是,当线程X需要通过使用锁的编写者侧对它可以拥有的地图进行独占访问时。
这里的一个大问题是在increaseCounter
中存在一个不保证是原子的依赖。你得到的价值然后你增加它。当两个线程调用此方法并且两者都获得相同的值,然后增加它时会发生什么。如果这是一个银行账户,有人在比赛条件下输了一分钱,那么......你明白了。这里有一个不变的维护,需要一些细粒度的原子性。考虑使用AtomicInteger
作为值侧,或者检查Atomic
包中提供具有原子操作的单个变量的所有优秀类。