Thread类的基本操作(JAVA多线程)

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并提供了一些API供外部使用。

JAVA中 Thread类 将系统提供的API又近一步进行了抽象和封装,所以如果想要使用多线程就离不开 Thread 这个类。

线程的创建(Thread类)

在JAVA中 创建线程可以有多种方法,这里简单介绍几种。

方法一:我们自己编写一个类使这个类继承自Thread类,然后重写里面的 run() 方法。

    class MeThread extends Thread {
        //必须要实现这个方法,此方法是新线程执行的入口方法(告诉线程应该做什么)
        @Override
        public void run() {
            System.out.println("这是新线程执行的任务");
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        MeThread t = new MeThread();
        //调用系统API启动线程
        t.start();
    }

当我们运行程序之后就会执行 run() 方法中的打印操作。

new 一个Thread类只是创建出了一个线程,并不会调用系统API创建线程,只有当调用了start() 方法之后才会调用系统API在系统中创建出线程并启动。(此时不理解可以,因为在介绍isAlive()方法时会验证)

这个重写的 run() 方法可以理解为一个任务,这个方法会在线程启动时自动被调用执行当线程执行完这个方法中的内容时该线程就会被销毁并且无法再次使用start()方法唤醒。

此时我们为了可以更好的呈现多线程并发编程的效果对上述代码进行了一些细微修改。

    class MeThread extends Thread {
        //必须要重写这个方法,此方法是新线程执行的入口(告诉线程应该做什么)
        @Override
        public void run() {
            //此处让这个新线程每隔0.1s执行一次打印操作
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("这是创建的新线程执行的任务");
            }
        }
    }

    public static void main(String[] args) {
        MeThread t = new MeThread();
        //调用系统API启动线程
        t.start();
        //让主线程每隔0.1s执行一次打印操作
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("主线程");
        }
    }

5420a28a4bc04b979e145e656e12fa9c.png

此时可以看到两个while循环是“同时执行的” ,每个线程都是一个独立的执行流。

代码执行之后可以看到程序在无规律的进行打印,其主要原因是系统对线程的调度是随机的。 


方法二:我们自己编写一个类使实现 Runnable接口 然后重写里面的 run() 方法。

  • Thread类 实现了 Runnable接口;
  • Thread类 中的 run() 方法也是重写的 Runnable接口中的;
  • 因为在 Thread类 中提供了一个这样的构造方法:57fd3e2dbd40417aa31283ca59e7d382.png

    class MeRunnable implements Runnable{
        //必须要重写这个方法,此方法是新线程执行的入口
        @Override
        public void run() {
            System.out.println("这是一个新线程执行的任务");
        }
    }

    public static void main(String[] args) {
        MeRunnable runnable = new MeRunnable();
        Thread t = new Thread(runnable);
        //调用系统API启动线程
        t.start();
    }

使用 Runnable 接口和直接继承 Thread 的区别就是:可以帮我们降低代码的耦合性也就是“解耦合”

Runnable 它表示一个可以执行的任务 而它并不关心这个任务是啥,在哪里执行;这个任务也不一定和线程强相关,因为这个代码可能使用单线程、多线程还是不使用线程或者是用其他方法(例:线程池,协程……)执行都没有任何区别。

而此时使用 Runnable 就可以将这个任务单独的提取出来,这样就可以随时改变这个任务是使用什么方法进行执行(例如:后面如果不想用线程了就可以直接在main方法中进行调用)例:

    class MeRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("这是一个任务");
        }
    }

    public static void main(String[] args) {
        MeRunnable runnable = new MeRunnable();
        //此时不想使用线程执行这个任务
        runnable.run();
    }

方法三:使用匿名内部类。

    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("这是创建的新线程执行的任务");
            }
        };
        //调用系统API启动线程
        t.start();
    }

因为在 Thread类 中提供了一个这样的构造方法:57fd3e2dbd40417aa31283ca59e7d382.png

所以我们可以写成过这样: 

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println("这是一个新线程执行的任务");
            }
        });
        //调用系统API启动线程
        t.start();
    }

 方法四:因为Runnable接口是一个函数式接口,所以可以利用 Lambda表达式 来创建线程。

    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("这是创建的新线程执行的任务"));
        //调用系统API启动线程
        t.start();
    }

除了观看控制台的输出结果来观察多线程之外,还可以使用JDK中带有的工具 jconsole 来更形象的观测:具体方法可以跳转这里icon-default.png?t=N7T8http://t.csdnimg.cn/b8Wca

