Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

import java.util.concurrent.atomic.AtomicReference;

public class SingletonAtomic implements SingletonInstance {

private static final AtomicReference<SingletonAtomic> INSTANCE = new AtomicReference<>();
private final long time = System.currentTimeMillis();

private SingletonAtomic() {
}

public static SingletonAtomic getInstance() {
INSTANCE.compareAndSet(null, new SingletonAtomic());
return INSTANCE.get();
}

@Override
public long getCreationTime() {
return time;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

public class SingletonEager implements SingletonInstance {

private static final SingletonEager INSTANCE = new SingletonEager();
private final long time = System.currentTimeMillis();

private SingletonEager() {
}

public static SingletonEager getInstance() {
return INSTANCE;
}

@Override
public long getCreationTime() {
return time;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

public enum SingletonEnum implements SingletonInstance {
INSTANCE;

private final long time = System.currentTimeMillis();

@Override
public long getCreationTime() {
return time;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

public class SingletonHolder implements SingletonInstance {

private final long time = System.currentTimeMillis();

private SingletonHolder() {
}

public static SingletonHolder getInstance() {
return Holder.INSTANCE;
}

@Override
public long getCreationTime() {
return time;
}

private static class Holder {
private static final SingletonHolder INSTANCE = new SingletonHolder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

public interface SingletonInstance {

long getCreationTime();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package pl.mperor.lab.java.design.pattern.creational.singleton;

import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;

public class SingletonSerializable implements Serializable, SingletonInstance {

@Serial
private static final long serialVersionUID = 1L;

private static final SingletonSerializable INSTANCE = new SingletonSerializable();
private final long time = System.currentTimeMillis();

private SingletonSerializable() {
}

public static SingletonSerializable getInstance() {
return INSTANCE;
}

@Override
public long getCreationTime() {
return time;
}

// Prevent Serialization from creating a new instance
protected Object readResolve() throws ObjectStreamException {
return getInstance();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,105 @@

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;

import java.util.concurrent.TimeUnit;
import java.io.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;

/**
* The purpose of this test is to randomly enable one of the test methods to execute.
* This is due to the fact that the tests involve the creation of a Singleton object.
* Since the Singleton is initialized only once, executing multiple tests could cause issues
* because subsequent tests would attempt to initialize the Singleton again, which is not allowed.
* By randomly selecting and enabling just one test per run, we avoid such conflicts and ensure
* the Singleton is properly handled without reinitialization.
*/
public class SingletonTest {

private static int random = ThreadLocalRandom.current().nextInt(1, 4);

static boolean isRandom1() {
return random == 1;
}

static boolean isRandom2() {
return random == 2;
}

static boolean isRandom3() {
return random == 3;
}

@EnabledIf(value = "isRandom1")
@Test
public void testLazyInitialized() throws InterruptedException {
assertLazyInitialized(() -> SingletonEnum.INSTANCE);
assertLazyInitialized(SingletonEager::getInstance);
assertLazyInitialized(SingletonHolder::getInstance);
assertLazyInitialized(SingletonAtomic::getInstance);
assertLazyInitialized(SingletonSerializable::getInstance);
}

private void assertLazyInitialized(Supplier<SingletonInstance> supplier) throws InterruptedException {
long startedTime = System.currentTimeMillis();
Thread.sleep(10);
var instance = supplier.get();
long instanceTime = instance.getCreationTime();
Assertions.assertTrue(startedTime < instanceTime);
}

@EnabledIf(value = "isRandom2")
@Test
public void shouldOnlyAllowToCreateOneInstanceOfSingleton() throws InterruptedException {
var instance = Singleton.getInstance();
long time = instance.getTime();
public void testSerializationSafe() throws IOException, ClassNotFoundException {
assertSerializationSafe(() -> SingletonEnum.INSTANCE);
assertSerializationSafe(SingletonSerializable::getInstance);
}

private void assertSerializationSafe(Supplier<SingletonInstance> supplier) throws IOException, ClassNotFoundException {
SingletonInstance instance = supplier.get();

TimeUnit.MILLISECONDS.sleep(50);
long timeAfterBreak = Singleton.getInstance().getTime();
var arrayOut = new ByteArrayOutputStream();
try (var out = new ObjectOutputStream(arrayOut)) {
out.writeObject(instance);
}

Assertions.assertSame(instance, Singleton.getInstance());
Assertions.assertEquals(time, timeAfterBreak);
SingletonInstance deserializedInstance;
try (var in = new ObjectInputStream(new ByteArrayInputStream(arrayOut.toByteArray()))) {
deserializedInstance = (SingletonInstance) in.readObject();
}

Assertions.assertSame(instance, deserializedInstance, "Singleton instance should be the same after deserialization");
}

@EnabledIf(value = "isRandom3")
@Test
public void testThreadSafe() {
assertThreadSafe(() -> SingletonEnum.INSTANCE);
assertThreadSafe(SingletonEager::getInstance);
assertThreadSafe(SingletonHolder::getInstance);
assertThreadSafe(SingletonAtomic::getInstance);
assertThreadSafe(SingletonSerializable::getInstance);
}

private void assertThreadSafe(Supplier<SingletonInstance> supplier) {
var first = CompletableFuture.supplyAsync(() -> {
var singleton = supplier.get();
System.out.println("First ... " + singleton.getClass().getSimpleName());
return singleton;
});

var second = CompletableFuture.supplyAsync(() -> {
var singleton = supplier.get();
System.out.println("Second ... " + singleton.getClass().getSimpleName());
return singleton;
});

SingletonInstance firstResult = first.join();
SingletonInstance secondResult = second.join();

Assertions.assertSame(firstResult, secondResult);
Assertions.assertEquals(firstResult.getCreationTime(), secondResult.getCreationTime());
}
}