程序,进程,线程

  • 程序:程序是一组指令和数据的集合,它们被存储在计算机的存储器中,并且在执行时会按照一定的顺序被处理器执行。程序通常以可执行文件的形式存在,例如.exe文件。

  • 进程:进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,包括代码、数据、堆栈等。进程之间是相互独立的,一个进程无法直接访问另一个进程的内存空间。进程可以包含多个线程,是程序执行的实体。

  • 线程:线程是进程中的一个执行单元,一个进程可以包含多个线程。线程共享进程的资源,如内存空间、文件等,但每个线程有自己的堆栈空间和局部变量。线程之间可以共享数据,可以更高效地完成任务。线程是操作系统进行调度的基本单位。

示例

  • 程序:一个文本编辑器程序,用于编辑文档。

  • 进程:打开一个文本编辑器程序时,操作系统会为该程序创建一个进程,该进程包含了文本编辑器程序的代码、数据等信息。

  • 线程:在文本编辑器程序中,可能有一个线程负责处理用户输入,另一个线程负责更新界面显示。这两个线程共享文本编辑器程序的资源,但有各自的执行流程。

基本概念:

  • ◆线程就是独立的执行路径

  • ◆在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程

  • ◆main()称之为主线程,为系统的入口,用于执行整个程序;

  • ◆在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。

  • ◆对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;

  • ◆线程会带来额外的开销,如cpu调度时间,并发控制开销。

  • ◆每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建方式

Java线程创建方式:

  1. 继承Thread类:创建一个类继承Thread类,并重写run()方法来定义线程执行的任务。然后通过创建该类的实例对象并调用start()方法来启动线程。线程不一定立即执行,由cpu调度;

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread using Thread class is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  1. 实现Runnable接口:创建一个类实现Runnable接口,并实现run()方法来定义线程执行的任务。然后通过创建该类的实例对象,将其作为参数传递给Thread类的构造方法,并调用start()方法来启动线程。

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread using Runnable interface is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
  1. 使用匿名内部类:可以在创建Thread对象时直接使用匿名内部类来实现线程的任务。

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("Thread using anonymous inner class is running");
            }
        });
        thread.start();
    }
}

4.通过Callable接口创建线程

除了使用Thread类和Runnable接口创建线程外,还可以使用Callable接口来创建线程。Callable接口是java.util.concurrent包中的接口,与Runnable接口类似,但可以返回线程执行的结果,也可以抛出异常。

  1. 实现Callable接口:创建一个类实现Callable接口,并实现call()方法来定义线程执行的任务。然后通过创建FutureTask对象,将其作为参数传递给Thread类的构造方法,并调用start()方法来启动线程。

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

class MyCallable implements Callable<String> {
    public String call() {
        return "Thread using Callable interface is running";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        
        String result = futureTask.get(); // 获取线程执行结果
        System.out.println(result);
    }
}
  1. 获取线程执行结果:通过FutureTask对象的get()方法可以获取线程执行的结果。在调用get()方法时,如果线程的任务还未执行完成,会阻塞当前线程,直到任务执行完成并返回结果。

开始线程的start方法和run方法的区别

  • start方法:调用start方法会告诉操作系统调度器准备好这个线程,当调度器认为合适的时候,会调用线程的run方法。start方法会启动一个新的线程,并在新线程中执行run方法。

  • run方法:run方法是线程的入口点,线程的任务代码通常在run方法中定义。直接调用run方法并不会启动一个新的线程,而是在当前线程中执行run方法的代码。

实例代码:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread using Thread class is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        
        // 调用start方法启动线程
        thread.start();
        
        // 直接调用run方法,不会启动新线程
        thread.run();
    }
}

在上面的示例代码中,调用thread.start()会启动一个新的线程,并在新线程中执行run方法中的代码。而直接调用thread.run()则会在当前线程中执行run方法的代码,不会启动新线程。

FutureTask 和 Future 的区别

  • FutureTask

    • FutureTaskFuture 接口的一个具体实现类,实现了 RunnableFuture 接口,同时也实现了 FutureRunnable 接口。

    • FutureTask 可以用来包装一个 CallableRunnable 对象,作为一个异步任务进行执行。

    • FutureTask 可以通过 get() 方法获取任务执行的结果,也可以通过 cancel() 方法取消任务的执行。

    • FutureTask 可以被用作 Thread 的构造方法的参数,从而创建一个线程来执行任务。

  • Future

    • Future 是一个接口,代表一个异步计算的结果。

    • Future 提供了方法来检查计算是否完成、等待计算完成以及获取计算的结果。

    • Future 接口的实现类可以是 FutureTask,也可以是其他实现了 Future 接口的类。

    • Future 接口的 get() 方法可以获取异步计算的结果,但在获取结果之前可能需要等待计算完成。

