多线程并发和锁机制原理

进程和线程:

  • 进程: 进程是操作系统中的一个执行单元,它包含了程序的代码、数据和系统资源。每个进程都有独立的内存空间,它们之间不能直接访问对方的内存。
  • 线程: 线程是进程中的一个执行单元,一个进程可以包含多个线程。线程共享进程的内存空间,因此它们可以直接访问相同进程中的数据。
特性区别:
  • 独立性: 进程是独立的执行单元,每个进程有自己的地址空间和资源。线程则共享相同的地址空间和资源,它们更轻量级。
  • 通信和同步: 进程之间通信相对复杂,通常需要使用进程间通信(IPC)机制。线程之间可以直接共享数据,但需要考虑同步问题,防止竞态条件。
  • 切换开销: 进程切换的开销较大,因为需要切换整个地址空间。线程切换的开销较小,因为它们共享相同的地址空间。
  • 容错性: 进程之间的容错性较好,一个进程的崩溃不会影响其他进程。线程之间共享相同的资源,因此一个线程的错误可能影响整个进程。
价值,扩展,利用的空间,场景:
  • 价值: 进程提供了更强的隔离性,适用于需要独立执行环境的任务。线程更轻量,适用于需要并发执行的任务。
  • 扩展: 进程的扩展受限于系统资源,而线程可以更容易地扩展,因为它们共享相同的资源。
  • 利用的空间: 进程间通信的开销较大,但可以充分利用多核处理器。线程更适合在单个核心上执行,并发地完成任务。
  • 场景: 进程适用于需要高度隔离和稳定性的任务,如服务器应用。线程适用于需要并发处理、响应时间较短的任务,如图形界面应用和网络通信。

实现线程的方式:

  • extends Thread类 

通过继承Thread类,可以创建一个新的线程类。子类需要重写Thread类的run()方法,该方法包含线程的执行逻辑。然后,可以创建该线程类的实例并调用start()方法启动线程。

public class MyThread extends Thread{
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("Value:"+i);
        }
    }

    public static void main(String args[]){
        MyThread t1 = new MyThread();
        t1.start();
    }
}
  • implements Runnable 接口 推荐 

通过实现Runnable接口,可以将线程的执行逻辑封装在一个实现了run()方法的类中。然后,创建Thread类的实例时,将实现了Runnable接口的类的实例传递给Thread的构造方法。

public class MyRunnable implements Runnable{
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("Value:"+i);
        }
    }
    public static void main(String args[]){
        MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr);
        t1.start();
    }
}
  • implements Callable 接口 

