当前位置:   article > 正文

【Java多线程的使用详解】_java 多线程使用

java 多线程使用


前言

古有愚公移山,愚公一人移山需耗时多年。但是如果有十个百个愚公一起移山,那么效率将大大提升。而多线程就是由单线程演变来的一种并发处理技术,可以使多个任务并行,提高工作效率。


注意: 此处所有的代码都是基于JDK 1.8

一、多线程是什么

多线程从字面含义上来说就是多个线程。在实际应用当中,多线程可以用来解决一些并发问题,例如:抢红包、抢车票等功能都需要用到多线程实现。

二、使用方法

1.继承Thread类

继承 Thread 类需要重写 run 方法

package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:54
 */
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": MyThread is running");    }
}


package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");


        MyThread myThread = new MyThread();
        // 启动线程执行
        myThread.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

输出结果: 可以观察到执行的线程 一个是主线程main,而另一个是我们继承Thread类创建的线程

main: ThreadTestDemo is running
Thread-0: MyThread is running
  • 1
  • 2

2.实现Runnable接口

实现 Runnable 接口,需要重写run方法

package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:54
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
		System.out.println(Thread.currentThread().getName() + ": MyRunnable.run");
    }
}


package com.curtis.demo.use;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");
		
		// 启动线程
        new Thread(new MyRunnable()).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

输出结果:

main: ThreadTestDemo is running
Thread-0: MyRunnable.run
  • 1
  • 2

3.实现Callable接口

实现 Callable 接口需要实现call方法,这种方式可以获取到多线程执行结束后的返回结果。通过 FutureTaskget 方法阻塞获取线程执行结果。

package com.curtis.demo.use;

import java.util.concurrent.Callable;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName() + ": myCallable result";
    }
}


package com.curtis.demo.use;

import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author Curtis
 * @since 2024-04-18 20:51
 */
public class ThreadTestDemo {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": ThreadTestDemo is running");
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        // 启动线程
        new Thread(futureTask).start();
        try {
        	// 获取任务结果
            System.out.println(futureTask.get());
        } catch (InterruptedException | ExecutionException e) {
            System.out.println(Arrays.toString(e.getStackTrace()));
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

执行结果:

main: ThreadTestDemo is running
Thread-0: myCallable result
  • 1
  • 2

4.线程安全问题

例如简单的抢票问题,开启3个线程抢100张票,不能超卖或者买到同一张票

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:56
 */
public class SyncTaskDemo implements Runnable {

    private int ticket = 1;

    @Override
    public void run() {
        while (true) {
            if (ticket > 100) {
                break;
            }

            System.out.println(Thread.currentThread().getName() + ": " + ticket);
            ticket++;
        }
    }
}

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:58
 */
public class SyncThreadTestDemo {

    public static void main(String[] args) {

        SyncTaskDemo syncTaskDemo = new SyncTaskDemo();

        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

结果其实跟预期料想的不同,会出现超卖和买到同一张票的情况
在这里插入图片描述

5.线程安全解决

线程安全问题一般是由于原子性、可见性、有序性其中之一导致的,而我们的代码当中是由于多线程抢占 ticket,并且对于 ticket++ 的操作其实并不是原子性的。这个时候我们需要对于抢票的动作进行加锁,确保同一时间内,仅有一个线程在抢票。

通过 Synchronized 关键字解决

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:56
 */
public class SyncTaskDemo implements Runnable {

    private int ticket = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 100) {
                    break;
                }

                System.out.println(Thread.currentThread().getName() + ": " + ticket);
                ticket++;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

由于线程是通过一个 Runnable 实例创建的,因此锁粒度可以直接锁在 this 也就是 Runnable 实例身上。
如果线程是通过继承 Thread 创建的,此时创建的三个线程需要共享资源 ticket,需要ticket 使用 static 修饰,否则会出现三个线程分别在卖一百张票的情况。
此时再次观察结果:已经正常售卖。

Thread-2: 91
Thread-2: 92
Thread-2: 93
Thread-2: 94
Thread-2: 95
Thread-2: 96
Thread-2: 97
Thread-2: 98
Thread-2: 99
Thread-2: 100
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Java还提供了其他锁,可以更加灵活控制加锁和解锁,例如:ReentrantLock

package com.curtis.demo.sync;

import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Curtis
 * @since 2024-04-18 21:03
 */
public class LockTaskDemo implements Runnable {

    private int ticket = 100;

    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                reentrantLock.lock();
                if (ticket <= 0) {
                    break;
                }

                System.out.println(Thread.currentThread().getName() + ": ticket-" + ticket);
                ticket--;
            } catch (Exception e) {
                System.out.println(Arrays.toString(e.getStackTrace()));
            } finally {
                reentrantLock.unlock();
            }
        }
    }
}

package com.curtis.demo.sync;

/**
 * @author Curtis
 * @since 2024-04-18 20:58
 */
public class SyncThreadTestDemo {