在总体上来说,FutureTaskFuture 接口的一个具体实现类,提供了更多的功能和灵活性,可以作为一个可运行的任务来执行,并且可以获取任务执行的结果。而 Future 接口则更多地用于表示一个异步计算的结果,提供了基本的等待和获取结果的方法。

静态代理和动态代理

静态代理

  • 静态代理:静态代理是在编译期间就已经确定代理类的代理方式。在静态代理中,代理类需要实现与目标类相同的接口,并在代理类中持有目标类的实例。代理类在调用目标类方法的前后可以添加额外的逻辑。

示例代码

// 定义接口
interface Subject {
    void request();
}

// 目标类
class RealSubject implements Subject {
    public void request() {
        System.out.println("RealSubject: Processing request");
    }
}

// 代理类
class ProxySubject implements Subject {
    private RealSubject realSubject;

    public ProxySubject(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    public void request() {
        System.out.println("ProxySubject: Pre-processing");
        realSubject.request();
        System.out.println("ProxySubject: Post-processing");
    }
}

public class Main {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        ProxySubject proxySubject = new ProxySubject(realSubject);

        proxySubject.request();
    }
}

动态代理

  • 动态代理:动态代理是在运行时动态生成代理类,无需手动编写代理类。Java中的动态代理主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口实现。动态代理可以代理任意实现了接口的类。

示例代码

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义接口
interface Subject {
    void request();
}

// 目标类
class RealSubject implements Subject {
    public void request() {
        System.out.println("RealSubject: Processing request");
    }
}

// InvocationHandler实现类
class DynamicProxyHandler implements InvocationHandler {
    private Object target;

    public DynamicProxyHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("DynamicProxy: Pre-processing");
        Object result = method.invoke(target, args);
        System.out.println("DynamicProxy: Post-processing");
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        InvocationHandler handler = new DynamicProxyHandler(realSubject);

        Subject proxySubject = (Subject) Proxy.newProxyInstance(
                realSubject.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(),
                handler);

        proxySubject.request();
    }
}

在动态代理的示例中,通过Proxy.newProxyInstance()方法动态生成代理对象,传入目标类的类加载器、目标类实现的接口以及InvocationHandler实现类。在invoke()方法中,可以在调用目标方法前后添加额外的逻辑。

lambda表达式

静态内部类

  • 静态内部类:静态内部类是在外部类中使用 static 修饰的内部类。静态内部类与外部类之间没有直接的依赖关系,可以直接通过外部类名访问静态内部类。静态内部类可以包含静态成员和非静态成员,但不能直接访问外部类的非静态成员。

示例代码

public class Outer {
    private static int outerStaticVar = 10;
    private int outerVar = 20;

    static class StaticInner {
        void innerMethod() {
            System.out.println("Accessing outerStaticVar from static inner class: " + outerStaticVar);
        }
    }

    public static void main(String[] args) {
        StaticInner staticInner = new StaticInner();
        staticInner.innerMethod();
    }
}

局部内部类

  • 局部内部类:局部内部类是定义在方法内部的类,它只在定义它的方法中可见。局部内部类可以访问外部类的成员变量,以及方法中的 final 局部变量。

示例代码

public class Outer {
    private int outerVar = 10;

    public void outerMethod() {
        final int localVar = 20;

        class LocalInner {
            void innerMethod() {
                System.out.println("Accessing outerVar from local inner class: " + outerVar);
                System.out.println("Accessing localVar from local inner class: " + localVar);
            }
        }

        LocalInner localInner = new LocalInner();
        localInner.innerMethod();
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.outerMethod();
    }
}

匿名内部类

  • 匿名内部类:匿名内部类是没有类名的局部内部类,通常用于创建只需要一次使用的类的实例。匿名内部类通常用于实现接口或继承父类,并在创建对象时实现其抽象方法或重写其方法。

示例代码

interface Greeting {
    void greet();
}

public class Outer {
    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            public void greet() {
                System.out.println("Hello, this is an anonymous inner class");
            }
        };

        greeting.greet();
    }
}

