在 Java 中,线程和多线程编程是处理并发任务的核心技术。多线程可以提高程序的性能和响应能力,特别是在处理 I/O 密集型和计算密集型任务时。

Java 多线程编程涉及创建和管理多个线程,允许并发执行任务,从而提高程序的性能和响应能力。

第一节 线程和多线程

1. 基础概念

什么是线程?

线程是一个程序中独立执行的路径。每个线程在 Java 虚拟机 (JVM) 中都有自己的执行栈、程序计数器和局部变量,但它们共享进程的资源(如堆内存)。

单线程与多线程
  • 单线程:程序中只有一个线程,从头到尾顺序执行代码。
  • 多线程:程序中有多个线程,可以同时执行多个任务。
多线程的优势
  • 并发执行:提高程序性能,尤其是多核处理器。
  • 资源共享:多个线程可以共享程序的资源。
  • 响应性:提升应用程序的响应能力,尤其是 GUI 应用程序。

第二节 创建线程

在 Java 中,有两种主要方式创建线程:

  1. 继承 Thread
  2. 实现 Runnable 接口

单线程示例

对比单线程和多线程计算的性能差异。我们将创建一个计算任务,它会计算从1到一个大数的所有整数的平方和。通过对比单线程和多线程的执行时间,将能够清楚地看到多线程的优势。

public class SingleThreadExample {

	public static void main(String[] args) {
		long startTime = System.currentTimeMillis(); // 当前时间
		System.out.println(startTime);
		
		long total = 0;
		for(long i=1; i<=10000000; i++) {  // 注意这里最好使用long类型;因为当大于int类型的最大值时会出现溢出
			total += i*i;
		}
		
		long endTime = System.currentTimeMillis();  // 当前时间
		System.out.println(endTime);
		System.out.println("单线程花费总时间:" + (endTime - startTime) + "ms");
		System.out.println("计算结果:" + total);
	}

}

执行结果:

开始时间:1718593701953
结束时间:1718593701985
单线程花费总时间:32ms
计算结果:1291990006563070912

使用继承 Thread 类的多线程实现

**核心要义:**创建一个类继承 Thread 类,并重写其 run 方法。

public class ThreadExample {

	public static void main(String[] args) throws InterruptedException {
		int numberOfThreads = 4;  // 线程数量
		long range = 1000000000/numberOfThreads;  // 诶个均分任务
		
		long startTime = System.currentTimeMillis();  // 当前时间
		System.out.println("开始时间:" + startTime);
		
		// 创建多线程并分别执行他们
		ThreadTask[] tasks = new ThreadTask[numberOfThreads];
		for (int i=0; i<numberOfThreads; i++) {
			tasks[i] = new ThreadTask(i*range+1, (i+1)*range);
			tasks[i].start();
		}
		
		// 计算总和
		long total = 0;
		for(ThreadTask task:tasks) {
			task.join();   // 可能户抛出InterruptedException异常
			total += task.getSum();
		}
		
		// 结果输出
		long endTime = System.currentTimeMillis();  // 当前时间
		System.out.println("结束时间:" + endTime);
		System.out.println("单线程花费总时间:" + (endTime - startTime) + "ms");
		System.out.println("计算结果:" + total);

	}

}


class ThreadTask extends Thread{
	private long start;
	private long end;
	private long sum;
	
	public ThreadTask(long start, long end) {
		this.start = start;
		this.end = end;
		this.sum = 0;
	}
	
	public long getSum() {  //为了返回数据给线程调用者
		return sum;
	}
	
	@Override  //可以不写,但是建议写上
	public void run(){  //特别注意,这里必须是void类型的返回值,如果改成long编译会报错
		for(long i=start; i<=end; i++) {
			sum += i*i;
		}
	}
	
}

执行结果:

开始时间:1718593906410
结束时间:1718593906446
单线程花费总时间:36ms
计算结果:1291990006563070912

