什么是线程
线程(Thread)是计算机科学中的一个重要概念,指的是在单个程序内部同时执行的一条独立的指令序列。简而言之,线程就是在一个进程内部并发执行的一段代码。每个线程都有自己的执行路径,可以独立地执行代码,访问内存和资源。
在操作系统中,一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。相比于多个进程之间的通信和同步机制复杂度高,线程之间的通信和同步相对简单,因为它们可以直接访问共享的内存空间。
线程如何使用
创建线程并启动:
public class MyThread extends Thread {public void run() {System.out.println("线程执行中");}public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start(); // 启动线程}
}
线程休眠(sleep):
public class SleepExample {public static void main(String[] args) {System.out.println("开始");try {Thread.sleep(2000); // 休眠2秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("结束");}
}
线程等待:
public class WaitExample {public static void main(String[] args) {final Object lock = new Object();Thread thread1 = new Thread(() -> {synchronized (lock) {System.out.println("线程1开始等待");try {lock.wait(); // 线程1等待} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1被唤醒");}});Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("线程2开始唤醒");lock.notify(); // 唤醒等待的线程}});thread1.start();thread2.start();}
}
线程中断
public class InterruptExample {public static void main(String[] args) {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("线程执行中");}System.out.println("线程被中断");});thread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt(); // 中断线程}
}
继承 Thread类 和 实现Runnable接口的区别
继承Thread类:
- 当一个类继承Thread类时,该类就成为一个线程类,直接重写run()方法来定义线程执行的逻辑。
- 缺点是Java不支持多重继承,如果继承了Thread类,就无法再继承其他类。
- 适用于简单的线程逻辑,不需要共享数据。
public class MyThread extends Thread {public void run() {System.out.println("线程执行中");}public static void main(String[] args) {MyThread myThread = new MyThread();myThread.start(); // 启动线程}
}
实现Runnable接口:
- 当一个类实现Runnable接口时,可以在这个类中实现线程执行的逻辑,在run()方法中定义线程的行为。
- 可以避免Java单继承的限制,因为一个类可以实现多个接口。
- 适用于需要共享数据或资源的多线程场
public class MyRunnable implements Runnable {public void run() {System.out.println("线程执行中");}public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);thread.start(); // 启动线程}
}
线程的生命周期
新建(New):当一个线程对象被创建但还没有开始运行时,处于新建状态。
运行(Runnable):通过调用 start() 方法,线程进入可运行状态。此时线程可能正在运行,也可能正在等待系统资源。
阻塞(Blocked):线程在某些条件下会进入阻塞状态,比如等待I/O操作完成或者获取锁资源。
无限期等待(Waiting):线程进入无限期等待状态时,它会一直等待直到其他线程通知或中断它。
限期等待(Timed Waiting):线程在限期等待状态下会等待一定的时间,超时后会自动恢复到可运行状态。
终止(Terminated):线程执行完任务或者出现异常导致线程终止,进入终止状态。
线程的同步
线程同步是指多个线程之间协调它们的操作顺序,以确保数据的一致性和避免竞争条件。在多线程编程中,如果多个线程同时访问共享资源,就会出现竞争条件,可能导致数据不一致或者错误的结果。因此,需要使用同步机制来确保线程安全。
常见的线程同步机制包括:
互斥锁(Mutex):通过对共享资源加锁的方式,保证同一时间只有一个线程能够访问共享资源,其他线程需要等待锁的释放。
信号量(Semaphore):用于控制同时访问共享资源的线程数量,可以设置一个计数器来限制资源的访问。
条件变量(Condition Variable):用于线程间的通信和协调,允许线程等待某个条件的发生。
读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但在有写操作时需要互斥排斥访问。
临界区(Critical Section):用于限制对共享资源的访问,确保在同一时间只有一个线程可以执行临界区代码
线程如何同步
synchronized关键字:通过在方法中使用synchronized关键字或者synchronized代码块,可以确保多个线程对共享资源的访问是同步的。当一个线程获取了对象的锁之后,其他线程必须等待该线程释放锁才能继续执行。
public synchronized void synchronizedMethod() {// 同步方法
}
// 或者
public void someMethod() {synchronized(this) {// 同步代码块}
}
Lock接口: Java中的java.util.concurrent.locks包提供了Lock接口及其实现类,如ReentrantLock。与synchronized不同,Lock接口提供了更灵活的锁定机制,包括可重入锁、公平锁等。
Lock lock = new ReentrantLock();lock.lock();
try {// 同步代码块
} finally {lock.unlock();
}
volatile关键字:在Java中,volatile关键字用于声明变量,保证了变量的可见性,并禁止指令重排序优化。尽管volatile关键字不能取代锁机制,但它可以在特定情况下用来确保共享变量的同步访问。
private volatile boolean flag = false;
Synchronize
使用 synchronized 关键字来实现线程同步,确保多个线程访问共享资源时的安全性。通过在方法上或代码块中添加
synchronized 关键字,可以将方法或代码块标记为同步的,这样只有一个线程能够访问该方法或代码块,其他线程需要等待
public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}public static void main(String[] args) {SynchronizedExample syncExample = new SynchronizedExample();// 创建两个线程分别对 count 执行增加操作Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {syncExample.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {syncExample.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final Count: " + syncExample.getCount());}public synchronized int getCount() {return count;}
}
synchronized 关键字:
特点:synchronized 是 Java 语言提供的内置机制,可以用来实现线程同步,通过对代码块或方法加锁来确保同一时间只有一个线程可以执行该代码块或方法。
优点:简单易用,不需要显式地创建锁对象;可以确保每次只有一个线程访问共享资源。
缺点:灵活性较差,只能实现基本的互斥同步,不能支持尝试获取锁、超时等特性。
ReentrantLock 类:
特点:ReentrantLock 是 Lock 接口的实现类,提供了比 synchronized 更灵活的锁定机制,可以支持可重入锁、公平性设置、尝试获取锁、超时获取锁等功能。
优点:比 synchronized 更灵活,可以支持更多高级特性;可以避免死锁情况。
缺点:使用复杂一些,需要手动释放锁,并且需要注意异常处理。
AtomicInteger 类:
特点:AtomicInteger 是 java.util.concurrent.atomic 包下的原子操作类,提供了一种线程安全的整数类型,支持原子性的增减操作。
优点:适用于简单的原子操作,不需要显式加锁,性能较好。
缺点:只适用于特定类型的原子操作,不适用于复杂的同步场景。
互斥锁
互斥锁(Mutex
Lock)是一种用于确保在多线程环境中不会同时执行特定代码段的同步机制。当一个线程获得了互斥锁时,其他线程就无法再获取该锁,直到持有锁的线程释放它为止。
一般情况下,使用互斥锁的基本流程如下:
当一个线程希望访问共享资源时,它尝试获取互斥锁。
如果互斥锁已经被其他线程持有,则当前线程将被阻塞,直到互斥锁被释放。
一旦获取了互斥锁,线程就可以安全地访问共享资源。
当线程不再需要访问共享资源时,它会释放互斥锁,以便其他线程可以获取并访问共享资源。
当使用synchronized关键字或ReentrantLock类时,都可以实现互斥锁
使用synchronized关键字实现互斥锁:
public class SynchronizedMutexExample {private static int counter = 0;public static void main(String[] args) {Object lock = new Object(); // 创建一个对象作为锁Thread thread1 = new Thread(() -> {synchronized (lock) { // 使用lock对象进行同步for (int i = 0; i < 1000; i++) {counter++;}}});Thread thread2 = new Thread(() -> {synchronized (lock) { // 使用相同的lock对象进行同步for (int i = 0; i < 1000; i++) {counter++;}}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter);}
}
ps: 在使用synchronized关键字时,当线程执行完同步代码块或同步方法后,会自动释放对象锁。在synchronized块或方法结束时,系统会自动释放锁,不需要显式调用解锁的操作,这是synchronized关键字的特性之一。
使用ReentrantLock类实现互斥锁:
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockMutexExample {private static int counter = 0;private static ReentrantLock lock = new ReentrantLock(); // 创建ReentrantLock对象作为锁public static void main(String[] args) {Thread thread1 = new Thread(() -> {lock.lock(); // 获取锁try {for (int i = 0; i < 1000; i++) {counter++;}} finally {lock.unlock(); // 释放锁}});Thread thread2 = new Thread(() -> {lock.lock(); // 获取锁try {for (int i = 0; i < 1000; i++) {counter++;}} finally {lock.unlock(); // 释放锁}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Counter: " + counter);}
}
线程死锁
如果不释放锁,将导致其他线程无法获取该锁,从而造成死锁。当一个线程持有锁并且不释放锁时,其他线程就无法进入同步代码块或方法,这可能会导致所有线程都在等待获取锁而无法继续执行,最终导致程序被阻塞。
死锁是多线程编程中常见的问题,应该尽量避免。因此,一定要确保在合适的时机释放锁,以允许其他线程获得锁并执行同步代码块,从而避免死锁情况的发生。
下面我们模拟一下线程死锁:
public class DeadlockExample {private static Object lock1 = new Object();private static Object lock2 = new Object();public static void main(String[] args) {// 线程1尝试获取lock1,然后尝试获取lock2Thread thread1 = new Thread(() -> {synchronized (lock1) {System.out.println("Thread 1: Holding lock 1...");try {Thread.sleep(100); // 为了让线程2有机会获取lock2} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 1: Waiting for lock 2...");synchronized (lock2) {System.out.println("Thread 1: Holding lock 1 and lock 2...");}}});// 线程2尝试获取lock2,然后尝试获取lock1Thread thread2 = new Thread(() -> {synchronized (lock2) {System.out.println("Thread 2: Holding lock 2...");try {Thread.sleep(100); // 为了让线程1有机会获取lock1} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread 2: Waiting for lock 1...");synchronized (lock1) {System.out.println("Thread 2: Holding lock 1 and lock 2...");}}});// 启动两个线程thread1.start();thread2.start();}
}
如何避免线程死锁
避免线程持有多个锁:
线程在持有一个锁的同时,尝试获取另一个锁时容易导致死锁。尽量设计避免一个线程同时持有多个锁的情况。
按固定的顺序获取锁:
多个线程都按照相同的顺序获取共享资源的锁,可以避免循环等待的情况,从而避免死锁的发生。
设置超时时间:
在获取锁的过程中设置超时时间,如果超过一定时间还未获取到锁,就放弃并释放已经获取的锁,避免长时间等待导致死锁。
使用并发工具类:
Java提供了一些并发工具类,如java.util.concurrent包下的工具类,可以更方便地管理线程之间的资源竞争,避免死锁。
避免嵌套锁:
尽量避免在持有一个锁的情况下再去申请另一个锁,这样容易导致死锁的发生。
良好的代码设计:
在编写多线程程序时,要注意良好的代码设计,避免复杂的线程交互关系,减少出现死锁的可能性。
使用同步块代替同步方法:
在对共享资源进行操作时,尽量使用同步块而不是同步方法,这样可以更灵活地控制锁的获取顺序,降低死锁的风险。