初识多线程

Posted bit_zhy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识多线程相关的知识,希望对你有一定的参考价值。

初始Java多线程

1)线程的创建(五种方式)

1.创建一个继承Thread的类

创建一个子类 重写run方法 run方法中写该线程需要执行的内容 继承Thread 实现这个子类

class MyThread extends Thread
    @Override
    public void run() 
        System.out.println("创建类继承Thread,重写run");
    

2.创建实现Runnable接口的类

创建一个类,实现Runnable接口,之后创建这个类的实例,创建一个Thread对象,其构造方法中
传入刚刚创建的类的实例,实现执行代码和多线程分开

class MyRunnable implements Runnable
    @Override
    public void run()
        System.out.println("创建Runnable类,实现Runnable接口,重写run");
    

3.创建Thread实例,使用匿名内部类

可以直接在主线程中创建一个Thread的实例,在括号后直接加一个大括号,重写run()方法

        Thread t2 = new Thread()
            @Override
            public void run() 
                System.out.println("使用匿名内部类,创建Thread");
            
        ;

该种方法相当于直接创建了一个继承Thread的匿名内部类,重写run()方法,和第一种方法本质上一样。

4.创建Thread实例,在Thread类的构造方法中传入一个Runnable 实例 创造一个Runnable的匿名内部类

        Thread t3 = new Thread(new Runnable()
            @Override
            public void run() 
                System.out.println("使用runnable的匿名内部类");
            
        );

该种方法相当于创建了一个Runnable实例,将该实例传给了Thread 的构造方法,本质上与方法二一样。

5.使用lambda表达式简化创建

我们在创建Thread实例的时候使用lambda表达式,这样在lambda表达式种直接相当于重写了run()方法

        Thread t4 = new Thread(() -> 
            System.out.println("使用lambda表达式创建的Thread");
        );

这种方法本质上与第四个方法一样,其实是使用lambda表达式代替了Runnable实例

总结:

在创建新线程时,我们推荐使用带有Runnable的方法,因为Runnable中保存的是线程实际执行的代码,将其作为实例传送到Thread 中,可以更好的实现线程和线程中执行代码的分离,在想要修改执行代码内容的时候可能更为方便一点。同时,我们真正在操作系统中想要创建新线程,必须要调用start()方法,否则,我们的线程其实是没有被创建出来的。

2)线程的命名

我们在查看Thread构造方法时,可以看到

这个构造方法的意思是,创建一个线程并且命名,在我们命名线程后,可以更容易的分辨出来它,我们可以用Java自带的jConsole.exe文件来查看进程
中的各个线程,在这里就不过多演示了。

3)多线程的利用

在我们之前的了解中,可以知道,要是将进程比作一个工厂,那么线程相当于工厂中的流水线,我们想要提高工作效率,可以多建造几个同时工作的流水线,这样其实相当于更好的利用了多核的CPU。那么我们现在就介绍一个多线程的好处。

例子

当我们想要实现两个数从零自增到一个很大的数的时候,换做我们之前的思路,我们可以在main方法中写两个循环,就像这样

    public static void main(String[] args) 
        long a = 0;
        long b = 0;
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10_0000_0000; i++) 
            a++;
        
        for (int i = 0; i < 10_0000_0000; i++) 
            b++;
        
        long end = System.currentTimeMillis();
        System.out.println("方法一耗时:" + (end - start) + "ms");
    

我们可以计算一下这种方法消耗的时间,在上述代码中使用了System.currentTimeMillis()方法,可以得到当前的时间(以毫秒为单位)
那么我们运行程序,得到

也就是说,我们的程序自上而下串行执行,需要消耗49ms左右。
但是我们可以使用第二个方法,创建两个线程,线程一负责a的增加,线程二负责b的增加,我们这个时候来看一下消耗时间

        long start1 = System.currentTimeMillis();
        Thread t1 = new Thread(() -> 
            long number = 0;
            for (int i = 0; i < 10_0000_0000; i++) 
                number++;
            
        );
        Thread t2 = new Thread(() -> 
            long number = 0;
            for (int i = 0; i < 10_0000_0000; i++) 
                number++;
            
        );
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long end1 = System.currentTimeMillis();
        System.out.println("方法二耗时:" + (end1 - start1) + "ms");
    

我们可以看到,方法二竟然比方法一慢了,这是为什么呢,和我们预料到的不一样,按理来说,两个线程并发执行,应该比方法一这种串行执行要快,尽管快不到百分之百(在操作系统底层,并不是一直并行,而是并发和并行都会存在,这样就不可能提升百分之一百的效率),但是也不该慢啊,事实上,这是因为我们的数太小了,我们创建线程同样也是需要时间的,这个创建新线程所消耗的时间占了任务的大部分时间,这样就得不偿失了。因此,多线程更适用于任务量比较大的程序之中。

4)线程的中断

我们刚刚提到了,线程创建中最重要的是重写run()方法,因为我们的线程中执行的就是run()方法中的代码,那么中断线程,本质上就是使得线程的run()方法中断,我们可以使用几种方式来中断线程。

1.自定义标志位

