美文网首页
简单聊聊 Java线程的基础知识

简单聊聊 Java线程的基础知识

作者: Jevely | 来源:发表于2019-08-12 12:59 被阅读0次

    哈喽,大家好,线程是Java中很重要的一个知识点,我相信大家都知道如何运用多线程来处理任务,但是其中有很多细节可能不是特别的明白,我打算做一系列有关线程的文章,就当是个记录,顺便和大家分享一下有关线程的知识。

    这篇文章我们先来讲一讲线程的基础知识,那么下面直接开始。


    进程

    一说到线程,那就不得不提进程。这两个概念很多人最开始容易混淆,而且面试的时候,有的面试官也会问到。那么什么是进程呢,进程是程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

    这样说可能还是有点懵逼,举个简单的栗子,你在手机上启动一个软件,那么这个软件就是一个进程。或者说你在电脑上打开QQ,那么这个QQ就是一个进程。

    线程

    进程说完了,来说说线程,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程可以在进程中独立运行子任务,并且一个进程至少有一个线程。

    来举个栗子,假如你在手机上启动了QQ,在QQ中你可以和好友聊天,下载文件,传输数据,其中每一项工作我们都可以理解为一个线程在执行。这些工作也可以同时执行,当它们同时进行的时候我们可以理解为多个线程同时执行,这也是线程的好处之一,同时处理多个任务,以节约时间。

    多线程同时工作的时候其实是CPU在各个线程之间快速切换,速度很快,使我们感觉是在同时进行。

    线程运用

    线程的调用大家肯定都很熟悉了,有两种方法来调用线程执行任务,下面我们来分别讲一讲。

    新建一个类并继承Thread类

    下面我们看下代码

    public class TestMain {
    
        public static void main(String[] args) {
            TestThread testThread = new TestThread();
            testThread.start();
        }
    
        public static class TestThread extends Thread {
            @Override
            public void run() {
                System.out.println("TestThread is run");
            }
        }
    }
    

    代码大家肯定都很熟悉,需要注意的是start方法重复调用会报错。
    当我们继承Thread类的时候有一个不好的地方是Java并不能多继承,这样可能会影响代码的灵活性,所以一般来说实现Runnable接口是一个更好的选择。

    新建一个类实现Runnable接口

    我们在Thread源码中看到,Thread的构造函数可以传入Runnable。

        public Thread(Runnable target) {
            init(null, target, "Thread-" + nextThreadNum(), 0);
        }
    

    所以我们也可以新建一个类并实现Runnable接口传入Thread中,并执行该线程。

    下面我们先看看代码

    public class TestMain {
    
        public static void main(String[] args) {
            Thread thread = new Thread(new TestThread());
            thread.start();
        }
    
        public static class TestThread implements Runnable{
            @Override
            public void run() {
                System.out.println("test is run");
            }
        }
    }
    

    这个相信大家也是写了很多遍了,没什么好说的。

    线程执行不确定性

    线程在执行的过程中有不确定性,这里我们先来看个例子。

    public class TestMain {
    
        public static void main(String[] args) {
            Thread thread = new Thread(new TestThread());
            thread.start();
    
            for (int i = 0; i < 100; i++) {
                System.out.println("main");
            }
        }
    
        public static class TestThread implements Runnable {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("thread");
                }
            }
        }
    }
    

    运行结果

    thread
    thread
    thread
    main
    main
    main
    main
    main
    thread
    thread
    thread
    

    我们可以看到在运行的结果中thread和main是交叉打印出来的,并不是先执行完thread或者main。当我们调用start方法的时候,会告诉"线程规划器"这个线程已经准备好了,等待调用线程对象的run方法。这个过程就是让系统安排一个时间来调用该线程中的run方法,使线程得到运行,具有异步执行的效果。所以我们会看到thread和main会交叉打印出来。

    线程安全

    线程安全是线程知识里面一个重要的知识点,简单来说就是当多个线程同时访问同一个变量时,可能会造成变量的不同步。我们先来举例,加入有5张门票,5个售票员,每个售票员卖出一张门票,门票数量就少1。下面先看看代码。

    public class TestMain {
    
        private static int count = 5;
    
        public static void main(String[] args) {
            TestThread testThread = new TestThread();
            Thread a = new Thread(testThread, "A");
            Thread b = new Thread(testThread, "B");
            Thread c = new Thread(testThread, "C");
            Thread d = new Thread(testThread, "D");
            Thread e = new Thread(testThread, "E");
            a.start();
            b.start();
            c.start();
            d.start();
            e.start();
        }
    
        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                count--;
                System.out.println(currentThread().getName() + "卖出一张票,还剩余:" + count);
            }
        }
    }
    

    代码很简单,就是依照上面的栗子写的,那么我们来看看运行结果

    A卖出一张票,还剩余:3
    B卖出一张票,还剩余:3
    C卖出一张票,还剩余:2
    D卖出一张票,还剩余:1
    E卖出一张票,还剩余:0
    

    我们可以看到结果中出现了两个3,这个是因为A,B同时访问了这个变量造成的。这就是线程安全问题,那么我们如何解决这个问题呢。Java给我们提供了synchronized字符,我们先来修改一下代码。

        public static class TestThread extends Thread {
    
            @Override
            synchronized public void run() {
                count--;
                System.out.println(currentThread().getName() + "卖出一张票,还剩余:" + count);
            }
        }
    

    我们在run方法前面加入synchronized。下面我们来看看运行结果。

    B卖出一张票,还剩余:4
    C卖出一张票,还剩余:3
    A卖出一张票,还剩余:2
    D卖出一张票,还剩余:1
    E卖出一张票,还剩余:0
    

    结果中并没有重复的数字出现。当在run方法前面加入synchronized的时,运行到run方法,会先去判断run方法是否有加锁,如果加锁了,证明别的线程在调用这个方法,就先等待其他线程调用完毕后再执行这个方法。这样run方法就是排队执行完成的,所以结果正常,没有同时访问同一个变量。当运行run方法的时候,如果没有加锁,那么线程会去拿这个锁,注意这里是所有线程同时抢这把锁,谁抢到了就先执行谁的run方法。

    isAlive

    isAlive方法是判断线程是否处于激活状态。我们先来看看代码

    public class TestMain {
    
        public static void main(String[] args) {
            TestThread testThread = new TestThread();
            System.out.println("start testThread isAlive = " + testThread.isAlive());
            testThread.start();
            System.out.println("end testThread isAlive = " + testThread.isAlive());
        }
    
        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                System.out.println("testThread isAlive = " + currentThread().isAlive());
            }
        }
    }
    

    运行结果为

    start testThread isAlive = false
    end testThread isAlive = true
    testThread isAlive = true
    

    我们可以发现当调用start方法过后,线程就处于激活状态了。因为这里end在线程执行完成之前就打印了,所以也是true,如果我们修改下代码,那么end就可能为false了。

    public static void main(String[] args) {
            try {
                TestThread testThread = new TestThread();
                System.out.println("start testThread isAlive = " + testThread.isAlive());
                testThread.start();
                Thread.sleep(1000);
                System.out.println("end testThread isAlive = " + testThread.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    结果为:

    start testThread isAlive = false
    testThread isAlive = true
    end testThread isAlive = false
    

    下面我们再看一个有趣的栗子:

    public class TestMain {
    
        public static void main(String[] args) {
            TestThread testThread = new TestThread();
            Thread thread = new Thread(testThread);
            thread.start();
        }
    
        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                System.out.println("currentThread isAlive = " + currentThread().isAlive());
                System.out.println("this isAlive = " + this.isAlive());
            }
        }
    }
    

    这个运行结果为:

    currentThread isAlive = true
    this isAlive = false
    

    这里第一个为true我相信大家都可以理解,那么为什么第二个为false呢。这个就要说说currentThread这个方法了,这个方法获取的是在哪个线程中运行,而this获取的是当前线程。因为testThread 是以参数传入到了Thread中,在Thread中并不是像线程调用start方法那样来运行run方法的。而是直接调用run方法,所以this.isAlive()获取的当前线程并没有调用start方法,所以为false。而currentThread获取的是运行的线程,所以结果为true。

    线程停止

    线程的停止我们主要来讲一讲interrupt方法。我们先来看一段代码:

    public class TestMain {
    
        public static void main(String[] args) {
            TestThread testThread = new TestThread();
            testThread.start();
            testThread.interrupt();
        }
    
        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    System.out.println(i);
                }
            }
        }
    }
    

    我们在线程调用start方法过后马上又调用了interrupt方法,按理来说线程应该立马停止,那么我们看看结果:

    .......
    49997
    49998
    49999
    

    最后我们可以看到线程是完完整整执行完成了的。难道interrupt方法没有作用吗?我们先来看看另外两个方法

        public static boolean interrupted() {
            return currentThread().isInterrupted(true);
        }
    
        public boolean isInterrupted() {
            return isInterrupted(false);
        }
    

    这两个方法都是判断线程是否已经中断。第一个方法是一个静态方法,并且在方法中调用了currentThread方法,所以它判断的是当前运行的线程是否已经中断。第二个方法是判断线程对象是否已经中断。我们发现他们最终都是调用了同一个方法,我们先来看看这个方法:

        /**
         * Tests if some Thread has been interrupted.  The interrupted state
         * is reset or not based on the value of ClearInterrupted that is
         * passed.
         */
        private native boolean isInterrupted(boolean ClearInterrupted);
    

    这是一个native方法,传入的参数是指是否清除线程的中断状态。true为清除,false为不清除。我们在代码中加入判断试试

    public static void main(String[] args) {
            TestThread testThread = new TestThread();
            testThread.start();
            testThread.interrupt();
            System.out.println("线程是否中断:" + testThread.isInterrupted());
        }
    

    结果为:

    线程是否中断:true
    

    我们可以发现在调用interrupt方法过后其实是给线程加了一个中断的标识,我们调用isInterrupted方法就可以看出。那么我们就可以运用这个特性,让线程实现真正的中断。下面来看看修改的代码:

    public class TestMain {
    
        public static void main(String[] args) {
            try {
                TestThread testThread = new TestThread();
                testThread.start();
                Thread.sleep(200);
                testThread.interrupt();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    if (isInterrupted()) {
                        System.out.println("线程已经终止");
                        break;
                    }
                    System.out.println(i);
                }
            }
        }
    }
    

    结果为:

    ......
    35289
    35290
    线程已经终止
    

    我们可以发现线程进入了中断判断并跳出了for循环。这样虽然可以终止for循环,但是for循环以下的代码依然会执行,有的人肯定会想到用return,这样也是可以的,并且不会执行for循环下面的代码,但是return太多会造成代码污染,这里我们推荐另一个方法。先来看看代码:

        public static class TestThread extends Thread {
    
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 50000; i++) {
                        if (isInterrupted()) {
                            System.out.println("线程已经终止");
                            throw new InterruptedException();
                        }
                        System.out.println(i);
                    }
                    System.out.println("for循环后面的代码");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("进入catch:" + e.toString());
                }
            }
        }
    

    我们利用try catch来停止线程,并可以在catch中做一些释放等操作。
    结果为:

    ......
    39160
    线程已经终止
    java.lang.InterruptedException
        at test.TestMain$TestThread.run(TestMain.java:24)
    进入catch:java.lang.InterruptedException
    

    yield()

    yield()方法的作用是先放弃当前的CPU资源,让其他线程去占用CPU执行时间。但是放弃的时间不确定,可能刚刚放弃马上又占有CPU资源了。下面我们举个栗子:

    public class TestMain {
    
        public static void main(String[] args) {
            TestThread1 testThread1 = new TestThread1();
            testThread1.start();
            TestThread2 testThread2 = new TestThread2();
            testThread2.start();
        }
    
        public static class TestThread1 extends Thread {
    
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (int i = 0; i < 50000; i++) {
                    int a = i;
                }
                long end = System.currentTimeMillis();
                System.out.println("1使用时间为:" + (end - start));
            }
        }
    
        public static class TestThread2 extends Thread {
    
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (int i = 0; i < 50000; i++) {
                    yield();
                    int a = i;
                }
                long end = System.currentTimeMillis();
                System.out.println("2使用时间为:" + (end - start));
            }
        }
    
    }
    

    结果为:

    1使用时间为:1
    2使用时间为:8
    

    我们可以明显的看到2使用的时间长于1使用的时间。

    线程优先级

    在线程中有一个方法可以设置线程的优先级

    public final void setPriority(int newPriority)
    

    Java线程中线程分为1-10个等级,等级越高,线程被执行的几率也就越大,这里要注意是执行的几率,而不是优先级高的就比优先级低的先执行。

    另外线程的优先级是有传递效果的,举个栗子,A线程启动B线程,如果A线程优先级为5,那么B线程的优先级也为5。

    守护线程

    守护线程可能大家平时都没有怎么用,我们平时经常使用的是用户线程,守护线程是一个特殊的线程,当我们进程中没有用户线程的时候,守护线程就会自动销毁。Java中典型的守护线程就是垃圾回收线程。下面我们来举个栗子:

    public class TestMain {
    
        public static void main(String[] args) {
            try {
                TestThread1 testThread1 = new TestThread1();
                testThread1.setDaemon(true);
                testThread1.start();
                Thread.sleep(1000);
                System.out.println("end");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static class TestThread1 extends Thread {
    
            @Override
            public void run() {
                try {
                    int i = 0;
                    while (true) {
                        i++;
                        System.out.println(i);
                        Thread.sleep(200);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    结果为:

    1
    2
    3
    4
    5
    end
    

    而当我们去掉testThread1.setDaemon(true);这句代码,结果为:

    ......
    3
    4
    5
    end
    6
    7
    ......
    

    这样我们就发现当线程为守护线程的时候,main结束了,守护线程也就结束了,如果不是守护线程,则会一直执行。


    到这里线程的基础就讲完了,上文中有错误的地方欢迎大家指出。

    3Q

    相关文章

      网友评论

          本文标题:简单聊聊 Java线程的基础知识

          本文链接:https://www.haomeiwen.com/subject/tauejctx.html