多线程在某些情况下可能比单线程花费更多时间,这通常与以下几个因素有关:

  1. 线程启动和上下文切换的开销:创建和启动线程需要一定的时间,线程之间的上下文切换也会引入额外的开销。这些开销在处理较小的任务时可能会使多线程的性能劣于单线程。
  2. 任务的粒度:如果每个线程处理的任务粒度过小,那么多线程的优势无法体现,反而会因为线程管理和切换的开销导致性能下降。
  3. 硬件资源的限制:如果系统的CPU核心数量较少,多个线程可能会争抢CPU资源,导致性能下降。
  4. 同步开销:在多线程中,如果需要进行同步操作(如对共享资源的访问进行同步),那么同步操作也会引入额外的开销。

在上面的例子中,任务量比较小,线程启动和上下文切换的开销占比较大,因此多线程反而比单线程慢。

我们将任务的量增加到1000000000,通过增加任务量,可以更好地体现多线程的优势。在处理更大规模的数据时,多线程的性能提升会更加明显。

任务量是1000000000时单线程的处理结果:

开始时间:1718595585654
结束时间:1718595586107
单线程花费总时间:453ms
计算结果:4338615082255021824

// 这里注意如果你的计算结果出现负数的情况;检查下for循环中的i类型是不是写成了int

任务量是1000000000时多线程的处理结果:

开始时间:1718594117404
结束时间:1718594117579
单线程花费总时间:175ms
计算结果:4338615082255021824

使用实现 Runnable 接口的多线程实现

在 Java 中使用 Runnable 接口实现多线程的过程中,Thread 类是必须的,因为 Runnable 接口本身只是定义了一个任务,而 Thread 类是负责管理和执行任务的。Runnable 接口没有独立启动线程的能力,它只是定义了任务的 run 方法。必须将 Runnable 对象传递给 Thread 对象,并通过 Thread 对象启动线程。

public class RunnableExample {
	public static void main(String[] args) throws InterruptedException {
		// 任务分解
		int numberOfThreads = 4;
		long range = 1000000000 / numberOfThreads;
		
		// 开始时间
		long startTime = System.currentTimeMillis();  // 当前时间
		System.out.println("开始时间:" + startTime);
		
		// 创建多个线程【数组】并执行他们
		Thread[] threads = new Thread[numberOfThreads];   // 创建存放多个线程的数组;对线程的控制就通过线程来做  // 这里还是时使用到了Thread类
		ThreadTask1[] tasks = new ThreadTask1[numberOfThreads];   //创建存放线程章的实际任务的数组;也就是代码逻辑;实际的任务代码序列都放在这里
		
		
		for(int i=0; i<numberOfThreads; i++) {
			tasks[i] = new ThreadTask1(i*range+1, (i+1)*range);  // 初始化每个任务
			threads[i] = new Thread(tasks[i]);  // 将任务作为参数与对应编号的线程关联
			threads[i].start();  // 开始线程
		}
		
		// 等待线程结束
		for(int i=0; i<numberOfThreads; i++) {
			threads[i].join();  // 可能会抛出InterruptException异常
		}
		
		// 计算总和
		long total = 0;
		for(ThreadTask1 task:tasks) {
			total += task.getSum();
		}
		
		// 结果输出
		long endTime = System.currentTimeMillis();  // 当前时间
		System.out.println("结束时间:" + endTime);
		System.out.println("单线程花费总时间:" + (endTime - startTime) + "ms");
		System.out.println("计算结果:" + total);
	}
}


class ThreadTask1 implements Runnable{  // 实现接口:Runnable
	private long start;
	private long end;
	private long sum;
	
	public ThreadTask1(long start, long end) {  // 构造器
		this.start = start;
		this.end = end;
		this.sum = 0;
	}

	@Override
	public void run() {  //特别注意,这里必须是void类型的返回值,如果改成long编译会报错
		for(long i=start; i<=end; i++) {
			sum += i*i;
		}
	}
	
	public long getSum() {  //为了返回数据给线程调用者
		return sum;
	}
	
}

执行结果:

开始时间:1718601706366
结束时间:1718601706543
单线程花费总时间:177ms
计算结果:4338615082255021824

