Java 多线程梳理(一、线程概述)

链接: Java 多线程梳理(一、线程概述).


  • 线程相关概念
  • 线程的创建与启动
  • 线程的常用方法
  • 线程的生命周期
  • 多线程编程的优势与存在的风险

1 线程概述

1.1 线程相关概念

- 进程:
  1. 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,
  2. 是操作系统进行资源分配与调度的基本单位
  3. 可以把进程简单的理解为正在操作系统中运行的一个程序.
- 线程:
  1. 线程(thread)是进程的一个执行单元. 一个线程就是进程中一个单一顺序的控制流, 进程的一个执行分支
  2. 进程是线程的容器,一个进程至少有一个线程.一个进程中也可以有多个线程.
  3. 在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等. 每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储.
- 主线程与子线程
  1. JVM 启动时会创建一个主线程,该主线程负责执行 main 方法 . 主线程就是运行 main 方法的线程
  2. Java 中的线程不孤立的,线程之间存在一些联系. 如果在 A 线程中创建了 B 线程, 称 B 线程为 A 线程的子线程, 相应的 A 线程就是 B 线
    程的父线程
- 串行,并发与并行!

串行,并发与并行!
并发 可以提高以事物的处理效率, 即一段时间内可以处理或者完成更多的事情.
并行 是一种更为严格,理想的并发
从硬件角度来说, 如果单核 CPU,一个处理器一次只能执行一个线程的情况下,处理器可以使用时间片轮转技术 ,可以让 CPU 快速的在各个线程之间进行切换
对于用来来说,感觉是三个线程在同时执行. 如果是多核心 CPU,可以为不同的线程分配不同的 CPU 内核

cpu核心与逻辑处理器小知识:
单核是一个cpu中的一个内核,能单独运行一个计算。
cpu核心与逻辑处理器

1、物理cpu数:主板上实际插入的cpu数量,可以数不重复的 physical id 有几个。

2、cpu核数:单块CPU上面能处理数据的芯片组的数量,如双核、四核等 (cpu cores)

3、逻辑cpu数:简单来说,它可使处理器中的1颗内核,如2颗内核那样在操作系统中发挥作用。

这样一来,操作系统可使用的执行资源扩大了一倍,大幅提高了系统的整体性能,此时逻辑cpu=物理CPU个数×每颗核数x2。

1.2 线程创建与启动

在 Java 中,创建一个线程就是创建一个 Thread 类(子类)的对象(实例).
Thread 类有两个常用 的构造方法:Thread()与 Thread(Runnable).
对应的创建线程的两种方式(这两种创建线程的方式没有本质的区别):

  • 定义 Thread 类的子类
package cn.qnut;

/**
 * 1) 定义一个类继承Thread
 * @author Qnut
 */
public class MyThread extends Thread {
    /**
     * 2)重写Thread父类中的run()方法
     * run()方法里的代码就是要执行的任务
     */
    @Override
    public void run() {
        System.out.println("这是子线程要打印的内容");
    }
}
package cn.qnut;

/**
 * @author Qnut
 */
public class TestThread {
    public static void main(String[] args) {
        System.out.println("Java 虚拟机启动Main线程,启动Main方法");
        // 3)创建子线程对象
        MyThread myThread = new MyThread();
        // 4)启动子线程
        myThread.start();
        /*
         * 调用线程的 start()方法来启动线程, 启动线程的实质就是请求 JVM 运行相应的线程,
         * 这个线程具体在什么时候运行由线程调度器(Scheduler)决定
         * 注意:
         * start()方法调用结束并不意味着子线程开始运行
         * 新开启的线程会执行 run()方法
         * 如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序
         * 多线程运行结果与代码执行顺序或调用顺序无关
         */
        System.out.println("main 线程后面其他 的代码...");
    }
}

  • 定义一个 Runnable 接口的实现
package cn.qnut;

/**
 * 当线程类已经有父类了,就不能用继承 Thread 类的形式创建线程
 * 可以使用实现 Runnable接口的形式
 * 1) 定义类实现 Runnable 接口
 *
 * @author Qnut
 */
public class MyRunnable implements Runnable {
    public static final Integer number = 1000;
    /**
     * 2) 重写 Runnable 接口中的抽象方法 run()
     * run()方法就是子线程要执行的代码
     */
    @Override
    public void run() {
        for (int i = 1; i <= number; i++) {
            System.out.println("sub thread --> " + i);
        }
    }
}

package cn.qnut;

/**
 * 测试实现 Runnable 接口的形式创建线
 * @author Qnut
 */
