深入理解Java多线程(一)—练基本功


多线程是Java的重要知识点,也是跳槽面试的高频考点。多线程涉及的内容比较多,想要深入理解并不容易,但如果掌握了,对写出安全高效的程序有极大的帮助。

一、概念:进程与线程,并行与并发

进程:程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一个完整的过程,这个过程也是进程本身从产生,发展到最终消亡的过程。
多进程操作系统能同时达运行多个进程(程序),由于 CPU 具备分时机制,所以每个进程都能循环获得自己的CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好像是在同时运行一样。

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径

并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事,是真正的同时。

并发:一个CPU(采用时间片)同时执行多个任务,比如一个人同时做多件事。通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,每个时间片只执行一个任务。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

多线程是实现并发机制的一种有效手段,目的是更好的利用cpu的资源。

进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。进程与线程的区别如图所示:

image.png

线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确,例如加入@synchronized关键字。

二、线程的状态变化

image.png

上图中线程的各种状态一目了然,任何线程一般具有5种状态,即创建,就绪,运行,阻塞,终止。下面分别介绍一下这几种状态:

  • 创建状态 New

在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。

  • 就绪状态 Runnable

新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。

  • 运行状态 Running

被CPU调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。

  • 阻塞状态 Blocked

一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。进入Blocked状态的可能有几种:

  1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

此外,在Runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

  • 死亡状态 Dead

线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

在此提出一个问题,Java 程序每次运行至少启动几个线程

回答:至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程(用户线程),另外一个是垃圾收集线程(守护线程)。

三、线程的创建方式

在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。下面我们就分别来介绍这两种方式的使用。

3.1 实现 Runnable 接口

public class MyRunnable implements Runnable {
    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            Log.e("MyRunnable", name + "运行,i = " + i);
        }
    }
}

public static void test() {
    MyRunnable myRunnable1 = new MyRunnable("MyRunnable A");
    MyRunnable myRunnable2 = new MyRunnable("MyRunnable B");
    Thread thread1 = new Thread(myRunnable1);
    Thread thread2 = new Thread(myRunnable2);
    thread1.start();
    thread2.start();
}

运行结果:

MyRunnable A 运行,i = 0
MyRunnable B 运行,i = 0
MyRunnable B 运行,i = 1
MyRunnable A 运行,i = 1
MyRunnable A 运行,i = 2
MyRunnable B 运行,i = 2
MyRunnable B 运行,i = 3
MyRunnable A 运行,i = 3

3.2 继承 Thread 类

public class MyThread extends Thread {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            Log.e("Thread", name + "运行,i = " + i);
        }
    }
}

public static void test() {
    MyThread myThread1 = new MyThread("MyThread A");
    MyThread myThread2 = new MyThread("MyThread B");
    myThread1.start();
    myThread2.start();
}

运行结果:

MyThread A 运行,i = 0
MyThread B 运行,i = 0
MyThread A 运行,i = 1
MyThread B 运行,i = 1
MyThread A 运行,i = 2
MyThread B 运行,i = 2
MyThread A 运行,i = 3

从程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。

3.3 Thread 类和 Runnable 接口的区别

两者都可以创建多线程,那到底有什么区别?

public class Thread implements Runnable

从 Thread 类的定义可以清楚的发现,Thread 类也是 Runnable 接口的子类,但在Thread类中并没有完全实现 Runnable 接口中的 run() 方法,下面是 Thread 类的部分定义。

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

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
   ...
   this.target = target;
   ...
}

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

从Thread类代码可以看出,Thread类是Runnable接口的子类。如果定义Thread时传入了Runnable对象,调用Thread类的start方法运行线程,实际是执行了传入的Runnable对象的run方法。如果是继承Thread类,则需要重写run方法。

实现Runnable接口创建多线程相比继承Thread类的好处:
1、避免了单继承的局限性,实现Runnable还可以继承其他类,实现其他接口;
2、降低耦合性,实现runnable接口的方式,把设置线程任务和开启新线程进行了分离。

所以,创建线程,一般我们用实现Runnable接口来实现,简洁明了。

注意:实现了Runnable接口的类,称为 线程辅助类Thread类才是真正的线程类

image.png

四、线程的操作

4.1 线程的强制运行

在线程操作中,可以使用 join() 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。

public static void testJoin() {
    MyRunnable myRunnable = new MyRunnable("MyRunnable");
    Thread thread = new Thread(myRunnable);
    thread.start();
    for (int i = 1; i < 100; i++) {
        if (i == 5) {
            try {
                Log.e("MyRunnable", "main thread MyRunnable join");
                thread.join();
            } catch (Exception e) {

            }
        }
        Log.e("MyRunnable", "main thread 运行," + i);
    }
}

运行结果:

main thread 运行,1
main thread 运行,2
main thread 运行,3
main thread 运行,4
main thread 运行, MyRunnable join
MyRunnable 运行,i = 0
MyRunnable 运行,i = 1
MyRunnable 运行,i = 2

4.2 线程的休眠

在线程操作中,可直接使用 Thread.sleep() 即实现线程的暂时休眠,代码见上面定义的MyRunnable类。

4.3 中断线程

