Java多线程详解(附详细代码演示)

本文介绍了Java多线程的基础概念,包括并发与并行的区别、进程与线程的关系,并详细讲解了创建线程的方法及线程的操作如暂停、加入和中断等。此外,还探讨了并发问题及其解决方案,包括使用锁、原子类、volatile关键字等技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java多线程
一、关键概念与术语
1.1 并发与并行

并行:同一时刻,多个线程都在执行,这就需要计算机具有多核CPU,不同的CPU执行不同的线程。

并发:同一时刻,只有一个线程在执行,但在一段时间内多个线程都被执行。由于CPU切换线程较快,上下文切换,用户感觉不到,似乎多个线程同时进行。

1.2 进程与线程

进程:进程是一个程序的一次运行,同一程序运行多次会产生多个进程,进程是系统进行资源分配和调度的基本单位(CPU资源除外)。

线程:线程是进程的一个执行路径,一个进程中至少有一个线程(main线程),线程是CPU分配和调度的基本单位。

在Java程序一运行,相当于开启了一个JVM进程,其中Main函数就是其中的一个线程,GC也是一个线程。合理利用多线程,可以充分使用多核CPU。我们可以在程序中,查看当前活跃线程数和可用处理器个数,代码如下所示:

public class Main {
    public static void main(String[] args) {
        System.out.println(Thread.activeCount()); //查看当前活跃线程数量
        System.out.println(Runtime.getRuntime().availableProcessors()); //查看可用处理器数
    }
}

执行结果:两个线程为Main线程和GC线程。

在这里插入图片描述

二、创建线程的方法

创建线程的方法,一般有三种。

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,实现run()方法。

  • 实现Callable接口,实现run()方法。

​ 这里不推荐使用继承Thread类的方法,因为Java是单继承机制,继承了Thread类,就不能继承其它类,代码复用效果较差。Java是多实现机制,可以实现多个接口,推荐使用实现接口的方法,它们的差异在于Runnable接口无返回值,Callable接口有返回值。下面,以实现Runnable接口为例,展示多线程的使用:

/**
 * @author tqwstart
 * @creat 2022-08-12 11:03
 */
public class DownloadFileTask implements Runnable{
    //创建线程的方法之一,创建下载文件的线程,实现Runnable接口,并重写run方法,将类实例传递给Thread方法
    @Override
    public void run() {
        //线程需要做的事情,放在run方法里。
        System.out.println("Downloading File ......:"+Thread.currentThread().getName());
    }
}
/**
 * @author tqwstart
 * @creat 2022-08-12 10:56 测试多线程的使用
 */
public class ThreadDemo {
    public static void show(){
        System.out.println(Thread.currentThread().getName());//查看当前线程的名字
        for(int i=0;i<10;i++){ //创建10个线程
            Thread thread = new Thread(new DownloadFileTask());
            thread.start();//注意启动线程需要调用start()方法,不是run()方法
        }
    }
}

执行结果如下:

在这里插入图片描述

三、线程的操作
3.1 线程的暂停(sleep)

​ 有时候线程的操作会耗费一定的时间,例如前文提到的下载文件线程,文件的下载需要耗费一定的时间。Thread下的sleep()方法,可以暂停线程一段时间模拟文件下载,这个方法会抛出一个中断异常,需要try-catch处理,如下所示:

/**
 * @author tqwstart
 * @creat 2022-08-12 11:03
 */
