Java多线程(上)

进程概述

进程 是资源分配的最小单位。每一个进程都有它自己的内存空间和系统资源。比如,一边玩游戏(游戏进程),一边听音乐(音乐进程)。计算机都是支持多进程的,可以在一个时间段内执行多个任务,提高 CPU 的使用率。

注意:单 CPU 在某一个时间点上只能做一件事情,我们玩游戏可以同时听音乐,是因为 CPU 在程序间的高效切换让我们觉得是同时进行的。


线程概述

线程 是CPU调度的最小单位。线程是依赖于进程而存在。线程是程序的执行单元,执行路径。同一个进程内又可以执行多个线程,程序的执行其实都是在抢 CPU 的资源,CPU的执行权。多个进程都在抢 CPU 执行权,而其中某一个进程如果执行路径比较多,就会有更高的几率抢到 CPU 的执行权。

注意:多线程技术不是万金油,它有一个致命的缺点:在一个进程内,不管你创建了多少线程,它们总是被限定在一颗 CPU 内,或者多核 CPU 的一个核内。这意味着,多线程在宏观上是并行的,在微观上则是分时切换串行的,多线程编程无法充分发挥多核计算资源的优势。这也是使用多线程做任务并行处理时,线程数量超过一定数值后,线程越多速度反倒越慢的原因

而多进程技术正好弥补了多线程编程的不足,我们可以在每一颗 CPU 上,或者多核 CPU 的每一个核上启动一个进程。但是,多进程不在同一块内存区域内,和线程相比,进程间的资源共享、通信、同步等,都要麻烦得多,受到的限制也更多。


线程实现方式

1. 继承Thread类

public class Thread extends Object implements Runnable

  • 构造方法
    • public Thread()
    • public Thread(String name)
    • public Thread(Runnable target)
    • public Thread(Runnable target, String name)
    • public Thread(ThreadGroup group, String name)
    • public Thread(ThreadGroup group, Runnable target)
    • public Thread(ThreadGroup group, Runnable target, String name):分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员
    • public Thread(ThreadGroup group, Runnable target, String name, long stackSize):指定的堆栈大小
  • 常用方法
    • public static Thread currentThread():返回当前正在执行的线程对象
    • public final String getName():获取线程的名称
    • public final void setName(String name):设置线程的名称
public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<5; i++){
            System.out.println(Thread.currentThread().getName()+"=" + i);
        }
    }
}

public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();

        m1.run();
        m1.start();
        // m1.start();  报错IllegalThreadStateException
    }
}

运行结果:
main=0
main=1
main=2
main=3
main=4
Thread-0=0
Thread-0=1
Thread-0=2
Thread-0=3
Thread-0=4

结果 main=0 看出,调用 run() 其实就相当于普通的方法调用,还是在main主线程调用,没有创建线程执行
结果 Thread-0=0 看出,调用 start() 首先启动了线程,然后再由jvm去调用该线程的run()方法

:多次执行同一个线程,会报错IllegalThreadStateException:非法的线程状态异常,因为这个相当于是 MyThread 线程被调用了两次,而不是两个线程启动。

2. 实现Runnable接口(无返回值)

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int x = 0; x < 5; x++) {
            // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}

public class MyRunnableDemo {
    public static void main(String[] args) throws InterruptedException {
        /* 创建MyRunnable类的对象 */
        MyRunnable my = new MyRunnable();

        /* Thread(Runnable target, String name) */
        Thread t1 = new Thread(my, "lili");
        Thread t2 = new Thread(my, "shwen");
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int x = 0; x < 5; x++) {
                    System.out.println(Thread.currentThread().getName() + ":" + x);
                }
            }
        },"lucy").start();

        System.out.println(t1.getThreadGroup().getName());
        System.out.println(t2.getThreadGroup().getName());
        t1.start();
        t2.start();

        Thread.sleep(3000);

        ThreadGroup tg = new ThreadGroup("这是一个新的组");

        /* Thread(ThreadGroup group, Runnable target, String name) */
        Thread t3 = new Thread(tg, my, "线程1");
        Thread t4 = new Thread(tg, my, "线程2");

        System.out.println(t3.getThreadGroup().getName());
        System.out.println(t4.getThreadGroup().getName());

        /* 通过组名称设置后台线程,表示该组的线程都是后台线程 */
        tg.setDaemon(true);
    }
}

