Java多线程、线程池、线程数

Java多线程、线程池、线程数

Java线程Thread、Runable、Callable的使用与区别

Thread

最开始学习线程的时候就是学习的Thread线程,通过继承Thread并重写run()方法来在多线程中执行一些必要的业务操作。

// 线程测试
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread("线程1");
        myThread.start();
        MyThread myThread_2 = new MyThread("线程2");
        myThread_2.start();
    }
}

// 继承Thread实开启多线程
class MyThread extends Thread {
    
    public int num = 20;

    public MyThread(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        // TODO: 将要执行的业务
        System.out.println(this.getName());
        for (int i = 0; i < 10; i++) {
            System.err.println(--num);
        }
    }
}

但是通过继承Thread实现线程有缺点,因为一个类只能继承一个父类,如果继承了Thread就不能继承其他类了。但是如果不需要继承其他类的时候就另当别论了!

为了解决实现多继承的问题,通过实现Runable接口来实现多线程。

Runable

// 测试实现Runnable接口实现线程
public class TestRunnable {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        // 在main主线程中,开启两个线程
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
    }
}

// 实现Runnable接口实现多线程,也可以实现其他接口并继承其他父类
class MyRunnable extends Father implements Runnable, Serializable {

    public int num = 20;
    
    @Override
    public void run() {
        // TODO: 接下来进行的业务操作
        for (int i = 0; i < 10; i++) {
             System.out.println((i + 1) + " -> " + super.name + "诡异数:" + (--num));
        }
    }
}

// 一个抽象接口
abstract class Father {
    public String name = "I am father!";
}

通过Thread可以加载同一个Runnable开启多线程并且Runnable中的资源是共享的,这样使用起来方便了很多,他既继承了Father类,也实现了其他的两个类,扩展性和灵活性提高了很多。

到这里需要注意一个问题! 继承Thread类和实现Runnable接口里面都有一个变量num变量。

继承Thread类实现多线程,在开启多线程的时候需要分别new一个MyThread的对象才能开启。这就说明在使用继承Thread实现多线程的时候变量是不共享的因为创建的两个对象跑多线程。

但是实现Runnable接口开启多线程,创建的是一个对象,使用new Thread(myRunnable)开启线程因为使用的是一个对象所在Runnable中的num变量对于两个多线程来说是共享的。因为在Runnable中变量共享所以这时候就涉及到了线程不安全的问题,要使用同步锁保证num变量的线程安全,还要慎重使用!因为一不小心就会造成死锁或者是锁不住的问题!比如可以使用this当前对象来同步num保证不让两个线程同实操作。

现在使用多线程不仅是局限于只是按照某一个流程跑一遍代码而已了,更多的使用场景是需要获取到在线程中执行的返回结果,然后在处理返回的结果。

Callable

Thread和Runnable都是执行工作的独立任务,他们不返回任何值,如果想要线程执行之后返回结果可以实现Callable接口指定Callable的泛型,并且实现Callable接口中的call()方法,call()方法能够返回执行的结果。

// 测试callable实现线程
public class TestCallable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);

        new Thread(futureTask).start();
		
        //get()获取到返回结果
        System.err.println(futureTask.get());
    }
}

class MyCallable implements Callable<Integer> {

    public Integer num = 2;

    @Override
    public Integer call() throws Exception {
        return --num;
    }
}

Java线程池

为什么要使用线程池?

在Java中频繁的创建和销毁线程开销是非常大的,他们在实际中花费的时间和消耗的资源可能比实际用户消耗的资源还要庞大,而且创建太多的线程也会使JVM过度消耗内存或者频繁切换导致系统资源不足或者崩溃。为了避免这种资源消耗的情况就使用线程池。

假设线程池中的线程是10个,如果有100个请求过来去创建线程,那么就只能使用这10个线程去开启子线程,只有当其中的线程被释放,剩余的线程才会被继续开启。

Runnable实现线程池

// Runnable线程使用线程池
public class TestRunnablePool {

    public static void main(String[] args) {
        // 打开线程池,里面可以获取两个线程
        ExecutorService es = Executors.newFixedThreadPool(2);

        MyRunnable myRunnable = new MyRunnable();

        // 开启6个线程
        Future<?> submit1 = es.submit(myRunnable);
        Future<?> submit2 = es.submit(myRunnable);
        Future<?> submit3 = es.submit(myRunnable);
        Future<?> submit4 = es.submit(myRunnable);
        Future<?> submit5 = es.submit(myRunnable);
        Future<?> submit6 = es.submit(myRunnable);

        // 还可以这么用
        es.submit(() -> {
            System.out.println("Lambda");
        });
        
        // 关闭线程池
        es.shutdown();
    }
}

