Java核心概念之(线程、进程、同步、互斥)

‌线程与进程

‌进程的概念

进程(‌Process)‌是计算机中的程序关于某数据集合上的一次运行活动,‌是系统进行资源分配和调度的基本单位,‌是操作系统结构的基础‌。‌在早期面向进程设计的计算机结构中,‌进程是程序的基本执行实体;‌在当代面向线程设计的计算机结构中,‌进程是线程的容器。‌程序是指令、‌数据及其组织形式的描述,‌进程是程序的实体。‌

进程具有动态、‌独立、‌异步、‌并发的特征,‌是一个具有独立功能的程序关于某个数据集合的一次运行活动。‌它不只是程序的代码,‌还包括当前的活动,‌通过程序计数器的值和处理寄存器的内容来表示‌。‌


‌线程的概念

线程是计算机科学中的基本概念,‌也被称为轻量级进程。‌它是操作系统能够进行运算调度的最小单位,‌被包含在进程之中,‌是进程中的实际运作单位。‌一个进程中可以有一个或多个线程,‌每个线程负责单独执行一个任务,‌它们共享进程的资源,‌如内存空间、‌文件句柄等,‌但每个线程有自己的独立执行流和线程描述表。‌线程是独立调度和分派的基本单位,‌可以为操作系统内核调度的内核线程,‌或由用户进程自行调度的用户线程,‌或者由内核与用户进程进行混合调度。‌线程是实现多任务并发执行的重要机制,‌广泛应用于提高程序的执行效率和响应速度


‌线程与进程‌的关系及其区别

线程是操作系统执行处理机制的基本单位,‌每个进程至少有一个线程。‌进程则是操作系统资源分配的基本单位,‌所有与该进程有关的资源,‌都会被记录在进程控制块中。‌

关系:‌

线程是进程的基本执行单元,‌一个进程的所有任务都在线程中执行。‌线程是轻量级的进程,‌共享进程的资源,‌如内存、‌文件等。‌


区别:‌

  1. 资源占用‌:‌进程拥有独立的地址空间和资源,‌线程共享进程的资源,‌因此线程的资源占用更少。‌
  2. 切换开销‌:‌进程切换涉及更多的资源保存和恢复,‌开销较大;‌而线程切换开销较小,‌能更高效地利用CPU。‌
  3. 并发性‌:‌线程之间可以并发执行,‌提高程序的并发性能;‌进程间并发执行需要更多的资源和管理开销。‌
  4. 健壮性‌:‌多进程程序更加健壮,‌一个进程崩溃不会影响其他进程;‌而多线程程序中,‌一个线程崩溃可能导致整个进程崩溃‌。‌

‌线程是进程中执行运算的最小单元,‌进程是操作系统资源分配的基本单位,‌同步和互斥则是进程或线程间的协作方式。‌‌


‌同步‌:‌

同步是指为完成某种任务而建立的两个或多个进程或线程,‌在某些位置上需要协调它们的工作次序而产生的制约关系。‌

Java实现线程同步的方法:

使用synchronized关键字 :

可以修饰方法或代码块, 确保同一时间只有一个线程能够访问被修饰的代码, 从而避免多线程同时访问同一资源导致的数据不一致问题。‌

示例

在这个例子中,‌我们将创建一个简单的计数器类,‌该类使用修饰方法来保证线程安全。‌

public class Counter {
    private int count = 0;

    // 使用synchronized修饰方法,‌保证线程安全
    public synchronized void increment() {
        count++; // 增加计数
    }

    // 同步方法,‌用于获取当前计数值
    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // 创建两个线程,‌同时增加计数器的值
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数值
        System.out.println("Final count is: " + counter.getCount());
    }
}

在这个例子中,‌Counter类有一个方法increment,‌用于增加计数器的值。‌由于这个方法被synchronized修饰,‌因此当一个线程访问这个方法时,‌其他线程必须等待,‌直到该线程执行完毕。‌这样可以确保计数器的值在并发环境下是正确的。‌

getCount方法也被synchronized修饰,‌以确保在获取计数值时也是线程安全的。‌虽然在这个特定的例子中可能不是必需的(‌因为我们只是读取一个值,‌而没有修改它)‌,‌但在更复杂的场景中,‌同步获取方法可能是有必要的。‌

最后,‌在main方法中,‌我们创建了两个线程,‌每个线程都会调用increment方法1000次。‌由于increment方法是同步的,‌因此最终输出的计数值应该是2000,‌而不是一个不确定的值。‌


使用ReentrantLock :

这是一种可重入的互斥锁, 与synchronized关键字类似, 但提供了更多的灵活性和扩展性, 如支持公平锁、 可中断锁等。‌

示例