public class TestRunnable {
    public static void main(String[] args) {
        // 3)创建 Runnable 接口的实现类对象
        MyRunnable runnable = new MyRunnable();
        // 4)创建线程对象
        Thread thread = new Thread(runnable);
        // 5)开启线程
        thread.start();
        //当前是 main 线程
        for(int i = 1; i <= 1000; i++){
            System.out.println( "main==> " + i);
        }

        // 有时调用 Thread(Runnable)构造方法时,实参也会传递匿名内部类对象
        // 但是不推荐显示创建线程,一般使用线程池
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 1; i<=1000; i++){
                    System.out.println( "sub ------------------------------> " + i);
                }
            }
        });
        thread2.start();
    }
}

1.3 线程的常用方法

1.3.1 currentThread()方法

Thread.currentThread() 方法可以获得当前线程
Java 中的任何一段代码都是执行在某个线程当中的. 执行当前代码的线程就是当前线程.
同一段代码可能被不同的线程执行, 因此当前线程是相对的,
Thread.currentThread() 方法的返回值是在代码实际运行时候的线程对象

package cn.qnut;

/**
 * @author Qnut
 */
public class SubThread1 extends Thread {
    public SubThread1(){
        System.out.println(" 构 造 方 法 打 印 当 前 线 程 的 名 称 : " +
                Thread.currentThread().getName());
    }

    @Override
    public void run() {
        System.out.println("run 方 法 打 印 当 前 线 程 名 称 : " +
                Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        System.out.println("main 方 法 中 打 印 当 前 线 程 : " +
                Thread.currentThread().getName());
        //创建子线程, 调用 SubThread1()构造方法, 在 main 线程中调用构造方法,所以构造方法中 的当前线程就是 main 线程
        SubThread1 t1 = new SubThread1();
        // 启动子线程,子线程会调用 run()方法,所以 run()方法中 的当前线程就是 Thread-0 子线程
        t1.start();
        // 在 main 方法中直接调用 run()方法,没有开启新的线程,所以在 run 方法中的当前线程就是 main 线程
        t1.run();
    }
}

SubThread1

package cn.qnut;

/**
 * 当前线程的复杂案例
 * @author Qnut
 */
public class SubThread2 extends Thread {
    public SubThread2(){
        System.out.println(" 构 造 方 法 中 ,Thread.currentThread().getName() : " +
                Thread.currentThread().getName() );
        System.out.println("构造方法, this.getName() : " + this.getName());
    }
    @Override
    public void run() {
        System.out.println("run 方 法 中 ,Thread.currentThread().getName() : " +
                Thread.currentThread().getName() );
        System.out.println("run 方法,this.getName() : " + this.getName());
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建子线程对象
        SubThread2 t2 = new SubThread2();
        // 设置线程的名称
        t2.setName("t2");
        t2.start();
        // main 线程睡眠 500 毫秒
        Thread.sleep(500);
        // Thread(Runnable)构造方法形参是 Runnable 接口,调用时传递的实参是接口的实现类对象t2
        // Thread有实现Runnable接口
        Thread t3 = new Thread(t2);
        t3.start();
    }
}

SubThread2

1.3.2 setName()/getName()

thread.setName(线程名称), 设置线程名称
thread.getName() 返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性, 建议为每个线程都设置一个能够体现线程功能的名称

1.3.3 isAlive()

thread.isAlive()判断当前线程是否处于活动状态
活动状态就是线程已启动并且尚未终止

package cn.qnut;

/**
 * @author Qnut
 */
public class SubThread3 extends Thread{
    @Override
    public void run() {
        // 运行状态,true
        System.out.println("run 方法, isAlive = " + this.isAlive());
    }