Lambda表达式

  • Lambda表达式:Lambda表达式是一种简洁的语法,用于表示一个匿名函数。Lambda表达式可以用来代替只包含一个抽象方法的接口的匿名内部类实例。Lambda表达式的语法形式为:(参数列表) -> {方法体}

示例代码

interface MathOperation {
    int operate(int a, int b);
}

public class Outer {
    public static void main(String[] args) {
        MathOperation addition = (a, b) -> a + b;
        MathOperation subtraction = (a, b) -> a - b;

        System.out.println("10 + 5 = " + addition.operate(10, 5));
        System.out.println("10 - 5 = " + subtraction.operate(10, 5));
    }
}

在上述示例中,additionsubtraction 是使用Lambda表达式创建的函数式接口实例,分别实现了 MathOperation 接口中的 operate 方法。

线程状态

在 Java 中,线程有五种状态,分别是:

  1. 新建状态(New):当线程对象被创建但还未调用 start() 方法时,线程处于新建状态。

  2. 就绪状态(Runnable):当线程调用 start() 方法后,线程进入就绪状态,等待获取 CPU 时间片执行。

  3. 运行状态(Running):当线程获取 CPU 时间片并执行时,线程处于运行状态。

  4. 阻塞状态(Blocked):线程在等待某个条件满足时会进入阻塞状态,如等待 I/O 完成、等待获取锁等。

  5. 终止状态(Terminated):线程执行完任务或者发生异常导致线程终止时,线程进入终止状态。

Java中的线程方法

Java 中常用的线程方法包括:

  • start():启动线程,使线程进入就绪状态等待 CPU 调度执行。

  • run():线程的执行体,包含线程要执行的任务代码。

  • sleep(long millis):使当前线程休眠指定的毫秒数。

  • join():等待线程终止。

  • yield():暂停当前正在执行的线程对象,并执行其他线程。

  • interrupt():中断线程。

  • wait():使当前线程等待,直到其他线程调用 notify()notifyAll() 方法唤醒它。

  • notify():唤醒在当前对象上等待的单个线程。

  • notifyAll():唤醒在当前对象上等待的所有线程。

  • setPriority(int priority):setPriority方法用于设置线程的优先级,优先级范围从1到10,其中1为最低优先级,10为最高优先级。线程的优先级会影响到线程获取 CPU 时间片的概率,但并不是绝对的执行顺序。可以通过调用线程对象的 `setPriority` 方法来设置线程的优先级。

  • isAlive():isAlive 方法用于判断线程是否处于活动状态,即线程是否已经启动且尚未终止。如果线程处于新建状态、终止状态或者尚未启动,`isAlive` 方法会返回 `false`;如果线程处于就绪、运行或阻塞状态,`isAlive` 方法会返回 `true`。可以通过调用线程对象的 `isAlive` 方法来判断线程的活动状态。

在上面的代码中,展示了如何使用 wait()notify()notifyAll() 方法来实现线程间的协作。通过 synchronized 关键字锁定对象,确保线程之间的同步操作。

示例代码

public class ThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread is running: " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动线程
        thread.start();

        // 获取线程状态
        System.out.println("Thread state: " + thread.getState());

        // 等待线程终止
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程终止后的状态
        System.out.println("Thread state after termination: " + thread.getState());
    }
}

在上述示例代码中,创建了一个新的线程并启动,线程会输出 1 到 5 的数字,每隔一秒输出一次。通过 getState() 方法可以获取线程的状态,包括新建状态、就绪状态、运行状态等。使用 join() 方法等待线程终止,然后再次获取线程的状态。

线程停止的方法

在 Java 中,线程的停止是一个重要的问题,需要谨慎处理以避免出现意外情况。在早期的 Java 版本中,提供了 stop()destroy() 方法来停止线程,但这两种方法存在一些问题,因此不推荐使用。

stop() 和 destroy() 方法的问题

  • stop() 方法stop() 方法会立即停止线程的执行,可能导致线程在执行过程中的资源没有得到释放,导致数据不一致或资源泄漏的问题。因此,stop() 方法已被标记为废弃,不推荐使用。

  • destroy() 方法destroy() 方法会立即终止线程,同样存在资源未释放的风险,可能导致系统不稳定。因此,destroy() 方法也已被标记为废弃,不推荐使用。