Thread类的一些常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可

Thread类中的一些常见属性:

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

getId():

获取当前线程的 id 

id 是线程的唯一身份标识,这个 id 是 JAVA 为这个线程分配的,并不是系统 API 分配的 ID ,更不是 PCB 的 ID 。

    public static void main(String[] args) {
        Thread t = new Thread(() -> {while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //打印异常
                e.printStackTrace();
            }
            System.out.println("这是创建的新线程执行的任务");
        }});

        Thread t1 = new Thread(() -> {while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("这是创建的新线程执行的任务");
        }});
        t.start();
        t1.start();
        System.out.println(t.getId());//获取并打印当前线程的ID
        System.out.println(t1.getId());//获取并打印当前线程的ID
    }

b804bcb5dcb64387821b90b2bdf7d29e.png

isDaemon()

判断当前线程是否为后台线程。

线程默认都是前台线程。

  • 前台线程:只要该进程中还有前台线程未执行完,那么该进程就不会结束(前台线程会影响进程的结束与否);
  • 后台线程:只要该进程中的前台线程都执行完毕,那么此时无论是否有未执行完的后台线程进程都会结束(后台线程不会影响进程的结束与否)。
    public static void main(String[] args) {
        Thread t = new Thread(() -> {while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //打印异常
                e.printStackTrace();
            }
            System.out.println("这是创建的新线程执行的任务");
        }});

        t.start();
        System.out.println(t.isDaemon());//获取并打印当前线程是否为后台线程
    }

0529f0a3cabe4a7cb3c1601bc1ed7ca9.png

此时将上述代码改成后台线程。

    public static void main(String[] args) {
        Thread t = new Thread(() -> {while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //打印异常
                e.printStackTrace();
            }
            System.out.println("这是创建的新线程执行的任务");
        }});
        //将当前线程设置为后台线程
        t.setDaemon(true);
        t.start();
        System.out.println(t.isDaemon());
    }

8bd1e4d9722f48f899f64e0cb9d291f1.png

此时因为主线程(main线程)飞快的执行完了所以没有任何打印。

isAlive()

分别在线程启动前后打印判断线程是否存活(注意:线程对象存活时线程并不一定会存活)

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //打印异常
                e.printStackTrace();
            }
            System.out.println("新线程执行完毕");
        });
        System.out.println("线程启动前");
        System.out.println("线程是否存活"+t.isAlive());
        t.start();
        System.out.println("线程已启动");
        System.out.println("线程是否存活"+t.isAlive());
        Thread.sleep(2000);
        System.out.println("线程是否存活"+t.isAlive());
    }

c5b74a4e3d544c9299f1eab678344a41.png

根据结论可以得知当我们创建线程对象之后并不会调用系统API创建线程,只有当调用了start() 方法之后才会调用系统API在系统中创建出线程并启动。

打断线程

在JAVA中打断线程的方法是比较唯一的:本质上都是让run()方法尽快执行结束;而在C++中是有办法可以在线程执行过程中直接销毁该线程,但是这样做有个坏处比如这个线程在写文章时突然中断了那就会令这篇文章有头无尾,而在JAVA中就可以允许在此处进行一些收尾工作。

而现实中令run()方法迟迟无法结束的原因一般都是应为循环,所以只要 结束循环就可以让线程尽快执行完run()方法,从而达到打断线程的效果。

此处介绍两种方法:

第一种方法:

可以手动创建出一个标志位,用来控制 run() 方法中循环的终止条件。

    //创建一个成员变量用来控制循环的终止条件,默认值为 false;
    private static boolean isQuit;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(!isQuit) {
                System.out.println("新线程正在工作");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    //打印异常
                    e.printStackTrace();                }
            }
            System.out.println("新线程执行完毕");
        });
        
        t.start();
        Thread.sleep(500);
        
        isQuit = true;
        System.out.println("打断新线程");
    }

cf6542b5d12c4b8abd3f50a8b87e8445.png

但是有以下两个问题:

  1. 需要手动创建标志位;
  2. 如果循环正处在sleep状态程序将不能进行及时的响应。

第二种方法:

在JAVA中默认就有一个标志位,我们可以利用JAVA中默认的标志位来进行快速结束run()方法的操作。

好处是这样我们就不用再单独创建一个变量,不用再思考变量捕获的问题了。

  • interrupt() :该方法可以将线程中默认的标志位设置为true
  • isInterrupt():判断对象关联的线程的标志位是否被设置,调用后不清除标志位。还可以使sleep()方法抛出InterruptedException异常,来强行中断sleep()方法。