这个执行结果跟使用Thread类基本一致。

为什么Runnable 接口实现多线程不能完全不使用 Thread

  • Runnable 接口:定义了一个任务。它有一个 run 方法,但没有任何方法来启动一个新的线程。
  • Thread:负责管理线程的生命周期。它可以启动、运行和停止线程。

当你创建一个 Runnable 对象时,你只是创建了一个任务。你必须将这个任务传递给 Thread 对象,并通过 Thread 对象的 start 方法来启动线程。Thread 类提供了线程管理的所有必要功能,而 Runnable 只是任务的载体。

主要区别

  1. 继承关系
  • Thread 类:由于 Java 只支持单继承,如果一个类已经继承了 Thread,它就不能再继承其他类。
  • Runnable 接口:可以实现多个接口,所以更加灵活。如果一个类需要继承其他类,并且还需要多线程功能,推荐使用 Runnable 接口。
  1. 代码复用
  • Thread 类:不推荐把业务逻辑直接写在 Thread 子类中,因为这样做不利于代码复用。
  • Runnable 接口:更推荐在实现 Runnable 接口的类中定义业务逻辑,然后通过不同的 Thread 实例来执行这些逻辑。这种方式可以更好地复用代码。
  1. 任务与线程的分离
  • Thread 类:任务与线程绑定在一起。
  • Runnable 接口:任务与线程是分离的,可以将任务(Runnable 实现类)提交给不同的线程去执行,这种设计符合面向对象的设计原则,也有助于提升代码的可维护性和可扩展性。
  1. 灵活性
  • Thread 类:较不灵活,因为一旦继承了 Thread 类,就无法再继承其他类。
  • Runnable 接口:更灵活,允许一个类实现多个接口,还可以继承其他类。

选择建议

  • 如果你不需要从其他类继承,且任务逻辑很简单,可以直接使用 Thread 类。
  • 如果你的类需要继承自其他类,或者希望任务与线程分离以便复用和扩展,推荐使用 Runnable 接口。

使用Runnable接口实现多线程的改写

public class RunnableExample1 {
	public static void main(String[] args) throws InterruptedException {
		// 分任务
		int numberOfThreads = 4; // 分4份
		long range = 1000000000 / numberOfThreads;  // 每一份的范围
		
		// 开始时间
		long startTime = System.currentTimeMillis();
		System.out.println("开始时间是:" + startTime);
		
		// 创建线程和任务
		Thread[] threads = new Thread[numberOfThreads];   // 创建存放多个线程的数组;对线程的控制就通过线程来做  // 这里还是时使用到了Thread类
		MyRun[] tasks = new MyRun[numberOfThreads];
		long total = 0;
		
		for(int i = 0; i < numberOfThreads; i++) {
			tasks[i] = new MyRun(i*range+1, (i+1)*range);
			threads[i] = new Thread(tasks[i]);
			threads[i].start();
			threads[i].join();  // 可能产生InterruptException异常  // 这样将start()和join()写在同一个循环体内是不合理的,这实际上将线程变成了串行执行,而不是并行执行。
			total += tasks[i].getSum();  // 这里来自task而不是thread
		}
		
		// 结果输出
		long endTime = System.currentTimeMillis();  // 当前时间
		System.out.println("结束时间:" + endTime);
		System.out.println("单线程花费总时间:" + (endTime - startTime) + "ms");
		System.out.println("计算结果:" + total);
	}
}



class MyRun implements Runnable{
	private long start;
	private long end;
	private long sum;
	
	MyRun(long start, long end){
		this.start = start;
		this.end = end;
		this.sum = 0;
	}
	
	@Override
	public void run() {
		for(long i=start; i<=end; i++) {
			sum += i*i;
		}
	}
	
	public long getSum() {
		return sum;
	}
}

执行结果

开始时间是:1718605287154
结束时间:1718605287602
单线程花费总时间:448ms
计算结果:4338615082255021824

这个示例中每个线程在启动后立即被 join(),这意味着主线程等待该线程完成后再继续启动下一个线程,这样实际上是在串行执行任务。