解决线程停止的方法

为了安全地停止线程,可以采用以下方法:

  1. 使用标识符控制线程:在线程内部使用一个标识符来控制线程的执行,当标识符为 true 时,线程继续执行;当标识符为 false 时,线程停止执行。

class MyThread extends Thread {
    private volatile boolean flag = true;

    public void run() {
        while (flag) {
            // 线程执行的任务
        }
    }

    public void stopThread() {
        flag = false;
    }
}
  1. 使用 interrupt() 方法:调用线程的 interrupt() 方法可以中断线程的执行,线程会收到一个中断信号,可以在适当的时候结束线程的执行。

class MyThread extends Thread {
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // 线程执行的任务
        }
    }
}

// 在外部调用线程的 interrupt() 方法来中断线程
myThread.interrupt();
  1. 使用 join() 方法:可以在主线程中调用子线程的 join() 方法,等待子线程执行完毕后再继续执行主线程。

class MyThread extends Thread {
    public void run() {
        // 线程执行的任务
    }
}

MyThread myThread = new MyThread();
myThread.start();

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

通过以上方法,可以安全地停止线程的执行,避免出现资源泄漏或数据不一致的问题。避免使用废弃的 stop()destroy() 方法,以确保线程的安全性和稳定性。

线程休眠

  • 线程休眠:线程休眠是指让当前正在执行的线程暂停一段时间,让其他线程有机会执行。线程休眠的主要作用是控制线程执行的速度,模拟网络延时、倒计时等场景。

线程休眠方法

Java 中线程休眠的方法是通过 Thread.sleep() 方法实现,该方法接受一个以毫秒为单位的时间参数,表示线程休眠的时间长度。

try {
    Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
    e.printStackTrace();
}

在上述代码中,Thread.sleep(1000) 表示让当前线程休眠1秒。

  • sleep(时间)指定当前线程阻塞的毫秒数

  • sleep存在异常InterruptedException;

  • sleep时间达到后线程进入就绪状态,

  • sleep可以模拟网络延时,倒计时等。

  • 每一个对象都有一个锁,sleep不会释放锁

模拟网络延时

public class NetworkDelaySimulation {
    public static void main(String[] args) {
        System.out.println("开始发送网络请求...");
        
        try {
            Thread.sleep(3000); // 模拟网络延时3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("网络请求完成,数据接收成功!");
    }
}

在上述代码中,通过 Thread.sleep(3000) 模拟了网络请求的延时,延时3秒后输出数据接收成功的信息。

模拟倒计时

public class CountdownTimer {
    public static void main(String[] args) {
        System.out.println("倒计时开始:");
        
        for (int i = 5; i > 0; i--) {
            System.out.println("倒计时:" + i);
            try {
                Thread.sleep(1000); // 每隔1秒打印一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("倒计时结束!");
    }
}

在上述代码中,通过 Thread.sleep(1000) 实现了每隔1秒打印一次倒计时信息,共倒计时5秒。

线程礼让

在线程中,礼让是一种线程间的协作机制,用于让当前正在执行的线程暂停执行,让其他线程有机会执行。通过调用Thread.yield()方法可以让当前线程让出 CPU 时间片,让其他线程有机会执行。

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞

  • 将线程从运行状态转为就绪状态

  • 让cpu重新调度,礼让不一定成功!看CPU心情

示例代码

public class ThreadYieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable(), "Thread 1");
        Thread thread2 = new Thread(new MyRunnable(), "Thread 2");

        thread1.start();
        thread2.start();
    }

    static class MyRunnable implements Runnable {
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " - Count: " + i);

                // 当 i 等于 3 时,线程礼让
                if (i == 3) {
                    System.out.println(Thread.currentThread().getName() + " is yielding...");
                    Thread.yield(); // 线程礼让
                }
            }
        }
    }
}

在上述示例代码中,创建了两个线程Thread 1Thread 2,它们都执行相同的MyRunnable任务。在MyRunnable任务中,当计数器i等于3时,线程会调用Thread.yield()方法进行礼让,让其他线程有机会执行。通过运行该示例代码,可以观察到线程礼让的效果。

线程强制执行join

  • join方法join() 方法是 Thread 类的一个方法,用于让一个线程等待另一个线程执行完成后再继续执行。调用 join() 方法的线程会被阻塞,直到被调用的线程执行完成或超时。

示例代码

