Java创建线程的3种方式和线程控制

一. 进程和线程

1. 什么是进程

进程是处于运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的独立单位。

2. 进程的三个特征

(1)独立性。进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有进过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
(2)动态性。进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,在进程中增加了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
(3)并发性。多个进程可以在单个处理器上并发的执行,多个进程之间不会互相影响。

操作系统多进程支持的理解:程序指令通过cpu执行,在某个时间点只有一个程序的指令得到执行,但是cpu执行指令的速度非常快,所以多个程序指令在cpu上轮流切换执行的速度也很快,这样在宏观上感觉是多个程序在并发的执行。可以这样理解程序 的并发,宏观上并发执行,微观上顺序执行。

3. 什么是线程

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须拥有一个父进程。线程可以拥有自己的的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程的所拥有的全部资源。

二.创建线程的3种方式

1. 继承Thread类创建线程

步骤如下:
a.定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。
b.创建Thread子类实例,即创建了线程对象。
c.调用线程对象的start()方法来创建并启动线程。

public class FirstThread extends Thread{
    private int i;
    
    //重写run()方法,run()方法的方法体就是线程的执行体
    public void run(){
        for(;i<100;i++){
            //  当线程类继承Thread类时,直接使用this即可获取当前线程
            //Thread对象的getName()方法返回当前线程的名字
            //因此可以直接调用getName()方法返回当前线程的名字
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        for(int i=0;i<100;i++){
            //调用Thread类的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                //创建并启动第一个线程
                new FirstThread().start();
                //创建并启动第二个线程
                new FirstThread().start();
            }
        }
    }
}

2. 实现Runnable接口创建线程类

a.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体就是线程的线程执行体。
b.创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

public class SecondThread implements Runnable{
    private int i;
    
    //run()方法同样是线程的执行体
    @Override
    public void run(){
        for(;i<100;i++){
            //当线程实现Runnable接口时
            //如果想获取当前线程,只能通过Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==20){
                SecondThread st=new SecondThread();
                //通过new Thread(target,name)方法创建新线程
                new Thread(st, "新线程1").start();
                new Thread(st, "新线程2").start();
            }
        }
    }
}

3. 使用Callable和Future创建线程