中断线程的操作稍微复杂,对于阻塞的线程,可直接使用 Thread.interrupt()进行终止,但无法终止正在运行的线程。

收到interrupt信号后,抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

Thread.currentThread().isInterrupted; //判断线程是否中断。

Thread.currentThread().interrupt();//重新设置中断标示

中断线程最好的,最受推荐的方式是,使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。

public class MyRunnable implements Runnable {
    private String name;
    private boolean stop = false;//共享变量

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            Log.e("MyRunnable", name + "运行,i = " + i);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Log.e("MyRunnable", "InterruptedException");
                Thread.currentThread().interrupt();//重新设置中断标示
            } catch (Exception e) {

            }

            if(Thread.currentThread().isInterrupted()){
                Log.e("MyRunnable", "isInterrupted");
                break;
            }
            
            if(stop){
               break;
            }
        }
    }
}

4.4 后台线程

在 Java 程序中,只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行,要想实现这样的操作,直接使用 setDaemon() 方法即可。

public class DaemonThread implements Runnable {
    private String name;

    public DaemonThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (true) {
            Log.e("DaemonThread", "DaemonThread running");
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }
    }
}

public static void main(String[] args) {
    DaemonThread daemonThread = new DaemonThread("DaemonThread");
    Thread thread = new Thread(daemonThread);
    thread.setDaemon(true); // 此线程在后台运行
    thread.start();
    Log.e("DaemonThread", "main thread running");
}

在线程类 DaemonThread 中,尽管 run() 方法中是死循环的方式,但是程序依然可以执行完,因为方法中死循环的线程操作已经设置成后台运行。

4.5 线程优先级

线程可以划分优先级,优先级高的线程得到的CPU资源比较多,也就是CPU优先执行优先级高的线程对象中的任务。

在java中线程优先级分为1~10,如果小于1或者大于10,则jdk报illegalArgumentException()异常。

public class MyRunnable implements Runnable {
    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Log.e("MyRunnable", name + "运行,Priority=" + Thread.currentThread().getPriority());
        Long st1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            Random random = new Random();
            random.nextInt();
        }
        Log.e("MyRunnable", name + "运行,totalTime=" + (System.currentTimeMillis() - st1));
    }
}

public static void testPriority() {
    MyRunnable myRunnable1 = new MyRunnable("MyRunnable1");
    Thread thread1 = new Thread(myRunnable1);
    MyRunnable myRunnable2 = new MyRunnable("MyRunnable2");
    Thread thread2 = new Thread(myRunnable2);
    MyRunnable myRunnable3 = new MyRunnable("MyRunnable3");
    Thread thread3 = new Thread(myRunnable3);
    thread1.setPriority(Thread.MIN_PRIORITY);
    thread2.setPriority(Thread.MAX_PRIORITY);
    thread3.setPriority(Thread.NORM_PRIORITY);
    thread1.start();
    thread2.start();
    thread3.start();
    Log.e("MyRunnable", "main thread 运行,Priority=" + Thread.currentThread().getPriority());
}

运行结果

main thread 运行,Priority=5
MyRunnable2运行,Priority=10
MyRunnable3运行,Priority=5
MyRunnable2运行,totalTime=61
MyRunnable1运行,Priority=1
MyRunnable3运行,totalTime=159
MyRunnable1运行,totalTime=124

主线程的优先级是5。

多次运行可以发现,线程的优先级具有一定的规则性,cpu尽量将执行资源让给优先级比较高的线程;但是也有随机性,即优先级高的线程不一定优先执行,哪个线程先执行将由 CPU 的调度决定。

4.6 线程礼让

在线程操作中,也可以使用 yield() 方法将一个线程的操作暂时让给其他线程执行。

public class MyRunnable implements Runnable {
    private String name;

    public MyRunnable(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            Log.e("MyRunnable", name + "运行,i=" + i);
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            if (i == 2) {
                Log.e("MyRunnable", name + "礼让");
                Thread.yield();
            }
        }
    }
}
public static void testYield() {
    MyRunnable myRunnable1 = new MyRunnable("MyRunnable A");
    MyRunnable myRunnable2 = new MyRunnable("MyRunnable B");
    Thread thread1 = new Thread(myRunnable1);
    Thread thread2 = new Thread(myRunnable2);
    thread1.start();
    thread2.start();
}

运行结果:

MyRunnable B运行,i=0
MyRunnable A运行,i=0
MyRunnable B运行,i=1
MyRunnable A运行,i=1
MyRunnable B运行,i=2
MyRunnable A运行,i=2
MyRunnable A礼让
MyRunnable B礼让
MyRunnable B运行,i=3
MyRunnable A运行,i=3
MyRunnable B运行,i=4

五、总结

本篇介绍了与多线程有关的一些概念,线程状态及基本操作,后续会继续深入讲解线程同步安全、复合应用等。

参考

Android多线程:实现Runnable接口 使用解析(含实例教程)

Java中的多线程你只要看这一篇就够了

Thread的中断机制(interrupt)

关注V “码农翻身记”,回复888,免费领取技术资料。关注后,你将不定期收到优质技术及职场干货分享,希望陪伴有梦想的你一起前进。

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

推荐阅读更多精彩内容