应该修正为:

首先启动所有线程,然后等待它们全部完成:

public class RunnableExample1 {
	public static void main(String[] args) throws InterruptedException {
		// 分任务
		int numberOfThreads = 4; // 分4份
		long range = 1000000000 / numberOfThreads;  // 每一份的范围
		
		// 开始时间
		long startTime = System.currentTimeMillis();
		System.out.println("开始时间是:" + startTime);
		
		// 创建线程和任务
		Thread[] threads = new Thread[numberOfThreads];   // 创建存放多个线程的数组;对线程的控制就通过线程来做  // 这里还是时使用到了Thread类
		MyRun[] tasks = new MyRun[numberOfThreads];
		long total = 0;
		
		for(int i = 0; i < numberOfThreads; i++) {
			tasks[i] = new MyRun(i*range+1, (i+1)*range);
			threads[i] = new Thread(tasks[i]);
			threads[i].start();
			//threads[i].join();  // 可能产生InterruptException异常  // 这样将start()和join()写在同一个循环体内是不合理的,这实际上将线程变成了串行执行,而不是并行执行。
			//total += tasks[i].getSum();  // 这里来自task而不是thread
		}
		
		// 在所有thread start完之后,使用 join() 方法等待所有线程完成
		for(int i = 0; i < numberOfThreads; i++) {
			threads[i].join();
			total += tasks[i].getSum();
		}
		
		// 结果输出
		long endTime = System.currentTimeMillis();  // 当前时间
		System.out.println("结束时间:" + endTime);
		System.out.println("单线程花费总时间:" + (endTime - startTime) + "ms");
		System.out.println("计算结果:" + total);
	}
}



class MyRun implements Runnable{
	private long start;
	private long end;
	private long sum;
	
	MyRun(long start, long end){
		this.start = start;
		this.end = end;
		this.sum = 0;
	}
	
	@Override
	public void run() {
		for(long i=start; i<=end; i++) {
			sum += i*i;
		}
	}
	
	public long getSum() {
		return sum;
	}
}

执行结果:

开始时间是:1718605668203
结束时间:1718605668382
单线程花费总时间:179ms
计算结果:4338615082255021824

第三节 线程的基本控制

线程完整的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。

【添油加醋的Java基础】第十章 多线程_夏明亮

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程的控制

在Java中,线程的基本控制涉及到创建、启动、暂停、恢复、中止线程等操作。

创建和启动线程

在Java中,可以通过继承 Thread 类或实现 Runnable 接口来创建线程。

// 继承 `Thread` 类
public class MyThread extends Thread {  //继承 Thread类
    @Override
    public void run() {  // 重写run()方法
        // 线程执行的代码
        System.out.println("Thread is running");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 启动线程
    }
}
// 实现 `Runnable` 接口
public class MyRunnable implements Runnable {  // 实现Runnable接口
    @Override
    public void run() {  // 重写run()方法
        // 线程执行的代码
        System.out.println("Runnable is running");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();  // 启动线程
    }
}
线程的控制方法

start()启动线程,使其进入可运行状态。

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

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

join()等待线程执行完毕。

Thread thread = new Thread(new MyRunnable());
thread.start();
try {
    thread.join();  // 等待线程结束
} catch (InterruptedException e) {
    e.printStackTrace();
}

interrupt()中断线程,设置线程的中断状态。

thread.interrupt();

isInterrupted()检查线程是否被中断,但不清除中断状态。

if (thread.isInterrupted()) {
    System.out.println("Thread was interrupted");
}

interrupted()检查当前线程是否被中断,并清除中断状态。

if (Thread.interrupted()) {
    System.out.println("Thread was interrupted");
}

yield()提示调度器当前线程愿意让出CPU资源,但不保证让出。

Thread.yield();

线程池

线程池可以管理和重用线程,避免频繁创建和销毁线程带来的开销。

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



public class ThreadPoolExample {

