多线程介绍
多线程是Java众多特性之一。在多线程程序中,线程是CPU分配资源的基本单位,多个线程可以同时运行。本篇文章将介绍Java多线程的创建方式、多个线程抢占CPU的过程以及多线程的底层代码。
多线程的创建方式
Java多线程有四种创建方式,分别为继承Thread类、实现Runnable接口、使用Callable和Future接口和配置线程池。下面分别介绍这四种创建方式的使用:
1. 继承Thread类
继承Thread类是Java多线程的最基本的方式,创建一个线程需要继承Thread类并实现run()方法,在启动线程时通过调用start()方法来调用run()方法。例如:
class MyThread extends Thread {
public void run() {
// 在此处编写线程代码
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
2. 实现Runnable接口
实现Runnable接口相比继承Thread类更加灵活,因为Java只允许单继承,而实现接口可以避免了这个限制。在实现Runnable接口时,需要在类中实现run()方法,在启动线程时,需要先创建Runnable对象,然后再通过调用Thread类的构造函数,将Runnable对象传递给Thread类。例如:
class MyRunnable implements Runnable {
public void run() {
// 在此处编写线程代码
}
}
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
}
3. 使用Callable和Future接口
使用Callable和Future接口相较于前两种方式更加灵活,因为它可以返回带有返回值的线程执行结果。在使用Callable和Future接口时,需要创建一个实现Callable接口的类,并实现call()方法,接着通过创建一个ExecutorService并调用submit()方法来执行Callable对象。例如:
class MyCallable implements Callable<String> {
public String call() {
return "Hello World";
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
String result = future.get();
}
4.线程池
使用线程池来创建多线程是一种比较优秀的方式,因为在实际的多线程应用场景中,线程的创建和销毁是非常消耗资源的,使用线程池可以避免这一问题。
下面是一个使用线程池创建多线程的例子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
// 在线程池中执行多个任务
for (int i = 0; i < 100; i++) {
executor.execute(new MyTask(i));
}
// 关闭线程池
executor.shutdown();
}
static class MyTask implements Runnable {
int taskId;
public MyTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
}
}
}
以上代码中,我们使用了线程池的方式来执行多线程任务。首先,我们通过Executors工具类创建了一个固定大小为10的线程池,然后使用execute方法在线程池中执行100个任务,最后通过shutdown方法关闭线程池。
线程池的好处如下:
-
重用线程。由于线程池中的线程是可重用的,因此可以避免创建和销毁线程的开销。
-
控制线程数量。通过创建一个固定大小的线程池,可以控制并发线程的数量,避免资源过度占用和线程饥饿。同时,线程池也能够根据任务的数量自动扩展或收缩线程池大小。
-
提高响应速度。由于线程池中的线程已经创建并在运行中,因此可以快速响应任务请求,没有创建线程和等待线程启动的开销。
线程池的具体使用方法和参数设置:
- 创建一个线程池对象
线程池的创建是通过Executor框架来实现的。可以使用以下方法进行线程池的创建:
- Executors.newFixedThreadPool(nThreads):创建一个固定大小的线程池,线程数量固定为nThreads。
- Executors.newCachedThreadPool():创建一个缓存的线程池,线程数根据需要而生成,不限制线程数量。当线程空闲时,它们会被保留在线程池中,并且将在以后的某个时候重用。
- Executors.newSingleThreadExecutor():创建一个单线程的线程池,它保证所有任务都在同一线程中按顺序执行。
- 提交任务至线程池中
当线程池被创建后,我们需要将多个任务提交至线程池中。有两种方式可以实现:
- execute()方法:将一个Runnable对象提交至线程池中,并通过线程池中的一个线程来执行该任务。
- submit()方法:与execute()方法类似,不同的是它会返回一个Future对象,可以通过该对象获取任务执行的结果。
例如,在前面的例子中,我们使用了execute()方法将多个任务提交至线程池中。
- 设置线程池参数
线程池的执行参数可通过Executor框架的ThreadPoolExecutor类来设置。ThreadPoolExecutor类提供了很多可控制的参数,包括线程数、线程等待时间、线程池大小、回收时间等。具体的参数含义和设置方式可以参考Java官方文档。
例如,我们可以通过以下方式来创建可控制参数的线程池对象:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 线程池中的基本线程数
maximumPoolSize, // 线程池中的最大线程数
keepAliveTime, // 线程池中的线程等待时间
unit, // keepAliveTime参数的时间单位
workQueue, // 执行任务的阻塞队列
threadFactory, // 创建线程的工厂对象
handler // 线程池中任务的拒绝策略
);
- 关闭线程池
当线程池不再需要使用时,需要手动调用shutdown()方法来关闭线程池。该方法将停止接受新的任务,并等待现有的任务完成后关闭线程池。
多线程的运行状态:
-
新建状态(New):线程对象创建后,就处于新建状态。此时,它还没有被启动,没有运行任何代码。
-
运行状态(Runnable):当线程对象调用start()方法后,线程进入到runnable状态。在这个状态下,线程等待获取CPU资源,执行run()方法中的代码。
-
阻塞状态(Blocked):当一个线程在等待某个资源时,它将会进入到阻塞状态,分为三种情况:
1 等待阻塞:运行的线程执行wait()方法,线程会释放占用的资源并等待notify或者notifyAll()方法的唤醒。
2 同步阻塞:线程在获取同步锁时,若该锁已被其他线程占用,则当前线程会进入同步阻塞状态。
3 其他阻塞:通过调用线程的sleep()或者join()方法,或者等待IO操作结束,当前线程会进入其他阻塞状态。
- 等待状态(Waiting):线程在以下情况下进入等待状态:
1 调用wait()方法,线程进入到等待状态,等待被唤醒;
2 调用了带超时时间的wait()方法,等待超时;
3 调用了join()方法,在其他线程结束之前等待。
- 终止状态(Terminated):线程执行完了run()方法中的所有代码,或者发生了未捕获异常,线程就处于终止状态。
多线程之线程安全
Java多线程中的线程安全问题是开发中需要注意的重要问题之一。线程安全是指多线程并发访问同一资源时,不会出现数据竞争和不一致的问题。在多线程并发访问同一资源时,我们需要保证线程安全,否则可能会引发一些严重的问题,如死锁、数据不一致等。
为了保证Java多线程下的线程安全,我们可以采用如下常用的方式:
- synchronized关键字
synchronized是Java中的关键字,可以将代码块或方法声明为同步代码块或同步方法,确保在任意时刻,最多只有一个线程执行该代码块或方法。使用synchronized关键字可以保证线程安全,从而避免了数据竞争和不一致的问题。
- Lock对象
除了使用synchronized关键字,Java还提供了Lock对象,可以通过Lock对象来实现线程之间的同步。与synchronized关键字不同,Lock对象提供了更细粒度的控制,使我们可以更加精确地控制线程同步,从而增强应用程序的性能。
- AtomicInteger类
Java提供了AtomicInteger类,可以用于在多线程环境下保证整数变量的操作是原子性的。AtomicInteger类是线程安全的,使用它可以不需要使用synchronized关键字或Lock对象来保证线程安全。
- volatile关键字
Java提供了volatile关键字,可以保证对于某个变量的读操作和写操作都是原子操作,从而保证线程安全。使用volatile关键字可以确保多个线程之间对于某个共享变量的操作都是可见的。
- ConcurrentHashMap类
Java提供了ConcurrentHashMap类,可以用于在多线程环境下保证HashMap的安全性。ConcurrentHashMap类是线程安全的,使用它可以不需要使用synchronized关键字或Lock对象来保证线程安全。
下面用代码案例的方式进行说明它们的区别
1.创建操作对象
/**
* 多线程下示例对象
*/
public class Counter {
private int count;
/**
* 累计添加的方法
*/
public void increment() {
count++;
}
/**
* 获取最终结果
* @return
*/
public int getCount() {
return count;
}
}
2.创建调用对象
/**
* 调用多线程
*/
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
System.out.println("开始计算...");
// 创建10个线程调用increment()方法增加计数器的值
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println("线程:" + Thread.currentThread().getName() + "开始操作... 当前 count 值: " + counter.getCount());
for (int j = 0; j < 1000; j++) {
counter.increment();
}
}).start();
}
// 等待所有线程执行完毕
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出计数器的值(创建了10个线程每次去加1000次,最终需要得到的结果是: 10000)
System.out.println("最终结果: " + counter.getCount());
}
}
直接多线程下调用
可以明显看出出现了数据混乱。因为多个线程同时调用increment()方法时,都会对count变量进行加1操作,但由于操作不是原子操作,会出现多线程竞争的情况。例如,在初始状态下count为0,线程1执行increment()方法将count增加1时,线程2也执行increment()方法,由于线程1还没来得及返回count的值,线程2拿到的count仍为0,将其加1后返回,此时count的值只增加了1,而不是2。
synchronized关键字修饰
使用synchronized关键字保证数据安全。synchronized可以保证在同一时刻只有一个线程可以进入加锁的代码块,从而避免了多线程竞争的情况。在上述代码中,可以将increment()方法加上synchronized关键字:
/**
* 累计添加的方法
*/
public synchronized void increment() {
count++;
}
最终结果如下:
Lock对象
使用Lock对象也可以保证数据安全。Lock对象本质上也是一种锁,不同的是它可以实现更多的并发控制功能。在上述代码中,可以声明一个Lock对象,然后在increment()方法中通过lock()方法加锁,在finally中通过unlock()方法释放锁:
private int count;
private Lock lock = new ReentrantLock();
/**
* 累计添加的方法
*/
public synchronized void increment() {
try {
lock.lock();
count++;
} finally {
lock.unlock();
}
}
最终结果如下:
AtomicInteger类
AtomicInteger类是Java提供的一种线程安全的整数处理类,在多线程情况下可以保证操作的原子性。可以将count的类型改为AtomicInteger,在increment()方法中调用AtomicInteger类的相应方法实现原子加1操作:
private AtomicInteger count = new AtomicInteger(0);
/**
* 累计添加的方法
*/
public synchronized void increment() {
count.incrementAndGet();
}
/**
* 获取最终结果
* @return
*/
public int getCount() {
return count.get();
}
最终结果如下:
volatile关键字
volatile关键字可以保证数据在多线程之间的可见性,当一个线程修改了volatile变量的值时,其他线程能够立即看到该值的改变。在上述代码中可以将count声明为volatile类型:
private volatile int count;
最终结果如上一致
ConcurrentHashMap类
ConcurrentHashMap类是Java提供的一种线程安全的HashMap实现类,可以保证在多线程情况下对HashMap的操作是线程安全的。在上述代码中,可以将Counter类修改为:
public class Counter {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
/**
* 累计添加的方法
*/
public void increment() {
map.compute(Thread.currentThread().getName(), (k, v) -> v == null ? 1 : v + 1);
}
/**
* 获取最终结果
* @return
*/
public int getCount() {
int sum = 0;
for (Integer val : map.values()) {
sum += val;
}
return sum;
}
}
在increment()方法中,使用ConcurrentHashMap类的compute()方法来实现计数器的递增,使用线程名作为key值,在getCount()方法中遍历ConcurrentHashMap,将所有值相加得到总的计数器值。
最终结果
在多线程情况下,对于上面所列出的代码案例,代码中的计数器会出现数据不安全的情况。因为多个线程同时对计数器进行修改,由于操作不是原子操作,会出现多线程竞争的情况,即使最终的输出结果并不符合预期。
为了保证在多线程环境下对计数器的操作是线程安全的,可以采用上面的五种方案。其中,使用ConcurrentHashMap类作为数据结构而实现的方案是最为简洁明了并且最容易扩展的,若希望针对该计数器实现更为复杂的逻辑,可以优先选择该方案。