头图

【多线程】线程初体验

cutey_none

上节讲了下线程和进程的基础知识,但是对于Java来说,可能讨论线程的时间会更多些,所以接下来的一系列文章都是着重在讨论线程。

创建线程

创建的线程的方式是老生常谈也是面试中喜欢问的问题之一了,网上的说法众说纷纭,说什么实现Runnable接口和实现Callable接口是同一种类型,这种说法也不是说错误,只不过需要看站在哪个角度看。但是这种细节其实没有必要太在意,不要钻牛角尖。

实现Runnable接口

实现Runnable接口,然后重写run() 方法,该方法定义了线程的工作方式和工作内容。

public class ImplementsRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "------Runnable线程工作中。。。");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            new Thread(new ImplementsRunnable()).start();
        }
    }
}

在main方法中,开启了50个线程运行,开启线程其实就是新建了一个Thread,然后把实现Runnable接口的类作为参数传进去,现在我们来看看运行的结果

可以看到虽然我们是按照顺序来新建线程的,但是线程的先后执行顺序是由CPU来控制的,可以说是不可控的,也正是这样才能说明了多线程在运行。

实现Callable接口

实现了接口后需要重写的是call() 方法

public class ImplementsCallable implements Callable {

    @Override
    public Object call() {
        System.out.println(Thread.currentThread().getName() + "--------callable线程工作中");
        return "实现callable,有返回值";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 50; i++) {
            ImplementsCallable callable = new ImplementsCallable();
            FutureTask<String> task = new FutureTask<String>(callable);
            new Thread(task).start();
            System.out.println(task.get());
        }
    }
}

值得注意的是,在Thread类中的构造函数中,并没有参数为Callable的重载构造函数,基本上都是Runnable

而借助了FutureTask 这个类算是线程工作原理中比较重要的一个类,以后可能会专门出一篇文章来学习,FutureTask 是实现了RunnableFuture 接口,而该接口又是继承了RunnableFuture

与实现Runnable接口方式最大的不同就是,Callable接口有返回值 ,这个返回值使用的场景是什么呢,比如在http调用中,需要返回某个结果,在多线程使用的情况下就会用到Callable和Future来实现。如何获取返回值呢,就是使用FutureTask中的get() 方法,让我们来看看运行结果

这里出现了一个有意思的问题,当我把第14行代码注释后 运行,出现以下结果,线程是混乱无序的,也正是期待的结果。

但是,当我保留第14行代码多次运行 ,又会出现以下结果,线程竟然变得有序了,如果有知道为什么的小伙伴可以留言呀

继承Thread类

继承Thread类后,Idea甚至没有提醒需要重写,需要手动去重写 run()方法

整体代码如下

public class ExtendsThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--------继承Thread的线程工作中");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            new ExtendsThread().start();
        }
    }
}

代码比较简单,我们来看下结果,也是和预期一样

两种方式优先选择实现接口,因为Java不支持多继承,继承了Thread类就不能继承其他类,但是可以实现多个接口。而且就性能开销方面来看,继承整个Thread类显得比较臃肿。

线程常用方法

线程有关的方法有比较多种,这里着重讲下4种常用的方法。

start

在上述例子种可以发现每次开启一个线程基本都是使用了start() 方法来开启,那它是run() 方法的区别是什么呢

public class CommonMethod {
    public static void main(String[] args) throws InterruptedException {
           startRunExample();
    }

    //start,run
    public static void startRunExample() {
        new MyThread().start();
        new MyThread().run();
    }
}

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

新建了一个类,然后创建一个内部类继承了Thread,调用了start()run() 两种方法,在主函数里面再调用封装的方法,来看下结果如何。

可以看到一个线程名字是主线程,一个是子线程,所以start() 方法是开启了一个线程,然后这个线程执行了run() 方法的内容。但是如果直接用run() 方法呢,就是主线程单纯地执行run() 方法的内容,并没有开启新的线程。

sleep

sleep是让当前线程睡眠,让出cpu给其它线程执行。

