在 Java 编程的世界里,多线程是提升程序效率、充分利用系统资源的重要手段。然而,多线程编程也伴随着诸多挑战,其中线程安全问题尤为突出,而锁机制则是解决线程安全问题的关键。本文将全面解析 Java 多线程的基础知识,并深入探讨锁机制的相关内容。
多线程的基本概念
线程是程序执行的最小单位,一个进程可以包含多个线程,这些线程共享进程的资源,但各自拥有独立的执行栈和程序计数器。多线程则是指在一个程序中同时运行多个线程,它们可以并发执行不同的任务。
与单线程相比,多线程具有明显的优势。首先,它能提高程序的执行效率,当一个线程因等待某个资源(如 I/O 操作)而阻塞时,其他线程可以继续执行,避免了 CPU 资源的浪费。其次,多线程可以更好地响应用户交互,例如在图形化界面程序中,一个线程负责处理用户输入,另一个线程负责后台数据处理,能让程序更加流畅。
但多线程也并非完美无缺,它会带来线程安全问题。当多个线程同时访问和操作共享资源时,如果没有合适的同步机制,就可能导致数据的不一致性。比如两个线程同时对一个变量进行自增操作,可能会出现结果与预期不符的情况。
多线程的创建方式
在 Java 中,创建多线程主要有三种方式:继承 Thread 类、实现 Runnable 接口和实现 Callable 接口。
继承 Thread 类
Thread 类是 Java 中用于表示线程的类,继承 Thread 类创建线程的步骤如下:
- 创建一个继承自 Thread 类的子类;
- 重写 Thread 类的 run () 方法,在 run () 方法中定义线程要执行的任务;
- 创建该子类的实例对象;
- 调用实例对象的 start () 方法启动线程。
示例代码如下:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行:" + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
上述代码中,MyThread 类继承了 Thread 类并重写了 run () 方法,在 main 方法中创建了两个 MyThread 实例并启动,两个线程会并发执行 run () 方法中的循环输出操作。
实现 Runnable 接口
Runnable 接口中只包含一个 run () 方法,实现 Runnable 接口创建线程的步骤为:
- 创建一个实现 Runnable 接口的类;
- 实现该接口的 run () 方法,定义线程任务;
- 创建该类的实例对象;
- 将该实例对象作为参数传递给 Thread 类的构造方法,创建 Thread 实例;
- 调用 Thread 实例的 start () 方法启动线程。
示例代码:
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行:" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
这种方式的优势在于,一个类可以同时实现多个接口,避免了单继承的限制,而且多个线程可以共享同一个 Runnable 实例中的资源。
实现 Callable 接口
Callable 接口与 Runnable 接口类似,但它可以返回线程执行的结果,并且可以抛出异常。创建步骤如下:
- 创建一个实现 Callable 接口的类,指定返回值类型;
- 实现 call () 方法,定义线程任务,该方法有返回值且可以抛出异常;
- 创建该类的实例对象;
- 将该实例对象包装到 FutureTask 对象中;
- 将 FutureTask 对象作为参数传递给 Thread 类的构造方法,创建 Thread 实例;
- 调用 Thread 实例的 start () 方法启动线程;
- 可以通过 FutureTask 的 get () 方法获取线程执行的结果。
示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("线程执行结果:" + futureTask.get());
}
}
在这个例子中,MyCallable 类实现了 Callable接口,call () 方法计算 1 到 5 的和并返回,通过 FutureTask 的 get () 方法可以获取到这个结果。
线程同步和锁机制
当多个线程共享资源时,为了保证数据的一致性,需要进行线程同步。锁机制是实现线程同步的重要方式。
synchronized 关键字
synchronized 关键字是 Java 中最基本的同步机制,它可以修饰方法和代码块。
- 修饰方法:当 synchronized 修饰一个非静态方法时,锁定的是当前对象实例;当修饰静态方法时,锁定的是该类的 Class 对象。
- 修饰代码块:synchronized 代码块需要指定锁定的对象,格式为synchronized(锁对象),它只对使用同一把锁的线程起同步作用。
示例代码(修饰方法):
class SynchronizedMethod {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedMethodTest {
public static void main(String[] args) throws InterruptedException {
SynchronizedMethod method = new SynchronizedMethod();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
method.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
method.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count值:" + method.getCount());
}
}
在上述代码中,increment () 和 getCount () 方法被 synchronized 修饰,保证了多个线程对 count 变量的操作是同步的,最终输出的 count 值一定是 2000。
示例代码(修饰代码块):
class SynchronizedBlock {
private int count = 0;
private Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
public class SynchronizedBlockTest {
public static void main(String[] args) throws InterruptedException {
SynchronizedBlock block = new SynchronizedBlock();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
block.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
block.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count值:" + block.getCount());
}
}
这里使用 synchronized 代码块,锁定了 lock 对象,同样能保证 count 变量操作的线程安全。
Lock 接口
除了 synchronized 关键字,Java 还提供了 Lock 接口及其实现类(如 ReentrantLock)来实现锁机制。与 synchronized 相比,Lock 接口提供了更灵活的锁定操作,例如可以手动获取和释放锁、尝试获取锁等。
使用 Lock 接口的基本步骤:
- 创建 Lock 接口的实现类对象(如 ReentrantLock);
- 在需要同步的代码块前调用 lock () 方法获取锁;
- 在代码块执行完毕后,调用 unlock () 方法释放锁,通常将 unlock () 方法放在 finally 块中,以确保锁能被正确释放。
示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
public class LockTest {
public static void main(String[] args) throws InterruptedException {
LockExample example = new LockExample();
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();
thread1.join();
thread2.join();
System.out.println("count值:" + example.getCount());
}
}
ReentrantLock 是 Lock 接口的常用实现类,它支持重入锁,即同一个线程可以多次获取该锁。
多线程编程的注意事项
在进行多线程编程时,有一些注意事项需要牢记:
- 尽量减少同步范围:同步操作会影响程序的性能,因此应只对必须同步的代码进行同步,避免不必要的同步。
- 避免死锁:死锁是指两个或多个线程相互等待对方释放资源而陷入无限等待的状态。为了避免死锁,在获取多个锁时,应保持一致的顺序。
- 合理设置线程优先级:线程优先级决定了线程获得 CPU 调度的机会大小,但过度依赖线程优先级可能会导致程序行为不稳定,应谨慎使用。
- 正确处理线程中断:线程中断是一种线程间的协作机制,当一个线程需要停止另一个线程时,应通过中断机制,而不是直接调用 stop () 方法(该方法已被废弃)。
总之,Java 多线程与锁机制是 Java 编程中的重要知识点,掌握它们对于编写高效、安全的多线程程序至关重要。在实际开发中,需要根据具体场景选择合适的线程创建方式和同步机制,并注意规避多线程编程带来的风险。