利用这两个方法就可以实现线程的打断

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread()该方法是用来得到该线程的实例也就是t(哪个线程调用该方法就返回哪个线程的实例)
            //因为此时t还没有被创建所以不能写为t.isInterrupted()
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("新线程正在工作");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //打印异常
                    e.printStackTrace();
                }
            }
            System.out.println("新线程执行完毕");
        });

        t.start();
        Thread.sleep(5000);
        //设置标志位为true
        t.interrupt();
        System.out.println("打断新线程");
    }

0a7d1cbb2cb043519d03f502c3c38b12.png

结果可以看出来此时标志位确实被设置了,sleep()方法也抛出了异常,可是循环并没有被终止。

原因是sleep()方法在抛出异常之后会自动将标志位清除,而此引起的结果就和没有设置标志位是相同的。

而JAVA如此设计的原因其实就是扩大程序员的可操作空间,可以再sleep()方法抛出异常之后进行一些收尾工作。

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //Thread.currentThread()该方法是用来得到该线程的实例也就是t(哪个线程调用该方法就返回哪个线程的实例)
            //因为此时t还没有被创建所以不能写为t.isInterrupted()
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("新线程正在工作");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //此处可以写一些收尾工作的代码
                    break;
                }
            }
            System.out.println("新线程执行完毕");
        });

        t.start();
        Thread.sleep(500);
        //设置标志位为true
        t.interrupt();
        System.out.println("打断新线程");
    }

37fd158250634d449c15b256f84593f7.png

线程等待

再多线程的代码中由于线程的调度是随机的,所以也就会导致每个线程的结束时间也是无法预测的,而这种情况下就会使得在有些场景下代码出现BUG。

而线程等待就是让一个线程来等待另一个线程执行结束,本质上就是来控制线程结束的顺序。

join()

实现线程等待的效果,让一个线程阻塞等待另一个线程执行结束之后再执行。

  • 等的线程:在哪个线程中调用 join 方法,哪个线程就阻塞等待;
  • 被等的线程:调用的哪个线程对象的 join 方法,哪个线程就是被等的线程,当这个线程执行完毕等的线程才会执行。

我们创建一个线程 t 让这个线程每隔一秒打印一次数据,让主线程等待该线程。

Thread t = new Thread(()->{
    for (int i = 0; i < 4; i++) {
        System.out.println("t线程执行中");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
});
t.start();
System.out.println("等待开始");
t.join();
System.out.println("等待结束");

注意:如果 t 线程已经执行结束,此时再调用 join 就会直接返回执行,不会发生阻塞等待。

join(等待时间)

上面的 join() 方法是一种“死等”的方法,只要被等待的线程不结束,那么就会一直等待下去。

但是一般情况下我们并不会死等而是等待超过了一定时间之后就不会再继续等待了,因为没有意义。

  • join(long millis)         最多等待 millis 毫秒
  • join(long millis, int nanos)       和上面的方法一样 就是时间精度更高,精确到了纳秒

休眠线程

  • sleep(long millis)  让线程休眠 millis 毫秒;
  • sleep(long millis,int nanos)  和上面的功能一样,就是精度更高。

但是下面的这一种其实意义不大,因为 sleep() 本身就存在一定的误差,并不是你写 sleep(1000) 就真的刚好等精确 1000 ms ,它还有一个调度的开销。系统会按照 1000 这个时间来休眠线程,当时间到了之后,系统会唤醒该线程(阻塞 -> 就绪),而且并不是线程进入就绪状态就能立即进入CPU执行。

//获取系统当前时间戳
long a = System.currentTimeMillis();
Thread.sleep(1000);
//获取系统当前时间戳
long b = System.currentTimeMillis();
System.out.println("时间:"+(b - a)+" ms");

而且每次运行之后结果都是不同的。

线程的状态

JAVA中线程的所有状态都存储在一个枚举类型中,Thread.State :

for (Thread.State str:Thread.State.values()) {
    System.out.println(str);
}

可以通过上述代码来打印所有的线程状态

通过 getState() 方法可以获取线程的状态

  • NEW:Thread对象已经有了,但还没有调用 start() 方法;
Thread t = new Thread(()->{

});
System.out.println(t.getState());

  • RUNNABLE:就绪状态(线程已经在CPU上执行了或者排队准备执行)
Thread t = new Thread(()->{
    while(true) {

    }
});
t.start();
System.out.println(t.getState());

  • BLOCKED:阻塞,由于锁竞争导致的阻塞
Object lock1 = new Object();
Object lock2 = new Object();

Thread t1 = new Thread(()->{
    synchronized(lock1) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        synchronized(lock2) {

        }
    }
});
Thread t2 = new Thread(()->{
    synchronized(lock2) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        synchronized(lock1) {

        }
    }
});
t1.start();
t2.start();
//让主线程等待 2 秒
Thread.sleep(2000);
//此时t1和t2两个线程会因为互相争对方的锁,而导致死锁
System.out.println(t1.getState());
System.out.println(t2.getState());

  • WAITING:阻塞由 wait 这种不固定时间的方式引起的阻塞