public class JoinExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is running");
            try {
                Thread.sleep(2000); // 模拟线程执行耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1 is done");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 is running");
            try {
                Thread.sleep(1000); // 模拟线程执行耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 is done");
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join(); // 让主线程等待 thread1 执行完成
            System.out.println("Thread 1 has completed, main thread continues");
            thread2.join(); // 让主线程等待 thread2 执行完成
            System.out.println("Thread 2 has completed, main thread continues");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main thread is done");
    }
}

在上述示例中,主线程启动了两个子线程 thread1thread2,然后通过 join() 方法强制主线程等待 thread1thread2 执行完成后再继续执行。这样可以确保在主线程继续执行之前,thread1thread2 已经完成了它们的任务。

线程优先级

线程优先级是操作系统调度线程时考虑的一个因素,用于决定线程获取 CPU 时间片的顺序。Java 中的线程优先级范围是 1 到 10,其中 1 是最低优先级,10 是最高优先级。默认情况下,线程的优先级与创建它的父线程的优先级相同。

线程优先级高并不意味着一定会先执行,只是获取 CPU 时间片的概率更高。线程优先级的调整可以通过 setPriority(int priority) 方法来实现。

实例代码

public class PriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable(), "Thread-1");
        Thread thread2 = new Thread(new MyRunnable(), "Thread-2");

        // 设置线程优先级
        thread1.setPriority(Thread.MIN_PRIORITY); // 最低优先级
        thread2.setPriority(Thread.MAX_PRIORITY); // 最高优先级

        thread1.start();
        thread2.start();
    }

    static class MyRunnable implements Runnable {
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " is running, count: " + i);
            }
        }
    }
}

在上面的示例代码中,创建了两个线程 Thread-1Thread-2,分别设置了最低优先级和最高优先级。两个线程执行相同的任务,输出线程名和计数值。由于线程 Thread-2 的优先级更高,它更有可能先执行。但实际上,线程的执行顺序受多种因素影响,包括线程调度器的具体实现、系统负载等。

守护线程

在 Java 中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。守护线程是一种特殊的线程,它的作用是为其他线程提供服务和支持,当所有用户线程结束时,守护线程会自动销毁。守护线程通常用于在后台执行任务,如垃圾回收、JVM 线程等。

特点

  • 守护线程的特点

    • 守护线程是通过设置 setDaemon(true) 方法将线程设置为守护线程。

    • 守护线程的生命周期取决于用户线程的生命周期,即当所有用户线程结束时,守护线程会自动销毁。

    • 守护线程不能持有程序中的资源,如文件、数据库连接等,因为它会在用户线程结束时立即销毁,可能导致资源未正确释放。

    • 守护线程通常用于执行后台任务,如垃圾回收、JVM 线程等。

示例

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        daemonThread.setDaemon(true); // 将线程设置为守护线程
        daemonThread.start();

        System.out.println("Main Thread is running");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Main Thread ends, Daemon Thread will be terminated");
    }
}

在上面的示例中,创建了一个守护线程 daemonThread,并将其设置为守护线程。守护线程会在主线程结束后自动销毁。在程序运行时,守护线程会每隔一秒输出一次信息,而主线程会在运行5秒后结束,此时守护线程也会被终止。

通过设置守护线程,可以在程序运行时执行一些后台任务,而无需手动管理线程的生命周期。

线程同步

在多线程环境下,多个线程同时访问共享资源可能会导致数据不一致的问题,为了避免这种情况,需要进行线程同步。线程同步是通过锁机制来保证在同一时刻只有一个线程可以访问共享资源,从而确保数据的一致性。

线程同步的方式

1. synchronized关键字

synchronized 关键字可以修饰方法或代码块,确保同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法。当一个线程进入 synchronized 代码块或方法时,会尝试获取对象的锁,其他线程在此时会被阻塞,直到获取到锁才能执行。

synchronized锁的是变化的量才有用。

同步块

同步块:synchronized (Obj){}

Obj 称之为 同步监视器

◆ Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器

◆同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是 class

同步监视器的执行过程

1.第一个线程访问,锁定同步监视器,执行其中代码

2.第二个线程访问,发现同步监视器被锁定,无法访问

3.第一个线程访问完毕,解锁同步监视器

4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问

示例代码

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

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

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

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}

2. ReentrantLock

