Java基础——多线程

Java语言提供了非常优秀的多线程支持。可以通过非常简单的方式来启动多线程

线程概述

线程和进程

  • 进程:所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,进程具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

进程的特征

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源。每一个进程都拥有自己私有的地址空间。在没有经过允许的情况下一个用户进程不可以直接访问其他进程的地址

  • 动态性:进程具有自己的生命周期和各种不同状态

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响


  • 线程:线程也被称作轻量级进程,线程是进程的执行单元。线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了,一般对于一个应用程序来说,通常仅要求有一个主线程,但也可以在进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的

    • 线程是进程的组成部分,一个进程可以拥有多个线程,但是一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源

    • 线程的执行时抢占式的,线程是独立运行的,线程并不知道进程中是否还有其他线程的存在

    多线程的优势

  • 多线程程序并发性高

  • 创建线程消耗的资源要比创建进程更加省

  • 多线程之间共享内存更加容易

  • Java语言内置了多线程功能的支持

多线程的创建和启动

Java中使用Thread来代表线程,所有的线程对象都必须是Thread类或其子类的实例

继承Thread类创建线程类

  1. 定义Thread类的子类,重写该类run()方法

  2. 创建该Thread子类的实例

  3. 调用线程对象的start()方法来启动线程
    class ThreadDemo extends Thread{

public void run(){
System.out.println(this.getName+"run");
}

public static void main(String[] args){
new ThreadDemo().start();
}

}</pre>

实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,并重写该接口的run()方法。run()方法的方法体同样是该线程的线程执行体

  2. 创建Runnable实现类的实例

  3. 调用线程对象的start()方法来启动线程
    class RunnableDemo implements Runnable{
    public void run(){
    System.out.println(this.getName()+"run");
    }

public static void main(String[] args){
new RunnableDemo().start();
}
}</pre>

使用Callable和Future创建线程

自Java5 开始 Java提供了Callable接口,Callable接口提供了一个call()方法作为线程的执行体

  • call()方法可以有返回值

  • call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现Future接口并实现runnable,可以作为Thread类的执行target

在Future接口里定义了如下的API来控制关联的Callable任务

  • boolean cancel(boolean mayInterruptIfRunning) 试图取消该Future里关联的Callable任务

  • V get() 返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等待子线程结束后才会得到返回值

  • V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值,让程序最多阻塞timeout 和unit指定的时间,如果在指定时间之后仍然没有返回值,将抛出TimeOutException异常

  • boolean isCancelled() 如果在Callable任务正常完成前被取消,则返回true

  • boolean isDone() 如果Callable任务已完成,则返回true

创建并启动有返回值的线程步骤

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例

  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法

  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程

  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建callable
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(2000);
return 20;
}
});
new Thread(futureTask).start();
System.out.println("线程的返回值"+futureTask.get());</pre>

//创建callable

//lambda
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(2000);
return 20;
}
});
new Thread(futureTask).start();
System.out.println("线程的返回值"+futureTask.get());
FutureTask<String> futureTask = new FutureTask<>((Callable<String>) () -> {
return "thread return";
});
new Thread(futureTask).start();
System.out.println(futureTask.get());

// Thread pool
ExecutorService threadPool = Executors.newFixedThreadPool(5);
System.out.println(threadPool.submit((Callable<Integer>) () -> {
return 20;
}).get());
threadPool.shutdown();</pre>

线程的生命周期

线程被创建并启动后,线程不是立即进入就绪状态,也不是一直处于执行状态,需要经过 新建(New) 就绪(Runnable) 运行(Running) 阻塞(Blocked) 死亡(Dead) 5种状态

新建和就绪状态

  • 当程序使用new关键字创建线程后,线程就处于新建状态,此时线程对象和java对象一样,仅仅由JAVA虚拟机为其分配内存,并初始化成员变量的值

  • 当线程对象调用start()方法后,线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的程序并没有开始运行,只是表示该线程可以运行了,至于程序何时开始运行取决于JVM里线程调度器的调度 只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException

运行和阻塞状态

  • 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

  • 当线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,使得其他线程获得执行机会。中断线程运行状态就会使线程进入阻塞状态

当发生如下情况使,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源

  • 线程调用了阻塞式I/O方法,在方法返回前处于阻塞状态

  • 线程试图获得锁对象,但该锁被其他线程所持有

  • 线程正在等待唤醒

当发生如下的情况会让线程重新进入就绪状态

  • 调用sleep()方法的线程经过了指定的时间

  • 线程调用的阻塞式方法已经返回

  • 线程成功的获得了锁对象

  • 线程被唤醒

  • 挂起状态的线程被调用resume()方法

[图片上传失败...(image-5f7d60-1571407713512)]

线程死亡

  • run()或call()方法执行完成,线程正常结束

  • 线程抛出一个未捕获异常

  • 直接调用线程的stop()方法

isAlive() 当线程处于就绪、运行、阻塞三种状态时返回true 当线程处于新建、死亡状态时返回false

控制线程

join线程

Thread提供了一个让一个线程等待另一个线程完成的方法——join

当在某个程序执行流中调用其他线程的Join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止

注意:这里是调用线程将被阻塞