我们可以在线程中利用我们自定义的标志位,例如

    public static boolean flg = true;
    public static void main(String[] args) throws InterruptedException 
        Thread t = new Thread(() -> 
            while(flg)
                System.out.println("thread!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t.start();
        Thread.sleep(5000);
        flg = false;
    


我们可以看到,在我们的定时5000ms,也就是输出了五次thread!后,线程中断了,这时,我们通过控制flg 就可以中断线程了。

2.利用Thread类中自带的标志位

1)Thread.currentThread().isInterrupted(); .currentThread()方法可以得到线程的实例,是一个实例方法,比较推荐
2)Thread.interruputed()方法 该方法是一个静态方法(因为该种方法属于Thread的静态成员,一个进程中只能有一个,因此会造成很多误会)

        Thread t1 = new Thread(() -> 
            while(!Thread.currentThread().isInterrupted())
                System.out.println("thread1!");
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t1.start();
        Thread.sleep(4000);
        t1.interrupt();
    

在这段代码中,我们使用了线程实例名.interrupt()方法,这个方法就是设置这个实例所创建的线程的标志位,中断该线程,但是我们在运行的途中,发现系统报出异常

这是为什么呢,其实这是因为我们在设置标志位时会有两种情况,如果线程处于正常的就绪态,那么我们设置标志位为true后,线程中断,但是我们在线程中有sleep()方法,这个方法是使线程进入阻塞状态,阻塞时间为1000ms,那么这就意味着我们的线程很大一部分时间都处在阻塞状态中,这时我们设置标志位的话,就会触发异常,并且,我们的标志位也没有设置成功,程序还在进行,那么解决这个的方法是什么?其实我们不难发现

                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                

出问题的是这段代码,那么我们完全可以在抓到这个异常的时候,直接给它break,或者执行一些代码再break,这样就可以达到一个中断进程的目的了。

                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                    System.out.println("收尾工作!");
                    break;
                

我们这时,再次运行发现

在捕捉到异常后,程序执行收尾工作后停止了运行,达到了我们的目的。

5)线程等待

我们在这里同时创建两个线程,让他输出不同的东西,来看看有什么发现

    public static void main(String[] args) 
        Thread t1 = new Thread(() -> 
            System.out.println("thread1!");
            try 
                Thread.sleep(500);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        );
        Thread t2 = new Thread(() -> 
            System.out.println("thread2!");
            try 
                Thread.sleep(500);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        );
        t1.start();
        t2.start();


我们竟然发现,t2连续运行了两次,按照我们之前的习惯,不应该是t1先运行一次,t2再运行一次,这样交替运行吗,但是事实不然,这是因为系统在调用线程时,优先级并不固定,是随机调用的,但是我们在有些需求中,可能需要线程交替运行,或者某一线程先运行完,再运行另一个线程,这个时候,我们就要用到线程等待的方法,join()方法。

join()方法介绍:

线程名1.join()这个方法,是调用join()方法这个线程,等待线程名1对应的线程先结束,之后再继续执行,这么说可能很绕,我们结合代码来看一下

        long start1 = System.currentTimeMillis();
        Thread t1 = new Thread(() -> 
            long number = 0;
            for (int i = 0; i < 10_0000_0000; i++) 
                number++;
            
        );
        Thread t2 = new Thread(() -> 
            long number = 0;
            for (int i = 0; i < 10_0000_0000; i++) 
                number++;
            
        );
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long end1 = System.currentTimeMillis();
        System.out.println("方法二耗时:" + (end1 - start1) + "ms");
    

我们在提及线程的利用时,我们使用过join方法,为了计算出准确的线程执行完的时间,我们相当于在main线程中,写了t1.join(),这意味着main方法执行到这一行的时候,程序暂停了下来,在等待t1线程执行完毕之后,再执行t2线程,在等待t2线程执行完毕之后,再执行计算时间的操作,这样就是实现了main线程等待t1,t2线程等待的情况,实现了线程等待。
join方法本质上是死等线程结束,比方说

        Thread t1 = new Thread(() -> 
            while(true)
                System.out.println("thread1!");
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        Thread t2 = new Thread(() -> 
            try 
                t1.join();
             catch (InterruptedException e) 
                e.printStackTrace();
            
            while(true)
                System.out.println("thread2!");
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t1.start();
        t2.start();

当我们在t2线程中先加一个t1.join,让t2线程等待t1线程执行完再开始执行,那么即使我们t2.start(),也不会输出thread2!,这就是死等,t1不结束,t2就不开始。

这时我们只需要在join()方法的括号中加一个等待时间,就可以实现等待一段时间后就不等了,比方说join(2000),那么预期效果应该是先输出四个threa1!后输出thread2!

我们发现确实如此。这就是线程的等待,join方法。

6)线程休眠

线程休眠事实上就是使线程暂时到阻塞态之中,过一段时间再调整到就绪态中,我们要利用的是sleep()方法,sleep中加入指定的时间(ms为单位),在之前我们已经使用过多次,这里就不仔细介绍了,我们需要知道的是,线程休眠实质上是将线程所对应的额PCB(process control block)从就绪态队列中调整到阻塞态队列中。

7)获取当前的线程实例

Thread.currentThread()方法,可以得到当前的实例,在哪个线程中使用这个方法,就可以得到当前线程的实例

我们在t线程中调用了Thread.currentThread(),得到的就是t线程的名字,
在main线程中调用则得到了main的名字,只是因为优先级的不确定先输出了main,其实,如果我们采用创造线程中的1.3方法时,完全可以将t线程中的Thread.currentThread()方法完全可以换成this,效果是一样的,但是我们在使用带有Runnable的表达式时(包含图中的利用lambda表达式创建线程),不可以用this来代替,因为这个时候的this指代的时Runnable实例,而非Thread实例。

以上是关于初识多线程的主要内容,如果未能解决你的问题,请参考以下文章

初识多线程

28初识线程

java学习笔记之初识多线程

进程线程区别,和线程初识

初识并发编程

初识Java多线程