Java基础——线程及并发机制

前言


       在Java中,线程是一个很关键的名词,也是很高频使用的一种资源。那么它的概念是什么呢,是如何定义的,用法又有哪些呢?为何说Android里只有一个主线程呢,什么是工作线程呢。线程又存在并发,并发机制的原理是什么。这些内容有些了解,有些又不是很清楚,所以有必要通过一篇文章的梳理,弄清其中的来龙去脉,为了之后的开发过程中提供更好的支持。


目录

一、线程定义

二、Java线程生命周期

三、线程用法

四、Android中的线程

五、工作线程

六、使用AsyncTask

七、什么是并发

八、并发机制原理

九、并发具体怎么用


一、线程定义


       说到线程,就离不开谈到进程了,比如在Android中,一个应用程序基本有一个进程,但是一个进程可以有多个线程组成。在应用程序中,线程和进程是两个基本执行单元,都是可以处理比较复杂的操作,比如网络请求、I/O读写等等,在Java中我们大部分操作的是线程(Thread),当然进程也是很重要的。

       进程通常有独立执行环境,有完整的可设置为私有基本运行资源,比如,每个进程会有自己的内存空间。而线程呢,去官网的查了下,原话如下:

Threads are sometimes called "lightweight processes". Both processes and threads provide an execution environment, 
but creating a new thread requires fewer resources than creating a new process.
       意思就是:线程相比进程所创建的资源要少很多,都是在执行环境下的执行单元。同时,每个线程有个优先级,高的优先级比低的优先级优先执行。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。


二、Java线程生命周期


  1. 新建状态(New):当线程对象创建后,即进入了新建状态。仅仅由java虚拟机分配内存,并初始化。如:Thread t = new MyThread();
  2. 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,java虚拟机创建方法调用栈和程序计数器,只是说明此线程已经做好了准备,随时等待CPU调度执行,此线程并 没有执行。
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,执行run()方法,此时线程才得以真正执行,即进入到运行状态。注:绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态,JVM会把该线程放入等待池中;同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。 当主线程结束时,其他线程不受任何影响。


三、线程用法


那该如何创建线程呢,有两种方式。

  • 使用Runnable
  • 继承Thread类,定义子类

使用Runnable:

       Runnable接口有个run方法,我们可以定义一个类实现Runnable接口,Thread类有个构造函数,参数是Runnable,我们定义好的类可以当参数传递进去。

public class HelloRunnable implements Runnable {public void run() {System.out.println("Hello from a thread!");}public static void main(String args[]) {(new Thread(new HelloRunnable())).start();}}

继承Thread类:

       Thread类它自身就包含了Runnable接口,我们可以定义一个子类来继承Thread类,进而在Run方法中执行相关代码。

public class HelloThread extends Thread {public void run() {System.out.println("Hello from a thread!");}public static void main(String args[]) {(new HelloThread()).start();}}
       从两个使用方式上看,定义好 Thread 后,都需要执行 start() 方法,线程才算开始执行。

四、Android中的线程


       当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。

       应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小工具,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。

       系统绝对不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法,例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。例如,当用户触摸屏幕上的按钮时,应用的 UI 线程会将触摸事件分派给小工具,而小工具反过来又设置其按下状态,并将无效请求发布到事件队列中。UI 线程从队列中取消该请求并通知小工具应该重绘自身。

       在应用执行繁重的任务以响应用户交互时,除非正确实施应用,否则这种单线程模式可能会导致性能低下。 特别地,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。一旦线程被阻塞,将无法分派任何事件,包括绘图事件。从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的“应用无响应”(ANR) 对话框。

此外,Android UI 工具包并非线程安全工具包。因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。因此,Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞 UI 线程
  2. 不要在 UI 线程之外访问 Android UI 工具包

       那为何Andorid是主线程模式呢,就不能多线程吗?在Java中默认情况下一个进程只有一个线程,这个线程就是主线。主线程主要处理界面交互相关的逻辑,因为用户随时会和界面发生交互,因此主线程在任何时候都必须有比较高的响应速度,否则就会产生一种界面卡顿的感觉。同样Android也是沿用了Java的线程模型,Android是基于事件驱动机制运行,如果没有一个主线程进行调度分配,那么线程间的事件传递就会显得杂乱无章,使用起来也冗余,还有线程的安全性因素也是一个值得考虑的一个点。


