多线程(2)

1.线程(Thread)

是程序执行流的最小单元。在多线程编程中,一个进程可以包含多个并发执行的线程,每个线程都是独立的、能够独立运行的子任务。

主要特点和概念:
并发执行:多线程使得多个任务可以同时执行,提高了程序的效率和资源利用率。

独立性:每个线程拥有自己的程序计数器(Program Counter)、栈(Stack)、寄存器集合等,线程之间不会直接干扰彼此的状态信息。

共享资源:多线程可以共享同一个进程的资源,如堆内存和静态方法。

线程调度:线程的执行是由操作系统的调度算法决定的,程序员无法精确控制线程的执行顺序。

Java 中的线程:
在Java中,线程是通过 java.lang.Thread 类来实现和管理的。创建线程可以通过以下几种方式:

继承 Thread 类:创建一个类继承 Thread,并重写其 run() 方法来定义线程的执行逻辑。

public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建线程实例并启动

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


实现 Runnable 接口:定义一个实现了 Runnable 接口的类,实现其 run() 方法,然后将其传递给 Thread 类的构造方法创建线程。

public class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建线程实例并启动

Thread thread = new Thread(new MyRunnable());
thread.start();


Java 提供了丰富的线程控制和管理方法,包括线程的状态监控、线程间的通信(如 wait() 和 notify())、线程池等功能,帮助开发者更好地利用多线程实现复杂的并发任务。

2.Thread 类

位于 java.lang 包中,是用来创建和操作线程的基本类之一。通过 Thread 类,可以方便地创建新线程并控制其执行。下面是关于 Thread 类的一些重要信息和用法:

创建线程的方式:
继承 Thread 类:

创建一个新的类,继承自 Thread 类。
在新类中重写 run() 方法,该方法定义了线程的执行逻辑。
通过创建该类的实例并调用 start() 方法来启动线程。

public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建线程实例并启动

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


实现 Runnable 接口:

创建一个类实现 Runnable 接口,并重写其 run() 方法。
将实现了 Runnable 接口的类实例作为参数传递给 Thread 类的构造方法。
同样通过调用 start() 方法启动线程。
 

public class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码逻辑
    }
}

// 创建线程实例并启动
Thread thread = new Thread(new MyRunnable());
thread.start();


(1)Thread 类的常用方法:


①start():

 start() 方法是 Thread 类中用于启动一个新线程的方法。具体作用如下:

启动线程:调用 start() 方法后,系统会安排该线程运行,并调用该线程的 run() 方法。
进入就绪状态:线程在调用 start() 方法后,会进入就绪状态,等待系统的调度,而不是立即执行 run() 方法。
线程生命周期:start() 方法调用后,系统会为该线程分配必要的资源,然后将其状态设置为可运行,直到线程真正执行完毕或终止。

②run():

run() 方法是 Thread 类的一个重要方法,包含了线程的实际运行逻辑。具体作用如下:线程任务逻辑:run() 方法定义了线程要执行的任务或操作。当 start() 方法被调用后,系统会自动调用该线程对象的 run() 方法来执行具体的任务代码。
线程执行完毕:run() 方法的执行代表着线程的主要活动。当 run() 方法执行完毕或线程代码执行完毕时,线程就会结束,资源被释放。

区别和用法
调用方式:应当直接调用 start() 方法来启动线程,而不是直接调用 run() 方法。直接调用 run() 方法会导致线程在当前线程下同步执行,不会创建新线程。
并发执行:start() 方法启动线程后,线程会并发执行,而 run() 方法直接调用则是在当前线程同步执行。
生命周期管理:start() 方法触发了线程的完整生命周期,包括资源分配、线程调度等;而 run() 方法仅是线程的执行体。
示例代码如下:

public class MyThread extends Thread {
    public void run() {
        // 线程执行的任务逻辑
        System.out.println("Thread running");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程,进入就绪状态,等待调度执行
    }
}

在这个例子中,start() 方法启动了一个新线程,并且线程的执行体逻辑定义在 run() 方法中。

总结来说,start() 方法启动了一个新线程,并使其进入就绪状态等待调度执行;run() 方法则定义了线程的主体执行逻辑。

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

④join():等待该线程终止。

在Java中,join()方法是Thread类的一个实例方法,用于让当前线程等待调用join()方法的线程执行完毕。使用方法
java

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

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 started");
            try {
                Thread.sleep(3000); // 模拟线程执行时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 finished");
        });

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

        try {
            thread1.join(); // 当前线程(主线程)等待线程1执行完毕
            System.out.println("Thread 1 has finished, now executing main thread");
            thread2.join(); // 当前线程(主线程)等待线程2执行完毕
            System.out.println("Thread 2 has finished, now executing main thread");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All threads have finished");
    }
}

解释与示例


创建线程对象:在main方法中创建了两个线程 thread1 和 thread2,它们分别执行一些任务(这里通过睡眠来模拟耗时操作)。