Object lock1 = new Object();

Thread t1 = new Thread(()->{
    synchronized(lock1) {
        try {
        //调用wait方法让线程阻塞
            lock1.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
});

t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());

  • TIMED_WAITING:由 sleep 这种固定时间限制的方式引起的阻塞
Thread t1 = new Thread(()->{
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
});

t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());

  • TERMINATED:Thread对象还在,可是线程已经没了
Thread t1 = new Thread(()->{

});

t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/119153.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python图像处理 ——图像锐化

python图像处理 ——图像锐化 前言一、原理二、 空间域锐化滤波1.拉普拉斯算子&#xff08;Laplacian&#xff09;2.罗伯茨算子&#xff08;Roberts&#xff09;3.Sobel算子4.Prewitt算子5.Scharr算子 三、实验对比 前言 由于收集图像数据的器件或传输图像的通道存在一些质量缺…

浅谈电力物联网时代物联网技术在电力系统中的应用

贾丽丽 安科瑞电气股份有限公司 上海嘉定201801 摘要&#xff1a;在电力系统建设中&#xff0c;物联网的应用不仅促进了我国电力工业的发展&#xff0c;而且对我国的物联网技术也起到了一定的促进作用。随着物联网技术应用于电力系统&#xff0c;推动了中国工业的快速发展。因…

GCN火车票识别项目 P1 火车票识别项目介绍 Pytorch LSTM/GCN

从本节开始&#xff0c;我将带大家完成一个深度学习项目&#xff1a;用图卷积神经网络(GCN)&#xff0c;实现一个「火车票文字信息提取」的项目&#xff0c;由于火车票上每个节点文字不是等长的&#xff0c;所以还需要添加一个前置的 LSTM 来提取句子特征。 课前说明 1、这是…

Linux CentOS配置阿里云yum源

一&#xff1a;先备份文件&#xff0c;在配置失败时可以恢复 cd /etc/yum.repos.d mkdir back mv *.repo back 二&#xff1a;下载阿里云yum源 wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo wget -O /etc/yum.repos.d/epel…

网络工程师回顾学习

根据书本目录&#xff0c;写下需要记忆的地方&#xff1a; 参考之前的笔记&#xff1a; 网络工程师回答问题_one day321的博客-CSDN博客 重构第一部分需要记忆的&#xff1a; 第一章&#xff1a;计算机网络概论 计算机网络的定义和分类&#xff1a;计算机网络是指将地理位…

如何定义类

类是将数据和方法封装在一起的一种数据结构&#xff0c;其中数据表示类的属性&#xff0c;方法表示类的行为&#xff0c;所以定义类实际上就是定义类的属性与方法。用户定义一个类实际上就是定义一个新的数据类型。在使用类之前&#xff0c;必须先定义它&#xff0c;然后才可利…

软件测试需求分析是什么?为什么需要进行测试需求分析?

在软件开发中&#xff0c;软件测试是确保软件质量的重要环节之一。而软件测试需求分析作为软件测试的前置工作&#xff0c;对于保证软件测试的顺利进行具有重要意义。软件测试需求分析是指对软件测试的需求进行细致的分析和规划&#xff0c;以明确测试的目标、任务和范围&#…

2021年电工杯数学建模B题光伏建筑一体化板块指数发展趋势分析及预测求解全过程论文及程序

2021年电工杯数学建模 B题 光伏建筑一体化板块指数发展趋势分析及预测 原题再现&#xff1a; 国家《第十四个五年规划和 2035 年远景目标纲要》中提出&#xff0c;将 2030 年实现“碳达峰”与 2060 年实现“碳中和”作为我国应对全球气候变暖的一个重要远景目标。光伏建筑一体…

RABC权限模型与Spring Security

