Java多线程

多线程介绍

多线程是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方法关闭线程池。

线程池的好处如下:

  1. 重用线程。由于线程池中的线程是可重用的,因此可以避免创建和销毁线程的开销。

  2. 控制线程数量。通过创建一个固定大小的线程池,可以控制并发线程的数量,避免资源过度占用和线程饥饿。同时,线程池也能够根据任务的数量自动扩展或收缩线程池大小。

  3. 提高响应速度。由于线程池中的线程已经创建并在运行中,因此可以快速响应任务请求,没有创建线程和等待线程启动的开销。

线程池的具体使用方法和参数设置:

  1. 创建一个线程池对象

线程池的创建是通过Executor框架来实现的。可以使用以下方法进行线程池的创建:

  1. Executors.newFixedThreadPool(nThreads):创建一个固定大小的线程池,线程数量固定为nThreads。
  2. Executors.newCachedThreadPool():创建一个缓存的线程池,线程数根据需要而生成,不限制线程数量。当线程空闲时,它们会被保留在线程池中,并且将在以后的某个时候重用。
  3. Executors.newSingleThreadExecutor():创建一个单线程的线程池,它保证所有任务都在同一线程中按顺序执行。
  1. 提交任务至线程池中

当线程池被创建后,我们需要将多个任务提交至线程池中。有两种方式可以实现:

  1. execute()方法:将一个Runnable对象提交至线程池中,并通过线程池中的一个线程来执行该任务。
  2. submit()方法:与execute()方法类似,不同的是它会返回一个Future对象,可以通过该对象获取任务执行的结果。
    例如,在前面的例子中,我们使用了execute()方法将多个任务提交至线程池中。
  1. 设置线程池参数

线程池的执行参数可通过Executor框架的ThreadPoolExecutor类来设置。ThreadPoolExecutor类提供了很多可控制的参数,包括线程数、线程等待时间、线程池大小、回收时间等。具体的参数含义和设置方式可以参考Java官方文档。

例如,我们可以通过以下方式来创建可控制参数的线程池对象:

            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    corePoolSize, // 线程池中的基本线程数
                    maximumPoolSize, // 线程池中的最大线程数
                    keepAliveTime, // 线程池中的线程等待时间
                    unit, // keepAliveTime参数的时间单位
                    workQueue, // 执行任务的阻塞队列
                    threadFactory, // 创建线程的工厂对象
                    handler // 线程池中任务的拒绝策略
            );
  1. 关闭线程池

当线程池不再需要使用时,需要手动调用shutdown()方法来关闭线程池。该方法将停止接受新的任务,并等待现有的任务完成后关闭线程池。

多线程的运行状态:

  1. 新建状态(New):线程对象创建后,就处于新建状态。此时,它还没有被启动,没有运行任何代码。

  2. 运行状态(Runnable):当线程对象调用start()方法后,线程进入到runnable状态。在这个状态下,线程等待获取CPU资源,执行run()方法中的代码。

  3. 阻塞状态(Blocked):当一个线程在等待某个资源时,它将会进入到阻塞状态,分为三种情况:

1 等待阻塞:运行的线程执行wait()方法,线程会释放占用的资源并等待notify或者notifyAll()方法的唤醒。
2 同步阻塞:线程在获取同步锁时,若该锁已被其他线程占用,则当前线程会进入同步阻塞状态。
3 其他阻塞:通过调用线程的sleep()或者join()方法,或者等待IO操作结束,当前线程会进入其他阻塞状态。

  1. 等待状态(Waiting):线程在以下情况下进入等待状态:

1 调用wait()方法,线程进入到等待状态,等待被唤醒;
2 调用了带超时时间的wait()方法,等待超时;
3 调用了join()方法,在其他线程结束之前等待。

  1. 终止状态(Terminated):线程执行完了run()方法中的所有代码,或者发生了未捕获异常,线程就处于终止状态。

多线程之线程安全

Java多线程中的线程安全问题是开发中需要注意的重要问题之一。线程安全是指多线程并发访问同一资源时,不会出现数据竞争和不一致的问题。在多线程并发访问同一资源时,我们需要保证线程安全,否则可能会引发一些严重的问题,如死锁、数据不一致等。

为了保证Java多线程下的线程安全,我们可以采用如下常用的方式:

  1. synchronized关键字

synchronized是Java中的关键字,可以将代码块或方法声明为同步代码块或同步方法,确保在任意时刻,最多只有一个线程执行该代码块或方法。使用synchronized关键字可以保证线程安全,从而避免了数据竞争和不一致的问题。

  1. Lock对象

除了使用synchronized关键字,Java还提供了Lock对象,可以通过Lock对象来实现线程之间的同步。与synchronized关键字不同,Lock对象提供了更细粒度的控制,使我们可以更加精确地控制线程同步,从而增强应用程序的性能。

  1. AtomicInteger类

Java提供了AtomicInteger类,可以用于在多线程环境下保证整数变量的操作是原子性的。AtomicInteger类是线程安全的,使用它可以不需要使用synchronized关键字或Lock对象来保证线程安全。

  1. volatile关键字

Java提供了volatile关键字,可以保证对于某个变量的读操作和写操作都是原子操作,从而保证线程安全。使用volatile关键字可以确保多个线程之间对于某个共享变量的操作都是可见的。

  1. 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类作为数据结构而实现的方案是最为简洁明了并且最容易扩展的,若希望针对该计数器实现更为复杂的逻辑,可以优先选择该方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值