Java - HashMap值 - 易变

时间:2016-07-28 16:55:02

标签: java multithreading hashmap

我有一种情况,我有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;
    }
}

4 个答案:

答案 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;
        }
      }
    }

  }

关于你的两个新问题:

  1. &#34;如果我使用ConcurrentHashMap保护代码的关键部分,为什么还需要使用HashMap代替ReentrantReaderWriterLock?&#34;
  2. 没有使用读写锁保护关键部分。当您表时,您正在获取读取锁,当读取时,锁。 HashMap的行为未在并发修改下定义。您可能导致使用该表的线程挂起。

    此外,您应该使用try-finally构造来确保无论发生任何错误都能解锁。

    如果您使用了ConcurrentMap,则线程可以在不获取整个表的锁定的情况下进行更新,这正是您在正确应用读写锁定时所做的事情。

    1. &#34; ReentrantReaderWriterLock的这种用法只会替换我以前的实现标志的用法,我现在看到的用法没有正确完成。但是,我仍然遇到HashMap中的值对象不是volatile的问题,因此[将]不同的线程...每个都有自己的本地缓存的值副本,该副本与本地缓存的副本不同步来自其他线程的价值?&#34;
    2. 不,Lock获取确实与其他线程同步,并且在获取锁之前发生的其他线程的更改将是可见的。如果您修复了锁定,HashMap将正常工作,但它会通过在更新期间锁定整个表来实现。

答案 1 :(得分:0)

是的,你需要volatile,没有它,就不能保证其他线程会看到变化; java可能(通常会)在另一个线程中本地缓存该值,因此在本地更改它不会对等待它的其他线程产生任何影响。

例如,这一行:

this.MMLCounterLocked = true;
如果字段为volatile,则

无效。

你为什么要尝试实现自己的锁?只需通过在静态字段或静态方法上同步来使用java锁定。

答案 2 :(得分:0)

您可能希望使用一种锁定方案,允许多个读取器和一个编写器的独占访问权限,例如java.util.concurrent.locks.ReentrantReadWriteLock,而不是尝试烘焙自己的同步机制。

The docs say

  

ReadWriteLock维护一对关联的锁,一个用于   只读操作和写操作。可以保持读锁定   同时由多个读者线程,只要没有   作家。写锁是独占的。

答案 3 :(得分:-1)

结合rob和erickson的说法:

使用ConcurrentHashMap这是线程安全的。

使用ReentrantReaderWriterLock,让对地图进行编辑的所有主题都抓取ReadLock,以便他们可以同时编辑所有主题。锁的读取端是共享锁。然后,需要一致查看整个地图的线程应使用WriteLock获得对地图的独占访问权。

您需要ConcurrentHashMap的原因是所有线程都会看到对地图的所有编辑,包括新条目。在这里使用ReentrantReaderWriterLock的读取侧是不够的。

你应该使用ReentrantReaderWriterLock的原因是,当线程X需要通过使用锁的编写者侧对它可以拥有的地图进行独占访问时。

这里的一个大问题是在increaseCounter中存在一个不保证是原子的依赖。你得到的价值然后你增加它。当两个线程调用此方法并且两者都获得相同的值,然后增加它时会发生什么。如果这是一个银行账户,有人在比赛条件下输了一分钱,那么......你明白了。这里有一个不变的维护,需要一些细粒度的原子性。考虑使用AtomicInteger作为值侧,或者检查Atomic包中提供具有原子操作的单个变量的所有优秀类。