Skip to content

Programming by Design

If you're not prepared to be wrong, you'll never come up with anything original. – Sir Ken Robinson

  • About
  • Java-PbD
  • C-PbD
  • ASM-PbD
  • Algorithms
  • More!
    • Commodore 64
    • Operating Systems
    • Odds and Ends
    • Theory

CISS-150 – Threads

Posted on October 6, 2019October 10, 2026 By William Jojo
CISS-150-Project

(Updated October 10, 2026)

This project examines the finer details of multi-programmed environments and the problems they introduce. You will be working with threads, locks, critical sections, and reviewing key aspects of how operating systems manage some details while leaving some to the programmer to handle.

This is to be done using your Ubuntu Linux virtual machine.

Important Note!
You may notice that the opportunity to increment and decrement values seems long-winded. This is by design. The goal of this exercise is to intentionally make things require many steps to drive home that critical sections typically contain many non-atomic steps and therefore cause data races when threads share data.

Learning outcomes

  • Threaded execution models.
  • Java Threads and synchronization.
  • C Threads and synchronization.
  • Understanding locking.
  • Understanding critical sections.

Overview

Threads are a useful extension to the application model. They allow for a single process to have multiple portions executing simultaneously while only occupying one process space. In other words, there is one process and multiple threads.

Traditionally, if we wanted to have multiple portions of a program running simultaneously, we would create additional processes using the fork() system call in C on Linux. But, this creates a new process (using the current running program as a blueprint) and that means a new address space. One idea behind threads is to share the address space, not necessarily create a new one.

If you have not already done so, you should read the chapter on threads.

The Atomicity-Violation bugs and the locks section are exellent additional bits when proceeding with this discovery project.

Now, we will present two thread models:

  • Java Threads
  • Linux Pthreads

Further we will introduce two forms of locking:

  • Synchronized methods (synchronized statements are left to the reader to research)
  • Pthread mutexes (mutual exclusion locks)

Java Threads

Java allows us to create threads at will by passing an object that implements Runnable to a Thread object. It is actually quite easy to implement.

Things to note in these Java examples are:

  • The Counter class is used to create the shared object.
  • The threads are created using the IncrementIt and DecrementIt classes.

Preparing the Software

Install the OpenJDK software in your Ubuntu VM using:

sudo apt update
sudo apt install openjdk-25-jdk-headless

Then using gedit or another editor, copy and paste the following code into two separate files. Compile each on the command line with:

ThreadInterference.java
public class ThreadInterference {

    static Counter shared;

    //Display a message, preceded by the name of the current thread
    static void threadMessage(String message) {
        String threadName = Thread.currentThread().getName();

        System.out.format("%s: %s%n", threadName, message);
    }

    // Inner class that will be manipulated by threads.
    static class Counter {
        private int c = 0;

        public void increment() {
            int t = c;
            //sleepMe();
            c = t + 1;
        }

        public void decrement() {
            int t = c;
            //sleepMe();
            c = t - 1;
        }

        public int value() {
            return c;
        }

    }

    // Thread to do 1000 increments of shared Counter.
    private static class IncrementIt implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //threadMessage("Incrementing!");
                shared.increment();
            }
        }
    }

    // Thread to do 1000 decrements of shared Counter.
    private static class DecrementIt implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //threadMessage("Decrementing!");
                shared.decrement();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{

        int count=0;

        for (int x = 0; x < 2000; x++) {
            shared = new Counter();

            // Start the threads!
            Thread i = new Thread(new IncrementIt());
            i.start();
            Thread d = new Thread(new DecrementIt());
            d.start();

            // Wait for them to complete!
            i.join();
            d.join();

            // What was the final counter value? It should be zero.
            //threadMessage("shared is " + shared.value());
            count += (shared.value() == 0) ? 0 : 1;
        }

        threadMessage("The shared value was not zero " + count + " out of 2000 times.");
    }
}

Java program that intentionally demonstrates thread interference.

Now this next bit of code has a subtle change to make it thread-safe. To do this, we add the synchronized modifier to the method declarations of the class being shared between the two threads.

This allows us to say that those methods are all mutually exclusive and therefore causes the Java runtime to lock the whole object while in one of those methods. Hence, the object being locked means that any thread desiring access to the object will block until the lock is freed.

Important Note!
This model is actually quite good in that it frees the programmer from remembering to set and clear the lock. This is automatically managed with code generated by the compiler simply because the methods are marked as synchronized.
ThreadSafe.java
public class ThreadSafe {

    static Counter shared;

    //Display a message, preceded by the name of the current thread
    static void threadMessage(String message) {
        String threadName = Thread.currentThread().getName();

        System.out.format("%s: %s%n", threadName, message);
    }

    // Inner class that will be manipulated by threads.
    static class Counter {
        private int c = 0;

        public synchronized void increment() {
            int t = c;
            //sleepMe();
            c = t + 1;
        }

        public synchronized void decrement() {
            int t = c;
            //sleepMe();
            c = t - 1;
        }

        public synchronized int value() {
            return c;
        }

    }