	public static void main(String[] args) {
		ExecutorService executor = Executors.newFixedThreadPool(5);  // 固定大小为5的线程池
		
		
		for(int i=0; i < 10; i++) {
			executor.submit(new Runnable() {
				@Override
				public void run() {
					System.out.println("Thread: " + Thread.currentThread().getName());  // 最多有同时有5个线程;多于5个就排队
				}
			});
		}
		
		executor.shutdown();  // 关闭线程池
	}
}

注意:

在Java中,不能直接实例化接口,因为接口是没有实现的抽象类型。但是,可以创建实现该接口的匿名类new Runnable(){...}的实例。

执行结果:

Thread: pool-1-thread-2
Thread: pool-1-thread-5
Thread: pool-1-thread-1
Thread: pool-1-thread-3
Thread: pool-1-thread-4
Thread: pool-1-thread-3
Thread: pool-1-thread-5
Thread: pool-1-thread-1
Thread: pool-1-thread-2
Thread: pool-1-thread-4
匿名类

匿名类是没有名称的类,在单个表达式中定义和实例化。当您需要创建具有某些“一次性”功能的类的实例时,特别是当您不想创建单独的命名类时,匿名类非常有用。

Runnable接口不能直接实例化,因为它没有任何实现。但是,您可以创建实现Runnable的类的实例。通过使用匿名类,您创建了一个实现Runnable的新类,同时提供了run()方法的实现。

如果你不使用匿名类,你需要创建一个命名类来实现Runnable

public class MyRunnable implements Runnable {  // 非匿名类;实现Runnable接口的名字叫MyRunnable的类
    @Override
    public void run() {  // 重写Runnable接口的run()方法
        System.out.println("Thread: " + Thread.currentThread().getName());
    }
}

// Later in my code
for (int i = 0; i < 10; i++) {
    executor.submit(new MyRunnable());  // 使用命名类:MyRunnable
}

当需要一次性实现接口时,使用匿名类可以使代码更简洁。这在多线程中特别有用,因为希望将简短的任务提交给执行器,而不会使代码与许多命名类混淆。

线程的优先级

每个线程都有一个优先级,用于帮助线程调度器确定线程的执行顺序。优先级用整数表示,范围是 Thread.MIN_PRIORITY (1) 到 Thread.MAX_PRIORITY (10),默认优先级为 Thread.NORM_PRIORITY (5)。

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

守护线程

守护线程在后台运行,用于执行后台任务,当所有非守护线程结束时,JVM 会自动退出。

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

第四节 线程的互斥

在Java中,线程同步和互斥对于确保多个线程可以在共享资源上操作而不会导致数据不一致或竞争条件至关重要。在这里,这里为了详细解释线程同步和互斥的概念,我提供一个详细的示例。

synchronized关键字

Java中的synchronized关键字用于确保一次只有一个线程可以访问一个代码块或方法。这可以通过获取对象上的锁来实现。”

public class Counter {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

ReentrantLock

java在java.util.concurrent.locks包中提供了ReentrantLock,用于更高级的线程同步。ReentrantLock提供了与synchronized相同的互斥,但具有额外的功能。

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

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();  // 创建一个不可变的ReentrantLock类型实例

    public void increment() {
        lock.lock();  // 锁定
        try {
            count++;
        } finally {
            lock.unlock();  // 解锁
        }
    }

    public int getCount() {
        lock.lock();  // 锁定
        try {
            return count;
        } finally {
            lock.unlock();  // 解锁
        }
    }

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

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

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

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

        System.out.println("Final count: " + counter.getCount());
    }
}

synchronized vs. ReentrantLock

synchronized

  • 更容易使用。
  • 当块或方法退出时自动释放锁。
  • 在等待获取锁时不能被中断。

ReentrantLock

  • 提供对锁获取和释放的更多控制。
  • 可以在等待获取锁时中断。
  • 提供公平策略(锁访问的FIFO顺序)。
  • 支持多个条件变量。

死锁Deadlock

当两个或多个线程被永久阻塞,等待对方释放所需的锁时,就会发生死锁。

这里有一个简单的例子:

public class DeadlockExample {
	private final Object res01 = new Object();  // 创建2个对象
	private final Object res02 = new Object();
	
	public void method01() {
		System.out.println("我是:method01"); 
		synchronized(res01) {  // 以独占方式使用res01对象
			System.out.println("label: 11"); 
			System.out.println("res01被:" + Thread.currentThread().getName() + "占用。");
			synchronized(res02) {  // 占用res01之后再尝试占用res02; 永远不会得到想要的资源,会永远等待
				System.out.println("label: 12"); 
				System.out.println("res02被:" + Thread.currentThread().getName() + "占用。");
			}
		}
	}
	
	public void method02() {
		System.out.println("我是:method02"); 
		synchronized(res02) {  // 以独占方式使用res02对象
			System.out.println("label: 21"); 
			System.out.println("res02被:" + Thread.currentThread().getName() + "占用。");
			synchronized(res01) {   // 占用res02之后再尝试占用res01; 永远不会得到想要的资源,会永远等待
				System.out.println("label: 22");
				System.out.println("res01被:" + Thread.currentThread().getName() + "占用。");
			}
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		DeadlockExample t = new DeadlockExample();
		Thread thread1 = new Thread(t::method01);  // deadlockExample::method1这种写法不是调用静态方法,而是引用deadlockExample实例的method1方法;将其内容作为new Thread()的参数。这不会立即调用该方法,而是提供对该方法的引用,然后线程可以在开始执行时调用该引用。
		Thread thread2 = new Thread(t::method02);
		
		thread1.start();
		thread2.start();
		
		thread1.join();
		thread2.join();
	}
}

执行结果:

我是:method01
label: 11
我是:method02
label: 21
res02被:Thread-1占用。
res01被:Thread-0占用。

【添油加醋的Java基础】第十章 多线程_同步_02

进程永远不会结束,因为他们互相在等待被对刚占用的资源。

第五节 线程的同步

当多个线程需要共享资源时,必须使用同步来防止数据冲突。

不同步

我们先看一个线程执行时不进行同步控制的例子:

public class NotSyncThread extends Thread {  // 继承Thread类实现多线程
	private Counter counter;  // 一个Counter类型的实例
	
	public NotSyncThread(Counter counter) { this.counter = counter; }
	
	@Override
	public void run() {
		for(int i=0; i<100000; i++) {
			counter.add();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Counter counter = new Counter();
		NotSyncThread thread1 = new NotSyncThread(counter);
		NotSyncThread thread2 = new NotSyncThread(counter);
		
		long startTime = System.currentTimeMillis();
		thread1.start();
		thread2.start();
		
		thread1.join();  // 可鞥抛出异常
		thread2.join();  // 可鞥抛出异常
		long endTime = System.currentTimeMillis();
		
		System.out.println("Final Count: " + counter.getCount());
		System.out.println("用时: " + (startTime - endTime) + "ms");
	}

}


class Counter{
	private int count = 0;
	
	public void add() { count++; }  // 自增1
	
	public int getCount() { return count;}
}

执行结果:

Final Count: 155773
用时: 3ms

明显可以看出结果是不符合预期的;按照正常逻辑计数器的最终结果是200000。

使用 synchronized 关键字

public class SyncThread extends Thread{  //继承Threead类,实现多线程
	private Counter1 counter;  // 线程共享的变量
	
	public SyncThread(Counter1 counter) { // 构造器
		this.counter = counter;
	}
	
	@Override
	public void run() {  // 重写Thread类的run()函数
		for(int i=0; i<100000; i++) {
			counter.add();
		}
	}
	

	public static void main(String[] args) throws InterruptedException {
		Counter1 counter = new Counter1();  // 实例化Counter1类的对象
		SyncThread thread1 = new SyncThread(counter);  // 实例化我们的SyncThread类
		SyncThread thread2 = new SyncThread(counter);  // 实例化我们的SyncThread类
		
		long startTime = System.currentTimeMillis();
		thread1.start();  // 开始线程
		thread2.start();  // 开始线程
		
		thread1.join();   // 等待线程结束
		thread2.join();   // 等待线程结束
		long endTime = System.currentTimeMillis();
		
		System.out.println("Final Count: " + counter.getCount());
		System.out.println("用时: " + (endTime - startTime) + "ms");
	}
}


class Counter1{  // 自定义一个工具类
	private int count = 0;
	