在这个例子中,‌我们将创建一个简单的计数器类,‌该类使用来保证线程安全。‌

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    // 使用ReentrantLock保证线程安全
    public void increment() {
        lock.lock(); // 加锁
        try {
            count++; // 增加计数
        } finally {
            lock.unlock(); // 解锁
        }
    }

    // 获取当前计数值
    public int getCount() {
        lock.lock(); // 加锁
        try {
            return count;
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        // 创建两个线程,‌同时增加计数器的值
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 等待线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数值
        System.out.println("Final count is: " + counter.getCount());
    }
}

在这个例子中,‌Counter类有一个ReentrantLock实例作为成员变量。‌increment方法和getCount方法都通过调用lock方法来加锁,‌并在方法结束时调用unlock方法来解锁。‌这样可以确保在并发环境下对计数器的访问是线程安全的。‌

synchronized关键字相比,‌ReentrantLock提供了更多的灵活性和扩展性。‌例如,‌你可以创建公平锁(‌new ReentrantLock(true))‌,‌这样线程将按照请求锁的顺序来获取锁。‌你还可以中断正在等待锁的线程,‌或者尝试在有限的时间内获取锁。‌


利用wait和notify方法 :

通过Object的wait和notify方法可以实现线程间的通信和同步, 但使用这些方法时必须加锁, 通常与synchronized关键字一起使用。‌

生产者-消费者模型示例

在这个例子中,‌我们将创建一个简单的生产者-消费者模型,‌其中生产者线程生产数据,‌消费者线程消费数据。‌

public class ProducerConsumerExample {
    private static final Object lock = new Object();
    private static int data = 0;
    private static boolean available = false;

    public static void main(String[] args) {
        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (available) {
                        try {
                            lock.wait(); // 等待消费者消费数据
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    data = i; // 生产数据
                    available = true;
                    System.out.println("Produced: " + data);
                    lock.notify(); // 通知消费者
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (!available) {
                        try {
                            lock.wait(); // 等待生产者生产数据
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consumed: " + data);
                    available = false;
                    lock.notify(); // 通知生产者
                }
            }
        });

        // 启动线程
        producer.start();
        consumer.start();
    }
}

在这个例子中,‌我们有一个共享资源data和一个标志available,‌available用于指示是否有数据可供消费。‌生产者线程在生产数据之前会检查标志,‌如果为true,‌则表示已有数据可供消费,‌生产者线程会等待(‌通过lock.wait())‌available。‌生产者生产数据后,‌将设置为true,‌并通过lock.notify()通知消费者线程。‌

消费者线程在消费数据之前也会检查标志available,‌如果为false,‌则表示没有数据可供消费,‌消费者线程会等待available。‌消费者消费数据后,‌将设置为false,‌并通过lock.notify()通知生产者线程。‌

注意,‌wait和​​​​​​​notify方法必须在同步块或同步方法中使用,‌因为它们需要对象锁来执行。‌在这个例子中,‌我们使用了一个名为lock的对象作为锁。‌


线程同步的方法特点:‌

  • 互斥锁‌:‌适用于线程可用的资源只有一个,‌需要互斥访问的情况。‌它能确保同一时间只有一个线程访问共享资源,‌从而避免数据不一致的问题,‌但可能会导致死锁或降低程序响应速度‌。‌
  • 条件变量‌:‌适用于线程之间构成条件等待关系的情况。‌它可以让一个线程等待另一个线程的信号,‌以实现线程之间的协调和合作,‌但需要配合互斥锁使用,‌增加了代码的复杂度‌。‌
  • 信号量‌:‌可以控制多个线程对共享资源的访问,‌适用于需要控制访问资源数量的场景,‌但使用较为复杂,‌需要仔细设计和实现‌。‌
  • synchronized关键字‌:‌是Java中常用的线程同步方式,‌可以确保代码块在同一时刻只能有一个线程执行,‌保证操作的原子性,‌但可能会导致锁的升级和性能问题‌。‌

互斥锁适用场景:‌

  • 资源独占访问‌:‌当多个线程或进程需要访问同一共享资源,‌且该资源在同一时刻只能被一个线程或进程使用时,‌可以使用互斥锁来确保资源的独占访问,‌防止数据竞争和不一致性‌。‌
  • 任务同步‌:‌在需要按照特定顺序执行多个任务时,‌互斥锁可以用于同步这些任务,‌确保它们按照预定的顺序执行‌。‌
  • 锁持有时间较长‌:‌当预计锁的持有时间较长或等待时间可能较长时,‌使用互斥锁可以更有效地利用CPU资源,‌因为互斥锁会导致线程阻塞,‌从而释放CPU给其他任务‌。‌

互斥锁通过上锁和解锁机制,‌确保在任意时刻只有一个线程或进程可以访问共享资源,‌从而保护数据的完整性和一致性。‌


