diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/Multiset.java b/bramble-api/src/main/java/org/briarproject/bramble/api/Multiset.java
new file mode 100644
index 0000000000000000000000000000000000000000..58db606b590583fa51ed63de59279524a874ad2f
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/Multiset.java
@@ -0,0 +1,101 @@
+package org.briarproject.bramble.api;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+@NotNullByDefault
+public class Multiset<T> {
+
+	private final Map<T, Integer> map = new HashMap<>();
+
+	private int total = 0;
+
+	/**
+	 * Returns how many items the multiset contains in total.
+	 */
+	public int getTotal() {
+		return total;
+	}
+
+	/**
+	 * Returns how many unique items the multiset contains.
+	 */
+	public int getUnique() {
+		return map.size();
+	}
+
+	/**
+	 * Returns how many of the given item the multiset contains.
+	 */
+	public int getCount(T t) {
+		Integer count = map.get(t);
+		return count == null ? 0 : count;
+	}
+
+	/**
+	 * Adds the given item to the multiset and returns how many of the item
+	 * the multiset now contains.
+	 */
+	public int add(T t) {
+		Integer count = map.get(t);
+		if (count == null) count = 0;
+		map.put(t, count + 1);
+		total++;
+		return count + 1;
+	}
+
+	/**
+	 * Removes the given item from the multiset and returns how many of the
+	 * item the multiset now contains.
+	 * @throws NoSuchElementException if the item is not in the multiset.
+	 */
+	public int remove(T t) {
+		Integer count = map.get(t);
+		if (count == null) throw new NoSuchElementException();
+		if (count == 1) map.remove(t);
+		else map.put(t, count - 1);
+		total--;
+		return count - 1;
+	}
+
+	/**
+	 * Removes all occurrences of the given item from the multiset.
+	 */
+	public int removeAll(T t) {
+		Integer count = map.remove(t);
+		if (count == null) return 0;
+		total -= count;
+		return count;
+	}
+
+	/**
+	 * Returns true if the multiset contains any occurrences of the given item.
+	 */
+	public boolean contains(T t) {
+		return map.containsKey(t);
+	}
+
+	/**
+	 * Removes all items from the multiset.
+	 */
+	public void clear() {
+		map.clear();
+		total = 0;
+	}
+
+	/**
+	 * Returns the set of unique items the multiset contains. The returned set
+	 * is unmodifiable.
+	 */
+	public Set<T> keySet() {
+		return Collections.unmodifiableSet(map.keySet());
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/ConnectionRegistryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/ConnectionRegistryImpl.java
index 780e46a6e3185646db974c6ee3982458d959188d..c12abae552fecc97a6e9fa21ad552159a37e39cb 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/ConnectionRegistryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/ConnectionRegistryImpl.java
@@ -1,5 +1,6 @@
 package org.briarproject.bramble.plugin;
 
+import org.briarproject.bramble.api.Multiset;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -36,14 +37,14 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 	private final Lock lock = new ReentrantLock();
 
 	// The following are locking: lock
-	private final Map<TransportId, Map<ContactId, Integer>> connections;
-	private final Map<ContactId, Integer> contactCounts;
+	private final Map<TransportId, Multiset<ContactId>> connections;
+	private final Multiset<ContactId> contactCounts;
 
 	@Inject
 	ConnectionRegistryImpl(EventBus eventBus) {
 		this.eventBus = eventBus;
 		connections = new HashMap<>();
-		contactCounts = new HashMap<>();
+		contactCounts = new Multiset<>();
 	}
 
 	@Override
@@ -56,21 +57,13 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 		boolean firstConnection = false;
 		lock.lock();
 		try {
-			Map<ContactId, Integer> m = connections.get(t);
+			Multiset<ContactId> m = connections.get(t);
 			if (m == null) {
-				m = new HashMap<>();
+				m = new Multiset<>();
 				connections.put(t, m);
 			}
-			Integer count = m.get(c);
-			if (count == null) m.put(c, 1);
-			else m.put(c, count + 1);
-			count = contactCounts.get(c);
-			if (count == null) {
-				firstConnection = true;
-				contactCounts.put(c, 1);
-			} else {
-				contactCounts.put(c, count + 1);
-			}
+			m.add(c);
+			if (contactCounts.add(c) == 1) firstConnection = true;
 		} finally {
 			lock.unlock();
 		}
@@ -91,23 +84,10 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 		boolean lastConnection = false;
 		lock.lock();
 		try {
-			Map<ContactId, Integer> m = connections.get(t);
+			Multiset<ContactId> m = connections.get(t);
 			if (m == null) throw new IllegalArgumentException();
-			Integer count = m.remove(c);
-			if (count == null) throw new IllegalArgumentException();
-			if (count == 1) {
-				if (m.isEmpty()) connections.remove(t);
-			} else {
-				m.put(c, count - 1);
-			}
-			count = contactCounts.get(c);
-			if (count == null) throw new IllegalArgumentException();
-			if (count == 1) {
-				lastConnection = true;
-				contactCounts.remove(c);
-			} else {
-				contactCounts.put(c, count - 1);
-			}
+			m.remove(c);
+			if (contactCounts.remove(c) == 0) lastConnection = true;
 		} finally {
 			lock.unlock();
 		}
@@ -122,7 +102,7 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 	public Collection<ContactId> getConnectedContacts(TransportId t) {
 		lock.lock();
 		try {
-			Map<ContactId, Integer> m = connections.get(t);
+			Multiset<ContactId> m = connections.get(t);
 			if (m == null) return Collections.emptyList();
 			List<ContactId> ids = new ArrayList<>(m.keySet());
 			if (LOG.isLoggable(INFO))
@@ -137,8 +117,8 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 	public boolean isConnected(ContactId c, TransportId t) {
 		lock.lock();
 		try {
-			Map<ContactId, Integer> m = connections.get(t);
-			return m != null && m.containsKey(c);
+			Multiset<ContactId> m = connections.get(t);
+			return m != null && m.contains(c);
 		} finally {
 			lock.unlock();
 		}
@@ -148,7 +128,7 @@ class ConnectionRegistryImpl implements ConnectionRegistry {
 	public boolean isConnected(ContactId c) {
 		lock.lock();
 		try {
-			return contactCounts.containsKey(c);
+			return contactCounts.contains(c);
 		} finally {
 			lock.unlock();
 		}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
index 5baa6830a9f24b742be522267b3dc1515631e51a..5c66a8d9caabae925382dbfd9d9c6fc71cf6d16f 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidNotificationManagerImpl.java
@@ -12,6 +12,7 @@ import android.support.annotation.StringRes;
 import android.support.annotation.UiThread;
 import android.support.v4.app.TaskStackBuilder;
 
+import org.briarproject.bramble.api.Multiset;
 import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DatabaseExecutor;
 import org.briarproject.bramble.api.db.DbException;
@@ -45,8 +46,7 @@ import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
 import org.briarproject.briar.api.sharing.event.InvitationRequestReceivedEvent;
 import org.briarproject.briar.api.sharing.event.InvitationResponseReceivedEvent;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
@@ -109,11 +109,10 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	// The following must only be accessed on the main UI thread
-	private final Map<ContactId, Integer> contactCounts = new HashMap<>();
-	private final Map<GroupId, Integer> groupCounts = new HashMap<>();
-	private final Map<GroupId, Integer> forumCounts = new HashMap<>();
-	private final Map<GroupId, Integer> blogCounts = new HashMap<>();
-	private int contactTotal = 0, groupTotal = 0, forumTotal = 0, blogTotal = 0;
+	private final Multiset<ContactId> contactCounts = new Multiset<>();
+	private final Multiset<GroupId> groupCounts = new Multiset<>();
+	private final Multiset<GroupId> forumCounts = new Multiset<>();
+	private final Multiset<GroupId> blogCounts = new Multiset<>();
 	private int introductionTotal = 0;
 	private int nextRequestId = 0;
 	private ContactId blockedContact = null;
@@ -197,28 +196,24 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	@UiThread
 	private void clearContactNotification() {
 		contactCounts.clear();
-		contactTotal = 0;
 		notificationManager.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
 	}
 
 	@UiThread
 	private void clearGroupMessageNotification() {
 		groupCounts.clear();
-		groupTotal = 0;
 		notificationManager.cancel(GROUP_MESSAGE_NOTIFICATION_ID);
 	}
 
 	@UiThread
 	private void clearForumPostNotification() {
 		forumCounts.clear();
-		forumTotal = 0;
 		notificationManager.cancel(FORUM_POST_NOTIFICATION_ID);
 	}
 
 	@UiThread
 	private void clearBlogPostNotification() {
 		blogCounts.clear();
-		blogTotal = 0;
 		notificationManager.cancel(BLOG_POST_NOTIFICATION_ID);
 	}
 
@@ -278,10 +273,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		androidExecutor.runOnUiThread(() -> {
 			if (blockContacts) return;
 			if (c.equals(blockedContact)) return;
-			Integer count = contactCounts.get(c);
-			if (count == null) contactCounts.put(c, 1);
-			else contactCounts.put(c, count + 1);
-			contactTotal++;
+			contactCounts.add(c);
 			updateContactNotification(true);
 		});
 	}
@@ -289,15 +281,14 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	@Override
 	public void clearContactNotification(ContactId c) {
 		androidExecutor.runOnUiThread(() -> {
-			Integer count = contactCounts.remove(c);
-			if (count == null) return; // Already cleared
-			contactTotal -= count;
-			updateContactNotification(false);
+			if (contactCounts.removeAll(c) > 0)
+				updateContactNotification(false);
 		});
 	}
 
 	@UiThread
 	private void updateContactNotification(boolean mayAlertAgain) {
+		int contactTotal = contactCounts.getTotal();
 		if (contactTotal == 0) {
 			clearContactNotification();
 		} else if (settings.getBoolean(PREF_NOTIFY_PRIVATE, true)) {
@@ -315,10 +306,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			b.setLockscreenVisibility(CATEGORY_MESSAGE, showOnLockScreen);
 			if (mayAlertAgain) setAlertProperties(b);
 			setDeleteIntent(b, CONTACT_URI);
-			if (contactCounts.size() == 1) {
+			Set<ContactId> contacts = contactCounts.keySet();
+			if (contacts.size() == 1) {
 				// Touching the notification shows the relevant conversation
 				Intent i = new Intent(appContext, ConversationActivity.class);
-				ContactId c = contactCounts.keySet().iterator().next();
+				ContactId c = contacts.iterator().next();
 				i.putExtra(CONTACT_ID, c.getInt());
 				i.setData(Uri.parse(CONTACT_URI + "/" + c.getInt()));
 				i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
@@ -385,10 +377,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		androidExecutor.runOnUiThread(() -> {
 			if (blockGroups) return;
 			if (g.equals(blockedGroup)) return;
-			Integer count = groupCounts.get(g);
-			if (count == null) groupCounts.put(g, 1);
-			else groupCounts.put(g, count + 1);
-			groupTotal++;
+			groupCounts.add(g);
 			updateGroupMessageNotification(true);
 		});
 	}
@@ -396,15 +385,14 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	@Override
 	public void clearGroupMessageNotification(GroupId g) {
 		androidExecutor.runOnUiThread(() -> {
-			Integer count = groupCounts.remove(g);
-			if (count == null) return; // Already cleared
-			groupTotal -= count;
-			updateGroupMessageNotification(false);
+			if (groupCounts.removeAll(g) > 0)
+				updateGroupMessageNotification(false);
 		});
 	}
 
 	@UiThread
 	private void updateGroupMessageNotification(boolean mayAlertAgain) {
+		int groupTotal = groupCounts.getTotal();
 		if (groupTotal == 0) {
 			clearGroupMessageNotification();
 		} else if (settings.getBoolean(PREF_NOTIFY_GROUP, true)) {
@@ -422,10 +410,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
 			if (mayAlertAgain) setAlertProperties(b);
 			setDeleteIntent(b, GROUP_URI);
-			if (groupCounts.size() == 1) {
+			Set<GroupId> groups = groupCounts.keySet();
+			if (groups.size() == 1) {
 				// Touching the notification shows the relevant group
 				Intent i = new Intent(appContext, GroupActivity.class);
-				GroupId g = groupCounts.keySet().iterator().next();
+				GroupId g = groups.iterator().next();
 				i.putExtra(GROUP_ID, g.getBytes());
 				String idHex = StringUtils.toHexString(g.getBytes());
 				i.setData(Uri.parse(GROUP_URI + "/" + idHex));
@@ -461,10 +450,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		androidExecutor.runOnUiThread(() -> {
 			if (blockForums) return;
 			if (g.equals(blockedGroup)) return;
-			Integer count = forumCounts.get(g);
-			if (count == null) forumCounts.put(g, 1);
-			else forumCounts.put(g, count + 1);
-			forumTotal++;
+			forumCounts.add(g);
 			updateForumPostNotification(true);
 		});
 	}
@@ -472,15 +458,14 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	@Override
 	public void clearForumPostNotification(GroupId g) {
 		androidExecutor.runOnUiThread(() -> {
-			Integer count = forumCounts.remove(g);
-			if (count == null) return; // Already cleared
-			forumTotal -= count;
-			updateForumPostNotification(false);
+			if (forumCounts.removeAll(g) > 0)
+				updateForumPostNotification(false);
 		});
 	}
 
 	@UiThread
 	private void updateForumPostNotification(boolean mayAlertAgain) {
+		int forumTotal = forumCounts.getTotal();
 		if (forumTotal == 0) {
 			clearForumPostNotification();
 		} else if (settings.getBoolean(PREF_NOTIFY_FORUM, true)) {
@@ -498,10 +483,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 			b.setLockscreenVisibility(CATEGORY_SOCIAL, showOnLockScreen);
 			if (mayAlertAgain) setAlertProperties(b);
 			setDeleteIntent(b, FORUM_URI);
-			if (forumCounts.size() == 1) {
+			Set<GroupId> forums = forumCounts.keySet();
+			if (forums.size() == 1) {
 				// Touching the notification shows the relevant forum
 				Intent i = new Intent(appContext, ForumActivity.class);
-				GroupId g = forumCounts.keySet().iterator().next();
+				GroupId g = forums.iterator().next();
 				i.putExtra(GROUP_ID, g.getBytes());
 				String idHex = StringUtils.toHexString(g.getBytes());
 				i.setData(Uri.parse(FORUM_URI + "/" + idHex));
@@ -536,10 +522,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 		androidExecutor.runOnUiThread(() -> {
 			if (blockBlogs) return;
 			if (g.equals(blockedGroup)) return;
-			Integer count = blogCounts.get(g);
-			if (count == null) blogCounts.put(g, 1);
-			else blogCounts.put(g, count + 1);
-			blogTotal++;
+			blogCounts.add(g);
 			updateBlogPostNotification(true);
 		});
 	}
@@ -547,15 +530,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
 	@Override
 	public void clearBlogPostNotification(GroupId g) {
 		androidExecutor.runOnUiThread(() -> {
-			Integer count = blogCounts.remove(g);
-			if (count == null) return; // Already cleared
-			blogTotal -= count;
-			updateBlogPostNotification(false);
+			if (blogCounts.removeAll(g) > 0) updateBlogPostNotification(false);
 		});
 	}
 
 	@UiThread
 	private void updateBlogPostNotification(boolean mayAlertAgain) {
+		int blogTotal = blogCounts.getTotal();
 		if (blogTotal == 0) {
 			clearBlogPostNotification();
 		} else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {