--- a/net/minecraft/server/players/GameProfileCache.java
+++ b/net/minecraft/server/players/GameProfileCache.java
@@ -14,21 +14,23 @@
 import com.mojang.authlib.GameProfileRepository;
 import com.mojang.authlib.ProfileLookupCallback;
 import com.mojang.logging.LogUtils;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.Reader;
-import java.io.Writer;
 import java.nio.charset.StandardCharsets;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
@@ -43,6 +45,7 @@
 import org.slf4j.Logger;
 
 public class GameProfileCache {
+
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final int GAMEPROFILES_MRU_LIMIT = 1000;
     private static final int GAMEPROFILES_EXPIRATION_MONTHS = 1;
@@ -51,12 +54,17 @@
     private final Map<UUID, GameProfileCache.GameProfileInfo> profilesByUUID = Maps.newConcurrentMap();
     private final Map<String, CompletableFuture<Optional<GameProfile>>> requests = Maps.newConcurrentMap();
     private final GameProfileRepository profileRepository;
-    private final Gson gson = new GsonBuilder().create();
+    private final Gson gson = (new GsonBuilder()).create();
     private final File file;
     private final AtomicLong operationCount = new AtomicLong();
     @Nullable
     private Executor executor;
 
+    // Paper start - Fix GameProfileCache concurrency
+    protected final java.util.concurrent.locks.ReentrantLock stateLock = new java.util.concurrent.locks.ReentrantLock();
+    protected final java.util.concurrent.locks.ReentrantLock lookupLock = new java.util.concurrent.locks.ReentrantLock();
+    // Paper end - Fix GameProfileCache concurrency
+
     public GameProfileCache(GameProfileRepository profileRepository, File cacheFile) {
         this.profileRepository = profileRepository;
         this.file = cacheFile;
@@ -64,114 +72,150 @@
     }
 
     private void safeAdd(GameProfileCache.GameProfileInfo entry) {
-        GameProfile gameProfile = entry.getProfile();
+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+        GameProfile gameprofile = entry.getProfile();
+
         entry.setLastAccess(this.getNextOperation());
-        this.profilesByName.put(gameProfile.getName().toLowerCase(Locale.ROOT), entry);
-        this.profilesByUUID.put(gameProfile.getId(), entry);
+        this.profilesByName.put(gameprofile.getName().toLowerCase(Locale.ROOT), entry);
+        this.profilesByUUID.put(gameprofile.getId(), entry);
+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
     }
 
     private static Optional<GameProfile> lookupGameProfile(GameProfileRepository repository, String name) {
         if (!StringUtil.isValidPlayerName(name)) {
-            return createUnknownProfile(name);
+            return GameProfileCache.createUnknownProfile(name);
         } else {
-            final AtomicReference<GameProfile> atomicReference = new AtomicReference<>();
-            ProfileLookupCallback profileLookupCallback = new ProfileLookupCallback() {
-                public void onProfileLookupSucceeded(GameProfile gameProfile) {
-                    atomicReference.set(gameProfile);
+            final AtomicReference<GameProfile> atomicreference = new AtomicReference();
+            ProfileLookupCallback profilelookupcallback = new ProfileLookupCallback() {
+                public void onProfileLookupSucceeded(GameProfile gameprofile) {
+                    atomicreference.set(gameprofile);
                 }
 
-                public void onProfileLookupFailed(String string, Exception exception) {
-                    atomicReference.set(null);
+                public void onProfileLookupFailed(String s1, Exception exception) {
+                    atomicreference.set(null); // CraftBukkit - decompile error
                 }
             };
-            repository.findProfilesByNames(new String[]{name}, profileLookupCallback);
-            GameProfile gameProfile = atomicReference.get();
-            return gameProfile != null ? Optional.of(gameProfile) : createUnknownProfile(name);
+
+        if (!org.apache.commons.lang3.StringUtils.isBlank(name) // Paper - Don't lookup a profile with a blank name
+                && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) // Paper - Add setting for proxy online mode status
+            repository.findProfilesByNames(new String[]{name}, profilelookupcallback);
+            GameProfile gameprofile = (GameProfile) atomicreference.get();
+
+            return gameprofile != null ? Optional.of(gameprofile) : GameProfileCache.createUnknownProfile(name);
         }
     }
 
     private static Optional<GameProfile> createUnknownProfile(String name) {
-        return usesAuthentication() ? Optional.empty() : Optional.of(UUIDUtil.createOfflineProfile(name));
+        return GameProfileCache.usesAuthentication() ? Optional.empty() : Optional.of(UUIDUtil.createOfflineProfile(name));
     }
 
     public static void setUsesAuthentication(boolean value) {
-        usesAuthentication = value;
+        GameProfileCache.usesAuthentication = value;
     }
 
     private static boolean usesAuthentication() {
-        return usesAuthentication;
+        return io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode(); // Paper - Add setting for proxy online mode status
     }
 
     public void add(GameProfile profile) {
         Calendar calendar = Calendar.getInstance();
+
         calendar.setTime(new Date());
         calendar.add(2, 1);
         Date date = calendar.getTime();
-        GameProfileCache.GameProfileInfo gameProfileInfo = new GameProfileCache.GameProfileInfo(profile, date);
-        this.safeAdd(gameProfileInfo);
-        this.save();
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = new GameProfileCache.GameProfileInfo(profile, date);
+
+        this.safeAdd(usercache_usercacheentry);
+        if( !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly ) this.save(true); // Spigot - skip saving if disabled // Paper - Perf: Async GameProfileCache saving
     }
 
     private long getNextOperation() {
         return this.operationCount.incrementAndGet();
     }
 
+    // Paper start
+    public @Nullable GameProfile getProfileIfCached(String name) {
+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+        GameProfileCache.GameProfileInfo entry = this.profilesByName.get(name.toLowerCase(Locale.ROOT));
+        if (entry == null) {
+            return null;
+        }
+        entry.setLastAccess(this.getNextOperation());
+        return entry.getProfile();
+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
+    }
+    // Paper end
+
     public Optional<GameProfile> get(String name) {
-        String string = name.toLowerCase(Locale.ROOT);
-        GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByName.get(string);
-        boolean bl = false;
-        if (gameProfileInfo != null && new Date().getTime() >= gameProfileInfo.expirationDate.getTime()) {
-            this.profilesByUUID.remove(gameProfileInfo.getProfile().getId());
-            this.profilesByName.remove(gameProfileInfo.getProfile().getName().toLowerCase(Locale.ROOT));
-            bl = true;
-            gameProfileInfo = null;
+        String s1 = name.toLowerCase(Locale.ROOT);
+        boolean stateLocked = true; try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByName.get(s1);
+        boolean flag = false;
+
+        if (usercache_usercacheentry != null && (new Date()).getTime() >= usercache_usercacheentry.expirationDate.getTime()) {
+            this.profilesByUUID.remove(usercache_usercacheentry.getProfile().getId());
+            this.profilesByName.remove(usercache_usercacheentry.getProfile().getName().toLowerCase(Locale.ROOT));
+            flag = true;
+            usercache_usercacheentry = null;
         }
 
-        Optional<GameProfile> optional;
-        if (gameProfileInfo != null) {
-            gameProfileInfo.setLastAccess(this.getNextOperation());
-            optional = Optional.of(gameProfileInfo.getProfile());
+        Optional optional;
+
+        if (usercache_usercacheentry != null) {
+            usercache_usercacheentry.setLastAccess(this.getNextOperation());
+            optional = Optional.of(usercache_usercacheentry.getProfile());
+            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
         } else {
-            optional = lookupGameProfile(this.profileRepository, string);
+            stateLocked = false; this.stateLock.unlock(); // Paper - Fix GameProfileCache concurrency
+            try { this.lookupLock.lock(); // Paper - Fix GameProfileCache concurrency
+            optional = GameProfileCache.lookupGameProfile(this.profileRepository, name); // CraftBukkit - use correct case for offline players
+            } finally { this.lookupLock.unlock(); } // Paper - Fix GameProfileCache concurrency
             if (optional.isPresent()) {
-                this.add(optional.get());
-                bl = false;
+                this.add((GameProfile) optional.get());
+                flag = false;
             }
         }
 
-        if (bl) {
-            this.save();
+        if (flag && !org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) { // Spigot - skip saving if disabled
+            this.save(true); // Paper - Perf: Async GameProfileCache saving
         }
 
         return optional;
+        } finally { if (stateLocked) {  this.stateLock.unlock(); } } // Paper - Fix GameProfileCache concurrency
     }
 
     public CompletableFuture<Optional<GameProfile>> getAsync(String username) {
         if (this.executor == null) {
             throw new IllegalStateException("No executor");
         } else {
-            CompletableFuture<Optional<GameProfile>> completableFuture = this.requests.get(username);
-            if (completableFuture != null) {
-                return completableFuture;
+            CompletableFuture<Optional<GameProfile>> completablefuture = (CompletableFuture) this.requests.get(username);
+
+            if (completablefuture != null) {
+                return completablefuture;
             } else {
-                CompletableFuture<Optional<GameProfile>> completableFuture2 = CompletableFuture.<Optional<GameProfile>>supplyAsync(
-                        () -> this.get(username), Util.backgroundExecutor()
-                    )
-                    .whenCompleteAsync((profile, throwable) -> this.requests.remove(username), this.executor);
-                this.requests.put(username, completableFuture2);
-                return completableFuture2;
+                CompletableFuture<Optional<GameProfile>> completablefuture1 = CompletableFuture.supplyAsync(() -> {
+                    return this.get(username);
+                }, Util.PROFILE_EXECUTOR).whenCompleteAsync((optional, throwable) -> { // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+                    this.requests.remove(username);
+                }, this.executor);
+
+                this.requests.put(username, completablefuture1);
+                return completablefuture1;
             }
         }
     }
 
     public Optional<GameProfile> get(UUID uuid) {
-        GameProfileCache.GameProfileInfo gameProfileInfo = this.profilesByUUID.get(uuid);
-        if (gameProfileInfo == null) {
+        try { this.stateLock.lock(); // Paper - Fix GameProfileCache concurrency
+        GameProfileCache.GameProfileInfo usercache_usercacheentry = (GameProfileCache.GameProfileInfo) this.profilesByUUID.get(uuid);
+
+        if (usercache_usercacheentry == null) {
             return Optional.empty();
         } else {
-            gameProfileInfo.setLastAccess(this.getNextOperation());
-            return Optional.of(gameProfileInfo.getProfile());
+            usercache_usercacheentry.setLastAccess(this.getNextOperation());
+            return Optional.of(usercache_usercacheentry.getProfile());
         }
+        } finally { this.stateLock.unlock(); } // Paper - Fix GameProfileCache concurrency
     }
 
     public void setExecutor(Executor executor) {
@@ -190,80 +234,162 @@
         List<GameProfileCache.GameProfileInfo> list = Lists.newArrayList();
 
         try {
-            Object var9;
-            try (Reader reader = Files.newReader(this.file, StandardCharsets.UTF_8)) {
-                JsonArray jsonArray = this.gson.fromJson(reader, JsonArray.class);
-                if (jsonArray != null) {
-                    DateFormat dateFormat = createDateFormat();
-                    jsonArray.forEach(json -> readGameProfile(json, dateFormat).ifPresent(list::add));
-                    return list;
+            BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8);
+
+            label54:
+            {
+                List<GameProfileCache.GameProfileInfo> arraylist; // CraftBukkit - decompile error
+
+                try {
+                    JsonArray jsonarray = (JsonArray) this.gson.fromJson(bufferedreader, JsonArray.class);
+
+                    if (jsonarray != null) {
+                        DateFormat dateformat = GameProfileCache.createDateFormat();
+
+                        jsonarray.forEach((jsonelement) -> {
+                            Optional<GameProfileCache.GameProfileInfo> optional = GameProfileCache.readGameProfile(jsonelement, dateformat); // CraftBukkit - decompile error
+
+                            Objects.requireNonNull(list);
+                            optional.ifPresent(list::add);
+                        });
+                        break label54;
+                    }
+
+                    arraylist = list;
+                } catch (Throwable throwable) {
+                    if (bufferedreader != null) {
+                        try {
+                            bufferedreader.close();
+                        } catch (Throwable throwable1) {
+                            throwable.addSuppressed(throwable1);
+                        }
+                    }
+
+                    throw throwable;
                 }
 
-                var9 = list;
+                if (bufferedreader != null) {
+                    bufferedreader.close();
+                }
+
+                return arraylist;
             }
 
-            return (List<GameProfileCache.GameProfileInfo>)var9;
-        } catch (FileNotFoundException var7) {
-        } catch (JsonParseException | IOException var8) {
-            LOGGER.warn("Failed to load profile cache {}", this.file, var8);
+            if (bufferedreader != null) {
+                bufferedreader.close();
+            }
+        } catch (FileNotFoundException filenotfoundexception) {
+            ;
+        // Spigot Start
+        } catch (com.google.gson.JsonSyntaxException | NullPointerException ex) {
+            GameProfileCache.LOGGER.warn( "Usercache.json is corrupted or has bad formatting. Deleting it to prevent further issues." );
+            this.file.delete();
+        // Spigot End
+        } catch (JsonParseException | IOException ioexception) {
+            GameProfileCache.LOGGER.warn("Failed to load profile cache {}", this.file, ioexception);
         }
 
         return list;
     }
 
-    public void save() {
-        JsonArray jsonArray = new JsonArray();
-        DateFormat dateFormat = createDateFormat();
-        this.getTopMRUProfiles(1000).forEach(entry -> jsonArray.add(writeGameProfile(entry, dateFormat)));
-        String string = this.gson.toJson((JsonElement)jsonArray);
+    public void save(boolean asyncSave) { // Paper - Perf: Async GameProfileCache saving
+        JsonArray jsonarray = new JsonArray();
+        DateFormat dateformat = GameProfileCache.createDateFormat();
 
-        try (Writer writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) {
-            writer.write(string);
-        } catch (IOException var9) {
+        this.listTopMRUProfiles(org.spigotmc.SpigotConfig.userCacheCap).forEach((usercache_usercacheentry) -> { // Spigot // Paper - Fix GameProfileCache concurrency
+            jsonarray.add(GameProfileCache.writeGameProfile(usercache_usercacheentry, dateformat));
+        });
+        String s = this.gson.toJson(jsonarray);
+        Runnable save = () -> { // Paper - Perf: Async GameProfileCache saving
+
+        try {
+            BufferedWriter bufferedwriter = Files.newWriter(this.file, StandardCharsets.UTF_8);
+
+            try {
+                bufferedwriter.write(s);
+            } catch (Throwable throwable) {
+                if (bufferedwriter != null) {
+                    try {
+                        bufferedwriter.close();
+                    } catch (Throwable throwable1) {
+                        throwable.addSuppressed(throwable1);
+                    }
+                }
+
+                throw throwable;
+            }
+
+            if (bufferedwriter != null) {
+                bufferedwriter.close();
+            }
+        } catch (IOException ioexception) {
+            ;
+        }
+        // Paper start - Perf: Async GameProfileCache saving
+        };
+        if (asyncSave) {
+            io.papermc.paper.util.MCUtil.scheduleAsyncTask(save);
+        } else {
+            save.run();
         }
+        // Paper end - Perf: Async GameProfileCache saving
+
     }
 
     private Stream<GameProfileCache.GameProfileInfo> getTopMRUProfiles(int limit) {
-        return ImmutableList.copyOf(this.profilesByUUID.values())
-            .stream()
-            .sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed())
-            .limit((long)limit);
+        // Paper start - Fix GameProfileCache concurrency
+        return this.listTopMRUProfiles(limit).stream();
+    }
+
+    private List<GameProfileCache.GameProfileInfo> listTopMRUProfiles(int limit) {
+        try {
+            this.stateLock.lock();
+            return this.profilesByUUID.values().stream().sorted(Comparator.comparing(GameProfileCache.GameProfileInfo::getLastAccess).reversed()).limit(limit).toList();
+        } finally {
+            this.stateLock.unlock();
+        }
     }
+    // Paper end - Fix GameProfileCache concurrency
 
     private static JsonElement writeGameProfile(GameProfileCache.GameProfileInfo entry, DateFormat dateFormat) {
-        JsonObject jsonObject = new JsonObject();
-        jsonObject.addProperty("name", entry.getProfile().getName());
-        jsonObject.addProperty("uuid", entry.getProfile().getId().toString());
-        jsonObject.addProperty("expiresOn", dateFormat.format(entry.getExpirationDate()));
-        return jsonObject;
+        JsonObject jsonobject = new JsonObject();
+
+        jsonobject.addProperty("name", entry.getProfile().getName());
+        jsonobject.addProperty("uuid", entry.getProfile().getId().toString());
+        jsonobject.addProperty("expiresOn", dateFormat.format(entry.getExpirationDate()));
+        return jsonobject;
     }
 
     private static Optional<GameProfileCache.GameProfileInfo> readGameProfile(JsonElement json, DateFormat dateFormat) {
         if (json.isJsonObject()) {
-            JsonObject jsonObject = json.getAsJsonObject();
-            JsonElement jsonElement = jsonObject.get("name");
-            JsonElement jsonElement2 = jsonObject.get("uuid");
-            JsonElement jsonElement3 = jsonObject.get("expiresOn");
-            if (jsonElement != null && jsonElement2 != null) {
-                String string = jsonElement2.getAsString();
-                String string2 = jsonElement.getAsString();
+            JsonObject jsonobject = json.getAsJsonObject();
+            JsonElement jsonelement1 = jsonobject.get("name");
+            JsonElement jsonelement2 = jsonobject.get("uuid");
+            JsonElement jsonelement3 = jsonobject.get("expiresOn");
+
+            if (jsonelement1 != null && jsonelement2 != null) {
+                String s = jsonelement2.getAsString();
+                String s1 = jsonelement1.getAsString();
                 Date date = null;
-                if (jsonElement3 != null) {
+
+                if (jsonelement3 != null) {
                     try {
-                        date = dateFormat.parse(jsonElement3.getAsString());
-                    } catch (ParseException var12) {
+                        date = dateFormat.parse(jsonelement3.getAsString());
+                    } catch (ParseException parseexception) {
+                        ;
                     }
                 }
 
-                if (string2 != null && string != null && date != null) {
-                    UUID uUID;
+                if (s1 != null && s != null && date != null) {
+                    UUID uuid;
+
                     try {
-                        uUID = UUID.fromString(string);
-                    } catch (Throwable var11) {
+                        uuid = UUID.fromString(s);
+                    } catch (Throwable throwable) {
                         return Optional.empty();
                     }
 
-                    return Optional.of(new GameProfileCache.GameProfileInfo(new GameProfile(uUID, string2), date));
+                    return Optional.of(new GameProfileCache.GameProfileInfo(new GameProfile(uuid, s1), date));
                 } else {
                     return Optional.empty();
                 }
@@ -275,7 +401,8 @@
         }
     }
 
-    static class GameProfileInfo {
+    private static class GameProfileInfo {
+
         private final GameProfile profile;
         final Date expirationDate;
         private volatile long lastAccess;
