Java多线程讲解

前言

接到菜鸟网络的电话面试,面试官让自己谈一下自己对多线程的理解,现将其内容整理如下。

线程生命周期

Java线程具有五种基本状态

新建状态(New):当线程对象创建后,即进入了新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

注 同步阻塞的正解


JAVA多线程实现的三种方式

JAVA多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用Callable、FutureTask实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

1.继承Thread类实现多线程

继承Thread类的方法尽管列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extends Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}

在合适的地方启动线程如下:

MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();

2.实现Runnable接口方式实现多线程

如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();  

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {
if (target != null) {
target.run();
}
}

3.使用Callable和Future创建线程

Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更为强大:call()方法可以有返回值;call()方法可以声明抛出异常。代码如下:

//实现Callable接口实现线程类
public class callableThread implements Callable<Integer>{
//实现call()方法作为线程执行体
public Integer call(){
int i = 0;
for(; i < 100; i++){
System.out.println(Thread.currentThread().getName() + “ 的循环变量i的值:” + i);
}
//call()方法可以有返回值
return i;
}
public static void main(String [] args){
//创建Callable对象
callableThread ct= new callableThread();
//使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(ct);
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 ex){
ex.printStackTrace();
}
}
}

多线程通信(摘录自课本)

经典场景:用2个线程,这2个线程分别代表存款和取款。——现在系统要求存款者和取款者不断重复的存款和取款的动作,而且每当存款者将钱存入账户后,取款者立即取出这笔钱。不允许2次连续存款、2次连续取款。

传统的线程通信

实现上述场景需要用到Object类提供的wait、notify和notifyAll三个方法,这3个方法并不属于Thread类。但这3个方法必须由同步监视器调用,可分为2种情况:

A、对于使用synchronized修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步中直接调用这3个方法。

B、对于使用synchronized修改的同步代码块,同步监视器是synchronized后可括号中的对象,所以必须使用括号中的对象调用这3个方法

传统方法概述:

一、wait方法:导致当前线程进入等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。

wait方法有3中形式:无参数的wait方法,会一直等待,直到其他线程通知;带毫秒参数的wait和微妙参数的wait,这2种形式都是等待时间到达后苏醒。调用wait方法的当前线程会释放对该对象同步监视器的锁定。

二、notify:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(用wait方法),才可以执行被唤醒的线程。

三、notifyAll:唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才能执行唤醒的线程。

使用条件变量Condition控制协调

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也不能使用wait、notify、notifyAll方法来协调进程的运行。