    // Thread to do 1000 increments of shared Counter.
    private static class IncrementIt implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //threadMessage("Incrementing!");
                shared.increment();
            }
        }
    }

    // Thread to do 1000 decrements of shared Counter.
    private static class DecrementIt implements Runnable {
        public void run() {
            for (int i = 0; i < 1000; i++) {
                //threadMessage("Decrementing!");
                shared.decrement();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{

        int count=0;

        for (int x = 0; x < 2000; x++) {
            shared = new Counter();

            // Start the threads!
            Thread i = new Thread(new IncrementIt());
            i.start();
            Thread d = new Thread(new DecrementIt());
            d.start();

            // Wait for them to complete!
            i.join();
            d.join();

            // What was the final counter value? It should be zero.
            //threadMessage("shared is " + shared.value());
            count += (shared.value() == 0) ? 0 : 1;
        }

        threadMessage("The shared value was not zero " + count + " out of 2000 times.");
    }
}

Java program that is thread-safe.


The Discovery

Run each program multiple times using:

java ThreadInterference

AND

java ThreadSafe
Technical Note
You may have to run the ThreadInterference program multiple times to see it, but it will show up. Sometimes it’s 0, sometimes not. And that is the point of this exercise – to see that it’s intermittent.

The 2000 threads will be created and allowed to change a new object for 1000 iterations. Note that the thread-safe version will always return zero for the shared value (the correct answer!). The other will return various values (very, very incorrect ones if they are not zero!). This shows that critical sections exist and need special treatment.

The solution is to mark the methods that manipulate the shared value. By making them synchronized, we guarantee that the methods will not interfere with each other by making the class block any other threads that try to run those synchronized methods.

This form of locking is only one way to handle race conditions within critical sections. The next set of examples shows how we can do it in C, closer to the OS and the hardware.


Threads in C

Now we look at a threading model that is directly in line with the operating system. The Pthreads model was introduced with the POSIX standard and is actually called POSIX Threads or Pthreads for short. Any POSIX-compliant operating system will likely have Pthreads available.

Things to note in these C examples are:

  • We have a sleep function(sleep_me()) to simulate going into an I/O mode - but is unused by default.
  • The shared global variable is used to create the shared space.
  • The threads are created using the increment_shared and decrement_shared functions as entry points of the thread.
  • The second implementation uses a Pthread mutex to synchronize access to the shared space.

Preparing the Software

The build-essential meta-package is likely already installed for you, but just in case, you can install it using:

sudo apt install build-essential

Then, using gedit or another editor, copy and paste the following code into two separate files. Compile each on the command line with:

gcc -o thread-int -pthread thread-int.c
gcc -o thread-safe -pthread thread-safe.c
thread-int.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>

int shared = 0;

void *increment_shared( void* m ) {
    
    for (int x = 0; x < 1000; x++) {
        int t = shared;
        t = t + 10;
        shared = t;
    }
    
    return NULL;
}

void *decrement_shared( void* m ) {
    
    for (int x = 0; x < 1000; x++) {
        int t = shared;
        t = t - 10;
        shared = t;
    }
    
    return NULL;
}

int main(void) {
    pthread_t thread1, thread2;
    int  iret1, iret2, count=0;

    /* Create separate threads pointing to their respective functions */

    for (int x = 0; x < 2000; x++) {
        shared = 0;
        iret1 = pthread_create( &thread1, NULL, increment_shared, NULL);
        iret2 = pthread_create( &thread2, NULL, decrement_shared, NULL);
        
        /* Wait till threads are complete before we allow main to continue. */
        
        pthread_join( thread1, NULL);
        pthread_join( thread2, NULL); 
        count += (shared == 0) ? 0 : 1;
    }
    
    printf("Shared was not zero %d out of 2000 times.\n", count);
    exit(0);
}

Now this next bit of code adds a mutex to make it thread-safe.

thread-safe.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>

int shared = 0;
pthread_mutex_t shared_mutex = PTHREAD_MUTEX_INITIALIZER;

void *increment_shared( void* m ) {

    for (int x = 0; x < 1000; x++) {
        pthread_mutex_lock(&shared_mutex);

        int t = shared;
        t = t + 10;
        shared = t;

        pthread_mutex_unlock(&shared_mutex);
    }

    return NULL;
}

void *decrement_shared( void* m ) {

    for (int x = 0; x < 1000; x++) {
        pthread_mutex_lock(&shared_mutex);

        int t = shared;
        t = t - 10;
        shared = t;

        pthread_mutex_unlock(&shared_mutex);
    }

    return NULL;
}

int main(void) {
    pthread_t thread1, thread2;
    int  iret1, iret2, count=0;

    pthread_mutex_init(&shared_mutex, NULL);
    /* Create separate threads pointing to their respective functions */
    for (int x = 0; x < 2000; x++) {
        shared = 0;
        iret1 = pthread_create( &thread1, NULL, increment_shared, NULL);
        iret2 = pthread_create( &thread2, NULL, decrement_shared, NULL);

        /* Wait till threads are complete before we allow main to continue. */

        pthread_join( thread1, NULL);
        pthread_join( thread2, NULL);
        count += (shared == 0) ? 0 : 1;
    }

    pthread_mutex_destroy(&shared_mutex);
    printf("Shared was not zero %d out of 2000 times.\n", count);
    exit(0);
}

Run each program multiple times using:

./thread-int

AND

./thread-safe

The 2000 threads will be created and allowed to change a new object for 1000 iterations. Note that the thread-safe version will always return zero for the shared value (the correct answer!). The other will return various values (very, very incorrect ones if they are not zero!). This shows that critical sections exist and need special treatment.

The solution is to use mutexes to lock the section of code that manipulates the shared value. By making the threads block and wait for the mutex, we guarantee that the methods will not interfere with each other.


Conclusion

At this point, you have experienced a couple of ways we control program flow to allow multiple threads to run in the same address space while manipulating a shared value.

You may have a few unanswered questions, and this is normal. No one masters multi-threading without a good deal of practice in writing applications that make use of it. If you have any further questions or need additional clarification on some of the details, please reach out to your instructor, who may also update this document for future students.

Post navigation

❮ Previous Post: Appendix B – Undefined Behavior
Next Post: CISS-110 Project 4 ❯

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Copyright © 2018 – 2026 Programming by Design.