    public static void main(String[] args) {
        // * 测试线程的活动状态
        SubThread3 t3 = new SubThread3();
        // false,在启动线程之前
        System.out.println("begin==" + t3.isAlive());
        t3.start();
        //结果不一定,打印这一行时,如果 t3线程还没结束就返回 true, 如果 t3 线程已结束,返回 false
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
        System.out.println("end==" + t3.isAlive());
    }
}

SubThread3

1.3.4 sleep()

Thread.sleep(millis); 让当前线程休眠指定的毫秒数
当前线程是指 Thread.currentThread() 返回的线

package cn.qnut;

/**
 * 子线程休眠
 * @author Qnut
 */
public class SubThread4 extends Thread {
    @Override
    public void run() {
        try {
            System.out.println("run, threadname=" + Thread.currentThread().getName()
                    + " ,begin= " + System.currentTimeMillis());
            //当前线程睡眠 2000 毫秒
            Thread.sleep(2000);
            System.out.println("run, threadname=" + Thread.currentThread().getName()
                    + " ,end= " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            //在子线程的 run 方法中, 如果有受检异常(编译时异常)需要处理,只有选择捕获处理,不能抛出处理
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SubThread4 t4 = new SubThread4();
        System.out.println("main__begin = " + System.currentTimeMillis());
        // 开启新的线程
        t4.start();
        // 在 main 线程中调用实例方法 run(),没有开启新的线程
        t4.run();
        System.out.println("main__end = " + System.currentTimeMillis());
    }
}

SubThread4
使用线程休眠 Thread.sleep 完成一个简易的计时器

package cn.qnut;

/**
 * 使用线程休眠 Thread.sleep 完成一个简易的计时器
 * @author Qnut
 */
public class SimpleTimer {
    public static void main(String[] args) {
        //从 10 秒开始计时
        int remaining = 10;
        //读取 main 方法的参数
        if (args.length == 1){
            remaining = Integer.parseInt(args[0]);
        }
        while(true){
            System.out.println("Remaining: " + remaining);
            remaining--;
            if (remaining < 0 ){
                break;
            }
            try {
                //线程休眠
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Done!!");
    }
}

SimpleTimer

1.3.5 getId()

thread.getId() 可以获得线程的唯一标识
注意:

  • 某个编号的线程运行结束后, 该编号可能被后续创建的线程使用
  • 重启的 JVM后,同一个线程的编号可能不一样
package cn.qnut;

/**
 * @author Qnut
 */
public class SubThread5 extends Thread {
    @Override
    public void run() {
        System.out.println("thread name = " + Thread.currentThread().getName()
                + ", id == " + this.getId() );
    }

    public static void main(String[] args) {
        System.out.println( Thread.currentThread().getName() + " , id = " +
                Thread.currentThread().getId());
        // 子线程的 id
        for(int i = 1; i <= 100; i++){
            new SubThread5().start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

SubThread5

1.3.6 yield()

Thread.yield()方法的作用是放弃当前的 CPU 资源

package cn.qnut;

/**
 * @author Qnut
 */
public class SubThread6 extends Thread {
    @Override
    public void run() {
        long begin = System.currentTimeMillis();
        long sum = 0;
        for(int i = 1; i <= 1000000; i++){
            sum += i;
            //线程让步, 放弃 CPU 执行权
            Thread.yield();
        }
        long end = System.currentTimeMillis();
        System.out.println("sub t6 用时: " + (end - begin));
    }

    public static void main(String[] args) {
        //开启子线程,计算累加和
        SubThread6 t6 = new SubThread6();
        t6.start();
        //在 main 线程中计算累加和
        long begin = System.currentTimeMillis();
        long sum = 0;
        for(int i = 1; i <= 1000000; i++){
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("main 方法 , 用时: " + (end - begin));
    }
}

放弃资源用时:
SubThread6——0
不放弃资源用时:
SubThread6——1

1.3.7 setPriority()

thread.setPriority( num ); 设置线程的优先级

  • java 线程的优先级取值范围是 1 ~ 10 , 如果超出这个范围会抛出异常 IllegalArgumentException.
  • 在操作系统中,优先级较高的线程获得 CPU 的资源越多
  • 线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程. 注意不能保证优先级高的线程先运行.
  • Java 优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿.
  • 线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级
  • 线程的优先级具有继承性, 在 A 线程中创建了 B 线程,则 B 线程的优先级与 A 线程是一样的.
package cn.qnut.P6priority;

/**
 * @author Qnut
 */
public class ThreadA extends Thread {
    @Override
    public void run() {
        long begin = System.currentTimeMillis();
        long sum = 0 ;
        for(long i = 0 ; i<= 10000000000L; i++){
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("thread a : " + (end - begin));
    }
}


package cn.qnut.P6priority;

/**
 * @author Qnut
 */
public class ThreadB extends Thread {
    @Override
    public void run() {
        long begin = System.currentTimeMillis();
        long sum = 0 ;
        for(long i = 0 ; i<= 10000000000L; i++){
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("thread b : " + (end - begin));
    }
}

package cn.qnut.P6priority;

/**
 * @author Qnut
 */
public class TestPriority {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        threadA.setPriority(1);
        threadA.start();
        ThreadB threadB = new ThreadB();
        threadB.setPriority(10);
        threadB.start();
    }
}

不设置优先级结果:
TestPriority -0
设置优先级之后:
在TestPriority -1

1.3.8 interrupt()

中断线程. 注意调用 interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程

package cn.qnut.P7interrupt;

/**
 * @author Qnut
 */
public class SubThread extends Thread {
    @Override
    public void run() {
        super.run();
        for(int i = 1; i <= 10000; i++) {
            //判断线程的中断标志,线程有 isInterrupted()方法,该方法返回线程的中断标志
            if ( this.isInterrupted() ){
                System.out.println("当前线程的中断标志为 true, 我要退出了");
                // 中断循环, run()方法体执行完毕, 子线程运行完毕
                // break;
                // 直接结束当前 run()方法的执行
                return;
            }
            System.out.println("sub thread --> " + i);
        }
    }
}

package cn.qnut.P7interrupt;

public class Test {
    public static void main(String[] args) {
        SubThread t1 = new SubThread();
        // 开启子线程
        t1.start();
        // 当前线程是 main 线程
        for(int i = 1; i <= 100; i++){
            System.out.println("main ==> " + i);
        }
        // 中断子线程
        // 仅仅是给子线程标记中断
        t1.interrupt();
    }
}

P7interrupt

1.3.9 setDaemon()

Java 中的线程分为用户线程守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能单独运行, 当 JVM 中没有其他用户线程,只有守护线程时,守护线程会自动销毁, JVM 会退出

package cn.qnut.P8subDaemonThread;

/**
 * @author Qnut
 */
public class SubDaemonThread extends Thread {
    @Override
    public void run() {
        super.run();
        while (true){
            System.out.println("sub thread.....");
        }
    }

    public static void main(String[] args) {
        SubDaemonThread thread = new SubDaemonThread();
        // 设置线程为守护线程
        // 设置守护线程的代码应该在线程启动前
        thread.setDaemon(true);
        thread.start();
        //当前线程为 main 线程
        for(int i = 1; i <= 3; i++){
            System.out.println("main== " + i);
        }
        //当 main 线程结束, 守护线程 thread 也销毁了
    }
}

SubDaemonThread

1.4 线程的生命周期

线程的生命周期是线程对象的生老病死, 即线程的状态

线程生命周期可以通过 getState() 方法获得, 线程的状态是Thread.State 枚举类型定义的, 有以下几种:

  • NEW: 新建状态.
    创建了线程对象,在调用 start()启动之前的状态;
  • RUNNABLE: 可运行状态.
    它 是一 个复 合状 态, 包 含: READYRUNNING 两个状态. READY 状态该线程可以被线程调度器进行调度使它 处 于 RUNNING 状 态 , RUNING 状 态 表 示 该 线 程 正 在 执 行 . Thread.yield()方法可以把线程由 RUNNING 状态转换为 READY 状态
  • BLOCKED: 阻塞状态.
    线程发起阻塞的 I/O 操作,或者申请由其他线程占用的独占资源,线程会转换为 BLOCKED 阻塞状态.
    处于阻塞状态的线程不会占用CPU 资源. 当阻塞I/O 操作执行完,
    或者线程获得了其申请的资源,线程可以转换为 RUNNABLE
  • WAITING: 等待状态.
    线程执行了 **object.wait(), thread.join()**方法会把线程转换为 WAITING 等待状态,
    执行 object.notify()方法,或者加入的线程执行完毕,当前线程会转换为 RUNNABLE 状态
  • TIMED_WAITING: 与 WAITING 状态类似,都是等待状态.
    区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为 RUNNABLE
  • TERMINATED: 终止状态,线程结束处于终止状态

多线程生命周期

1.5 多线程编程的优势与存在的风险

多线程编程具有以下优势:

  1. 提高系统的吞吐率(Throughout). 多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作
  2. 提高响应性(Responsiveness).Web 服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
  3. 充分利用多核(Multicore)处理器资源. 通过多线程可以充分的利用 CPU 资源

多线程编程存在的问题与风险:

  1. 线程安全(Thread safe)问题.多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,
    如读取脏数据(过期的数据), 如丢失数据更新.
  2. 线程活性(thread liveness)问题.由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 RUNNABLE 状态,
    这就是线程活性问题, 常见的活性故障有以下几种
    (1) 死锁(Deadlock). 类似鹬蚌相争.
    (2) 锁死(Lockout), 类似于睡美人故事中王子挂了
    (3) 活锁(Livelock). 类似于小猫咬自己尾巴
    (4) 饥饿(Starvation).类似于健壮的雏鸟总是从母鸟嘴中抢到食物.
  3. 上下文切换(Context Switch). 处理器从执行一个线程切换到执行另外一个线程
  4. 可靠性. 可能会由一个线程导致 JVM 意外终止,其他的线程也无法执行

参考资料:

Java多线程实战精讲-带你一次搞明白Java多线程高并发.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值