1 多线程简介
多线程(Multithreading)指的是在单个程序中同时执行多个线程,每个线程可以独立运行,拥有自己的执行堆栈和程序计数器,但是它们共享相同的内存空间。
多线程的优点是可以提高程序的执行效率和响应速度。在单个线程中,当程序执行到某个耗时的操作时,程序将被阻塞,不能执行其它任务,从而降低了程序的效率。而使用多线程,可以将耗时的操作放在一个线程中执行,而其它线程可以继续执行其它任务,从而提高了程序的效率。
多线程常用于实现并发编程、异步编程等场景,如网络编程、图形界面编程、并行计算等。
Java是一种面向对象的编程语言,提供了强大的多线程支持。Java多线程编程可以使用Thread类或Runnable接口来创建线程,也可以使用线程池来管理线程。同时,Java还提供了synchronized关键字、volatile关键字、Lock对象等机制来保证多线程并发访问的线程安全性。
虽然多线程能够提高程序效率和响应速度,但也会带来一些问题,如线程安全问题、死锁问题、竞争问题等,因此在多线程编程时需要特别注意这些问题。
1.1) 多线程的基本属性、方法、生命周期
- 线程属性:线程有自己的ID、状态、优先级、名称、线程组等属性。
- 线程方法:线程有创建、启动、休眠、恢复、等待、中断、加入等方法。
- 线程生命周期:线程从创建到销毁的整个过程称为线程生命周期,它包括以下几个阶段:
-
新建(New):线程被创建后,它处于新建状态。
-
就绪(Runnable):当线程启动时,它进入就绪状态,等待CPU调度。
-
运行(Running):当线程获得CPU时间片后,它进入运行状态,执行run()方法中的代码。
-
阻塞(Blocked):线程在等待某些资源时,进入阻塞状态。
-
等待(Waiting):线程在等待某个条件时,进入等待状态。
-
超时等待(Timed Waiting):线程在等待某个条件时,可以指定等待的时间,超时后进入就绪状态。
-
终止(Terminated):当线程执行完run()方法后,或者因异常或其他原因退出run()方法,线程进入终止状态。
在多线程编程中,需要注意线程安全问题,如共享变量的访问、同步、锁、原子性等。同时也需要注意死锁、竞态条件等问题,保证程序的正确性和性能。
2 多线程创建方式
-
继承Thread类,重写run()方法。这种方式是最基本的创建多线程的方式,需要定义一个类继承Thread类,并重写run()方法,然后实例化该类并调用start()方法来启动线程。
class MyThread extends Thread { @Override public void run() { // 执行任务 } } // 实例化并启动线程 MyThread thread = new MyThread(); thread.start(); -
实现Runnable接口,重写run()方法。这种方式可以避免单继承的限制,实现Runnable接口的类可以被多个线程共享。需要实例化Thread对象,并将Runnable对象作为参数传递给Thread的构造函数,然后调用start()方法来启动线程。
class MyRunnable implements Runnable { @Override public void run() { // 执行任务 } } // 实例化并启动线程 MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); -
使用Executor框架。这种方式可以将线程的创建、管理、调度等操作交给Executor框架处理,简化了线程编程。Executor框架提供了多种线程池类型,可以根据具体的需求选择不同的线程池。
ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new Runnable() { @Override public void run() { // 执行任务 } }); executorService.shutdown();
以上是多线程的几种方式,程序员可以根据实际需求和场景选择不同的方式。
3 多线程在jmm中的形式是如何的?
多线程在JMM中的形式可以用一个虚拟的内存模型来表示。这个内存模型中包含两个部分:主内存和工作内存。
主内存是共享的,所有线程都可以访问。它存储了所有的共享变量,包括实例变量、静态变量等。在多线程程序中,当一个线程对共享变量进行修改时,它首先要将变量的值从主内存复制到自己的工作内存中,然后再对这个工作内存中的副本进行操作。当一个线程需要读取共享变量的值时,它也需要先将变量的值从主内存复制到自己的工作内存中,然后再进行读取操作。
工作内存是每个线程独有的,它存储了当前线程使用到的共享变量的副本。每个线程对共享变量的操作都是在自己的工作内存中进行的,而不是直接在主内存中进行。因此,如果一个线程修改了共享变量的值,这个变化并不会立即被其他线程所感知,其他线程仍然在使用自己工作内存中的变量副本。
JMM定义了一些规则和限制来保证多线程程序的正确性和可靠性,包括:
- 有序性规则(Sequential Consistency) :在多线程环境下,程序的执行顺序可能会发生变化,为了保证程序执行的正确性,需要保证操作的执行顺序与程序的编写顺序一致。也就是说,线程在执行时必须遵循原子性、可见性和有序性三个规则。
- 可见性规则(Visibility) :由于多线程共享同一块内存,因此线程之间需要保证对内存中共享变量的读写操作可见。也就是说,当一个线程对共享变量进行修改后,其它线程需要能够立即看到该变量的修改。为了保证可见性,可以使用synchronized关键字、volatile关键字等。
- 原子性规则(Atomicity) :在多线程环境下,多个线程可能同时访问共享的变量,因此需要保证对共享变量的读写操作是原子性的。也就是说,一个线程在修改共享变量的值时,必须要完成整个操作,而不能被其它线程打断。
为了保证有序性规则、可见性规则、原子性规则,可以使用以下方法:
- 使用synchronized关键字或Lock对象来实现同步,保证线程访问共享变量的互斥性,从而保证原子性。
- 使用volatile关键字来修饰共享变量,保证线程对共享变量的修改对其它线程是可见的。
- 使用java.util.concurrent包中提供的工具类,如AtomicInteger、CountDownLatch等,保证多线程访问共享变量的原子性和可见性。
- 在多线程环境下,尽量避免使用静态变量、全局变量等共享变量,尽可能地将变量的作用域限定在方法内部,从而避免竞争和并发问题。
3.1) volatile关键字、synchronized关键字、锁
- volatile关键字:volatile关键字可以保证多线程之间的可见性和有序性。使用volatile修饰的变量在每次读写时都会从主内存中刷新,因此可以避免由于指令重排等原因导致的有序性问题。但是,volatile并不能保证原子性,因此在需要保证原子性的场合,还需要使用其他机制,如synchronized或Lock。
- synchronized关键字:synchronized关键字可以保证多线程之间的互斥访问。它可以修饰方法、代码块或者类。当一个线程获取了对象锁,其他线程就必须等待该线程释放锁后才能获取锁进入临界区。使用synchronized关键字可以避免由于竞态条件而导致的数据不一致等问题。
- 锁:锁是一种更加灵活的多线程同步机制。在Java中,常用的锁包括ReentrantLock、ReadWriteLock、StampedLock等。锁可以保证线程之间的互斥访问,并且可以支持更加复杂的同步场景,如读写锁、乐观锁等。锁的使用需要手动加锁和释放锁,因此需要注意死锁、饥饿等问题。
需要注意的是,使用同步机制可以保证多线程之间的安全性,但是也会降低程序的性能,因此在选择同步机制时需要权衡安全性和性能。同时,需要注意死锁、饥饿、性能瓶颈等问题,确保程序的正确性和性能。
3.2) 序性规则、可见性规则、原子性规则相关例子
下面是一个使用 Kotlin 编写的可见性问题程序:
class VisibilityProblem: Thread() {
private var flag = false
override fun run() {
while (!flag) {
// do something here
}
}
fun stopThread() {
flag = true
}
}
fun main() {
val visibilityProblem = VisibilityProblem()
visibilityProblem.start()
// 休眠一段时间后停止线程
Thread.sleep(1000)
visibilityProblem.stopThread()
}
这个程序中,VisibilityProblem 类继承自 Thread 类,重写了 run 方法,并在其中使用了一个 flag 变量来控制循环的终止。在 main 函数中,创建了一个 VisibilityProblem 的实例,并在其启动后休眠 1 秒钟后再调用 stopThread 方法来停止线程。然而,由于 flag 变量没有加上 volatile 关键字修饰,因此在多线程环境下,有可能会出现可见性问题,导致线程无法被停止。
解决这个可见性问题,可以使用 volatile 关键字来修饰 flag 变量,从而保证其在多线程环境下的可见性。修改后的程序如下所示:
class VisibilitySolution: Thread() {
@Volatile
private var flag = false
override fun run() {
while (!flag) {
// do something here
}
}
fun stopThread() {
flag = true
}
}
fun main() {
val visibilitySolution = VisibilitySolution()
visibilitySolution.start()
// 休眠一段时间后停止线程
Thread.sleep(1000)
visibilitySolution.stopThread()
}
在这个程序中,flag 变量前加上了 @Volatile 注解,使其成为一个 volatile 变量。这样,当一个线程修改了 flag 变量的值后,其它线程可以立即看到这个修改,从而保证了程序在多线程环境下的正确性。
下面是一个使用 Kotlin 编写的原子性问题程序:
class AtomicityProblem: Thread() {
private var count = 0
override fun run() {
for (i in 1..10000) {
count++
}
}
fun getCount(): Int {
return count
}
}
fun main() {
val threads = mutableListOf<AtomicityProblem>()
// 创建 10 个线程并启动
for (i in 1..10) {
val thread = AtomicityProblem()
thread.start()
threads.add(thread)
}
// 等待所有线程执行完毕
for (thread in threads) {
thread.join()
}
// 输出结果
println("count = ${threads[0].getCount()}")
}
这个程序中,AtomicityProblem 类继承自 Thread 类,重写了 run 方法,并在其中使用了一个 count 变量来记录循环次数。在 main 函数中,创建了 10 个 AtomicityProblem 的实例,并在它们启动后等待它们执行完毕。最后,输出其中一个实例的 count 变量的值。然而,由于 count 变量没有做任何同步措施,因此在多线程环境下,有可能会出现原子性问题,导致最终输出的结果不是预期的 100000。
解决这个原子性问题,可以使用 Kotlin 标准库提供的 AtomicInteger 类,从而保证对 count 变量的操作具有原子性。修改后的程序如下所示:
import java.util.concurrent.atomic.AtomicInteger
class AtomicitySolution: Thread() {
private var count = AtomicInteger(0)
override fun run() {
for (i in 1..10000) {
count.incrementAndGet()
}
}
fun getCount(): Int {
return count.get()
}
}
fun main() {
val threads = mutableListOf<AtomicitySolution>()
// 创建 10 个线程并启动
for (i in 1..10) {
val thread = AtomicitySolution()
thread.start()
threads.add(thread)
}
// 等待所有线程执行完毕
for (thread in threads) {
thread.join()
}
// 输出结果
println("count = ${threads[0].getCount()}")
}
这段代码是一个使用AtomicInteger类实现原子性的示例程序。该程序创建了10个线程,并将它们启动,每个线程执行了10000次原子递增操作。最后,主线程等待所有线程执行完毕,然后输出共享变量count的值。
具体来说,该程序定义了一个继承自Thread类的AtomicitySolution类,该类包含一个AtomicInteger类型的count变量和一个run()方法。run()方法使用AtomicInteger类的incrementAndGet()方法对count变量进行10000次原子递增操作。同时,该类还包含一个getCount()方法,用于获取count变量的值。
在main()函数中,该程序创建了10个AtomicitySolution对象,并将它们添加到一个线程列表中。然后,它启动所有线程,并等待它们执行完毕。最后,它输出线程列表中第一个元素的count变量值。
由于AtomicInteger类确保了多个线程同时访问共享变量时的原子性,因此该程序可以保证count变量的值在多线程环境下是正确的。
下面是一个使用volatile关键字解决有序性问题的示例程序:
class OrderingSolution : Thread() {
@Volatile private var ready: Boolean = false
private var number: Int = 0
override fun run() {
number = 42
ready = true
}
fun printNumber() {
while (!ready) {
Thread.sleep(1)
}
println("number = $number")
}
}
fun main() {
val thread = OrderingSolution()
thread.start()
thread.printNumber()
}
该程序定义了一个继承自Thread类的OrderingSolution类,该类包含一个使用volatile关键字修饰的Boolean类型的ready变量和一个Int类型的number变量。在run()方法中,number被赋值为42,然后ready被设置为true。在printNumber()方法中,程序循环检查ready变量是否为true,如果为true,则输出number变量的值。
使用volatile关键字可以解决多线程环境下的有序性问题。在该示例程序中,ready变量使用volatile关键字修饰,这意味着当一个线程修改了该变量的值时,其他线程可以立即看到这个变化。在printNumber()方法中,程序会循环检查ready变量的值,确保在读取number变量的值之前ready已经被设置为true。
使用volatile关键字可以保证在多线程环境下,变量的读取和写入操作是有序的,因此可以避免由于指令重排等原因导致的有序性问题。
多线程工具包
- Executor框架:Executor框架是Java多线程编程的核心框架之一,提供了线程池的管理和任务调度等功能,可以方便地管理和控制多线程任务的执行。
- Lock框架:Lock框架是Java多线程编程中用于实现锁机制的核心框架之一,提供了比synchronized更加灵活和精细的锁机制,支持可重入锁、读写锁、公平锁、非公平锁等。
- CountDownLatch:CountDownLatch是Java多线程编程中用于线程同步的工具类之一,可以用来等待一个或多个线程完成任务后再继续执行。
- CyclicBarrier:CyclicBarrier是Java多线程编程中用于线程同步的工具类之一,可以用来等待多个线程都达到某个状态后再继续执行。
- Semaphore:Semaphore是Java多线程编程中用于控制并发访问的工具类之一,可以用来限制同时访问某个共享资源的线程数量。
- BlockingQueue:BlockingQueue是Java多线程编程中用于线程间通信的工具类之一,可以用来实现生产者消费者模式。
- Future和Callable:Future和Callable是Java多线程编程中用于异步计算的机制之一,可以用来提交一个任务,并返回一个Future对象,用来获取任务执行的结果。