多线程

197 阅读12分钟

1 多线程简介

多线程(Multithreading)指的是在单个程序中同时执行多个线程,每个线程可以独立运行,拥有自己的执行堆栈和程序计数器,但是它们共享相同的内存空间。

多线程的优点是可以提高程序的执行效率和响应速度。在单个线程中,当程序执行到某个耗时的操作时,程序将被阻塞,不能执行其它任务,从而降低了程序的效率。而使用多线程,可以将耗时的操作放在一个线程中执行,而其它线程可以继续执行其它任务,从而提高了程序的效率。

多线程常用于实现并发编程、异步编程等场景,如网络编程、图形界面编程、并行计算等。

Java是一种面向对象的编程语言,提供了强大的多线程支持。Java多线程编程可以使用Thread类或Runnable接口来创建线程,也可以使用线程池来管理线程。同时,Java还提供了synchronized关键字、volatile关键字、Lock对象等机制来保证多线程并发访问的线程安全性。

虽然多线程能够提高程序效率和响应速度,但也会带来一些问题,如线程安全问题、死锁问题、竞争问题等,因此在多线程编程时需要特别注意这些问题。

1.1) 多线程的基本属性、方法、生命周期

  1. 线程属性:线程有自己的ID、状态、优先级、名称、线程组等属性。
  2. 线程方法:线程有创建、启动、休眠、恢复、等待、中断、加入等方法。
  3. 线程生命周期:线程从创建到销毁的整个过程称为线程生命周期,它包括以下几个阶段:
  • 新建(New):线程被创建后,它处于新建状态。

  • 就绪(Runnable):当线程启动时,它进入就绪状态,等待CPU调度。

  • 运行(Running):当线程获得CPU时间片后,它进入运行状态,执行run()方法中的代码。

  • 阻塞(Blocked):线程在等待某些资源时,进入阻塞状态。

  • 等待(Waiting):线程在等待某个条件时,进入等待状态。

  • 超时等待(Timed Waiting):线程在等待某个条件时,可以指定等待的时间,超时后进入就绪状态。

  • 终止(Terminated):当线程执行完run()方法后,或者因异常或其他原因退出run()方法,线程进入终止状态。

    线程生命周期图

在多线程编程中,需要注意线程安全问题,如共享变量的访问、同步、锁、原子性等。同时也需要注意死锁、竞态条件等问题,保证程序的正确性和性能。

2 多线程创建方式

  1. 继承Thread类,重写run()方法。这种方式是最基本的创建多线程的方式,需要定义一个类继承Thread类,并重写run()方法,然后实例化该类并调用start()方法来启动线程。

    class MyThread extends Thread {
        @Override
        public void run() {
            // 执行任务
        }
    }
    ​
    // 实例化并启动线程
    MyThread thread = new MyThread();
    thread.start();
    
  2. 实现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();
    ​
    
  3. 使用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) :在多线程环境下,多个线程可能同时访问共享的变量,因此需要保证对共享变量的读写操作是原子性的。也就是说,一个线程在修改共享变量的值时,必须要完成整个操作,而不能被其它线程打断。

为了保证有序性规则、可见性规则、原子性规则,可以使用以下方法:

  1. 使用synchronized关键字或Lock对象来实现同步,保证线程访问共享变量的互斥性,从而保证原子性。
  2. 使用volatile关键字来修饰共享变量,保证线程对共享变量的修改对其它线程是可见的。
  3. 使用java.util.concurrent包中提供的工具类,如AtomicInteger、CountDownLatch等,保证多线程访问共享变量的原子性和可见性。
  4. 在多线程环境下,尽量避免使用静态变量、全局变量等共享变量,尽可能地将变量的作用域限定在方法内部,从而避免竞争和并发问题。

3.1) volatile关键字、synchronized关键字、锁

  1. volatile关键字:volatile关键字可以保证多线程之间的可见性和有序性。使用volatile修饰的变量在每次读写时都会从主内存中刷新,因此可以避免由于指令重排等原因导致的有序性问题。但是,volatile并不能保证原子性,因此在需要保证原子性的场合,还需要使用其他机制,如synchronized或Lock。
  2. synchronized关键字:synchronized关键字可以保证多线程之间的互斥访问。它可以修饰方法、代码块或者类。当一个线程获取了对象锁,其他线程就必须等待该线程释放锁后才能获取锁进入临界区。使用synchronized关键字可以避免由于竞态条件而导致的数据不一致等问题。
  3. :锁是一种更加灵活的多线程同步机制。在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关键字可以保证在多线程环境下,变量的读取和写入操作是有序的,因此可以避免由于指令重排等原因导致的有序性问题。

多线程工具包

  1. Executor框架:Executor框架是Java多线程编程的核心框架之一,提供了线程池的管理和任务调度等功能,可以方便地管理和控制多线程任务的执行。
  2. Lock框架:Lock框架是Java多线程编程中用于实现锁机制的核心框架之一,提供了比synchronized更加灵活和精细的锁机制,支持可重入锁、读写锁、公平锁、非公平锁等。
  3. CountDownLatch:CountDownLatch是Java多线程编程中用于线程同步的工具类之一,可以用来等待一个或多个线程完成任务后再继续执行。
  4. CyclicBarrier:CyclicBarrier是Java多线程编程中用于线程同步的工具类之一,可以用来等待多个线程都达到某个状态后再继续执行。
  5. Semaphore:Semaphore是Java多线程编程中用于控制并发访问的工具类之一,可以用来限制同时访问某个共享资源的线程数量。
  6. BlockingQueue:BlockingQueue是Java多线程编程中用于线程间通信的工具类之一,可以用来实现生产者消费者模式。
  7. Future和Callable:Future和Callable是Java多线程编程中用于异步计算的机制之一,可以用来提交一个任务,并返回一个Future对象,用来获取任务执行的结果。