多线程基础:线程创建、同步与通信——学习指南
文章目录
- 多线程基础:线程创建、同步与通信——学习指南
- 前言
- 一、线程创建
- 1、原理
- 2、案例
- 3、使用场景推荐
- 二、线程同步
- 1、原理
- 2、案例
- 1)synchronized关键字
- 1.1)修饰实例方法:当synchronized修饰一个实例方法时,它锁定的是调用该方法的对象实例。
- 1.2)修饰静态方法:当synchronized修饰一个静态方法时,它锁定的是该方法所属的类对象。
- 1.3)修饰代码块:synchronized也可以用来修饰一个代码块,此时需要指定一个对象作为锁对象。
- 2)ReentrantLock
- 3、对比
- 三、线程通信
- 1、原理
- 2、案例
- 3、使用场景推荐
- 总结
前言
随着计算机技术的不断发展,多线程编程已经成为现代软件开发中不可或缺的一部分。多线程能够充分利用多核CPU的并行计算能力,提高程序的执行效率。对于初学者来说,掌握多线程的基础知识是迈向高级编程的重要一步。
本文将详细介绍线程的创建、同步与通信,包括怎么创建,有哪几种创建方式,怎么使用,优缺点等等内容。
一、线程创建
1、原理
- 线程是操作系统进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
- Java中创建线程主要方式有:继承Thread类、实现Runnable接口、实现Callable接口和线程池。(本文先省略线程池)。
2、案例
- 继承Thread类
继承Thread类创建线程示例:通过继承java.lang.Thread类,重写其run()方法,然后创建子类对象并调用start()方法启动线程。
public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread is running."); // 线程执行的代码 } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 // 直接new,然后重写其中的run方法 new Thread(()->{//重写run方法log.info("run方法打印");}).start();}
}
- 实现Runnable接口
实现Runnable接口创建线程示例:实现java.lang.Runnable接口的类也可以被用来创建线程。这种方式的好处是,一个类可以继承其他类并实现Runnable接口,从而避免Java单继承的限制。
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("MyRunnable is running."); // 线程执行的代码 } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 }
}
- 实现Callable接口
实现Callable接口创建线程示例:java.util.concurrent.Callable接口与Runnable类似,但Callable可以返回执行结果,并且可以抛出异常。通常与Future结合使用,以获取异步计算的结果。
import java.util.concurrent.*; public class MyCallable implements Callable<String> { @Override public String call() throws Exception { // 执行一些操作并返回结果 return "Result of MyCallable"; } public static void main(String[] args) throws ExecutionException, InterruptedException { //结合Future使用ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(new MyCallable()); // 获取计算结果 String result = future.get(); // 阻塞直到计算完成 System.out.println(result); executor.shutdown(); // 关闭线程池 }
}
3、使用场景推荐
- 继承Thread类:适用于简单的线程任务,且不需要与其他线程共享资源。
- 实现Runnable接口:当需要多个线程共享一个任务时,或者类已经继承了其他类时,使用Runnable更合适。
- 实现Callable接口:当需要线程执行后返回结果时,或者需要处理可能抛出的异常时,使用Callable结合Future。
二、线程同步
1、原理
- 由于多个线程可能同时访问共享资源,导致数据不一致或其他不可预测的问题,因此需要进行线程同步。
- Java提供了多种同步机制,如synchronized关键字、Lock接口及其实现类等。
2、案例
1)synchronized关键字
synchronized是Java提供的一种内置锁机制,它可以用来修饰方法或代码块。当一个线程进入一个对象的synchronized方法或代码块时,它获得该对象的锁,其他线程则无法进入该对象的synchronized方法或代码块,直到锁被释放。使用synchronized关键字实现同步方法或同步代码块,确保同一时间只有一个线程可以执行同步区域内的代码。
synchronized是非公平锁。这意味着多个线程在竞争synchronized锁时,它们的获取顺序是不确定的,不按照申请锁的顺序来排队。一个线程在等待锁时,不管自己是不是在等待队列的头部,都有机会在其他线程释放锁后立即获取锁。这种非公平性可能导致某些线程长时间无法获取到锁,产生饥饿现象。然而,synchronized锁也是可重入的,即同一个线程可以反复获取锁多次,然后需要释放多次。
1.1)修饰实例方法:当synchronized修饰一个实例方法时,它锁定的是调用该方法的对象实例。
public class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int value() { return count; } public static void main(String[] args) { SynchronizedCounter counter = new SynchronizedCounter(); // 假设有两个线程同时调用increment和decrement方法 // 由于increment和decrement方法都是synchronized的,因此它们是线程安全的 // ... }
}
1.2)修饰静态方法:当synchronized修饰一个静态方法时,它锁定的是该方法所属的类对象。
public class SynchronizedStaticCounter { private static int count = 0; public static synchronized void increment() { count++; } public static synchronized void decrement() { count--; } public static synchronized int value() { return count; } // ...
}
1.3)修饰代码块:synchronized也可以用来修饰一个代码块,此时需要指定一个对象作为锁对象。
public class SynchronizedBlock { private Object lock = new Object(); private int count = 0; public void increment() { synchronized (lock) { count++; } } public void decrement() { synchronized (lock) { count--; } } // ...
}
2)ReentrantLock
ReentrantLock是Java中提供的一个可重入互斥锁,它实现了Lock接口,提供了比synchronized关键字更灵活、更强大的锁定机制。ReentrantLock通过显式地获取和释放锁来控制对共享资源的访问。
ReentrantLock的主要特性包括:
- 公平性:可以创建公平或非公平的锁。公平的锁按照请求锁的顺序来授予访问权限,而非公平的锁则不保证这种顺序。
- 可重入:同一个线程可以多次获得同一个ReentrantLock实例,而不会造成死锁。
- 可中断:当一个线程尝试获取一个由其他线程持有的锁时,该线程可以选择等待,或者响应中断。
- 尝试锁定:线程可以尝试获取锁,如果锁不可用,线程可以立即返回,而不是阻塞。
示例:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); // 获取锁 try { count++; System.out.println("Incremented: " + count); } finally { lock.unlock(); // 释放锁 } } public void decrement() { lock.lock(); // 获取锁 try { count--; System.out.println("Decremented: " + count); } finally { lock.unlock(); // 释放锁 } } public static void main(String[] args) { ReentrantLockExample example = new ReentrantLockExample(); // 创建两个线程,一个增加计数,一个减少计数 Thread incrementThread = new Thread(() -> { for (int i = 0; i < 5; i++) { example.increment(); } }); Thread decrementThread = new Thread(() -> { for (int i = 0; i < 5; i++) { example.decrement(); } }); // 启动线程 incrementThread.start(); decrementThread.start(); // 等待线程结束 try { incrementThread.join(); decrementThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
在上面的代码中,increment()和decrement()方法都使用了ReentrantLock来控制对count变量的访问。在try块中执行临界区代码,在finally块中确保锁被释放,无论是否发生异常。
使用ReentrantLock时,需要注意以下几点:
- 一定要在finally块中释放锁,确保锁不会因为异常而没有被释放,从而导致死锁。
- 避免在持有锁时执行耗时的操作,以免其他线程长时间等待锁。
- 考虑锁的粒度,避免过度同步,以减少线程间的竞争。
ReentrantLock还提供了其他高级功能,比如条件变量(通过newCondition()方法获取),用于在多个线程之间实现更复杂的同步模式。这些高级功能使得ReentrantLock比synchronized关键字更加强大和灵活。
3、对比
- 使用场景:
ReentrantLock:适用于需要更精细控制同步的场景,如中断等待获取锁、尝试非阻塞地获取锁,或者与Condition结合使用实现复杂的线程间协调
synchronized:适用于简单的同步场景,如保护单个方法或代码块不被多个线程同时访问。 - 灵活性:
ReentrantLock:提供了更灵活的锁定机制,包括公平锁、可重入锁等特性,以及更精细的锁控制方法。
synchronized:相对固定,自动管理锁的获取和释放,无需显式操作。 - 性能:
性能因具体场景而异。在某些高并发场景下,ReentrantLock可能通过更精细的锁策略提供更佳性能。但在一些简单场景下,synchronized由于其内置性和JVM优化可能表现更好。 - 易用性:
ReentrantLock:需要显式管理锁的获取和释放,增加了代码复杂性和出错的可能性。
synchronized:作为语言特性,使用简单直观,易于理解和维护。 - 兼容性:
synchronized:作为Java的关键字,与Java平台完全兼容,无需额外依赖。
ReentrantLock:是Java标准库的一部分,但使用它需要显式导入相关类。 - 锁的类型对比
synchronized是非公平锁。
ReentrantLock可以创建公平锁和非公平锁。
在大多数情况下,synchronized是一个很好的选择,因为它简单、易用且性能通常足够。但在需要更精细控制同步的场景下,可以考虑使用ReentrantLock。
三、线程通信
1、原理
- 线程通信是指多个线程之间通过共享变量或其他机制进行信息交换和协作。
- wait()、notify()和notifyAll()是Java对象中的方法,它们必须与synchronized一起使用,用于在线程间通信。
2、案例
- wait():使当前线程等待(进入等待队列),直到其他线程调用该对象的notify()或notifyAll()方法。
- notify():唤醒在此对象监视器上等待的单个线程。
- notifyAll():唤醒在此对象监视器上等待的所有线程。
public class WaitNotifyExample { private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { while (count >= 5) { try { lock.wait(); // 当前线程等待,直到被唤醒 } catch (InterruptedException e) { e.printStackTrace(); } } count++; System.out.println("Incremented: " + count); lock.notifyAll(); // 唤醒所有在此对象上等待的线程 } } public void decrement() { synchronized (lock) { while (count <= 0) { try { lock.wait(); // 当前线程等待,直到被唤醒 } catch (InterruptedException e) { e.printStackTrace(); } } count--; System.out.println("Decremented: " + count); lock.notifyAll(); // 唤醒所有在此对象上等待的线程 } } // ...
}
这个示例展示了如何使用wait()和notifyAll()实现一个基本的生产者-消费者问题,其中increment()方法类似于生产者(增加计数),而decrement()方法类似于消费者(减少计数)。
3、使用场景推荐
- 当需要在多个线程之间实现协同工作时,可以使用线程通信机制。例如,生产者消费者模型中,生产者线程生产数据后唤醒消费者线程进行消费,消费者线程消费完数据后唤醒生产者线程继续生产。代码示例如第2点所示
总结
多线程编程是Java编程中的重要部分,掌握线程创建、同步与通信是成为高级程序员的关键。通过本文的介绍,相信你们已经对多线程的基础有了初步的了解。在实际开发中,还需要结合具体需求选择合适的线程创建方式、同步机制和通信方式,以实现高效、稳定的多线程程序。
需要注意的是,多线程编程涉及线程同步、数据共享和通信等复杂问题。在编写多线程程序时,务必谨慎处理这些问题,以避免出现竞态条件、死锁等线程安全问题。