实现Callable接口是Java中另一种实现多线程的方式,相对于Runnable接口,Callable接口提供了更强大的功能和更灵活的线程控制。主要区别在于,Callable接口允许线程执行任务后返回一个结果,并且可以抛出异常。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable {
    public Integer call(){
        int sum=0;
        for (int i=0;i<10;i++){
            sum+=i;
        }
        return sum;
    }
    public static void main(String args[]){
        Callable mc = new MyCallable();

        // 将Callable实例包装成FutureTask
        FutureTask ft = new FutureTask<>(mc);

        // 创建线程并启动
        Thread t1 = new Thread(ft);
        t1.start();
        try {
            // 获取线程执行结果
            Integer result = ft.get();
            System.out.println("Sum from Callable: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
区别:
  • 继承Thread类:

    • 优点:简单,直观,易于理解。
    • 缺点:由于Java是单继承,继承了Thread类后不能再继承其他类。
  • 实现Runnable接口:

    • 优点:更灵活,可以实现多个接口,可以作为参数传递给Thread类的构造方法。
    • 缺点:稍微繁琐一些,需要额外创建一个实现Runnable接口的类的实例。
选择建议:
  • 如果需要在定义线程的同时扩展其他类的功能,或者需要在多个线程之间共享数据,推荐使用实现Runnable接口的方式。

  • 如果简单地需要定义一个线程,而不用担心单继承的限制,并且不需要共享数据,可以使用继承Thread类的方式。

synchronized:

synchronized是Java中用于实现同步的关键字,它可以应用于方法和代码块。同步是为了确保多个线程在访问共享资源时的安全性,防止并发访问导致的数据不一致或其他问题。下面是对synchronized的详细介绍:

同步方法:

使用synchronized修饰方法,确保在同一时间只有一个线程能够访问该方法。这种方式适用于整个方法需要被同步的情况。

public class MyClass {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }
}
同步代码块:

使用synchronized修饰代码块,可以选择性地对一部分代码进行同步,而不是整个方法。这样可以提高程序的性能,因为只有必要的部分受到同步保护。

public class MyClass {
    private int count = 0;
    private Object lock = new Object();

    public void increment() {
        // 同步代码块
        synchronized (lock) {
            count++;
        }
    }
}
实例级别的锁和类级别的锁:
  • 实例级别的锁: 使用synchronized修饰非静态方法时,锁是当前对象实例。不同实例的方法调用之间互不影响。

public synchronized void instanceMethod() {
    // 同步方法
}
  • 类级别的锁: 使用synchronized修饰静态方法时,锁是当前类的Class对象。不同实例之间的静态方法调用之间也会受到同步保护。
public static synchronized void staticMethod() {
    // 同步静态方法
}
避免死锁:

在使用synchronized时,要注意避免死锁的情况。死锁发生在两个或多个线程相互等待对方释放锁的情况下。为了避免死锁,要确保线程获取锁的顺序是一致的。

创建一百个线程对num进行 ++ 操作 ,每个线程中都是一个死循环,判断跳出循环的条件是num==100w跳出,并打印本线程执行了多少次

public class IncrementThread1 extends Thread {
    private static int num = 0;
    private static final int TARGET = 1000000;

    @Override
    public void run() {
        int count = 0;
        while (num < TARGET) {
            num++;
            count++;
        }
        System.out.println(Thread.currentThread().getName() + " executed " + count + " times.");
    }

    public static void main(String[] args) {
        final int THREAD_COUNT = 100;
        IncrementThread1[] threads = new IncrementThread1[THREAD_COUNT];

        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new IncrementThread1();
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (int i = 0; i < THREAD_COUNT; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Final num value: " + num);
    }
}

多线程并发和锁机制原理_第1张图片

加上synchronized

public void run() {
        int count = 0;
        while (num < TARGET) {
            synchronized (IncrementThread.class) {
                if (num < TARGET) {
                    num++;
                    count++;
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + " 执行了 " + count + " 次。");
    }
多线程并发和锁机制原理_第2张图片

死锁问题:

死锁是多线程编程中一种常见的问题,它发生在两个或多个线程相互等待对方释放锁的情况下,导致程序无法继续执行。死锁通常是由于多个线程争夺资源时,每个线程都持有一部分资源并等待其他线程释放它所需要的资源,形成了相互等待的循环。

死锁发生的条件:
  • 互斥条件: 至少有一个资源是被独占的,即只能由一个线程同时使用。

  • 不可剥夺条件: 一个线程在持有资源的同时可以请求其他资源,而且不释放已经持有的资源。

  • 请求与保持条件: 线程已经持有了一些资源,又在请求其他资源,此时不能抢占已经被其他线程持有的资源。

  • 循环等待条件: 存在一个线程等待序列,其中每个线程都在等待下一个线程所持有的资源。

两个线程之间互相持有对方需要的锁,两个线程都持有了一把锁,接着都需要再持有一把锁(也是对方目前持有的锁)才可以执行任务

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TA extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.object1) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("TA - 持有 O1 Lock");
                synchronized (Main.object2) {
                    System.out.println("TA - 持有 O2 Lock");
                    Main.num++;
                }
            }
        }
        System.out.println("TA- end");
    }
}
class RA implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.object2) {// synchronized 大括号范围之内 在线程级别都是一个原子性的操作
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("RA - 持有 O2 Lock");
                synchronized (Main.object1) {
                    System.out.println("RA - 持有 O1 Lock");
                    Main.lock.lock();
                    Main.num++;
                    Main.lock.unlock();
                }
            }
        }
        System.out.println("RA- end");
    }
}
class Main {
    static int num = 0;
    static Object object1 = new Object();
    static Object object2 = new Object();
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
// 实例化线程对象
        TA ta = new TA();
        RA ra = new RA();
        ta.start();
        Thread t1 = new Thread(ra);
        t1.start();
// 等待线程执行完成
        try {
            ta.join(); // 阻塞方法直到线程执行完成
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(num);
    }
}
预防和解决死锁:
  • 加锁顺序: 确保所有线程按照相同的顺序获得锁,以减少循环等待的可能性。

  • 使用定时锁: 使用tryLock()来尝试获取锁,设置超时时间,如果超过时间仍未获得锁,就释放已经持有的锁。

  • 避免嵌套锁: 尽量避免在持有锁的情况下再去获取其他锁,以减少死锁的概率。

  • 定期检测: 定期检测系统中是否存在死锁,并采取相应的措施,如强制终止其中一个线程。

你可能感兴趣的:(java,jvm,开发语言)