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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,952评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,448评论 1 15
  • 一、进程和线程 进程 进程就是一个执行中的程序实例,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。...
    阿敏其人阅读 2,611评论 0 13
  • 整理来自互联网 1,JDK:Java Development Kit,java的开发和运行环境,java的开发工具...
    Ncompass阅读 1,537评论 0 6
  • 一、线程概念 1. 操作系统中的线程现在的操作系统是多任务操作系统,多线程是实现多任务的一种方式,在操作系统中,每...
    TyiMan阅读 1,908评论 1 35