Java进阶(四十一)多线程讲解

2024-02-16 20:18

本文主要是介绍Java进阶(四十一)多线程讲解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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进阶(四十一)多线程讲解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/715596

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定