ReentrantLock 是 Java 中显示锁的一种实现,通过 lock()unlock() 方法来控制锁的获取和释放。与 synchronized 相比,ReentrantLock 提供了更多的灵活性,如可中断锁、超时锁等。

示例代码

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

public class ReentrantLockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

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

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

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}

线程同步的实现原理

线程同步的实现原理是通过锁机制来保护共享资源,确保在同一时刻只有一个线程可以访问共享资源。当一个线程获取到锁时,其他线程会被阻塞,直到获取到锁的线程释放锁。

synchronized 中,锁的获取和释放是隐式的,由 JVM 自动管理。而在 ReentrantLock 中,锁的获取和释放是显式的,需要手动调用 lock()unlock() 方法。

通过合理地使用线程同步机制,可以确保多线程环境下的数据一致性和线程安全。

arraylist为什么线程不安全

ArrayList 是 Java 中的一个动态数组,它提供了一种可以动态增长和缩减的数组结构。然而,ArrayList 是线程不安全的,这是因为它不是同步的(non-synchronized)。

在多线程环境下,如果多个线程同时对 ArrayList 进行添加、删除等操作,可能会导致以下问题:

  1. 不确定的迭代结果:当一个线程在迭代 ArrayList 的同时,另一个线程对 ArrayList 进行结构上的修改(添加、删除等操作),可能会导致迭代器抛出 ConcurrentModificationException 异常,或者产生不确定的迭代结果。

  2. 数据不一致:当多个线程同时对 ArrayList 进行添加、删除等操作时,可能会导致数据不一致的问题,例如丢失数据、重复数据等。

为了解决 ArrayList 线程不安全的问题,可以使用以下方法之一:

  1. 使用 Collections.synchronizedList() 方法:可以通过 Collections.synchronizedList() 方法将 ArrayList 转换为线程安全的 List。例如:List list = Collections.synchronizedList(new ArrayList());

  2. 使用 CopyOnWriteArrayList:CopyOnWriteArrayList 是 Java 并发包中提供的线程安全的 List 实现,它通过在写操作时复制整个数组来实现线程安全,因此适合读多写少的场景。

  3. 使用并发集合类:Java 并发包中还提供了其他线程安全的集合类,如 ConcurrentHashMap、ConcurrentLinkedQueue 等,可以根据具体需求选择合适的线程安全集合类。

死锁

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有线程都无法继续执行。死锁通常发生在多个线程同时持有对方需要的资源时,导致彼此无法释放资源,从而陷入僵局。

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题

死锁产生的条件

死锁产生通常需要满足以下四个条件,称为死锁的必要条件:

  1. 互斥条件:线程对所需的资源进行排他性控制,即一次只能有一个线程使用资源。

  2. 请求与保持条件:线程持有至少一个资源,并且在等待获取其他线程持有的资源。

  3. 不剥夺条件:线程已经获得的资源在未使用完之前不能被其他线程抢占,只能由自己释放。

  4. 循环等待条件:存在一个等待循环,即若干线程之间形成一种头尾相接的循环等待资源的关系。

死锁实例代码

下面是一个简单的 Java 代码示例,演示了死锁的情况。在该示例中,两个线程分别需要获取对方持有的资源,由于互相等待对方释放资源而导致死锁。