a.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程的执行体,且该call()方法有返回值,再创建Callable实现类的实例。
b.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
c.使用FutureTask对象作为Thread对象的target创建并启动新线程。
d.使用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class ThridThread {
    public static void main(String[] args) {
        //创建Callable对象
        ThridThread rt=new ThridThread();
        //先使用Lambda表达式创建Callable<Integer>对象
        //使用FutureTask来包装Callable对象
        FutureTask<Integer> task=new FutureTask<>((Callable<Integer>)()->{
            int i=0;
            for(;i<100;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            //call()方法的返回值
            return i;
        });
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"循环变量i的值: "+i);
            if(i==20){
                //实质是以Callable对象来创建并启动线程
                new Thread(task,"有返回值的线程:").start();;
            }
        }
        try{
            //获取线程的返回值
            System.out.println("子线程的返回值:"+task.get());
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三.线程的生命周期

每个线程都要经历新建、就绪、运行、阻塞、死亡5种状态。

  • 新建状态:就是通过new关键字创建线程对象时的状态。

  • 就绪状态:即通过线程对象的start()方法启动线程时对应的状态,此时线程并不一定马上能进入运行状态,线程的运行由操作系统的调度程序进行线程的调度。

  • 运行状态:是指线程获得cpu的执行权,线程正在执行需要执行的代码。

  • 阻塞状态:当发生以下情况时线程进入阻塞状态。

    1. 线程调用sleep()方法主动放弃所占有的处理器资源。
    2. 线程调用了一个阻塞式IO方法,在方法返回之前线程阻塞。
    3. 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有。
    4. 线程正在等待某个通知(notify)。
    5. 程序调用了线程的suspend()方法将该线程挂起。
  • 死亡状态:线程会以以下三种方式结束,结束后的线程处于死亡状态。

    1. run()方法和call()方法执行完成,线程正常结束。
    2. 线程抛出一个未捕获的Exception或Error。
    3. 直接调用线程的stop()方法来结束线程。

线程的状态转换图如下:

线程的状态转换

注意点:

  1. 启动一个线程是使用线程对象的start()方法,而不是直接调用run()方法。如果直接调用run()方法,则和普通的对象调用实例方法一样,没有启动一个线程来执行该方法。启动一个线程只能对处于新建状态的线程启动,调用处于新建状态的线程对象使用start()方法来启动一个线程。如果对处于新建状态的线程对象调用了run()方法或其他方法,则此线程对象就不再处于新建状态了,以后调用该线程对象的start()方法将不会启动一个线程。
  2. 对于处于死亡状态的线程不能再次调用该线程的start()方法。程序只能对处于新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的,会引起IllegalThreadStateException异常。

四.控制线程

1. join线程

join()方法时Java的Thread类提供的让一个线程等待另一个线程完成的方法。当在某个程序的执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止。

public class JoinThread extends Thread {
    //提供一个有参构造器,用于设置该线程的名字
    public JoinThread(String name){
        super(name);
    }
    //重写run方法,定义线程的执行体
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<100;i++){
            if(i==20){
                JoinThread jt=new JoinThread("被join的线程");
                jt.start();
                //main线程调用了jt线程的join()方法,main线程必须等待jt线程执行结束才会向下执行
                jt.join();
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

2. 后台线程

后台线程(Daemon Thread)是在后台运行的,它的任务是为其他的线程提供服务,也被称为守护线程或精灵线程。
后台线程的特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将一个指定的线程设置为后台线程。

public class DaemonThread extends Thread {
    public void run(){
        for(int i=0;i<1000;i++){
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        DaemonThread dt=new DaemonThread();
        //将此线程设置为后台线程
        dt.setDaemon(true);
        dt.start();
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        //程序执行到此处,前台线程main线程结束
        //后台线程也应该随之结束
    }
}

3. 线程睡眠:sleep

Thread类的sleep()方法用来暂停线程的执行,调用sleep()的线程将会进入阻塞状态。Thread类的sleep()方法是Thread类的静态方法。

public class SleepTest {
    public static void main(String[] args) throws Exception {
        for(int i=0;i<10;i++){
            System.out.println("当前时间:"+new Date());
            //调用sleep()方法让当前线程暂停1s
            Thread.sleep(1000);
        }
    }
}

4. 线程让步:yield

yield()方法也是Thread类提供的静态方法,让线程暂停执行,与sleep()方法不同的是,yeild()方法不会将线程阻塞,当某个线程调用了yield()方法时,该线程会暂停执行进入就绪状态,只有优先级与当前线程相同或者优先级比当前线程高的处于就绪状态的线程才会获得执行的机会。

public class YieldTest extends Thread {
    public YieldTest(String name){
        super(name);
    }
    public void run(){
        for(int i=0;i<100;i++){
            System.out.println(getName()+" "+i);
            //当i=20时,使用yield()方法让当前线程让步
            if(i==20){
                Thread.yield();
            }
        }
    }
    public static void main(String[] args) {
        //启动两个并发线程
        YieldTest yt1=new YieldTest("高级");
        //将yt1线程的设置为最高优先级
        //yt1.setPriority(MAX_PRIORITY);
        yt1.start();
        YieldTest yt2=new YieldTest("低级");
        //将yt2线程的设置为最低优先级
        //yt2.setPriority(MIN_PRIORITY);
        yt2.start();
    }

执行上面的程序将会看到在i=20的时候yt1线程执行yield()方法,因为yt2线程与yt1线程处于同一优先级别,所以yt2线程将会获得执行权,然后在yt2执行到i=20的时候,yt2调用线程让步方法yeild(),同样的原因线程yt1将会获得执行权。

5. 改变线程的优先级

Java中每个线程都有一定的优先级,优先级高的线程获得执行的机会多,而优先级低的线程获得执行的机会少。对于创建的线程,Java默认的优先级同创建它的父线程的优先级相同。如果想改变线程的优先级,则可以使用Thread类提供的setPriority(int newPriority)方法设置线程的优先级,而getPriority()方法返回线程的优先级。Java中的优先级的参数范围是1-10的整数。

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

推荐阅读更多精彩内容

  • 一、线程的生命周期 线程状态转换图: 1、新建状态 用new关键字和Thread类或其子类建立一个线程对象后,该线...
    我是嘻哈大哥阅读 887评论 0 8
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,438评论 1 15
  • 一、进程和线程 进程 进程就是一个执行中的程序实例,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。...
    阿敏其人阅读 2,606评论 0 13
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,949评论 1 18
  • 首先说明,这不是一篇影评,纯属感悟。 我觉得人都有以偏概全的毛病,无论对什么职业,尤其是警察一类的官员,因为自己的...
    王小八16阅读 264评论 1 0