    public static void main(String[] args) {

        LockTaskDemo syncTaskDemo = new LockTaskDemo();

        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
        new Thread(syncTaskDemo).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

需要注意的是:unlock方法需要放在finally代码块中,确保程序不会出现锁死的情况,或者可以使用具有超时时间的加锁
输出结果:

Thread-2: ticket-10
Thread-2: ticket-9
Thread-2: ticket-8
Thread-2: ticket-7
Thread-2: ticket-6
Thread-2: ticket-5
Thread-2: ticket-4
Thread-2: ticket-3
Thread-2: ticket-2
Thread-2: ticket-1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

6.多线程应用实例

例如:有5个人抢100块的红包,三个人可以抢到,其余两个人没抢到

题解:此时需要设置抢红包的数量,对其进行扣减。并且红包金额需要进行扣减,不超过100。

package com.curtis.demo.test;

/**
 * @author Curtis
 * @since 2024-04-18 22:17
 */
public class RedEnvelope implements Runnable {

	// 总金额
    private int totalMoney = 100;
    // 总抢次数
    private int totalCount = 5;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
            	// 次数为0结束程序
                if (totalCount <= 0) {
                    break;
                }
				// 剩余两次都为没抢到
                if (totalCount <= 2) {
                    System.out.println(Thread.currentThread().getName() + ":没抢到");
                    totalCount--;
                    break;
                }
				// 随机金额每次不大于40, 这里只是为了保证三个红包都有金额, 比较简单, 可以自行修改
                int money = (int) (Math.random() * 40);
                if (totalCount != 3) {
                    System.out.println(Thread.currentThread().getName() + ":抢到了" + money);
                } else {
                	// 第三次的金额为剩余的金额
                    System.out.println(Thread.currentThread().getName() + ":抢到了" + totalMoney);
                }
                totalMoney -= money;
                totalCount--;
            }
        }
    }
}

package com.curtis.demo.test;

/**
 * @author Curtis
 * @since 2024-04-18 22:23
 */
public class RedEnvelopeTest {

    public static void main(String[] args) {
        RedEnvelope redEnvelope = new RedEnvelope();

        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
        new Thread(redEnvelope).start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

执行结果为:

Thread-0:抢到了23
Thread-0:抢到了30
Thread-0:抢到了47
Thread-0:没抢到
Thread-4:没抢到
  • 1
  • 2
  • 3
  • 4
  • 5

6.线程池的使用

首先,线程池本身是一个池化技术,是为了减少创建线程和线程销毁带来的资源损耗。能实现线程的复用,对线程进行统一管理。

线程池的创建: 自定义线程工厂,使用ThreadPoolExecutor创建。或者直接使用Executors创建。

package com.curtis.demo.pool;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Curtis
 * @since 2024-04-19 21:17
 */
public class MyThreadPoolFactory implements ThreadFactory {

    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    public MyThreadPoolFactory(String poolName) {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = poolName + poolNumber.getAndIncrement() + "-thread-";
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
        if (t.isDaemon()) {
            t.setDaemon(false);
        }
        if (t.getPriority() != Thread.NORM_PRIORITY) {
            t.setPriority(Thread.NORM_PRIORITY);
        }
        return t;
    }
}

package com.curtis.demo.pool;

import java.util.concurrent.*;

/**
 * @author Curtis
 * @since 2024-04-19 21:07
 */
public class MyThreadPoolTest {

    public static void main(String[] args) {

		// 工具类直接生成
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + ": execute Executors.newSingleThreadExecutor()");
            return "Executors.newSingleThreadExecutor()";
        }, executorService);
        executorService.shutdown();
		
		// 高级用法,自定义
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                60, // 线程存活时间
                TimeUnit.SECONDS, // 线程存活时间单位
                new LinkedBlockingQueue<>(), // 阻塞队列
                new MyThreadPoolFactory("my-thread-pool"), // 线程工场
                new ThreadPoolExecutor.AbortPolicy()); // 拒绝策略

        CompletableFuture.runAsync(() -> {
            System.out.println(Thread.currentThread().getName());
        }, threadPoolExecutor);
        threadPoolExecutor.shutdown();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

注意:线程池使用完毕之后需要关闭
执行结果:可以看到一个是自定义的线程名称,一个是默认的线程池名称

// executors 默认创建线程池线程名称
pool-1-thread-1: execute Executors.newSingleThreadExecutor()
// 自定义线程池线程名称
my-thread-pool-1-thread-1
  • 1
  • 2
  • 3
  • 4

线程池的运行原理:线程池在创建的时候默认不会初始化线程,只有在任务提交的时候才会去创建线程。
在这里插入图片描述
当当前运行线程未达到核心线程数时,任务提交的时候会创建线程执行。
在这里插入图片描述
当运行线程数量已经达到核心线程数时,会将任务放到阻塞队列当中。
在这里插入图片描述
阻塞队列满了之后会创建新线程直到达到最大线程数。
在这里插入图片描述
若线程数已经达到最大线程数,则会执行拒绝策略。
在这里插入图片描述


总结

多线程是为了帮助解决提高效率,减少响应时间的技术。同时也存在一定的线程安全问题,在使用的过程中需要注意原子性、可见性、和有序性。
一般真实项目中会使用线程池技术,对一些特定的任务设置特定的线程池,区分隔离开。例如一些导入导出的功能与正常的核心业务关联不大,且属于慢任务,此时即可开辟单独线程池执行相应的导入或者导出。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号