今天&#xff0c;我将带你进入一个充满策略和刺激的领域——权限之战。在这场战斗中&#xff0c;我们的主角是RABC权限模型&#xff08;Role-Based Access Control&#xff09;和Spring Security&#xff0c;他们将共同为我们打造一个安全稳定的世界。 权限模型&#xff1a;游戏…

通俗理解repartition和coalesce区别

官方的解释 reparation 返回一个具有恰好numPartitions分区的新RDD。 可以增加或减少此RDD中的并行级别。在内部,reparation会使用shuffle来重新分发的数据。 如果要减少此RDD中的分区数量,请考虑使用coalesce,这样可以避免执行shuffle。 coalesce 返回一个新的RDD,该RDD被…

Zookeeper3.7.1分布式安装部署

上传安装文件到linux系统上面 解压安装文件到安装目录 [zhangflink9wmwtivvjuibcd2e package]$ tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz -C /opt/software/3. 修改解压文件名 [zhangflink9wmwtivvjuibcd2e software]$ mv apache-zookeeper-3.7.1-bin/ zookeeper-3.7…

Qt OpenGL相机系统

文章目录 一、简介二、实现代码三、实现效果参考资料效果展示 一、简介 一直偷懒没有学习OpenGL,乘着这段有点时间重新学习一下OpenGL,做一个简单的小工具,有助于后面理解OSG。我们都知道OpenGL中存在着下面几个坐标空间:模型空间(物体空间)、世界空间、观察空间(或者称…

【Linux】:使用git命令行 || 在github创建项目 || Linux第一个小程序——进度条(进阶版本)

在本章开始之前还是先给大家分享一张图片 这是C的笔试题 感兴趣的同学可以去试一试 有难度的哟 也可以直接在牛客网直接搜索这几道题目哈 好了今天我们正式进入我们的正题部分 &#x1f556;1.使用git命令行 安装git yum install git&#x1f560;2.在github创建项目 使用…

【微服务】一体化智慧工地管理平台源码

智慧工地系统是一种利用人工智能和物联网技术来监测和管理建筑工地的系统。它可以通过感知设备、数据处理和分析、智能控制等技术手段&#xff0c;实现对工地施工、设备状态、人员安全等方面的实时监控和管理。 一、智慧工地让工程施工智能化 1、内容全面&#xff0c;多维度数…

在直播系统中使用SRT协议传输视频

目录 1、简述 2、NDI、RTSP协议的优缺点 3、SRT协议简介 4、SRT协议链接地址URL格式 &#xff08;1&#xff09;listener&#xff1a; &#xff08;2&#xff09;caller&#xff1a; 5、手机发送SRT实时音视频 6、OBS中的设置 7、在vMix中的设置 8、写在最后 1、简述 …

前端框架Vue学习 ——(六)Vue组件库Element

文章目录 Element 介绍快速入门常见组件表格分页Dialog 对话框组件表单 Container 布局容器 Element 介绍 Element&#xff1a;是饿了么团队研发的&#xff0c;一套为开发者、 设计师和产品经理准备的基于Vue 2.0的桌面端组件库。 组件&#xff1a;组成网页的部件&#xff0c;…

VR全景如何助力乡村振兴,乡村发展在哪些方面用到VR全景技术

引言&#xff1a; 乡村振兴是当今中国发展的重要战略&#xff0c;也是推动农村经济社会全面发展的关键举措。在这一过程中&#xff0c;虚拟现实&#xff08;VR&#xff09;全景技术正逐渐崭露头角&#xff0c;为乡村振兴提供了机遇。 一&#xff0e;VR全景技术的概念和应用 1…

Leetcode刷题详解——括号生成

1. 题目链接&#xff1a;22. 括号生成 2. 题目描述&#xff1a; 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;["((()))","(()…

Linux DataEase数据可视化分析工具结合cpolar实现远程访问

文章目录 前言1. 安装DataEase2. 本地访问测试3. 安装 cpolar内网穿透软件4. 配置DataEase公网访问地址5. 公网远程访问Data Ease6. 固定Data Ease公网地址 前言 DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务…

MapReduce性能优化之小文件问题和数据倾斜问题解决方案

文章目录 MapReduce性能优化小文件问题生成SequenceFileMapFile案例 &#xff1a;使用SequenceFile实现小文件的存储和计算 数据倾斜问题实际案例 MapReduce性能优化 针对MapReduce的案例我们并没有讲太多&#xff0c;主要是因为在实际工作中真正需要我们去写MapReduce代码的场…