五、工作线程


       既然了解主线程模式,除了UI线程,其他都是叫工作线程。根据单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。如果执行的操作不能很快完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。例如以下代码表示一个点击监听从单独的线程下载图像并将其显示在ImageView中:

public void onClick(View v) {new Thread(new Runnable() {public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png");mImageView.setImageBitmap(b);} }).start();
} 

       咋看起来貌似没什么问题,它创建了一个线程来处理网络操作, 但是呢,它却是在UI线程中执行,但是,它违反了单线程模式的第二条规则:不要在 UI 线程之外访问 Android UI 工具包。

       那么你会问个问题了,为什么子线程中不能更新UI。因为UI访问是没有加锁的,在多个线程中访问UI是不安全的,如果有多个子线程都去更新UI,会导致界面不断改变而混乱不堪。所以最好的解决办法就是只有一个线程有更新UI的权限。

当然,Android 提供了几种途径来从其他线程访问 UI 线程。以下列出了几种有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

例如,您可以通过使用 View.post(Runnable) 方法修复上述代码:

public void onClick(View v) {new Thread(new Runnable() {public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");mImageView.post(new Runnable() {public void run() { mImageView.setImageBitmap(bitmap);} }); } }).start();
} 

       现在,上述实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操纵 ImageView

       但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。 要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用 Handler 处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类,此类简化了与 UI 进行交互所需执行的工作线程任务。


六、使用AsyncTask


       AsyncTask 允许对用户界面执行异步操作。它会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而无需你亲自处理线程和/或处理程序。

       要使用它,必须创建 AsyncTask 子类并实现 doInBackground() 回调方法,该方法将在后台线程池中运行。要更新 UI,必须实现 onPostExecute() 以传递doInBackground() 返回的结果并在 UI 线程中运行,这样,即可安全更新 UI。稍后,您可以通过从 UI 线程调用 execute() 来运行任务。

例如,可以通过以下方式使用 AsyncTask 来实现上述示例:

public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png"); 
} private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {/** The system calls this to perform work in a worker thread and * delivers it the parameters given to AsyncTask.execute() */ protected Bitmap doInBackground(String... urls) {return loadImageFromNetwork(urls[0]);} /** The system calls this to perform work in the UI thread and delivers * the result from doInBackground() */ protected void onPostExecute(Bitmap result) {mImageView.setImageBitmap(result);} 
} 

       现在 UI 是安全的,代码也得到简化,因为任务分解成了两部分:一部分应在工作线程内完成,另一部分应在 UI 线程内完成。

下面简要概述了 AsyncTask 的工作方法,但要全面了解如何使用此类,您应阅读 AsyncTask 参考文档:

  • 可以使用泛型指定参数类型、进度值和任务最终值
  • 方法 doInBackground() 会在工作线程上自动执行
  • onPreExecute()onPostExecute() 和 onProgressUpdate() 均在 UI 线程中调用
  • doInBackground() 返回的值将发送到 onPostExecute()
  • 您可以随时在 doInBackground() 中调用publishProgress(),以在 UI 线程中执行 onProgressUpdate()
  • 您可以随时取消任何线程中的任务


七、什么是并发


说到并发,首先需要区别并发和并行这两个名词的区别。

并发性和并行性
并发是指在同一时间点只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
并行指在同一时间点,有多条指令在多个处理器上同时执行。

那么我们为什么需要并发呢?通常是为了提高程序的运行速度或者改善程序的设计。


八、并发机制原理


       Java对并发编程提供了语言级别的支持。Java通过线程来实现并发编程。一个线程通常完成某个特定的任务,一个进程可以拥有多个线程,当这些线程一起执行的时候,就实现了并发。与操作系统中的进程相似,每个线程看起来好像拥有自己的CPU,但是其底层是通过切分CPU时间来实现的。与进程不同的是,线程并不是相互独立的,它们通常要相互合作来完成一些任务。


九、并发具体怎么用


休眠

       我们可以让一个线程暂时休息一会儿。Thread类有一个sleep静态方法,你可以将一个long类型的数据当做参数传进去,单位是毫秒,表示线程将会休眠的时间。


让步

       Thread类还有一个名为yield()的静态方法。这个方法的作用是为了建议当前正在运行的线程做个让步,让出CPU时间给别的线程来运行。程序中可能会有一个线程在某个时刻已经完成了一大部分的任务,并且这个时候让别的线程来运行比较合理。这样的情况下,就可以调用yield()方法进行让步。不过,调用这个方法并不能保证一定会起作用,毕竟它只是建议性的。所以,不应该用这个方法来控制程序的执行流程。


串入(join)

       当一个线程t1在另一个线程t2上调用t1.join()方法的时候,线程t2将等待线程t1运行结束之后再开始运行。正如下面这个例子:

public class ThreadTest {public static void main(String[] args) {SimpleThread simpleThread = new SimpleThread();Thread t = new Thread(simpleThread);t.start();}
}
public class SimpleThread implements Runnable{@Overridepublic void run() {Thread tempThread = new Thread() {@Overridepublic void run() {for(int i = 10; i < 15 ;i++) {System.out.println(i);}}};tempThread.start();try {tempThread.join();        //tempThread串入} catch (InterruptedException e) {e.printStackTrace();}for(int i = 0; i < 5; i++) {System.out.println(i);}}
}
输出结果:

10
11
12
13
14
0
1
2
3
4


优先级

       我们可以给一个线程设定一个优先级。线程调度器在做调度工作的时候,优先级越高的线程越可能得到先运行的机会。Thread类的setPriority方法和getPriority方法分别用来设置线程的优先级和获取线程的优先级。由于线程调度器根据优先级的大小来调度线程的效果在各种不同的JVM上差别很大,所以在绝大多数情况下,我们不应该依靠设定优先级来完成我们的工作,保持默认的优先级是一条很好的建议。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/446974.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

密码机 密钥管理项目安装配置 从零开始

安装gcc 更新sudo apt-get update下载gcc sudo apt-get install gcc参考链接 不推荐 安装g 下载g sudo apt-get install g 安装make sudo apt -get install make参考链接 安装cmake 下载地址参考链接 安装ssh sudo apt-get install ssh 安装git和配置 sudo apt-get inst…

Androud 如何有效减少重复代码

前言 重复的代码一直都是可维护性的大敌&#xff0c;重构的重要任务之一也就是要去除掉重复的代码&#xff0c;有效的减少重复代码&#xff0c;可以大大提高软件的扩展性。 在Android开发中&#xff0c;很容易产生重复的代码。因为Android是组件&#xff0c;模板式开发&#xf…

解决在sample文件夹里面写代码,在测试的时候因为virtual原因,make编译报错

代码的结构 错误显示 解决办法 添加一句话&#xff0c;具体的cpp依据情况而定set_source_files_properties(${PROJECT_SOURCE_DIR}/src/sample_storage_test.cpp COMPILE_FLAGS "-Wno-unused-parameter")

Android SharedPreferences总结及优化

一、SharedPreferences简介 Android 中的 SharedPreferences&#xff08;后续简称SP&#xff09;是轻量级的数据存储方式&#xff0c;能够保存简单的数据类型&#xff0c;比如 String、int、boolean 值等。应用场合主要是数据比较少的配置信息。其内部是以 XML 结构保存在 /dat…

Java基础——深入理解ReentrantLock

一、简介在Java中通常实现锁有两种方式&#xff0c;一种是synchronized关键字&#xff0c;另一种是Lock。二者其实并没有什么必然联系&#xff0c;但是各有各的特点&#xff0c;在使用中可以进行取舍的使用。二、ReentrantLock与synchronized的比较相同点&#xff1a; &#xf…

使用开源的openssl的md5头文件,实现对于文件的md5代码

需要安装openssl的库 sudo apt-get install opensslsudo apt-get install libssl-dev参考链接 代码 #include "openssl/md5.h" #include <iostream> #include <fstream> #include <iomanip>//#define MAX_DATA_BUFF 1024; //#define MD5_LENGTH…

Android 多进程开发

前言正常情况下&#xff0c;一个apk启动后只会运行在一个进程中&#xff0c;其进程名为AndroidManifest.xml文件中指定的应用包名&#xff0c;所有的基本组件都会在这个进程中运行。但是如果需要将某些组件&#xff08;如Service、Activity等&#xff09;运行在单独的进程中&am…

clion中链接openssl库

错误显示 前提条件 apt-get install opensslapt-get install openssl-dev 解决办法 在CMakeLists.txt文件中加入如下命令link_libraries(crypto) 参考链接 无法将openssl库链接到CLion C 程序c - 无法将openssl库链接到CLion C程序

Java中String、StringBuffer、StringBuilder三者的区别

一、简介String、StringBuffer、StringBuilder三个类之间的区别主要是在两个方面&#xff1a;运行速度和线程安全。二、区别1、运行速度&#xff0c;或者说是执行速度在这方面运行速度快慢为&#xff1a;StringBuilder > StringBuffer > String StringString为字符串常量…

Ubuntu环境下,使用clion编译器,使用开源opensll的对称AES算法对于文件进行加密,C++代码

前提准备条件 需要安装openssl需要安装openssl-dev需要配置CMakeLists.txt文件集体内容可以参考我提供的相关参考链接 AES_file.h #include <openssl/aes.h> #include <iostream> #include <fstream> #include <cstring>#define RELEASE_ARRAY(P) if…

Java提高篇 —— Java关键字之static的四种用法

一、前言 在java的关键字中&#xff0c;static和final是两个我们必须掌握的关键字。不同于其他关键字&#xff0c;他们都有多种用法&#xff0c;而且在一定环境下使用&#xff0c;可以提高程序的运行性能&#xff0c;优化程序的结构。下面我们先来了解一下static关键字及其用法…

C++ 使用move来删除用户指定的文件

代码 #include <iostream>bool remove_file(std::string path){if (remove(path.c_str())0){std::cout << "success!" << std::endl;}else{std::cout << "False!" << std::endl;} } int main() {std::string path "/…

Java提高篇 —— Java关键字之final的几种用法

一、前言 在java的关键字中&#xff0c;static和final是两个我们必须掌握的关键字。不同于其他关键字&#xff0c;他们都有多种用法&#xff0c;而且在一定环境下使用&#xff0c;可以提高程序的运行性能&#xff0c;优化程序的结构。下面我们来了解一下final关键字及其用法。 …

使用C++的方式实现AES算法

aes_file.h #include <iostream> #include <fstream> #include <bitset> #include <string> using namespace std; typedef bitset<8> byte; typedef bitset<32> word;const int Nr 10; // AES-128需要 10 轮加密 const int Nk 4; /…

Java提高篇 —— Java三大特性之封装

一、封装 封装从字面上来理解就是包装的意思&#xff0c;专业点就是信息隐藏&#xff0c;是指利用抽象数据类型将数据和基于数据的操作封装在一起&#xff0c;使其构成一个不可分割的独立实体&#xff0c;数据被保护在抽象数据类型的内部&#xff0c;尽可能地隐藏内部的细节&am…

sqlite3的backup和restore函数的使用

参考代码 第一段这个亲测可以使用 #include <sqlite3.h> #include <iostream> /* ** Perform an online backup of database pDb to the database file named ** by zFilename. This function copies 5 database pages from pDb to ** zFilename, then unlocks pD…

Java提高篇 —— Java三大特性之继承

一、前言 在《Think in java》中有这样一句话&#xff1a;复用代码是Java众多引人注目的功能之一。但要想成为极具革命性的语言&#xff0c;仅仅能够复制代码并对加以改变是不够的&#xff0c;它还必须能够做更多的事情。在这句话中最引人注目的是“复用代码”,尽可能的复用代码…

Java提高篇 —— Java三大特性之多态

一、前言 面向对象编程有三大特性&#xff1a;封装、继承、多态。 封装&#xff1a;隐藏了类的内部实现机制&#xff0c;可以在不影响使用的情况下改变类的内部结构&#xff0c;同时也保护了数据。对外界而已它的内部细节是隐藏的&#xff0c;暴露给外界的只是它的访问方法。 继…

光盘刻录制作Ubuntu等操作系统的启动盘

前提条件 软媒刻录 空白光盘&#xff08;至少4.7G&#xff09;电脑&#xff08;最好使用外置的光驱&#xff09;系统镜像&#xff08;ISO格式&#xff09; 具体操作 打开软媒魔方选择光盘刻录按照标红的进行选择选择镜像->选择或者拖拽都可以选择刻录机->如果使用外部刻…

Java提高篇 —— 抽象类与接口

一、前言 接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。 抽象类与接口是java语言中对抽象概念进行定义的两种机制&#xff0c;正是由于他们的存在才赋予java强大的面向对象的能力。他们两者之间对抽象概念的支持有很大的相似&#xff0c;甚至可以互换&…