diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 666e5f8bd3..1676c1cbe0 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -55,6 +55,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; @@ -143,7 +144,6 @@ public final class Gson { static final ToNumberStrategy DEFAULT_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE; static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER; - private static final TypeToken NULL_KEY_SURROGATE = TypeToken.get(Object.class); private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n"; /** @@ -156,7 +156,7 @@ public final class Gson { private final ThreadLocal, FutureTypeAdapter>> calls = new ThreadLocal<>(); - private final Map, TypeAdapter> typeTokenCache = new ConcurrentHashMap<>(); + private final ConcurrentMap, TypeAdapter> typeTokenCache = new ConcurrentHashMap<>(); private final ConstructorConstructor constructorConstructor; private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory; @@ -504,7 +504,10 @@ private static TypeAdapter atomicLongArrayAdapter(final TypeAda */ @SuppressWarnings("unchecked") public TypeAdapter getAdapter(TypeToken type) { - TypeAdapter cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type); + if (type == null) { + throw new NullPointerException("type must not be null"); + } + TypeAdapter cached = typeTokenCache.get(type); if (cached != null) { return (TypeAdapter) cached; } @@ -530,8 +533,13 @@ public TypeAdapter getAdapter(TypeToken type) { for (TypeAdapterFactory factory : factories) { TypeAdapter candidate = factory.create(this, type); if (candidate != null) { + TypeAdapter existingAdapter = (TypeAdapter) typeTokenCache.putIfAbsent(type, candidate); + // If other thread concurrently added adapter prefer that one instead + if (existingAdapter != null) { + candidate = existingAdapter; + } + call.setDelegate(candidate); - typeTokenCache.put(type, candidate); return candidate; } } diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index abb0de2113..3cc24722ba 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -17,6 +17,7 @@ package com.google.gson; import com.google.gson.internal.Excluder; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.google.gson.stream.MalformedJsonException; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; import junit.framework.TestCase; /** @@ -89,6 +91,67 @@ private static final class TestTypeAdapter extends TypeAdapter { @Override public Object read(JsonReader in) throws IOException { return null; } } + public void testGetAdapter_Null() { + Gson gson = new Gson(); + try { + gson.getAdapter((TypeToken) null); + fail(); + } catch (NullPointerException e) { + assertEquals("type must not be null", e.getMessage()); + } + } + + public void testGetAdapter_Concurrency() { + final AtomicReference> threadAdapter = new AtomicReference<>(); + final Class requestedType = Number.class; + + Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(new TypeAdapterFactory() { + private volatile boolean isFirstCall = true; + + @Override public TypeAdapter create(final Gson gson, TypeToken type) { + if (isFirstCall) { + isFirstCall = false; + + // Create a separate thread which requests an adapter for the same type + // This will cause this factory to return a different adapter instance than + // the one it is currently creating + Thread thread = new Thread() { + @Override public void run() { + threadAdapter.set(gson.getAdapter(requestedType)); + } + }; + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + // Create a new dummy adapter instance + + @SuppressWarnings("unchecked") + TypeAdapter r = (TypeAdapter) new TypeAdapter() { + @Override public void write(JsonWriter out, Number value) throws IOException { + throw new AssertionError("not needed for test"); + } + + @Override public Number read(JsonReader in) throws IOException { + throw new AssertionError("not needed for test"); + } + }; + return r; + } + }) + .create(); + + TypeAdapter adapter = gson.getAdapter(requestedType); + assertNotNull(adapter); + // Should be the same adapter instance the concurrent thread received + assertSame(threadAdapter.get(), adapter); + } + public void testNewJsonWriter_Default() throws IOException { StringWriter writer = new StringWriter(); JsonWriter jsonWriter = new Gson().newJsonWriter(writer);