启动线程:使用 thread1.start() 和 thread2.start() 启动线程。

使用join()方法:

thread1.join():主线程调用了thread1.join()方法,这会使主线程等待,直到thread1 执行完毕后才继续执行。
thread2.join():主线程再调用thread2.join()方法,同样会等待thread2 执行完毕后才继续执行。
打印输出:在 join() 方法返回后,主线程会继续执行后续代码,例如打印 "All threads have finished"。

注意事项:

join()方法会使得当前线程进入阻塞状态,直到被调用的线程执行完毕或超时。
如果调用了 join() 方法但是被等待的线程已经执行完毕,join() 方法会立即返回。
join()方法常用于多线程场景中,用于等待其他线程的执行结果或确保多个线程任务的顺序执行。

interrupt():中断线程。
isAlive():判断线程是否还活跃(即线程是否已启动且尚未终止)。
getName() 和 setName():获取和设置线程的名称。
线程生命周期:
Java 线程具有不同的生命周期,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)和终止(Terminated)等状态,通过 Thread 类及其方法可以操作和管理线程的状态转换和执行过程。

阻塞状态(Blocking State)

指的是线程因某些条件而暂时无法继续执行的状态。线程在阻塞状态下会暂时停止运行,并且不会占用 CPU 时间,直到某些条件满足后才能继续执行。

常见的阻塞状态及其条件
等待阻塞(Wait Blocking):

线程执行了wait()方法,使得线程进入等待状态,直到被唤醒(通过notify()或notifyAll())或超时。
线程在等待某个对象锁时,也可能进入等待阻塞状态。
同步阻塞(Synchronized Blocking):

线程在等待获取对象的同步锁时,如果未能获取到(即锁被其他线程占用),就会进入同步阻塞状态。
睡眠阻塞(Sleep Blocking):

线程执行了Thread.sleep()方法,使得线程进入睡眠状态,暂时停止执行一段时间后自动唤醒。
睡眠阻塞是一种时间上的阻塞,即线程在指定时间内不会执行任务。
I/O阻塞(I/O Blocking):

线程在执行输入输出操作时,如果遇到了无法立即完成的情况(例如网络通信、文件读写等),会进入I/O阻塞状态,直到I/O操作完成或超时。
条件阻塞(Condition Blocking):

在使用显式的条件变量(Condition)进行线程间通信时,线程可能因等待某个条件成立而进入阻塞状态,直到条件被满足。
解除阻塞
等待阻塞:调用notify()或notifyAll()方法唤醒等待中的线程,或者等待超时后自动恢复。
同步阻塞:等待获取对象锁成功后,线程可以继续执行同步代码块。
睡眠阻塞:指定的睡眠时间结束后,线程自动唤醒并继续执行。
I/O阻塞:I/O操作完成后,线程可以继续处理接下来的任务。
条件阻塞:条件成立后,线程被唤醒并可以继续执行后续代码。
阻塞状态的管理
在编写多线程程序时,合理管理阻塞状态是确保线程安全和程序效率的关键。合理使用同步机制、合理设置睡眠时间、及时处理I/O操作和条件变量都是保证线程能够正确阻塞和解除阻塞的重要策略。

线程安全性和注意事项:
线程之间共享数据时需要注意线程安全性,可以通过同步(synchronization)和锁(Lock)来保证数据的正确性和一致性。
避免过多的线程创建和销毁,可以使用线程池来管理线程以提高性能和资源利用率。
总结来说,Thread 类提供了一种简单而强大的方式来实现多线程编程,是Java并发编程的基础之一。通过合理使用 Thread 类及其方法,可以实现复杂的并发任务和优化程序性能。

3.栈(Stack)

是一种数据结构,具有特定的操作规则:后进先出(Last In, First Out,LIFO)。这意味着最后压入栈的元素将第一个被弹出。栈通常用于存储需要按顺序处理的数据,例如函数调用的执行过程、表达式求值等场景。

主要特点和操作:
压栈(Push):将数据压入栈顶。新的元素成为栈顶,之前的栈顶元素称为次栈顶元素。

弹栈(Pop):从栈顶移除元素。被移除的元素是最后压入栈的元素。

栈顶(Top):访问或者操作栈顶元素,但不移除它。

空栈检测:判断栈中是否有元素。

栈的大小:记录栈中元素的数量。

应用场景:
函数调用栈:在程序执行时,每次函数调用都会将调用信息(如参数、返回地址等)压入栈中,函数执行完成后再从栈中弹出。

表达式求值:使用栈来处理中缀表达式转换为后缀表达式(逆波兰表达式),然后利用栈进行后缀表达式的计算。

递归算法:递归函数本质上也是通过栈来实现的,每次递归调用都会创建一个新的栈帧。

在计算机内存中的实现:
栈通常被实现为一块连续的内存区域,操作系统和编程语言都会提供栈的支持和管理。每个线程通常都会拥有自己的栈空间,用来存储局部变量、函数参数和返回地址等信息。栈的大小通常有限制,超出限制会导致栈溢出错误。