public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for resource 2");
                synchronized (resource2) {
                    System.out.println("Thread 1: Holding resource 1 and resource 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for resource 1");
                synchronized (resource1) {
                    System.out.println("Thread 2: Holding resource 2 and resource 1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述示例中,thread1 线程首先获取 resource1,然后尝试获取 resource2;而 thread2 线程首先获取 resource2,然后尝试获取 resource1。由于两个线程互相持有对方需要的资源,因此会发生死锁。

避免死锁的方法

避免死锁的方法包括:

  1. 破坏互斥条件:允许多个线程同时访问资源,如使用读写锁。

  2. 破坏请求与保持条件:一次性申请所有需要的资源,或者按照固定的顺序申请资源。

  3. 破坏不剥夺条件:当线程无法获取资源时,释放已经持有的资源,等待一段时间后再次尝试获取资源。

  4. 破坏循环等待条件:对资源进行排序,按照顺序申请资源,避免循环等待。

通过合理设计和编程,可以避免死锁的发生。

Lock 锁

  • Lock 锁:Lock 锁是 Java 中用于控制多线程并发访问的一种机制,相比于传统的 synchronized 关键字,Lock 锁提供了更灵活、更强大的线程操作功能。Lock 锁的常用实现类包括 ReentrantLock、ReentrantReadWriteLock 等。

ReentrantLock

  • ReentrantLock:ReentrantLock 是 Lock 接口的一个实现类,它提供了与 synchronized 关键字类似的同步功能,但更加灵活。

实例代码

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

public class LockExample {
    private static int counter = 0;
    private static Lock lock = new ReentrantLock();

    public static void increment() {
        
        try {
            lock.lock(); // 获取锁
            counter++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                increment();
            }
        });

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

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Counter: " + counter);
    }
}

在上述示例中,通过 ReentrantLock 实现了一个简单的计数器。在 increment() 方法中,首先通过 lock() 方法获取锁,然后在 try-finally 块中执行需要同步的操作,最后通过 unlock() 方法释放锁。这样可以确保在多线程环境下对共享资源的安全访问。

通过 Lock 锁的机制,可以更加灵活地控制线程的同步和互斥访问,避免出现死锁等问题。

生产者消费者问题

生产者消费者问题是一个经典的多线程同步问题,涉及到生产者向共享资源中生产数据,消费者从共享资源中消费数据的场景。为了避免生产者和消费者之间的竞争条件和数据不一致问题,需要合理地进行线程同步和互斥操作。

实现方式

  1. 共享资源:生产者和消费者共享的资源,如一个队列。

  2. 生产者:向共享资源中生产数据的线程。

  3. 消费者:从共享资源中消费数据的线程。

  4. 互斥锁:用于保护共享资源,防止多个线程同时访问。

  5. 条件变量:用于线程间的通信,当共享资源为空或已满时,生产者和消费者需要等待或唤醒对方。

实例代码

import java.util.LinkedList;

public class ProducerConsumer {

    public static void main(String[] args) {
        Buffer buffer = new Buffer(5); // 创建共享资源,设置缓冲区大小为5

        Thread producerThread = new Thread(new Producer(buffer));
        Thread consumerThread = new Thread(new Consumer(buffer));

        producerThread.start();
        consumerThread.start();
    }

    static class Buffer {
        private LinkedList<Integer> queue;
        private int maxSize;

        public Buffer(int maxSize) {
            this.queue = new LinkedList<>();
            this.maxSize = maxSize;
        }

        public synchronized void produce(int item) throws InterruptedException {
            while (queue.size() == maxSize) {
                wait(); // 等待消费者消费
            }
            queue.add(item);
            System.out.println("Produced: " + item);
            notify(); // 唤醒消费者
        }

        public synchronized int consume() throws InterruptedException {
            while (queue.isEmpty()) {
                wait(); // 等待生产者生产
            }
            int item = queue.remove();
            System.out.println("Consumed: " + item);
            notify(); // 唤醒生产者
            return item;
        }
    }

    static class Producer implements Runnable {
        private Buffer buffer;

        public Producer(Buffer buffer) {
            this.buffer = buffer;
        }

        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    buffer.produce(i);
                    Thread.sleep((int) (Math.random() * 100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        private Buffer buffer;

        public Consumer(Buffer buffer) {
            this.buffer = buffer;
        }

        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    buffer.consume();
                    Thread.sleep((int) (Math.random() * 100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在上述代码中,Buffer 类表示共享资源,Producer 类表示生产者,Consumer 类表示消费者。生产者向缓冲区中生产数据,消费者从缓冲区中消费数据。通过synchronized关键字实现对共享资源的互斥访问,通过wait()notify()方法实现线程间的等待和唤醒。

管程法实现生产者消费者问题

管程法是一种解决并发编程中生产者消费者问题的经典方法,其中使用一个共享的缓冲区作为生产者和消费者之间的通信桥梁。下面是一个使用管程法实现生产者消费者问题的示例代码:

import java.util.LinkedList;

class Buffer {
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int capacity;

    public Buffer(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void produce(int item) {
        while (buffer.size() == capacity) {
            try {
                wait(); // 缓冲区已满,生产者等待
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        buffer.add(item);
        System.out.println("Produced: " + item);
        notifyAll(); // 通知消费者可以消费
    }

    public synchronized int consume() {
        while (buffer.isEmpty()) {
            try {
                wait(); // 缓冲区为空,消费者等待
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        int item = buffer.remove();
        System.out.println("Consumed: " + item);
        notifyAll(); // 通知生产者可以生产
        return item;
    }
}

class Producer implements Runnable {
    private Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            buffer.produce(i);
            try {
                Thread.sleep((int) (Math.random() * 100));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

class Consumer implements Runnable {
    private Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            buffer.consume();
            try {
                Thread.sleep((int) (Math.random() * 100));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(5);
        Producer producer = new Producer(buffer);
        Consumer consumer = new Consumer(buffer);

        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);

        producerThread.start();
        consumerThread.start();
    }
}

在上述示例中,Buffer 类表示共享的缓冲区,其中包含了生产者生产和消费者消费的方法。生产者通过调用 produce() 方法向缓冲区中生产物品,消费者通过调用 consume() 方法从缓冲区中消费物品。生产者和消费者在缓冲区满或空时会进入等待状态,并通过 wait()notifyAll() 方法进行线程间的通信。

Producer 类和 Consumer 类分别表示生产者和消费者,它们实现了 Runnable 接口,通过调用 Buffer 类的方法来生产和消费物品。

Main 类中创建了一个共享的 Buffer 对象,并分别创建了生产者和消费者线程,启动这两个线程后,生产者和消费者会交替进行生产和消费操作。

使用信号灯法实现生产者消费者问题

Java实现

下面是一个使用信号灯法实现生产者消费者问题的Java示例代码:

// 信号灯
    static class Signallamp{
        boolean flag = true; // 绿灯

        // 绿灯: 车走,人停
        public synchronized void CarGO(){
            // 不是绿灯,则停止等到人走
            if(!flag){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 否则车走了,变成红灯,通只人行走
            System.out.println(Thread.currentThread().getName()+":现在是"+flag+"灯:车子行驶中");
            System.out.println("变灯了");
            flag = !flag;
            notify();
        }


        // 红灯: 车停了,人走
        public synchronized void ManGO(){
            // 绿灯,则车走(司机角度是绿灯,自己角度真实应该是红灯)
            if(flag){
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 否则人走了,变成绿灯,通知车走
            System.out.println(Thread.currentThread().getName()+":现在是"+flag+"灯:人在走路");
            System.out.println("变灯了");
            flag = !flag;
            notify();
        }
    }
   // 车
    static class Car implements  Runnable{
        Signallamp signallamp;
        public Car(Signallamp signallamp){
            this.signallamp = signallamp;
        }


        // 车在行走
        public void run() {
            for (int i = 0; i <20 ; i++) {
                signallamp.CarGO();
            }
        }
    }

  // 人
    static class Man implements  Runnable{
        Signallamp signallamp;
        public Man(Signallamp signallamp){
            this.signallamp = signallamp;
        }

        // 人在行走
        public void run() {
            for (int i = 0; i <20 ; i++) {
                signallamp.ManGO();
            }
        }
    }
   public static void main(String[] args) {
        Signallamp signallamp = new Signallamp();
        new Thread(new Car(signallamp)).start();
        new Thread(new Man(signallamp)).start();
    }
 

线程池

  • 线程池:线程池是一种管理和复用线程的机制,它可以减少线程创建和销毁的开销,提高程序的性能和响应速度。线程池中包含一定数量的线程,可以根据需要执行任务,并在任务执行完毕后将线程放回线程池中以供复用。

Java中的线程池

在 Java 中,线程池由 java.util.concurrent 包提供支持,常用的线程池实现类是 ThreadPoolExecutor。以下是线程池的一些重要参数:

  • corePoolSize:线程池的核心线程数,即线程池中保持存活的线程数量。

  • maximumPoolSize:线程池的最大线程数,即线程池中允许的最大线程数量。

  • keepAliveTime:线程空闲时的存活时间,超过该时间的空闲线程会被回收。

  • workQueue:用于存放等待执行的任务的阻塞队列。

  • ThreadFactory:用于创建新线程的工厂。

示例代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池,包含3个线程
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交任务给线程池
        for (int i = 1; i <= 5; i++) {
            final int task = i;
            executor.submit(() -> {
                System.out.println("Task " + task + " is running on thread: " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在上述示例中,通过 Executors.newFixedThreadPool(3) 创建了一个固定大小为3的线程池。然后通过 executor.submit() 方法提交了5个任务给线程池执行。每个任务输出了当前任务编号和执行线程的名称。最后调用 executor.shutdown() 关闭线程池。

通过线程池,可以有效管理线程的生命周期,提高程序的性能和效率。