当使用Lock对象同步,Java提供一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其它处于等待的线程,Condition将同步监视器(wait()、notify()、notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法和同步代码块,Condition替代同步监视器的功能。

Condition实例实质上被绑定在一个Lock对象上,要获得特定的Lock实例的Condition实例,调用Lock对象的newCondition即可。

使用阻塞队列(BlockingQueue)控制线程通信

java中阻塞BlockingQueue 接口实现类中用的较多的通常是ArrayBlockingQueue,LinkedBlockingQueue.它们都是线程安全的.ArrayBlockingQueue以数组的形式存储,LinkedBlockingQueue以node节点的方式进行存储.

开发中如果队列的插入操作比较频繁建议使用LinkedBlockingQueue,因为每个node节点都有一个前后指针,插入新元素仅需要变更前后的指针引用即可, ArrayBlockingQueue插入新元素,则新元素之后的元素数组下标位置都要发生变化,性能较差. 如果队列的读取操作比较频繁建议使用ArrayBlockingQueue, ArrayBlockingQueue通过数组下标直接能找到对应元素,LinkedBlockingQueue则需要遍历node链来找到元素.

BlockingQueue 队列常用的操作方法:

1.往队列中添加元素: add(), put(), offer()

2.从队列中取出或者删除元素: remove() element()  peek()   pool()  take()

队列添加新元素一般都是往队尾添加元素,

offer()方法往队列添加元素如果队列已满直接返回false,队列未满则直接插入并返回true;

add()方法是对offer()方法的简单封装.如果队列已满,抛出异常new IllegalStateException("Queue full");

put()方法往队列里插入元素,如果队列已经满,则会一直等待直到队列为空插入新元素,或者线程被中断抛出异常.

队列中取出或者删除元素都是针对队头的元素

remove()方法直接删除队头的元素:

peek()方法直接取出队头的元素,并不删除.

element()方法对peek方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出异常NoSuchElementException()

pool()方法取出并删除队头的元素,当队列为空,返回null;

take()方法取出并删除队头的元素,当队列为空,则会一直等待直到队列有新元素可以取出,或者线程被中断抛出异常

offer()方法一般跟pool()方法相对应, put()方法一般跟take()方法相对应.日常开发过程中offer()与pool()方法用的相对比较频繁.

sleep()和yield()的区别

sleep()和yield()的区别:sleep()使当前线程进入停滞状态(阻塞态),失去锁的占有权,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()不会阻塞线程,它只是将该线程转入就绪态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

sleep方法使当前运行中的线程睡眠一段时间,进入不可运行状态(阻塞态),这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

另外,sleep方法允许较低优先级的线程获得运行机会,但 yield()方法执行时,当前线程仍处在可运行状态(仍未失去锁),所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到I\O阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

死锁与线程阻塞的区别

死锁

所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

那么为什么会产生死锁呢?

1.因为系统资源不足。

2.进程运行推进的顺序不合适。

3.资源分配不当。

学过操作系统的朋友都知道:产生死锁的条件有四个:

1.互斥条件:所谓互斥就是进程在某一时间内独占资源。

2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

阻塞

线程被堵塞可能是由下述五方面的原因造成的:

(1) 调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。

(2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回“可运行”状态。

(3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成“可运行”。

(4) 线程正在等候一些IO(输入输出)操作完成。

(5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

亦可调用yield()(Thread类的一个方法)自动放弃CPU,以便其他线程能够运行。然而,假如调度机制觉得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止调度机制重新启动我们的线程。

美文美图

Java进阶(四十一)多线程讲解的更多相关文章

  1. Java进阶(四十二)Java中多线程使用匿名内部类的方式进行创建3种方式

    Java中多线程使用匿名内部类的方式进行创建3种方式 package cn.edu.ujn.demo; // 匿名内部类的格式: public class ThreadDemo { public st ...

  2. Java进阶(四十三)线程与进程的区别

    Java进阶(四十三)线程与进程的区别 1.线程的基本概念   概念:线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必 ...

  3. Java进阶(四十)Java类、变量、方法修饰符讲解

    Java进阶(四十)Java类.变量.方法修饰符讲解 Java类修饰符 abstract: 将一个类声明为抽象类,没有实现的方法,需要子类提供方法实现. final: 将一个类生命为最终(即非继承类) ...

  4. Java进阶(四十七)Socket通信

    Java进阶(四十七)Socket通信   今天讲解一个 Hello Word 级别的 Java Socket 通信的例子.具体通讯过程如下: 先启动Server端,进入一个死循环以便一直监听某端口是 ...

  5. Java进阶(三十一) Web服务调用

    Java进阶(三十一) Web服务调用 前言 有朋友问了一个问题:如何调用已知的音乐服务接口,服务文档如下: https://www.evernote.com/shard/s744/sh/c37cd5 ...

  6. Java进阶(五十一)Could not create the view: An unexpected exception was thrown

    Java进阶(五十一)Could not create the view: An unexpected exception was thrown 今天打开Myeclipse10的时候,发现server ...

  7. Java进阶(五十一)必须记住的Myeclipse快捷键

    Java进阶(五十一)必须记住的Myeclipse快捷键 在调试程序的时候,我们经常需要注释一些代码,在用Myeclipse编程时,就可以用 Ctrl+/ 为选中的一段代码加上以 // 打头的注释:当 ...

  8. Java进阶专题(十一) 想理解JVM看了这篇文章,就知道了!(中)

    前言 ​ 上次讲解了JVM内存相关知识,今天继续JVM专题. JVM垃圾回收算法 什么是垃圾回收 ​ 程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢 ...

  9. Java进阶专题(十一) 探究JMM

    前言 ​ JMM即java内存模型,JMM研究的就是多线程下Java代码的执行顺序,共享变量的读写.它定义了Java虚拟机在计算机内存中的工作方式.从抽象角度看,JMM定义了线程和主存之间的抽象关系: ...

随机推荐

  1. ●UVa 1589 Xiangqi(模拟)

    ●赘述题意 给出一个中国象棋残局,告诉各个棋子的位置,黑方只有1枚“将”,红方有至少2枚,至多7枚棋子,包含1枚“帅G”,和若干枚“车R”,“马H”,“炮C”.当前为黑方的回合,问黑方的“将”能否在移 ...

  2. bzoj4447[Scoi2015]小凸解密码

    4447: [Scoi2015]小凸解密码 Time Limit: 20 Sec  Memory Limit: 128 MBSubmit: 150  Solved: 58[Submit][Status ...

  3. Linux(CentOs6.3)网络配置

    新装好的虚拟机往往还无法连接网络,本文描述了如何在CentOs6.3系统上配置网络信息 1.windows系统下快捷键windows+r,输入cmd并确定,打开黑窗口 2.黑窗口中输入ipconfig ...

  4. Java多线程volatile和synchronized总结

    volatile是轻量级的synchronized,在多处理器(多线程)开发中保证了共享变量的"可见性".可见性表示当一个线程修改了一个共享变量时,另外一个线程能读到这个修改的值. ...

  5. Java Native方法

    一. 什么是Native Method   简单地讲,一个Native Method就是一个java调用非java代码的接口.一个Native Method是这样一个java的方法:该方法的实现由非j ...

  6. Linux sort -n 与 -g 排序对比

    公司业务需要,天天用awk统计数值然后排序,出问题了,如下: count.sh 是一个统计脚本,把awk输出的值按占比.条数.类型 在重新输出 awk -F\| '{print $16}' *MQTT ...

  7. 2018春招实习笔试面试总结(PHP)

    博主双非渣本计算机软件大三狗一枚,眼看着春招就要结束了,现将自己所经历的的整个春招做一个个人总结. 首先就是关于投递计划,博主自己整理了一份各大公司的春招信息,包括网申地址,开始时间,结束时间,以及自 ...

  8. 读书笔记-《Maven实战》-2018/4/16

    第一章:Maven简介 1:Maven:Maven原本的单词意思为"知识的积累",谷歌翻译为"行家",而作为Apache的开源项目,Maven是一个主要服务于基 ...

  9. event工具集

    eventTool = { // 页面加载完成后 readyEvent : function(fn) { if (fn==null) { fn=document; } var oldonload = ...

  10. ftp传二进制文件时一定要用二进制模式,否则内容会有变化,造成后处理莫名其妙的错误,还以为传输前后内容一致,其实已变化。

    ftp传二进制文件时一定要用二进制模式,否则内容会有变化,造成后处理莫名其妙的错误,还以为传输前后内容一致,其实已变化.