join() 方法的重载

  • join() 等待被join的线程执行完成

  • Join(long millis) 等待被join的线程的时间最长为millis毫秒,如果时间过后还没执行结束则不再等待

  • join(long millis,int nanos) 等待被join的线程的时间最长为millis毫秒加nanos毫微秒

public class JoinThread extends Thread {
private String name;

public JoinThread(String name) {
setName(name);
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " " + i);
}

}

//main
public static void main(String[] args) throws InterruptedException {
//创建执行线程
new JoinThread("线程1").start();
//主线程
for (int i = 0; i < 100; i++) {
if (i == 30) {
//创建要加入的线程
JoinThread joinThread = new JoinThread("join线程");
joinThread.start();
joinThread.join();

}
System.out.println(Thread.currentThread().getName() + " "+i);
}
}
}</pre>

上面的代码中 在main线程中调用了joinThread的join方法 所以需要主线程等待joinThread执行结束后方可继续执行

后台线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程称为守护线程。典型的守护线程就是JVM

  • 特征:如果所有的前台线程都死亡,后台线程会自动死亡

  • 方式:调用线程对象的setDaemon(true)方法可以将指定线程设置成后台线程

public class DaemonDemo extends Thread {
public DaemonDemo(String name) {
setName(name);
}

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(getName()+"\t"+i);
}
}

public static void main(String[] args) {
DaemonDemo daemonDemo = new DaemonDemo("守护线程");
daemonDemo.setDaemon(true);
daemonDemo.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}</pre>

需要设置为守护线程必须在线程start()之前设置,否则会引发异常

线程睡眠 sleep

使当前正在执行的线程暂停一段时间,进入阻塞状态。

sleep()重载:

  • sleep(long millis):让当前正在执行的线程暂停millis毫秒。并进入阻塞状态

  • sleep(long millis,int nanos)

当当前线程调用sleep()方法进入阻塞状态后,在睡眠时间内,该线程不会获得执行激活,即使没有线程可以执行,处于sleep的线程也不会执行

线程优先级

每个线程都有一定的优先级,优先级高的线程会获得较高的执行机会,优先级低的线程则获得较少的执行机会

  • 每个线程默认的优先级都与创建它的父线程的优先级相同,main线程具有普通优先级

Thread类提供了setPriority(int new)、getPriority()来设置和返回指定线程的优先级

  • setPriority(int priority) 参数可以是一个整数,范围是1~10之间

    • MAX_PRIORITY 10 最高优先级

    • MIN_PRIORITY 1 最低优先级

    • NORM_PRIORITY 普通优先级

线程同步

同步代码块

为解决线程同步问题,java多线程支持引入了同步监视器来解决

  • 同步代码块语法

synchronized(obj){
...
}</pre>

sychronized后面的参数obj就是同步监视器。当线程开始执行同步代码块之前,必须先获得同步监视器的锁定

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,线程会释放对该同步监视器的锁定

  • 任何对象都可以作为同步监视器,但是推荐使用可以被功能访问的共享资源作为同步监视器

同步方法

同步方法就是使用synchronized关键字来修饰的方法

用synchronized修饰的实例方法不需要指定锁对象,同步方法的监视器就是this。

语法格式

public synchronized void method(param){
...
}

tips 线程何时释放同步锁?
  • 同步方法、代码块执行结束

  • 同步方法、代码块中遇到reture终止执行

  • 在同步方法、代码块中出现异常

  • 在同步方法、代码块中执行时,程序执行了同步监视器对象的wait()方法,当前线程暂停,并释放同步监视器

tips 线程不会释放同步锁
  • 使用Thread.sleep() 来暂停当前线程的执行

  • 线程被挂起

JAVA5 LOCK

java5开始 java提供了一种功能强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当

在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显式地加锁、释放锁

class X{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
public void m(){
//加锁
lock.lock();
try{
//需要保证线程同步的代码
}finally{
//释放锁
lock.unlock();
}
}
}</pre>

ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁。ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程每次显式调用lock()加锁后,必须显式调用unlock()来释放锁

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁。

线程通信

传统方式

传统方式中可以使用Object类提供的wait() notify() notifyAll()来进行线程通信和控制

  • wait():导致当前线程等待,直到其他线程调用该锁对象的notify()方法或notifyAll()来唤醒该线程

  • notify():唤醒在此同步监视器上等待的单个的线程。有多个可唤醒的线程时,选择是任意的

  • notifyAll():唤醒此同步监视器上等待的所有线程。

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

推荐阅读更多精彩内容

  • 1. 多线程概述 1.1 多线程引入 由上图中程序的调用流程可知,这个程序只有一个执行流程,所以这样的程序就是单线...
    JackChen1024阅读 403评论 0 1
  • 35. 并行和并发有什么区别? 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生...
    C乖阅读 326评论 0 0
  • 多线程 并行和并发 这里的时间都是微观上的概念 并行:指两个或多个事件在同一时刻发生,强调的是时间点的瞬间 并发:...
    ADMAS阅读 353评论 0 0
  • 进程、线程概念 进程:每个进程都有独立的代码和数据空间(进程上下文),进程切换开销比较大,进程中可以包含多个线程。...
    dtdh阅读 294评论 0 0
  • 多线程、单例 线程线程是程序执行的一条路径, 一个进程中可以包含多条线程。多线程并发执行可以提高程序的效率, 可以...
    xiaohan_zhang阅读 207评论 0 0