public class CommonMethod {
    public static void main(String[] args) throws InterruptedException {
//        startRunExample();
        sleepExample();
//        yieldExample();
//        waitExample();
        
    }

    //省略start,run
    
    //sleep
    public static void sleepExample() throws InterruptedException {
        new MyThread().start();
        Thread.sleep(3000);
        System.out.println(Thread.currentThread().getName() + " is running");

    }
}

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

比如我开启了一个新的线程,但是我让主线程休眠3s再运行,结果应该先是Thread-0 is running 然后3s后输出main is running

为了做个对比,我把sleep代码给注释掉,再来看多几遍结果

可以看到两个线程的结果几乎是同时出来,至于哪个前哪个后在这个例子里不是我们能控制的。

yield

yield是指程序员建议计算机把当前线程占用的CPU让给其它线程,但是CPU鸟不鸟我们,又是另外一回事了,通俗地来说就是把线程从Running状态转换成Runnable状态。

再次强调是建议计算机把当前线程挂起,执行其它线程,但是做不做是计算机的事情。

再次新建一个内部类YieldThread

public class CommonMethod {
    public static void main(String[] args) throws InterruptedException {
//        startRunExample();
//        sleepExample();
        yieldExample();
//        waitExample();
        
    }

    //省略start,run
    
    //省略sleep
    
    //yield
    public static void yieldExample() throws InterruptedException {
        YieldThread yieldThread = new YieldThread();
        Thread thread = new Thread(yieldThread, "thread1");
        Thread thread1 = new Thread(yieldThread, "thread2");
        thread.start();
        thread1.start();
    }
}

class YieldThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " is running " + i);
            if (Thread.currentThread().getName().equals("thread1")) {
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + " yield " + i);
        }
    }
}

其实这个例子不太准确,但是能够勉强看,整个run的逻辑就是每个线程跑10遍,每遍输出一个running,一个yield。但是当我们加了Thread.yield() 之后,预期结果是

thread1 is running
thread2 is running
thread2 yield
thread1 yield

就是thread1执行了running语句后,把cpu使用权交出来,cpu选择了执行thread2的一套逻辑后thread1再拿到cpu时间片来执行thread1 yield语句

接着来看下结果是否能和预期一样

可以看到只有部分能够和预期结果一样,当我们去掉了Thread.yield() 这行代码后呢

没错,你会发现偶尔也有这种情况发生,但是没有上面存在的频繁。是因为这两个线程有可能是并行的,而不是并发(交替运行的),所以两者同时执行了running语句,然后线程2接着执行了yield,线程1执行了yield。

这里说得不一定准确,所以说是不太准确的例子,如果有更好的理解和例子可以留言呀!!!

wait

相比于前面的yield而言,接下来的例子可控性更强一点,前者是建议,后者可以对应地说成强制。是把线程从Running状态转变成Block状态,直接挂起线程,没有外力唤醒前不会执行。

public class CommonMethod {
    public static void main(String[] args) throws InterruptedException {
//        startRunExample();
//        sleepExample();
//        yieldExample();
        waitExample();
        
    }

    //省略start,run
    
    //省略sleep
    
    //省略yield
    
    //wait
    public static void waitExample() {
        WaitThread waitThread = new WaitThread();
        Thread thread1 = new Thread(waitThread, "thread1");
        Thread thread2 = new Thread(waitThread, "thread2");
        thread1.start();
        thread2.start();
    }
    
}

class WaitThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " is running " + i);
            if (Thread.currentThread().getName().equals("thread1")) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

逻辑都差不多,只不过把Thread.yield() 换成了wait() ,正常来说是线程名为thread1的线程只要执行一次就不再执行了,让我们来看下结果

和预期结果是一样的,并且还报错java.lang.IllegalMonitorStateException

创作不易,如果对你有帮助,欢迎点赞,收藏和分享啦!

下面是个人公众号,有兴趣的可以关注一下,说不定就是你的宝藏公众号哦,基本2,3天1更技术文章!!!

阅读 495
3 声望
0 粉丝
0 条评论
3 声望
0 粉丝
文章目录
宣传栏