Concepts / Thread Priority in Java

Thread Priority in Java

The program computes rowsPerThread as the image height divided by the thread count, using integer division.

  • Programming

Splitting One Image Among Several Threads

Imagine a program that computes the pixels of a large image. A single thread would have to visit every row by itself. The program in this lesson takes a different approach. When the user clicks the Start button, it creates and starts a specified number of threads, and it gives each thread its own segment of the image to compute. The question this article answers is simple to state but easy to get wrong: how does the program decide which rows belong to which thread?

Throughout this article we will use one small case. The image is 10 rows tall and is divided among 3 threads. Every idea below is checked against that case. Before the explanation of each step, you will be asked to predict one result, so try to answer before reading on.

Computing rowsPerThread With Integer Division

The first step is to decide how many rows each thread should compute. The program stores this value in an integer called rowsPerThread, and it obtains the value by dividing the image height by the number of threads. Here, height is the vertical size of the image in pixels, and threadCount is the number of threads the user asked for.

Both height and threadCount are integers, so the division is integer division. Integer division drops any fractional part of the result. This matters because it means the division never produces a value that is 'between' whole rows. Whatever is left over has to be handled by a separate rule.

Worked example

Dividing 10 rows among 3 threads

The image height is 10 and threadCount is 3. What value does rowsPerThread receive, and how many rows are left over?

  1. 1
    Divide the height by the thread count

    10 divided by 3 is 3 with a fractional part of one third. Because both values are integers, the fraction is dropped.

  2. 2
    Read off the quotient

    rowsPerThread becomes 3. Each thread is planned to handle 3 rows.

  3. 3
    Compute the leftover rows

    Three threads with 3 rows each cover 9 rows. The image has 10 rows, so 1 row is left over.

Result

rowsPerThread is 3, and 1 row is left over. That leftover row is why the assignment needs a rule for the last thread.

Tracing the Row Range for Each Thread

The program loops over the thread numbers, from 0 up to threadCount minus 1. In each pass, it calculates a first row and a last row for that thread. Both values are inclusive, which means both the first row and the last row belong to that thread's work. The loop is where the division of labor becomes concrete.

Formula, step by step
startRow = rowsPerThread * i
endRow = rowsPerThread * (i + 1) - 1
  1. 1
    i is the thread number, counting from 0 up to threadCount minus 1.
  2. 2
    startRow multiplies the block size by the thread number, so thread 0 starts at row 0, thread 1 starts after one full block, and so on.
  3. 3
    endRow adds one block to the start and subtracts 1, because the end row is inclusive and the next thread's block begins right after it.
  4. 4
    The last thread is an exception. Its endRow is set to height minus 1 instead, so that any leftover rows are not skipped.

Applying these formulas to the 10-row, 3-thread case gives the following ranges. Thread 0 starts at row 0 and ends at row 2. Thread 1 starts at row 3 and ends at row 5. Thread 2 is the last thread, so its start row is 3 times 2, which is 6, and its end row is set to height minus 1, which is 9. The figure below shows which thread owns each row.

Row 0Thread 0Row 1Thread 0Row 2Thread 0Row 3Thread 1Row 4Thread 1Row 5Thread 1Row 6Thread 2Row 7Thread 2Row 8Thread 2Row 9Thread 2
Which rows does each thread own, given the start and end index it is assigned in the loop?

Notice that row 9 is marked as the one that differs. Threads 0 and 1 each receive exactly rowsPerThread rows, but thread 2 receives four rows. That extra row is produced entirely by the final-thread rule, not by the general formulas.

The same loop can be traced one iteration at a time. The next figure shows how the start and end values change as i moves from 0 to 2. Pay attention to the last iteration, because that is where the exception applies.

i = 0rows 0 to 2i = 1rows 3 to 5i = 2rows 6 to 9i + 1i + 1
On each loop iteration, how do the start and end values change, and at which iteration does the bug first produce a wrong range?
Worked example

Computing the range for the last thread

In the loop for the 10-row, 3-thread case, what startRow and endRow does the program assign when i equals 2?

  1. 1
    Compute the general start

    startRow equals rowsPerThread times i, which is 3 times 2, giving 6.

  2. 2
    Compute the general end

    The general formula gives rowsPerThread times (i + 1) minus 1, which is 3 times 3 minus 1, giving 8.

  3. 3
    Apply the last-thread rule

    Because i equals threadCount minus 1, the program replaces endRow with height minus 1, which is 9.

