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

2019-08-12  本文已影响0人  Jevely

哈喽,大家好,线程是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

上一篇下一篇

猜你喜欢

热点阅读