结果输出:
main
main
lucy:0
lucy:1
lucy:2
lucy:3
lucy:4
lili:0
lili:1
lili:2
lili:3
lili:4
shwen:0
shwen:1
shwen:2
shwen:3
shwen:4
这是一个新的组
这是一个新的组

需要说明一点:实现 Runnable 的类应该被看作一项任务,而不是一个线程。Java多线程中,任务和线程是不同的概念。可以使用线程(Thread)执行任务(比如Runnable),但任务不是线程。

有了Thread方法,为什么还有实现Runable创建线程的方式呢?

  1. 可以避免由于Java单继承带来的局限性
  2. 适合多个相同程序的代码去处理同一个资源的情况,把线程处理代码和数据有效的分离,体现了面向对象设计思路。

3. 匿名内部类

匿名内部类的方式

public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(){
            public void run() {
                for (int x = 0; x < 5; x++) {
                    System.out.println(Thread.currentThread().getName() + ":" + x);
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int x = 0; x < 5; x++) {
                    System.out.println(Thread.currentThread().getName() + ":"  + x);
                }
            }
        }, "线程1").start();

        // 组合
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int x = 0; x < 5; x++) {
                    System.out.println(Thread.currentThread().getName() + "-hello:"  + x);
                }
            }
        }, "线程2"){
            public void run() {
                for (int x = 0; x < 5; x++) {
                    System.out.println(Thread.currentThread().getName() + "-world:"  + x);
                }
            }
        }.start();
    }
}

输出:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
线程1:0
线程1:1
线程1:2
线程1:3
线程1:4
线程2-world:0
线程2-world:1
线程2-world:2
线程2-world:3
线程2-world:4

第三种方式纯粹是为了体现组合,最终继承 Thread 的方式会 覆盖 实现Runnable接口的方式。

4. 基于Callable实现(有返回值)

Callable接口类似于 Runnable 接口,但比 Runnable 接口更强大,增加了异常和返回值。

  1. 创建FutureTask对象,指定实现Callable接口对象作为线程任务。
  2. 创建线程,指定线程任务。
  3. 启动线程,通过get()获取任务返回值。

MyCallable:

//1、创建一个类实现 Callable 接口,实现 call 方法。
public class MyCallable implements Callable<Integer> {

    private int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }
}

CallableDemo:

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1、创建 FutureTask,指向 Callable 对象,作为线程任务;
        FutureTask<Integer> task = new FutureTask<>(new MyCallable(100));

        //2、创建线程,指定线程任务;
        new Thread(task, "FutureTask").start();

        //3、拿到线程返回值
        Integer integer = task.get();
        System.out.println("主线程拿到异步线程返回结果:" + integer);
    }
}

输出:
主线程拿到异步线程返回结果:5050

5、线程池

以下简单说下,后续详细介绍 线程池整理汇总

  • void execute(Runnable command)
  • <T> Future<T> submit(Callable<T> task)
public class ExecutorsDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
         * 创建一个线程池对象,nThreads指定要创建线程对象个数。
         * public static ExecutorService newFixedThreadPool(int nThreads)
        */
        ExecutorService pool = Executors.newFixedThreadPool(2);

        /* 可以执行Runnable对象或者Callable对象代表的线程 */
        pool.execute(new MyRunnable());
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        System.out.println(f1.get());

        /* 结束线程池 */
        pool.shutdown();
    }
}

运行结果:
pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
5050

6、定时器

详见 Java定时器Timer和第三方定时器Quartz

线程组

线程组把多个线程组合到一起,以便批量进行管理,Java允许程序直接对线程组进行控制。

  • public Thread(ThreadGroup group, Runnable target, String name)
  • public final ThreadGroup getThreadGroup():获取线程所在线程组
  • public final String getName():获取线程所在线程组名称
public class ThreadGroupDemo {
    public static void main(String[] args) {
         method1();

        // 我们如何修改线程所在的组呢?
        // 创建一个线程组
        // 创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
        method2();

    }

    private static void method1() {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my, "线程1");
        Thread t2 = new Thread(my, "线程2");

        // 我不知道他们属于那个线程组,我想知道,怎么办
        // 线程类里面的方法:public final ThreadGroup getThreadGroup()
        ThreadGroup tg1 = t1.getThreadGroup();
        ThreadGroup tg2 = t2.getThreadGroup();

        // 线程组里面的方法:public final String getName()
        System.out.println(tg1.getName());
        System.out.println(tg2.getName());

