Locks PostgreSQL takes for you
An UPDATE on a row takes a FOR UPDATE-style row lock until the transaction ends. A second UPDATE on the same row from another transaction waits. SELECTs don't take row locks (MVCC handles them) unless you explicitly ask with SELECT ... FOR UPDATE.
Explicit row locking
When you read a row with the intent of updating it, lock it explicitly: SELECT ... FOR UPDATE. This prevents a "lost update" where two concurrent transactions both read the same value and overwrite each other's change.
Table-level locks
DDL (ALTER TABLE, DROP TABLE) takes an exclusive lock on the table — nobody can read or write while it runs. The cure for "schema migration locked production for 30 seconds" is the CONCURRENTLY variants of DDL where they exist (CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY, etc.).