条件变量适用场景:‌

  1. 生产者-消费者问题 : 在生产者-消费者问题中, 生产者线程负责生成数据, 消费者线程负责消费数据。条件变量用于通知消费者线程何时可以消费数据。‌生产者线程在生成数据后会发送信号到条件变量,‌然后解锁互斥锁,‌使消费者线程得以执行。‌
  2. 读者-写者问题‌:‌在读者-写者问题中,‌多个读者线程可以同时读取共享数据,‌但只有一个写者线程可以写入共享数据。‌条件变量用于控制读者和写者的访问顺序。‌当没有写者时,‌读者线程可以获取互斥锁并读取数据;‌当没有读者时,‌写者线程可以获取互斥锁并写入数据。‌
  3. 线程池中的任务调度 : 在线程池中, 多个线程同时竞争执行任务。条件变量用于通知空闲线程何时有任务可执行。‌当任务队列为空时,‌线程会等待在条件变量上,‌直到有新的任务加入。‌
  4. 等待-通知机制‌:‌条件变量常用于实现等待-通知机制,‌其中一个或多个线程等待某个条件成立,‌而其他线程在满足条件时发出通知。‌等待线程会在条件变量上等待,‌直到被通知者发出信号。‌
  5. 任务同步‌:‌在需要实现任务之间的同步时,‌互斥锁用于保护共享数据的访问,‌而条件变量用于通知等待的任务何时可以继续执行。‌

条件变量的主要优势在于它允许线程在等待条件满足时进入休眠状态,‌而不是持续检查条件是否满足,‌这样可以节省CPU资源,‌并提高程序的效率2。‌条件变量通常与互斥锁一起使用,‌以保护对共享资源的访问并防止竞态条件。‌

请注意, 条件变量的使用需要谨慎, 因为它可能会导致虚假唤醒( spurious wakeup) , 即线程可能在条件未满足时被唤醒。为了避免这种情况,‌通常在使用条件变量时,‌会将其放在一个循环中,‌并在循环中重新检查条件是否满足。‌


信号量适用场景:‌

  • 控制并发线程数量‌:‌当需要限制同时访问共享资源的线程数量时,‌可以使用信号量来管理并发访问,‌确保系统资源不被过度占用‌。‌
  • 线程同步‌:‌在多线程程序中,‌信号量可用于实现线程间的同步,‌确保线程按照预定的顺序执行或等待某个条件成立‌。‌
  • 资源池管理‌:‌如数据库连接池、‌线程池等,‌信号量可用于控制资源池中的资源数量,‌避免资源耗尽‌。‌

信号量通过维护一个计数器来管理访问权限,‌线程在访问资源前需要获取信号量,‌从而实现对并发访问的有效控制。‌


synchronized关键字适用场景 :

synchronized关键字适用需要线程同步的场景 

  • 共享资源的访问控制‌:‌当多个线程需要访问同一资源时,‌使用synchronized可以确保同一时刻只有一个线程能访问该资源,‌避免数据不一致或竞态条件。‌
  • 线程间通信‌:‌在多线程程序中,‌线程间经常需要通信,‌synchronized关键字结合wait/notify机制可以实现线程间的等待和通知,‌从而协调线程的执行顺序。‌
  • 单例模式的实现‌:‌在单例模式的实现中,‌使用synchronized可以确保在多线程环境下只能创建一个实例,‌保证单例的唯一性。‌

‌互斥‌:‌

互斥是指当一个进程或线程访问某临界资源时,‌另一个想要访问该临界资源的进程或线程必须等待,‌直到当前访问临界资源的进程或线程访问结束并释放该资源。‌

Java实现线程互斥主要方式:

使用synchronized关键字 :

这是Java中最基本的同步机制, 可以用来修饰方法或代码块, 确保同一时间只有一个线程可以访问被锁定的方法或代码块。‌

在Java中, 关键字synchronized是实现线程互斥的一种基本方式。它可以用来修饰方法或者代码块,‌确保在同一时间内只有一个线程可以执行某个方法或代码块。‌

使用synchronized关键字修饰方法


public class Counter {
    private int count = 0;

    // 使用synchronized修饰方法,‌确保线程安全
    public synchronized void increment() {
        count++; // 增加计数
    }

    // 另一个synchronized方法
    public synchronized int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) {
        final Counter counter = new Counter();

        // 创建并启动两个线程,‌对计数器进行增加操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        // 等待两个线程执行完毕
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数结果
        System.out.println("Final count is: " + counter.getCount());
    }
}

在这个例子中,‌Counter类有一个increment方法,‌用来增加计数器的值。‌这个方法被关键字​​​​​​​​​​​​​​synchronized修饰,‌因此,‌在同一时间内只有一个线程可以执行这个方法。‌