        // 通过结果我们知道了:线程默认情况下属于main线程组
        // 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
        System.out.println(Thread.currentThread().getThreadGroup().getName());
    }

    private static void method2() {
        // ThreadGroup(String name)
        ThreadGroup tg = new ThreadGroup("这是一个新的组");

        MyRunnable my = new MyRunnable();
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, my, "线程1");
        Thread t2 = new Thread(tg, my, "线程2");

        System.out.println(t1.getThreadGroup().getName());
        System.out.println(t2.getThreadGroup().getName());

        //通过组名称设置后台线程,表示该组的线程都是后台线程
        tg.setDaemon(true);
    }
}

输出:
main
main
main
这是一个新的组
这是一个新的组

多线程共享资源

以一个例子,创建MyRunnable ,属性有个数组。

public class MyRunnable implements Runnable {
    public int[] num = {1, 2, 3};

    @Override
    public void run() {
        // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
        System.out.println(Thread.currentThread().getName() + ":" + "num=" + num);
    }
}

创建多线程启动:

public class MyRunnableDemo {
    public static void main(String[] args) {
        // 创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();

        new Thread(my,"thread1").start();
        new Thread(my,"thread2").start();
        new Thread(my,"thread3").start();
    }
}

输出:
thread2:num=[I@60174d9
thread1:num=[I@60174d9
thread3:num=[I@60174d9

结果看出,线程thread1、thread2、thread3共享同一个对象 my,从而共享对象my的所有资源,如果其中一个线程修改属性值,其他线程读取是修改后的值,具体 Java多线程(下)会详说。

问题:那如果线程间想独立资源呢?

那就创建多个MyRunnable类对象:

public class MyRunnableDemo {
    public static void main(String[] args) {
        // 创建多个MyRunnable类的对象
        MyRunnable my1 = new MyRunnable();
        MyRunnable my2 = new MyRunnable();
        MyRunnable my3 = new MyRunnable();

        new Thread(my1,"thread1").start();
        new Thread(my2,"thread2").start();
        new Thread(my3,"thread3").start();
    }
}

输出:
thread1:num=[I@7708d4c1
thread3:num=[I@37ff831d
thread2:num=[I@1f474fa

线程调度和线程控制

线程有两种调度模型:

  1. 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。
  2. 抢占式调度模型优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

Java使用的是抢占式调度模型。

  • 如何获取线程对象的优先级?
    • public final int getPriority():返回线程对象的优先级
  • 如何设置线程对象的优先级呢?
    • public final void setPriority(int newPriority):更改线程的优先级,线程默认优先级是5。线程优先级的范围是:1-10。

线程的控制:

  • public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
  • public final void join()等待该线程运行完成
  • public static void sleep(long millis)线程休眠
  • public static void yield()暂停当前正在执行的线程对象并执行其他线程
  • public final void stop():让线程停止,过时了,但是还可以使用。
  • public void interrup()中断线程。 把线程的状态终止,并抛出一个 InterruptedException

setDaemon

public class ThreadDaemonDemo {
    public static void main(String[] args) {
        MyThread td1 = new MyThread ();
        MyThread td2 = new MyThread ();

        td1.setName("关羽");
        td2.setName("张飞");

        // 设置收获线程
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        Thread.currentThread().setName("刘备");
        for (int x = 0; x < 5; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}

输出:
刘备:0
刘备:1
刘备:2
刘备:3
刘备:4

线程td1 、td2均后台执行。

setPriority

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        MyThread tp1 = new MyThread();
        MyThread tp2 = new MyThread();
        MyThread tp3 = new MyThread();

        tp1.setName("东方不败");
        tp2.setName("岳不群");
        tp3.setName("林平之");

        //设置正确的线程优先级
        tp1.setPriority(10);
        tp2.setPriority(1);

        tp1.start();
        tp2.start();
        tp3.start();
    }
}

输出:
东方不败=0
东方不败=1
东方不败=2
东方不败=3
东方不败=4
林平之=0
林平之=1
林平之=2
林平之=3
林平之=4
岳不群=0
岳不群=1
岳不群=2
岳不群=3
岳不群=4

线程优先级高仅仅表示线程获取的 CPU时间片的几率高,并不是等优先级高的执行完毕后才执行优先级低的,当执行数据大的时候,还是会看到交替执行现象。

join

阻塞等待该线程执行完毕后,其他线程才能执行。

public class ThreadJoinDemo {
    public static void main(String[] args) {
        MyThread tj1 = new MyThread();
        MyThread tj2 = new MyThread();
        MyThread tj3 = new MyThread();

        tj1.setName("李渊");
        tj2.setName("李世民");
        tj3.setName("李元霸");

        tj1.start();
        try {
            tj1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tj2.start();
        tj3.start();
    }
}

输出结果:
李渊=0
李渊=1
李渊=2
李渊=3
李渊=4
李世民=0
李世民=1
李世民=2
李世民=3
李元霸=0
李元霸=1
李元霸=2
李元霸=3
李元霸=4
李世民=4

等待线程tj1执行完毕后,才会执行其他线程。

sleep

public class ThreadSleep extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 5; x++) {
            System.out.println(getName() + ":" + x + ",日期:" + new Date());
            // 睡眠
            // 困了,我稍微休息1秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadSleepDemo {
    public static void main(String[] args) {
        ThreadSleep ts1 = new ThreadSleep();
        ThreadSleep ts2 = new ThreadSleep();
        ThreadSleep ts3 = new ThreadSleep();

        ts1.setName("林青霞");
        ts2.setName("林志玲");
        ts3.setName("林志颖");

        ts1.start();
        ts2.start();
        ts3.start();
    }
}

输出:
林青霞:0,日期:Wed Jun 16 00:24:21 CST 2021
林志颖:0,日期:Wed Jun 16 00:24:21 CST 2021
林志玲:0,日期:Wed Jun 16 00:24:21 CST 2021
林青霞:1,日期:Wed Jun 16 00:24:22 CST 2021
林志颖:1,日期:Wed Jun 16 00:24:22 CST 2021
林志玲:1,日期:Wed Jun 16 00:24:22 CST 2021
林青霞:2,日期:Wed Jun 16 00:24:23 CST 2021
林志颖:2,日期:Wed Jun 16 00:24:23 CST 2021
林志玲:2,日期:Wed Jun 16 00:24:23 CST 2021
林志玲:3,日期:Wed Jun 16 00:24:24 CST 2021
林志颖:3,日期:Wed Jun 16 00:24:24 CST 2021
林青霞:3,日期:Wed Jun 16 00:24:24 CST 2021
林志玲:4,日期:Wed Jun 16 00:24:25 CST 2021
林志颖:4,日期:Wed Jun 16 00:24:25 CST 2021
林青霞:4,日期:Wed Jun 16 00:24:25 CST 2021

yield

礼让。使线程状态从运行变为就绪。

public class ThreadYield extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 5; x++) {
            System.out.println(getName() + ":" + x);
            Thread.yield();
        }
    }
}

public class ThreadYieldDemo {
    public static void main(String[] args) {
        ThreadYield ty1 = new ThreadYield();
        ThreadYield ty2 = new ThreadYield();

        ty1.setName("lili");
        ty2.setName("shwen");

        ty1.start();
        ty2.start();
    }
}

输出:
lili:0
shwen:0
lili:1
shwen:1
lili:2
shwen:2
lili:3
shwen:3
lili:4
shwen:4

yield只能保证多线程和谐执行但不能保证线程交替执行,当执行数据大的时候,还是会有某个线程长时间占据CPU执行。

interrupt

public class ThreadStop extends Thread {
    @Override
    public void run() {
        System.out.println("开始执行:" + new Date());

        // 休息10秒钟
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // e.printStackTrace();
            System.out.println("线程被终止了");
        }

        System.out.println("结束执行:" + new Date());
    }
}

public class ThreadStopDemo {
    public static void main(String[] args) {
        ThreadStop ts = new ThreadStop();
        ts.start();

        // 超过三秒不醒过来,我就终结你
        try {
            Thread.sleep(3000);
            // ts.stop();
            ts.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:
开始执行:Wed Jun 16 00:33:27 CST 2021
线程被终止了
结束执行:Wed Jun 16 00:33:30 CST 2021

stop()已经过时,替代使用interrupt()


线程生命周期

线程的生命周期包含5个阶段:

  • 新建:new出来的线程;
  • 就绪调用的线程的start()方法后,这时候线程处于等待 CPU 分配资源阶段,谁先抢的 CPU 资源,谁开始执行;
  • 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run()方法定义了线程的操作和功能;
  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify() 或者notifyAll()。唤醒的线程不会立刻执行run()方法,而是进入就绪状态,等待 CPU 分配资源进入运行状态;
  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值