The code is already thread-safe because CREATE SYNONYM takes a SCH-M (schema-modification) lock, which blocks others' attempts to create the same synonym, and they will wait until the lock owner's transaction completes.
This will also inherently prevent errors related to the synonym object already existing. In fact, even checking for existence of the synonym (without using NOLOCK/READ UNCOMMITTED) from another thread will also block until the lock is released.
Therefore, this design could never allow concurrent operations in the first place. If concurrent access is a requirement, the design could be changed in the following way:
Since most of the time the local version of the synonym would be used, create it permanently in the database with the local definition. When the remote version is needed, the code can (within a transaction) drop and recreate the synonym with the remote definition, do its work, then drop and recreate with the local definition to return the state to where it was when the transaction started.
This will block other callers for the same reason above if the remote caller acquires the lock first. If a local caller is using the synonym, it acquires and potentially holds a SCH-S (schema-stability) lock, which will prevent the synonym from being dropped, and therefore block a remote caller for as long as the lock is held. This sets up a situation where a local caller could be in process and wait in the middle while a remote caller does its work.
I'm a little wary of allowing that because it makes things a little unpredictable (i.e., if you're recording timing metrics of the process, it could be all over the place). To solve that, we can use sp_getapplock and request a Shared lock for a local caller, and an Exclusive lock for a remote caller. This will allow concurrent local callers (which will wait if a remote caller is running), and a remote caller will wait for all local callers to complete before running.