线程详解

1. 概念

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

2. 特点

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

3. 组成

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

4. 状态

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

5. 生命周期

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

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的对象只被创建一次,之后以该对象为参数实例化的线程共享同一片内存数据;而继承的方式每次实例化一个对象会重新开辟一个内存空间,如果要操作同一片内存空间就不得不用public static final修饰成常量,这种方式修饰的内存空间生命周期长。

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) {
          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();  
  }
}

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

20
19
18
17
16
15
14
13
12
11
10
10
9
8
7
6
5
4
3
2
1
1

由此可见上述程序是存在安全隐患的,程序的本意是让两个线程相互协作打印出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图制作,请多包涵):

synchrosized示意图.gif

11. 线程的通信

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

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

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

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

推荐阅读更多精彩内容

  • 欢迎大家指出文章中需要改正或者需要补充的地方,我会及时更新,非常感谢。 一. 多线程基础 1. 进程 进程是指在系...
    xx_cc阅读 7,196评论 11 70
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,262评论 11 349
  • 今天还是听曲,最后三支曲。 《晚韶华》 镜里恩情,更那堪梦里功名!那美韶华去之何迅!再休提绣帐鸳衾。只这戴珠冠,披...
    无风TINO阅读 606评论 0 1
  • 前几天我和外婆一起去逛超市,在文具区看到了这个大富翁游戏,于是我就把它买了下来。 这两天,下午的阅...
    33小溪流王铭锐阅读 588评论 0 0
  • 今天和孩子们一起做啦选择轮
    乐乐1006阅读 205评论 1 0