public class DownloadFileTask implements Runnable{
    //创建线程的方法之一,实现Runnable接口,并重写run方法,将类实例传递给Thread方法
    @Override
    public void run() {
        //线程需要做的事情,放在run方法里。
        System.out.println("Downloading File ......:"+Thread.currentThread().getName());
        try {
            Thread.sleep(5000); //暂停线程一段时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Downloaded File "+Thread.currentThread().getName());
    }
}
3.2 线程的加入(join)

​ 上面提到的下载文件线程由于网络拥塞等原因,下载时间可能不确定。后续的病毒扫描线程,需要在下载文件线程完成后才能进行。这里不能使用简单的sleep()来估计下载时间,可以使用join()方法,它是一个阻塞方法,在主线程中加入下载文件线程,会阻塞主线程,代码如下:

public class ThreadDemo {
    public static void show(){
            Thread thread = new Thread(new DownloadFileTask());
            thread.start();
        try {
            thread.join(); //由于文件下载耗费的时间不定,不能使用sleep()方法,使得线程睡眠。可以使用join方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("File is ready to be scanned.");//文件下载线程完成后,才进行后续病毒扫描操作。
    }
}
3.3 线程的中断(interrupt)

​ 在长时间的线程任务执行中,我们想在某一时刻中断某个线程。可以考虑使用interrupt()方法,但是光使用interrupt()方法,不能让线程中断。它只是向线程发送中断请求,需要任务线程时刻监测是否收到中断请求,与isInterrupt()联合使用,完成线程的中断。代码如下所示:

public class DownloadFileTask implements Runnable{
    @Override
    public void run() {
        System.out.println("Downloading a File ......:"+Thread.currentThread().getName());
        for(int i=0;i<Integer.MAX_VALUE;i++){
            if(Thread.currentThread().isInterrupted()) return; // 监测是否收到interrupt()请求
            System.out.println("Downloading byte "+i);
        }
        System.out.println("Download complete "+Thread.currentThread().getName());
    }
}
public class ThreadDemo {
    public static void show(){
            Thread thread = new Thread(new DownloadFileTask());
            thread.start();
        try {
            Thread.sleep(1000);//下载文件线程执行1秒后中断
        } catch (Exception e) {
            e.printStackTrace();
        }
        thread.interrupt();

    }
}

执行结果:

在这里插入图片描述

四、并发问题初探
4.1 并发问题的引入

场景假设:假设10个线程并发进行文件下载任务,共同维护下载文件的总字节数变量。各线程拥有共同访问变量,是发生竞态的前提条件,代码如下所示:

//单个线程的任务
public class DownloadFileTask implements Runnable{
    private DownloadStatus status;
    public DownloadFileTask(DownloadStatus status){
        this.status = status;
    }
    @Override
    public void run() {
        for(int i=0;i<10_000;i++){//每个线程下载1万字节的任务
            if(Thread.currentThread().isInterrupted()) return; 
           status.incrementTotalBytes();
        }
        System.out.println("Download complete "+Thread.currentThread().getName());
    }
}
public class DownloadStatus {
    //下载状态类,多线程共同访问
    private int totalBytes;
    public int getTotalBytes(){
        return totalBytes;
    }
    public void incrementTotalBytes(){
        totalBytes++;//非原子操作,从主存中读取,CPU使得值加1,再赋值,可分为3个步骤。原子操作,不可分。
    }
}
public class ThreadDemo {
    public static void show(){
        DownloadStatus status = new DownloadStatus();
        List<Thread> threads = new ArrayList<>();
        for(int i=0;i<10;i++){
            Thread thread = new Thread(new DownloadFileTask(status));
            thread.start();
            threads.add(thread);
        }
        for(Thread thread:threads){
            try {
                thread.join(); //等待十个线程均下载完毕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(status.getTotalBytes()); //统计下载总字节数,应该为10万字节
    }
}

执行结果:

在这里插入图片描述

4.2 并发问题的解决

预防线程安全问题的策略:

  1. 取消共享资源,让线程拥有各自资源,最后统计相加。
  2. 使用不可变对象,只能读取,不能修改。
  3. 使用锁进行同步处理,预防死锁。
  4. 使用原子类。
  5. 使用分段锁。
4.3 取消共享资源解决并发问题

​ 在并发问题中,如果能通过代码逻辑的改变,从而取消共享资源,就可以解决并发问题。上文所提到的多线程下载文件,计算总下载字节数量就可以用这个策略解决。

策略1(取消共享资源)的代码演示:

//取消共同访问的对象DownloadStatus,让各打印任务线程拥有各自的DownloadStatus对象。
public class ThreadDemo {
    public static void show(){
        List<DownloadFileTask> tasks = new ArrayList<>();
        List<Thread> threads = new ArrayList<>();
        for(int i=0;i<10;i++){
            DownloadFileTask task=new DownloadFileTask();
            tasks.add(task);
            Thread thread = new Thread(task);
            thread.start();
            threads.add(thread);
        }
        for(Thread thread:threads){
            try {
                thread.join(); //等待十个线程均下载完毕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Integer totalBytes = tasks.stream().
                map(t -> t.getStatus().getTotalBytes())
                .reduce(0, (a, b) -> a + b);
        System.out.println(totalBytes ); //统计下载总字节数,应该为10万字节
    }
}
public class DownloadFileTask implements Runnable{
    private DownloadStatus status;
    public DownloadFileTask(){
        status = new DownloadStatus(); //让各线程拥有各自的DownloadStatus变量
    }
    @Override
    public void run() {
        for(int i=0;i<10_000;i++){//每个线程下载1万字节的任务
            if(Thread.currentThread().isInterrupted()) return;
           status.incrementTotalBytes();
        }
        System.out.println("Download complete "+Thread.currentThread().getName());
    }

    public DownloadStatus getStatus() {
        return status;
    }
}

执行结果:

在这里插入图片描述

4.4 使用同步机制解决并发问题

​ 同步机制是保证多线程的环境下,使用同步代码块的地方或者lock()之后,同一时间,只能有单个线程访问。同步的方法,是使用synchronized关键字或Lock锁实现。

策略3(使用锁同步)代码演示:

//方法一:代码与并发问题初探时代码一致,这是在DownloadStatus代码中,使用Lock锁做了同步处理。
public class DownloadStatus {
    //下载状态类,多线程共同访问
    private int totalBytes;
    private Lock lock = new ReentrantLock();
    public int getTotalBytes(){
        return totalBytes;
    }
    public void incrementTotalBytes(){
        lock.lock();
        try {
            totalBytes++;//非原子操作,从主存中读取,CPU使得值加1,再赋值,可分为3个步骤。原子操作,不可分。
        } finally {
            lock.unlock(); //防止死锁,保证解锁一定进行。
        }
    }
}
方法二:使用synchronized关键字做同步处理,使用synchronized关键字没有显式加锁与解锁,看起来更简单。
public class DownloadStatus {
    //下载状态类,多线程共同访问
    private int totalBytes;
    private int totalFiles;
    private Object totalBytesLock = new Object();
    private Object totalFilesLock = new Object();
    public int getTotalBytes(){
        return totalBytes;
    }
    public void incrementTotalBytes(){
        synchronized(totalBytesLock) {
            totalBytes++;//非原子操作,从主存中读取,CPU使得值加1,再赋值,可分为3个步骤。原子操作,不可分。
        }
    }
    public void incrementTotalFiles(){
        synchronized(totalFilesLock) {
            totalFiles++; //这里的synchronized的监视器对象,不推荐使用this,
            //(需要注意的是,不使用同步代码块,直接使用同步方法,与使用this功能一致)
            // 可能导致不同的并发方法,不能够同时进行。推荐使用专用监视器对象,不同的并发方法
            //使用不同的监视器。
        }
    }
}
4.5 使用原子类解决并发问题

​ 并发问题的产生,由于操作的非原子性造成的,原子操作不需要做同步处理,同时是并发安全的。

策略4(使用原子类),代码演示:

public class ThreadDemo {
    public static void show(){
        DownloadStatus status = new DownloadStatus();
        List<Thread> threads = new ArrayList<>();
        for(int i=0;i<10;i++){
            Thread thread = new Thread(new DownloadFileTask(status));
            thread.start();
            threads.add(thread);
        }
        for(Thread thread:threads){
            try {
                thread.join(); //等待十个线程均下载完毕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(status.getTotalBytes()); //统计下载总字节数,应该为10万字节
    }
}
public class DownloadStatus {
    private AtomicInteger totalBytes=new AtomicInteger();//使用原子类
    public int getTotalBytes(){
        return totalBytes.get();
    }
    public void incrementTotalBytes(){
            totalBytes.getAndIncrement();//原子操作,不可分。
    }
}

​ 上面的代码我们使用AtomicInteger原子类实现,原子类会导致吞吐量减少。维护一个经常加减的变量,可以考虑使用加法器LongAdder代替AtomicInteger原子类。

4.6 volatile机制

​ volatile的使用:一般来说,synchronized关键字同步效果带来的CPU开销比较大,在多个线程并发的某些特殊情况下。可以考虑volatile关键字的使用,在多个线程读数据,一个线程写数据的场合下可以使用。volatile可以使读取数据时,保证用volatile修饰的字段为最新值,解决了可见性的问题,代码如下所示:

public class ThreadDemo {
    public static void show(){
        DownloadStatus status = new DownloadStatus();
        Thread thread1 = new Thread(new DownloadFileTask(status));
        Thread thread2 = new Thread(()->{
           while(!status.isDone()){} //线程2如果不获取新值,在线程1完成任务后,线程2会一直阻塞在while循环中
            System.out.println(status.getTotalBytes());
        });
        thread1.start();
        thread2.start();
    }
}
public class DownloadStatus {
    private volatile boolean isDone; //使用volatile修饰,使得线程2可以获取最新值 
    private int totalBytes;
    private Object totalBytesLock = new Object();
    public int getTotalBytes(){
        return totalBytes;
    }
    public void incrementTotalBytes(){
        synchronized(totalBytesLock) {
            totalBytes++;
        }
    }
    public  boolean isDone() {
        return isDone;
    }
    public  void Done() {
        isDone = true;
    }
}
public class DownloadFileTask implements Runnable{
    private DownloadStatus status;
    public DownloadFileTask(DownloadStatus status){
        this.status = status;
    }
    @Override
    public void run() {
        System.out.println("Downloading a file: "+Thread.currentThread().getName());
        for(int i=0;i<1_000_000;i++){//每个线程下载100万字节的任务
            if(Thread.currentThread().isInterrupted()) return;
           status.incrementTotalBytes();
        }
        status.Done(); //下载完成之后,通知status状态改变
        System.out.println("Download complete "+Thread.currentThread().getName());
    }
}
4.7 集合中的线程安全问题

​ 我们在集合中经常使用的ArrayList和HashMap等类,都是线程不安全的。在并发的条件下,需要使用并发安全的集合相关类,场景演示代码如下所示:

public class ThreadDemo {
    public static void show(){
      Collection<Integer> collection = new ArrayList();
        Thread thread1 = new Thread(()->{
          collection.addAll(Arrays.asList(1,2,3));//往集合中添加1,2,3元素
        });
        Thread thread2 = new Thread(()->{
            collection.addAll(Arrays.asList(4,5,6));//往集合中添加4,5,6元素
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(collection); //集合结果不一定是1,2,3,4,5,6
    }
}

执行结果:

在这里插入图片描述

解决方法是,使用Collections工具类得到线程安全的集合类:

    Collection<Integer> collection = Collections.synchronizedCollection(new ArrayList());
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值