class MyRunnable implements Runnable {

    public int num = 20;

    @Override
    public void run() {
        System.err.println(num--);
    }
}

Callable实现线程池

// 测试Callable实现多线程
public class TestCallablePool {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 设置线程池的线程个数是2个
        ExecutorService es = Executors.newFixedThreadPool(2);

        MyCallable myCallable = new MyCallable();

        // 提交的第一个线程
        Future<List<String>> submit_1 = es.submit(myCallable);
        // 提交的第二个线程
        Future<List<String>> submit_2 = es.submit(myCallable);
        // 提交的第三个线程
        Future<List<String>> submit_3 = es.submit(myCallable);
        // 提交的第四个线程
        Future<List<String>> submit_4 = es.submit(myCallable);

        // 把提交的所有线程装进一个集合等待返回结果
        List<Future<List<String>>> futureList = new ArrayList<>();
        futureList.add(submit_1);
        futureList.add(submit_2);
        futureList.add(submit_3);
        futureList.add(submit_4);
        // 一个集合放所有线程返回的结果
        List<List<String>> result = new ArrayList<>();
        // 接收数据返回的结果
        List<String> data = new ArrayList<>();
        // 线程返回结果
        List<List<String>> futureResult = getFutureResult(futureList, result);

        // 关闭线程
        es.shutdown();
        // ----------------看结果------------------------
        // 取出线程的返回结果
        for (List<String> list : futureResult) {
            data.addAll(list);
        }

        // 打印看最后的返回结果
        for (String datum : data) {
            System.err.println(datum);
        }
    }

    // 重点是这一个递归获取线程数据的方法
    private static List<List<String>> getFutureResult(List<Future<List<String>>> futureList, List<List<String>> result) throws InterruptedException, ExecutionException {

        List<Future<List<String>>> futures = new ArrayList<>();

        for (Future<List<String>> future : futureList) {
            // 判断当前线程是否执行完毕
            if (future.isDone()) {
                // 执行完毕将数据添加到集合当中
                result.add(future.get());
            } else {
                Thread.sleep(10);
                // 没执行完毕将线程放在一个集合中进行递归的判断
                futures.add(future);
            }
        }
        // 如果递归的线程中存在(一直没有执行完)就进行地柜操作
        if (futures.size() > 0) {
            return getFutureResult(futures, result);
        }
        // 最后将所有线程执行完的数据进行返回
        return result;
    }
}

// 实现Callable接口 来实现线程
class MyCallable implements Callable<List<String>> {

    @Override
    public List<String> call() throws Exception {
        // TODO: 将要执行的业务操作和返回的数据
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1; i++) {
            list.add("The number is " + i);
        }
        return list;
    }
}

线程数设置多少合适?

既然不要无休止的去创建线程,那就使用线程池来限制一次性创建的线程数。

那线程数设置多少最合适呢?

设置线程数主要就是为了合理的使用CPU和内存等资源使应用程序的性能得以提高。

首要考虑的就是CPU的核心数。核心数在Java中使用Runtime.getRuntime().availableProcessor()方法来获取。确认核心数之后,在判断当前的操作是是CPU密集型任务还是IO密集型任务。

  • CPU密集型任务:类似解密、压缩、计算等耗费CPU资源的任务。(计算型任务)
  • IO密集型任务:文件读写、网络通信等任务,不会特别耗费CPU,但是IO操作费时。

CPU密集任务

对于CPU密集型的任务,多线程本质上是提升多核CPU的利用率,每个核处理一个线程,可以将CPU资源全部利用起来。

如果线程数量是CPU核数的数倍,这时的计算任务已经全部占满CPU,而多余的线程回去抢占CPU资源执行自己的任务而造成不必要的上下文切换导致性能下降。

所以说,理论上线程数量=CPU核数,通常可以设置线程数量=CPU核数+1

// 测试CPU密集型任务
public class TestProcessor2 {

    public static void main(String[] args) throws InterruptedException {
        // TODO: 线程池使用4个在一瞬间明显拉满的CPU的使用,使用2个的使用CPU的使用百分比明显不是很高,说明有富余的CPU还可以使用:可以尝试线程数2、4、5,CPU使用率从低到高,时间从高到底,但是时间提速越来越慢
        ExecutorService es = Executors.newFixedThreadPool(4);
        // 开始时间
        Long startTime = System.currentTimeMillis();
        List<Future<Integer>> futureList = new ArrayList<>();
        // 循环开启线程
        for (int i = 0; i < 50; i++) {
            futureList.add(es.submit(new MyCallable2()));
        }
        getCallableResult(futureList);
        Long endTime = System.currentTimeMillis();
        System.err.println("线程耗时:" + (endTime - startTime));
        es.shutdown();
    }