	public synchronized void add() {  // 线程同步的方法;也叫线程安全的方法;当一个线程使用它时,其他线程必须等待
		count++;
	}
	
	public int getCount() {
		return count;
	}
}

执行结果

Final Count: 200000
用时: 17ms

这个结果关注两点:

  1. 执行的结果永远是固定的次数;线程数量倍数的循环次数。
  2. 用时比非线程同步要长一些。

使用 ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class LockThread extends Thread {
	private Counter2 counter; //声明一个同一个实例(多线程)共享的变量
	
	public LockThread(Counter2 counter) {
		this.counter = counter;
	}
	
	@Override
	public void run() {
		for(int i=0; i<100000; i++) {
			counter.add();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Counter2 counter = new Counter2();  // 实例化一个Counter2类的对象
		LockThread t1 = new LockThread(counter);  // 创建并实例化一个LockThread类的线程对象,并将Counter2类的对象作为构造参数
		LockThread t2 = new LockThread(counter);  // 创建并实例化一个LockThread类的线程对象,并将Counter2类的对象作为构造参数
		
		long startTime = System.currentTimeMillis();
		t1.start();  //start线程
		t2.start();  //start线程
		
		t1.join();  // 等待线程结束
		t2.join();  // 等待线程结束
		long endTime = System.currentTimeMillis();
		
		System.out.println("Final count: " + counter.getCount());
		System.out.println("用时: " + (endTime - startTime) + "ms");
	}
}



class Counter2{
	private int count = 0;  // 定义一个计数变量
	private ReentrantLock lock = new ReentrantLock();  // 定义一个锁 
	
	public void add() { // 定义一个额自增的方法
		lock.lock();  // 加锁
		try {
			count++;
		}finally {
			lock.unlock();  // 释放锁
		}
	}
	
	public int getCount() { return count; }
}

执行结果:

Final count: 200000
用时: 18ms

ReentrantLock和Lock

Lock 是 Java 中并发包 (java.util.concurrent.locks) 下的一个接口,它定义了锁的基本操作。而 ReentrantLockLock 接口的一个实现,提供了比 synchronized 关键字更灵活的锁机制。

区别

  1. 接口与实现:
  • Lock 是一个接口,定义了获取和释放锁的基本方法,如 lock(), unlock(), tryLock() 等。
  • ReentrantLockLock 接口的一个实现类,支持重入锁,即同一个线程可以多次获取锁,而不会被自己阻塞。
  1. 可重入性:
  • ReentrantLock 是可重入的,意味着如果一个线程已经持有了这个锁,那么它可以再次获取这个锁而不会被阻塞。它会记录获取锁的次数,直到所有的获取都被释放,锁才会被完全释放。
  1. 公平锁与非公平锁:
  • ReentrantLock:支持公平锁和非公平锁两种模式。
  • 公平锁:线程获取锁的顺序是按照请求锁的顺序(先来先得)。
  • 非公平锁:线程获取锁的顺序是不确定的,可能会让一个线程多次获取锁,即便有其他线程在等待。这种模式通常会有更高的吞吐量。
  • 通过构造函数可以选择锁的公平性:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(); // 非公平锁(默认)
  1. 条件变量:
  • ReentrantLock 提供了 newCondition() 方法,可以创建多个条件变量,用于实现复杂的线程间协作(类似于 Objectwait()notify())。
  1. 可中断锁获取:
  • ReentrantLock 提供了 lockInterruptibly() 方法,允许在获取锁时可以被中断。
  1. 尝试获取锁:
  • ReentrantLock 提供了 tryLock() 方法,允许尝试获取锁,如果获取不到则立即返回 false,而不会一直阻塞。

本章小结

习题