When you run the two statements concurrently (a SELECT and an UPDATE) the actual behavior will be basically random. This is because neither of the operations is instantaneous. To simplify, consider your table a list and SELECT is traversing this list, looking at one row at a time. UPDATE is also trying to update one or more rows. When the UPDATE is trying to update a row behind the SELECT then nothing happens (no blocking) because the SELECT has already progressed past the UPDATE point. If the UPDATE is trying to update the row at which SELECT is looking right now then the UPDATE will have to wait for SELECT to move on, which will happen very very very fast and the UPDATE will unblock and succeed, while the SELECT is moving ahead. But if the UPDATE is updating a row ahead of the SELECT then the update will succeed and, later, SELECT will eventually reach exactly this row and will stop, blocked. Now SELECT has to wait until the transaction that did the UPDATE commits.
This is the simplified story. The real life is much more complicated. The SELECT can have multiple read points (parallel plans). Both the SELECT and the UPDATE are subject to choosing an access path, meaning use one or more secondary indexes to locate the rows. Complex queries may contain operators that cause multiple lookups into a table (eg. joins). Both the SELECT and the UPDATE can do bookmark lookups to fetch BLOB data, which changes significantly the locking behavior. Cardinality estimation may cause the SELECT to run at a high granularity lock mode (eg. table level Shared lock). The UPDATE can trigger lock escalation, and the escalation can fail or succeed. Choosing different access paths can lead to deadlock. False lock contention can occur due to hash collisions. There are just about one myriad variables that have a say in this. And I didn't even mention higher isolation levels (repeatable read, serializable).
Perhaps you should use SNAPSHOT isolation and stop worrying about this issue?