main方法中创建了两个线程t1和​​​​​​​t2,‌它们都执行方法​​​​​​​counter.increment()。‌由于​​​​​​​increment方法是同步的,‌所以这两个线程将互斥地访问这个方法,‌确保计数器的值正确地增加。‌

最后,‌主线程等待这两个线程执行完毕,‌然后输出计数器的最终值。‌由于使用了synchronized关键字,‌最终的输出将会是2000,‌表示两个线程成功地各自将计数器的值增加了​​​​​​​​​​​​​​1000次。‌


使用ReentrantLock :

java.util.concurrent.locks包中的一个ReentrantLock类, 提供了更灵活的锁定机制, 可以实现更复杂的同步需求, 包括等待可中断、 公平锁等高级特性。‌

在Java中,ReentrantLockjava.util.concurrent.locks包中的一个类, 它提供了比synchronized关键字更灵活的锁定机制。使用​​​​​​​​​​​​​​​​​​​​​ReentrantLock,‌你可以实现更复杂的同步需求,‌比如等待可中断、‌公平锁等高级特性。‌

使用ReentrantLock实现线程互斥

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++; // 增加计数
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public int getCount() {
        lock.lock(); // 获取锁
        try {
            return count;
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

public class Main {
    public static void main(String[] args) {
        final Counter counter = new Counter();

        // 创建并启动两个线程,‌对计数器进行增加操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        // 等待两个线程执行完毕
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数结果
        System.out.println("Final count is: " + counter.getCount());
    }
}

在这个例子中,‌Counter类有一个​​​​​​​ReentrantLock的​​​​​​​lock实例。‌​​​​​​​increment方法和​​​​​​​getCount方法都通过​​​​​​​lock和​​​​​​​unlock方法来获取和释放锁,‌以确保在同一时间内只有一个线程可以执行这些方法。‌

main方法中创建了两个线程t1和​​​​​​​t2,‌它们都执行​​​​​​​counter.increment()方法。‌由于使用了​​​​​​​ReentrantLock,‌这两个线程将互斥地访问这个方法,‌确保计数器的值正确地增加。‌

最后,‌主线程等待这两个线程执行完毕,‌然后输出计数器的最终值。‌由于使用了ReentrantLock,‌最终的输出将会是​​​​​​​2000,‌表示两个线程成功地各自将计数器的值增加了​​​​​​​1000次。‌


使用Semaphore :

信号量也是一种实现互斥的方式, 它可以控制同时访问某个特定资源的操作数量, 通过协调各个线程, 以保证合理的使用公共资源。‌

在Java中,‌​​​​​​​Semaphore是一种基于计数的同步机制,‌它可以用来实现线程间的互斥,‌同时可以控制同时访问某个特定资源的线程数量。‌

使用Semaphore实现线程互斥

import java.util.concurrent.Semaphore;

public class Counter {
    private int count = 0;
    private final Semaphore semaphore = new Semaphore(1); // 创建一个信号量,‌初始化为1,‌表示只允许一个线程访问资源

    public void increment() {
        try {
            semaphore.acquire(); // 获取信号量,‌如果信号量为0,‌则当前线程等待
            count++; // 增加计数
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放信号量,‌增加信号量的计数,‌唤醒等待的线程
        }
    }

    public int getCount() {
        try {
            semaphore.acquire(); // 获取信号量
            return count;
        } finally {
            semaphore.release(); // 释放信号量
        }
    }
}

public class Main {
    public static void main(String[] args) {
        final Counter counter = new Counter();

        // 创建并启动两个线程,‌对计数器进行增加操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        // 等待两个线程执行完毕
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终的计数结果
        System.out.println("Final count is: " + counter.getCount());
    }
}

在这个例子中,‌Counter类有一个Semaphoresemaphore实例,‌初始化为1,‌表示只允许一个线程访问资源。‌​​​​​​​​​​​​​​​​​​​​​increment方法和​​​​​​​getCount方法都通过acquire和​​​​​​​​​​​​​​release方法来获取和释放信号量,‌以确保在同一时间内只有一个线程可以执行这些方法。‌

main方法中创建了两个线程t1和​​​​​​​t2,‌它们都执行方法​​​​​​​counter.increment()。‌由于使用了​​​​​​​Semaphore,‌这两个线程将互斥地访问这个方法,‌确保计数器的值正确地增加。‌

最后,‌主线程等待这两个线程执行完毕,‌然后输出计数器的最终值。‌由于使用了Semaphore,‌最终的输出将会是​​​​​​​2000,‌表示两个线程成功地各自将计数器的值增加了​​​​​​​1000次。‌

你可能感兴趣的:(java,进程与线程,线程同步,线程互斥)