Ik heb een eenvoudige klasse FileChangeMonitor geschreven, waarbij objecten die een FileChangeListener interface implementeren zich kunnen registreren om genotificeerd te worden van wijzingen aan een specifiek bestand. Deze klasse wil ik thread safe maken, maar ik heb hier niet bijzonder veel ervaring mee en ik vraag mij af hoe ik dit het beste kan aanpakken. Daarmee bedoel ik zo efficiënt mogelijk; zonder overbodige synchronisatie.
De relevante niet thread-safe code:
Nogal wat code, maar aangezien ik deze klasse thread-safe wil krijgen is het m.i. alleemaal relevant.
Natuurlijk verwacht ik geen uitgekauwde oplossing en heb ik er zelf ook al over nagedacht
Hoe ik de situatie inschat:
1) De klasse is als Singleton geïmplementeerd. Deze implementatie is thread-safe, omdat er geen 'lazy loading' gebruikt wordt en de constructor volledig afgeschermd is.
2) In de registerListener methode kan het in een MT situatie fout gaan, omdat de Map die de listeners bevat in de class scope zit. Meerdere threads gebruiken allemaal per definitie dezelfde instantie van FileChangeMonitor, want --> Singleton. Bij het muteren van de Map kunnen daardoor problemen ontstaan, want de concrete imlementatie (HashMap) is van zichzelf niet thread-safe.
3) Bovenstaande geldt ook voor de removeListener methode.
4) De class scope variabelen pollingInterval en initialDelay kunnen gemuteerd worden m.b.v. standaard accessor methoden. Ook hier kan een probleem ontstaan in een MT omgeving.
Oplossingen die ik zelf zie:
1) De methoden registerListener, removeListener en de accessor methoden voor pollingInterval en initialDelay in hun geheel synchroniseren m.b.v. het synchronized keyword. Correct?
2) fileListenerMap instantiëren als threadsafe collection m.b.v. Collections.synchronizedMap(..) en binnen de methoden registerListener en removeListener een synchronized block aanbrengen, waarbij gesynchroniseerd wordt op fileListenerMap. De convinience methoden scheduleFileMonitorTask en cancelFileMonitoringTask worden aangeroepen vanuit deze synchronized context, waardoor hier niets aan te veranderd moet worden en ook taskMap niet als synchronizedMap geïnstantieerd moet worden. De class scope variabelen pollingInterval en initialDelay volatile maken.
Lijkt mij dat oplossing 2 sowieso efficiënter zal zijn, maar is thread safety op die manier gegarandeerd. Is dat überhaupt bij oplossing 1 wel het geval? Heb ik nog mogelijke problemen over het hoofd gezien?
De relevante niet thread-safe code:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| public class FileChangeMonitor { private static FileChangeMonitor instance = new FileChangeMonitor(); private Map<String, Set<FileChangeListener>> fileListenerMap; private Map<String, FileMonitoringTask> taskMap; private Timer timer; private long pollingInterval = 3000L; private long initialDelay = 3000L; private FileChangeMonitor() { fileListenerMap = new HashMap<String, Set<FileChangeListener>>(); taskMap = new HashMap<String, FileMonitoringTask>(); timer = new Timer(true); } public static FileChangeMonitor getInstance() { return instance; } public void registerListener(FileChangeListener listener, String file) { if (listener == null || file == null) { throw new IllegalArgumentException( "Supplied parameter may not be null."); } else { Set<FileChangeListener> listenerSet; if (fileListenerMap.containsKey(file)) { listenerSet = fileListenerMap.get(file); listenerSet.add(listener); } else { listenerSet = new HashSet<FileChangeListener>(); listenerSet.add(listener); fileListenerMap.put(file, listenerSet); scheduleFileMonitoringTask(file); } } } public void removeListener(FileChangeListener listener, String file) { Set<FileChangeListener> listenerSet = null; if (fileListenerMap.containsKey(file)) { listenerSet = fileListenerMap.get(file); listenerSet.remove(listener); if (listenerSet.size() == 0) { fileListenerMap.remove(listenerSet); cancelFileMonitoringTask(file); } } } private void cancelFileMonitoringTask(String file) { FileMonitoringTask task = taskMap.get(file); task.cancel(); taskMap.remove(file); } private void scheduleFileMonitoringTask(String file) { FileMonitoringTask task = new FileMonitoringTask(file); timer.scheduleAtFixedRate(task, initialDelay, pollingInterval); taskMap.put(file, task); } public long getPollingInterval() { return pollingInterval; } public void setPollingInterval(long pollingInterval) { this.pollingInterval = pollingInterval; } public long getInitialDelay() { return initialDelay; } public void setInitialDelay(long initialDelay) { this.initialDelay = initialDelay; } private void fireFileChangeEvent(String file) { Set<FileChangeListener> listenerSet = fileListenerMap.get(file); for (FileChangeListener listener : listenerSet) { listener.fileChanged(); } } } |
Nogal wat code, maar aangezien ik deze klasse thread-safe wil krijgen is het m.i. alleemaal relevant.
Natuurlijk verwacht ik geen uitgekauwde oplossing en heb ik er zelf ook al over nagedacht
Hoe ik de situatie inschat:
1) De klasse is als Singleton geïmplementeerd. Deze implementatie is thread-safe, omdat er geen 'lazy loading' gebruikt wordt en de constructor volledig afgeschermd is.
2) In de registerListener methode kan het in een MT situatie fout gaan, omdat de Map die de listeners bevat in de class scope zit. Meerdere threads gebruiken allemaal per definitie dezelfde instantie van FileChangeMonitor, want --> Singleton. Bij het muteren van de Map kunnen daardoor problemen ontstaan, want de concrete imlementatie (HashMap) is van zichzelf niet thread-safe.
3) Bovenstaande geldt ook voor de removeListener methode.
4) De class scope variabelen pollingInterval en initialDelay kunnen gemuteerd worden m.b.v. standaard accessor methoden. Ook hier kan een probleem ontstaan in een MT omgeving.
Oplossingen die ik zelf zie:
1) De methoden registerListener, removeListener en de accessor methoden voor pollingInterval en initialDelay in hun geheel synchroniseren m.b.v. het synchronized keyword. Correct?
2) fileListenerMap instantiëren als threadsafe collection m.b.v. Collections.synchronizedMap(..) en binnen de methoden registerListener en removeListener een synchronized block aanbrengen, waarbij gesynchroniseerd wordt op fileListenerMap. De convinience methoden scheduleFileMonitorTask en cancelFileMonitoringTask worden aangeroepen vanuit deze synchronized context, waardoor hier niets aan te veranderd moet worden en ook taskMap niet als synchronizedMap geïnstantieerd moet worden. De class scope variabelen pollingInterval en initialDelay volatile maken.
Lijkt mij dat oplossing 2 sowieso efficiënter zal zijn, maar is thread safety op die manier gegarandeerd. Is dat überhaupt bij oplossing 1 wel het geval? Heb ik nog mogelijke problemen over het hoofd gezien?