Result

Thread 2 is assigned rows 6 through 9, four rows in total.

Why the Last Thread Takes the Leftover Rows

The rule for the last thread exists because integer division can leave a remainder. Without the rule, the general formula would assign rows 6 through 8 to thread 2 and leave row 9 belonging to no thread. The program would then skip that row entirely. The exception guarantees that every row from 0 to height minus 1 is covered by exactly one thread.

What do you think happens?

Before reading the answer, predict: for the 10-row image split among 3 threads, what is the endRow of the last thread?

Reveal answer
Answer:

9

The last thread's endRow is set to height minus 1, which is 10 minus 1, giving 9. The general formula would have produced 8, which is why the override is needed.

Where Thread Priority Fits In

Once a thread's rows are known, the program creates a Runner object with those two values and stores it in the workers array. The source then sets the new thread's priority to one less than the priority of the thread that is doing the creating, and only after that does it start the thread. The order for each thread is therefore create, set priority, start.

Priority is set during creation, but it does not change which rows a thread receives. The row boundaries are fixed by rowsPerThread and the loop variable i before any thread starts. When debugging a wrong result, you should therefore check the row arithmetic first, because priority only affects how the threads are set up and started.

The source also sets a shared flag named running to true before any thread starts, with a comment warning that the signal must be set before the threads begin. It initializes a counter named threadsCompleted to zero so that the program can later record how many threads have finished. The source excerpt does not show the Runner class itself, so this article treats its constructor only as taking a start row and an end row.

Mistakes That Break Row Assignment

java
Output
For height 10 and threadCount 3: thread 0 gets rows 0-2, thread 1 gets rows 3-5, thread 2 gets rows 6-9.
Notes on the code
  • Integer division on the first line drops the fraction, so rowsPerThread is 3.
  • The general endRow formula subtracts 1 because the end row is inclusive.
  • The if block applies the last-thread rule that absorbs leftover rows.
  • Common mistake

    Omitting the last-thread override

    What it looks like

    Leaving endRow at the general formula gives thread 2 an endRow of 8 when the image is 10 rows tall.

    Why it fails

    Row 9 then belongs to no thread, so the program skips it.

    Do this instead

    Set endRow to height minus 1 when i equals threadCount minus 1.

  • Common mistake

    Forgetting the minus 1 in the general endRow

    What it looks like

    Using rowsPerThread times (i + 1) without subtracting 1 gives thread 0 an endRow of 3.

    Why it fails

    The end row is inclusive, so row 3 is counted by both thread 0 and thread 1.

    Do this instead

    Subtract 1 so that each block ends just before the next block starts.

  • Common mistake

    Assuming division rounds up

    What it looks like

    Treating 10 divided by 3 as 4 rows per thread.

    Why it fails

    Integer division drops the fraction, so rowsPerThread is 3.

    Do this instead

    Use the truncated value as the block size, and let the last thread absorb the remainder.

  • Common mistake

    Looking for the bug in priority settings

    What it looks like

    Changing the thread priority when the wrong rows are being computed.

    Why it fails

    Priority does not change the row boundaries, which are set by rowsPerThread and i.

    Do this instead

    Trace the start and end values for each i first, because that is where the wrong range first appears.

Practice: Partitioning a Different Image

MEDIUM

An image is 14 rows tall and is divided among 4 threads. Compute rowsPerThread, then trace the startRow and endRow for every thread, including the last one. State which thread absorbs the leftover rows and how many rows it receives.

Hints
  • Start with rowsPerThread equal to height divided by threadCount using integer division.
  • Use startRow equal to rowsPerThread times i and endRow equal to rowsPerThread times (i + 1) minus 1 for every thread except the last.
  • Check your work by confirming that the ranges cover rows 0 through 13 with no gaps and no overlaps.

Key Takeaways

  • rowsPerThread is computed as the image height divided by the thread count using integer division, which drops any fractional part.
  • For thread i, startRow equals rowsPerThread times i, and endRow equals rowsPerThread times (i + 1) minus 1, because both rows are inclusive.
  • The last thread's endRow is set to height minus 1, so leftover rows from the remainder are assigned rather than skipped.
  • Each worker is created with its row range, given a priority one less than the creating thread's priority, and then started, but priority does not change the row boundaries.
  • When a result is wrong, trace the start and end values for each loop iteration, because the first incorrect range shows where the row-assignment bug begins.