    // 只有这个方法执行完毕,所有的线程才算全部完成
    private static String getCallableResult(List<Future<Integer>> futureList) throws InterruptedException {
        List<Future<Integer>> futures = new ArrayList<>();
        for (Future<Integer> future : futureList) {
            if (future.isDone()) {
                // TODO: 存放线程返回的数据
            } else {
                Thread.sleep(10);
                futures.add(future);
            }
        }
        if (futures.size() > 0) {
            return getCallableResult(futures);
        }
        return null;
    }

}

final class MyCallable2 implements Callable<Integer> {


    @Override
    public Integer call() throws Exception {
        // TODO: 假设将要进行的是纯CPU密集型的业务
        int sum = 0;
        for (int i = 1; i <= 100000000; i++) {
            sum += i;
        }
        return sum;
    }
}

IO密集任务

IO密集任务,在很多文章上和书上有不同的计算公式去计算时间,并且还通过测试来设置线程数的多少。

公式一:线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)

公式二:线程数 = CPU 核心数 / (1 - 阻塞系数)

一般地,线程数量=CPU核心数*2或者线程数量=CPU核心数*2+1

// 测试设置线程数
public class TestProcessor {

    public static void main(String[] args) throws InterruptedException {

        // 获取CPU的核心数量(可能不准确)
        int processors = Runtime.getRuntime().availableProcessors();
        // 4核
        System.err.println("当前CPU的核心数量:" + processors);
        // 设置线程池
        // TODO: IO等待耗时1秒左右:线程数尝试:8(14291)、9(12692)、16(7660)、32(4241)、64(2419)
        // TODO: IO等待耗时600毫秒左右:线程数尝试:8(8061)、9(7256)、16(4051)、32(2412)、64(1650)
        // IO耗时越短,线程数开的越多性能提升越不明显,反之,线程数可以开很大
        ExecutorService es = Executors.newFixedThreadPool(100);
        List<Future<String>> futureList = new ArrayList<>();
        // 开始时间
        Long startTime = System.currentTimeMillis();
        // 循环开启线程
        for (int i = 0; i < 100; i++) {
            futureList.add(es.submit(new MyCallableTest(i)));
        }
        // 只有这个方法执行完毕,所有的线程才算全部完成
        getCallableResult(futureList);
        Long endTime = System.currentTimeMillis();
        System.err.println("线程耗时:" + (endTime - startTime));
        es.shutdown();
    }

    // 只有这个方法执行完毕,所有的线程才算全部完成
    private static String getCallableResult(List<Future<String>> futureList) throws InterruptedException {
        List<Future<String>> futures = new ArrayList<>();
        for (Future<String> future : futureList) {
            if (future.isDone()) {
                // TODO: 存放线程返回的数据
            } else {
                Thread.sleep(10);
                futures.add(future);
            }
        }
        if (futures.size() > 0) {
            return getCallableResult(futures);
        }
        return null;
    }
}

// 模拟IO密集型的操作--线程
class MyCallableTest implements Callable<String> {

    private Integer flag;

    public MyCallableTest(Integer flag) {
        this.flag = flag;
    }

    @Override
    public String call() throws Exception {
        // TODO: 假设下面的业务仅处理一个IO请求,开很多线程处理很多相同的IO
        if (this.flag % 2 == 0) {
            // 模拟IO耗时 100ms
            Thread.sleep(1200);
        } else {
            // 模拟IO耗时70ms
            Thread.sleep(1000);
        }
        return "返回的结果";
    }
}

提示!

在递归处理结果的时候如果不在中间加一个线程睡眠的延时会导致:堆栈溢出异常Exception in thread "main" java.lang.StackOverflowError有一个延时之后,被认为递归不会无休止的进行下去,所以不会在报堆栈溢出的异常了。

参考资料

https://blog.csdn.net/u012501054/article/details/80384996
https://www.jianshu.com/p/2d2e13fc4047
https://www.cnblogs.com/jmsjh/p/7762034.html
https://blog.csdn.net/qq_27276045/article/details/105086183
https://blog.csdn.net/qq_41973536/article/details/81181429

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白说(๑• . •๑)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值