线程详解

来源:互联网 发布:取消3.5mm耳机接口知乎 编辑:程序博客网 时间:2024/05/23 14:02

1. 概念

线程,CPU调度的基本单位,被包裹在进程里面,同一个进程里的线程共享同一片内存空间。其中,守护线程是特殊的线程,守护线程自创建伊始会在后台为用户线程提供服务,其生命贯彻进程的整个生命周期。

2. 特点

  • 轻型实体:这种轻体现在线程是程序执行流的最小单位,执行时仅向系统请求执行所必须的一点儿资源。
  • 共享进程内存空间:同一个进程里的线程拥有同一个内存空间地址(进程空间地址),共享这片内存空间,同一个进程里的线程互相通信不需要调用内核。
  • 并发执行:线程通过互夺CPU资源实现并发执行,这种并发效果不是严格意义的同时执行,而是在多个线程之间高速切换,以至于给人并发执行的错觉。

3. 组成

  • 线程ID:线程标识符
  • 当前指令指针(PC)
  • 寄存器集合:存储单元寄存器的集合
  • 堆栈:两种数据结构

4. 状态

  • 运行:线程占有处理机正在执行
  • 阻塞:线程在等待某个事件(如某个信号量),逻辑上不可执行
  • 就绪:线程一切准备就绪,等待处理机执行,逻辑上可执行

5. 生命周期

  1. 新建状态
    • 通过new方法新建一个线程,该线程被加载进堆内存,此时的线程不具有运行所必须的资源
  2. 可运行态
    • 调用线程的start()方法,使该线程获得运行所需的一点系统资源,同时调用run()方法。
  3. 不可运行态
    • 以下方法会使线程进入不可运行态
      • 线程调用sleep()方法进入睡眠
      • 线程调用wait()方法
      • 发生I/O阻塞
    • 以下方法可使线程脱离不可运行态
      • sleep()时间结束
      • 被notify()唤醒
      • 等待输入输出完成
  4. 消亡态
    • 当线程的run()方法走到生命的尽头,线程进入消亡态,消亡了的线程不能再被start()

线程的生命周期

5. 多线程

  • 多个线程并发执行的过程称作多线程

6. Java实现多线程的两个方法

1、 继承Thread类,重写run()方法

代码示例

class SubThread extends Thread{    public void run(){        ... ; //线程体,线程所要实现的功能    }}public class TestThread{    public static void main(String[] args){        SubThread st = new SubThread();        st.start();    }}

2、 实现Runable方法,重写run()方法,并使用Thread构造函数构造线程

代码示例

class SubThread implememts Runable{    public void run(){        ... ; //线程体,线程所要实现的功能    }}public class TestThread{    public static void main(String[] args){        SubThread r = new SubThread();        Thread st = new Thread(r);        st.start()    }

7. 继承 VS 实现

  • 实现的方式优于继承的方式,原因如下:

    1. 继承实现多线程难免会走进单继承的尴尬,而实现的方式则可以避免这个情况实现多继承。
    2. 在子线程中需要对共享数据进行操作的时候,实现的方式只要在实现Runable的类中定义一个普通的变量就可以实现数据共享,因为在实例化时实现Runable的对象只被创建一次,之后以该对象为参数实例化的线程共享同一片内存数据;而继承的方式每次实例化一个对象会重新开辟一个内存空间,如果要操作同一片内存空间就不得不用static修饰,这种方式修饰的内存空间生命周期长。

8. 线程的方法

  1. start():开启一个线程,获得运行所需的系统资源;调用run()方法
  2. run():线程体,线程要实现的主体功能
  3. sleep(Long l):显式地让线程睡眠l毫秒,睡眠时让出CPU执行权
  4. join():暂停当前线程,让调用该方法的线程参与进来,并在该线程执行结束后才开始执行当前线程
  5. currentThread():返回当前占有处理机的线程
  6. setName():设置线程名字
  7. getName():返回线程名字
  8. yield():让出CPU的执行权,但并不是说让出CPU执行权就一定是下一个线程执行,因为有可能让出执行权后又抢到执行权,继续执行。
  9. isAlive():判断一个线程是否消亡

代码示例

public class TestThread{    public static void main(String[] args){        SubThread st = new SubThread();          st.start();        //st.run();   该方法仅仅是调用st对象的run()方法,而不是一个线程,执行这一步的还是主线程        //st.start();   一个线程在消亡之后就等同于人类的死亡,是不会有重新开始的            st.sleep(1000); //这里要阐述线程和对象的关系,线程是线程,对象是对象,即使这里使用SubThread对象                            //调用的sleep()方法,但是实际上是主线程在执行这个方法,两者不冲突    }}

9. 线程的优先级

线程的优先级并不是绝对的优先,而是混沌的。被给予了高优先级的线程仅仅只是在概率上有较大概率抢到CPU执行权,而非绝对。线程预设的优先级别有以下三个:

  • NORM_PRIORITY = 5
  • MIN_PRIORITY = 1
  • MAX_PRIORITY = 10

其中,一般我们创建一个线程优先级都为NORM_PRIORITY。

设置和获得线程优先级的方法如下:

  • setPriority(int i)
  • getPriority()

PS:线程创建时继承父类线程优先级。线程优先级在1~10之间,离开这个范围虚拟机会报java.lang.IllegalArgumentException错误。

10. 线程的同步机制

1. 线程的安全问题

代码示例

class RunnableImpl implements Runnable{    private int num = 20;    public void run(){        while(true){            if(num >= 1){                try {                    Thread.currentThread().sleep(10);                } catch (InterruptedException e) {                    // TODO Auto-generated catch block                    e.printStackTrace();                }                System.out.println(num--);            }else{                break;            }        }    }}public class TestThread{    public static void main(String[] args){        RunnableImpl ri = new RunnableImpl();        Thread t1 = new Thread(ri);        Thread t2 = new Thread(ri);        t1.start();        t2.start();     }}

上述程序其中一次输出结果如下:

2019181716151413121110109876543211

由此可见上述程序是存在安全隐患的,程序的本意是让两个线程相互协作打印出20到1,然而由程序可以看出,出现了一些重复值,这些重复值的出现意味着该程序是线程不安全的。
出现上述线程安全的原因是:当线程t1通过if条件判断后,还没来得及执行num--的的操作就失去了CPU操作权,而后线程t2进来自然会导致一些数字被打印多次。解决这种线程不安全的问题的关键是:当某个线程进入某个共享区域后,对该区域数据进行操作期间其他线程必须不能再进入该区域,直到这个线程对该区域的操作完毕。

2. 解决线程安全问题的两个方案

  • 当多个线程尝试操作共享数据时,将操作共享数据的代码块用synchrosized(mutex)声明为同步代码块。

代码示例

class RunnableImpl implements Runnable{    private int num = 20;    public void run(){        while(true){            synchronized(this){                if(num >= 1){                    try {                        Thread.currentThread().sleep(10);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(num--);                }else{                    break;                }            }        }    }}public class TestThread{    public static void main(String[] args){        RunnableImpl ri = new RunnableImpl();        Thread t1 = new Thread(ri);        Thread t2 = new Thread(ri);        t1.start();        t2.start();     }}

其中mutex是互斥锁,它可以是任意对象,但必须是同一对象,不同对象代表不同锁。在这里用this表示用RunnableImpl实例化的对象本身。

  • 将对共享数据操作部分封装成一个方法,用synchrosized修饰。

代码示例

class RunnableImpl implements Runnable{    private int num = 20;    public void run(){        while(num >= 1){            printNum();        }    }    public synchronized void printNum(){        try {            Thread.currentThread().sleep(10);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(num--);    }}public class TestThread{    public static void main(String[] args){        RunnableImpl ri = new RunnableImpl();        Thread t1 = new Thread(ri);        Thread t2 = new Thread(ri);        t1.start();        t2.start();     }}

上述解决方案是将共享数据操作放进一个synchronized声明的方法中,该解决方案的互斥锁默认为this。

其实,解决这种线程问题的方法就是提出“锁”的概念,当一个线程进入共享数据操作时就上锁(lockd),直到该线程对操作完毕。实质上当某个线程进入被synchronized修饰的代码块或方法时,该方法块的状态改变,只有被同步监视器(mutex)标记的线程可进入该区域,当此条线程离开,状态恢复。示意图如下(只是想试试GIF图制作,请多包涵):

这里写图片描述

11. 线程的通信

线程的通信主要是三个方法:

  • wait():使线程挂起,并交出CPU执行权,被挂起的线程会被放进等待队列中等待唤醒
  • notify():唤醒等待队列中优先级最高的一个线程
  • notifyAll():唤醒所有线程

首先,这些方法不是Java.lang.Thread里的方法,而是Java.lang.Object的方法,也就是所有的对象都具有该方法,但该方法只能使用在同步代码块或同步方法中。

0 0