The very problem and the main design reason with such activities, as interrupt processing in ISR, is that they can not be put waiting on releasing of some common lock (mutex, semaphore, whatever named) because no code will do this releasing. When ISR really comes, it shall obtain a context (memory contents, other settings) that is correct for its action: at least, data state is consistent. The only way for doing this is to postpone ISR entering while other parties (as usual code, either userland or kernel) manipulates the data structures that an ISR will touch. That's why all CPUs have means to enable or disable hardware interrupts. This enabling/disabling can be total (all hardware interrupts) or per-interrupt, per-group... this increases performance but doesn't change the main principle.
So, the non-ISR code first shall setup common data structures and configure ISR when respective interrupt can't appear yet (because hardware isn't allowed or CPU prevents this), and, when finished, it enables interrupts. When non-ISR code comes to do anything with common data, it disables the interrupt, works with the common data and reenables the interrupt. That's nearly the only really safe manner with the current CPU style (I could imagine some exotic implementations of other style but they aren't widespread).
The things get more complicated with multiprocessing. When some code disables interrupts on a single hart (CPU, core...), others can still allow ISR to enter. In this case, additional protection of common data is done with spinlocks: if some code acquired a spinlock on some hart, ISR on another hart can wait for spinlock freeing and then start its activity, because code at the first hart isn't stopped and will release the spinlock soon.
There are more details to this in many implementations - for example, some (most Unix-like systems, Windows, etc.) differentiate so-called "top half" (true ISR) and "bottom half" (actions outside of top half but scheduled for futher processing outside of any user process); Windows calls bottom half actions "deferred procedure calls".
I hope you got enough words for further study; I'd recommend you consult to well-known books on operating system design by Andrew Tanenbaum or Morris Bach (or similar books for Windows, I don't know names), respective online courses, etc.
For common data structure design, it's quite goal-specific and no common recommendations can be specified except common conforming to the goal. But I'd expect, considering UART, a kind of cyclic byte buffer.
UPDATE: You have mentioned user thread. Nearly all implementations tend to implement hardware interaction within kernel, at least, due to security reasons - hardware should be protected against malicious actions of arbitrary user, so does OS (userland shall not be able to disable interrupts). There are few exceptions like XWindow servers, though. In common, a kernel driver implements an intermediate between userland and hardware, and all potential problems are isolated within this intermediate.