在Java中,每个线程都有一个与之关联的调用栈,用于方法调用和局部变量存储。当一个方法被调用时,其参数和局部变量会被压入栈中,方法执行完成后,这些数据会被弹出。

4.wait() 和 notify()

 Java 中用于线程间通信的重要方法,它们通常与 synchronized 关键字一起使用,用于实现线程之间的协作和同步。

wait() 方法
wait() 方法使当前线程进入等待状态,直到其他线程调用相同对象上的 notify() 或 notifyAll() 方法唤醒该线程,或者指定的等待时间已经过去。
主要用法和特点:
在 synchronized 块或方法中使用:

wait() 方法只能在同步方法或同步块中调用,即在已经获得对象的锁的情况下调用。

synchronized (obj) {
    while (condition) {
        obj.wait(); // 当前线程等待,并释放对象锁
    }
}
释放对象锁:

调用 wait() 方法后,当前线程释放对象锁,并处于等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法,或者等待时间结束。
线程被唤醒:

当其他线程调用相同对象上的 notify() 或 notifyAll() 方法时,处于等待状态的线程才有机会被唤醒,重新竞争对象锁。
超时等待:

可以使用带有超时参数的 wait(long timeout) 方法,指定等待时间,超过这个时间线程会自动唤醒。
notify() 和 notifyAll() 方法
notify() 方法用于唤醒在相同对象上调用 wait() 方法进入等待状态的一个线程(具体唤醒哪个线程不确定,取决于线程调度器)。
notifyAll() 方法用于唤醒在相同对象上调用 wait() 方法进入等待状态的所有线程。
特点和使用场景:
在 synchronized 块或方法中使用:

notify() 和 notifyAll() 方法也必须在已经获得对象锁的情况下调用。
java

synchronized (obj) {
    // 修改共享变量状态
    obj.notify(); // 唤醒一个等待的线程
    // 或者使用 obj.notifyAll(); 唤醒所有等待的线程
}

唤醒等待线程:

调用 notify() 将唤醒等待队列中的一个线程(具体哪个取决于线程调度器),而 notifyAll() 将唤醒所有等待的线程。
避免竞争条件:

使用 notifyAll() 可以确保所有等待线程都有机会重新竞争对象锁,避免因为竞争条件而导致程序逻辑错误。
注意事项:
使用 wait()、notify() 和 notifyAll() 方法时,必须在同步块或同步方法中,否则会抛出 IllegalMonitorStateException 异常。
使用 wait() 方法时,通常与一个条件(condition)结合使用,例如在生产者-消费者模式中,当条件不满足时,生产者线程调用 wait() 进入等待状态,等待消费者线程处理完数据后调用 notify() 或 notifyAll() 唤醒生产者线程。
这些方法提供了强大的线程协作能力,可以实现复杂的线程间通信和同步。

5.synchronized 关键字

synchronized 是 Java 中的关键字,用于实现线程之间的同步和互斥访问。它可以应用于方法或代码块,作用是确保同一时刻只有一个线程可以执行被 synchronized 保护的代码段,从而避免多个线程同时访问和修改共享数据导致的数据不一致或其他并发问题。

使用方式
1. 同步方法

public synchronized void synchronizedMethod() {
    // 同步代码
}


当一个线程进入 synchronizedMethod() 方法时,会尝试获取当前实例(对象)的锁。
如果锁已被其他线程获取,则该线程将被阻塞,直到获取到锁为止。
2. 同步代码块
 

public void someMethod() {
    synchronized (this) {
        // 同步代码块
    }
}


synchronized 关键字还可以应用于代码块,指定一个对象作为锁。
当一个线程执行到 synchronized 代码块时,它会尝试获取指定对象(这里是 this)的锁。
特点和作用
互斥性:

synchronized 确保同一时刻只有一个线程可以执行同步方法或同步代码块中的代码。这样可以避免多个线程同时修改共享数据导致的数据不一致问题。
可重入性:

Java 中的 synchronized 是可重入的,即同一个线程可以多次获取同一个锁,而不会发生死锁。
保证可见性:

进入 synchronized 代码块会使得线程从主内存中读取共享变量,保证线程看到最新的值。
原子性:

单个 synchronized 方法或代码块内的操作是原子的,不会被其他线程中断,从而确保复合操作的完整性。
示例
考虑一个简单的银行账户类:

java
public class BankAccount {
    private int balance;

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }


}
在 deposit 和 withdraw 方法上使用 synchronized,确保了对账户余额的修改操作是原子的,并且只有一个线程可以执行这些方法。

synchronized 是 Java 中用于实现线程安全的基本机制之一,通过锁的获取和释放来控制多个线程对共享资源的访问。虽然在 Java 5 中引入了更灵活和高级的并发工具(如 java.util.concurrent 包),但 synchronized 仍然是保证线程安全的重要手段之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值