Java基础之多线程详细分析
在了解多线程之前,先来了解一下进程与线程之间的关系。
进程和线程:
进程是指在系统中正在执行的一个程序,每个进程之间是独立的。
线程是进程的一个基本执行单元。一个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)
主线程:
在java程序中存在一个主线程(JVM线程),main方法自带的一个线程。
之所以在系统进行多个程序的时候(QQ,web网页等),看似是在同步执行,那是因为cpu在进程中进行多线程的切换,cpu切换的速度之快让我们 觉得是在同步执行,其实在进程的执行过程中是存在一定的先后顺序的。
线程的执行原理:
线程运行的结果每次都不同,因为多个线程都在获取cpu的执行权,cpu执行到了谁,谁就执行。
明确一点:在某一时刻,只能有一个程序在运行(多核cpu除外)——也可以说多线程的运行行为是在互相抢夺cpu的执行权,同时cpu在做着快速的切换动作,已达到看似同时运行的结果。因此多线程的特性是随机性。
实现多线程的方式有两种:继承Thread类、实现Runnable接口
继承Thread类
1、定义类继承Thread类
2、复写Thread中的run()方法
目的:将自定义代码写在run()方法中,让线程运行
3、调用start()方法,启动线程,调用run()方法
(一个线程是不允许调用两次start()方法,第二次调用时会抛出java.lang.IllegalThreadStateException异常。多次调用会认为是变成错误。
在第二次调用 start() 方法的时候,线程可能处于终止或其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。)
public class ThreadTest extends Thread{ @Override
public void run(){ //复写Thread中的run方法
for(int i=0;i<10;i++){
System.out.print(" demo run");
}
} }
class ThreadDemo{
public static void main(String[] args) {
ThreadTest dt = new ThreadTest();
dt.start(); //调用start方法(启动线程,调用run方法)
for(int i=0;i<10;i++){
System.out.print("main run");
}
}
}
代码解说:在这一个程序中,存在两个线程:main主线程和ThreadTest自定义线程。
这两个线程同是在一个进程(程序)中执行,为确保程序的正常执行,因此在进程的内存空间中,开辟了两个线程空间,这两个线程空间在要执行的时候,都要获取cpu的执行权,所以,会互相的去抢夺cpu的执行权。会先抢到谁就执行。因此,这段程序的运行结果是随机的。
意思就是说:运行结果一会是main run,一会是demo run。存在无序性。
至于为什么要复写run方法,原因是:
Thread用于描述线程,该线程就只定义了一个功能,用于存储线程要运行的代码,该存储功能就是run()方法。也就是说Thread中的run()方法,用于存储线程要允许的代码。
(主线程的代码写在main方法中,而main方法是JVM定义的。所以JVM调用main方法的原因是因为主线程要使用,如果将自定义的代码写在main方法中,让JVM去执行,那就是一种单线程(主线程),不是多线程了。)
public class ThreadTest extends Thread{
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.print(" demo run");
}
} }
class ThreadDemo{
public static void main(String[] args) {
ThreadTest dt = new ThreadTest();
dt.start(); //①
dt.run(); //②
for(int i=0;i<10;i++){
System.out.print("main run");
}
}
}
代码解说:在上段程序中,如果①被注释掉,直接dt.run(),那这就不是一个多线程的执行,只是相当于一个简单的对象调用方法,而线程创建了,并没有运行。
所以,如果②被注释掉,直接dt.start(),那就是创建了一个线程对象dt,然后dt.start()将线程dt开启,并调用了run()方法,让线程去运行。
实现Runnable接口
1、定义类实现Runnable接口
2、复写Runnable接口中的run()方法
3、通过Runnable建立线程对象
4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
目的:因为自定义的run()方法,所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run()方法,就必须明确该run()方法的所属对象
5、调用Thread类的static方法开启线程并调用run()方法
class RunnableTestDemo implements Runnable{ @Override
public void run() {
System.out.println("实现Runnable接口");
} }
public class RunnableTest { public static void main(String[] args) {
RunnableTestDemo r = new RunnableTestDemo();
Thread t1 = new Thread(r);
t1.start();
}
}
实现Runnable接口与继承Thread的区别:
1、接口方法避免了Java单继承的局限性
2、继承Thread,线程代码存放在Thread子类run()方法中
3、实现Runnable接口,线程代码存放在接口紫子类的 run()方法中
所以,在选择实现Runnable接口与继承Thread这两者中,建议使用实现Runnable接口的方式
线程的状态
见上图,可看到线程的状态分为:
1、创建线程(new 线程类),调用start()方法执行run()方法
2、运行状态:具有执行资格,并且有执行权
3、冻结状态:没有执行资格,没有执行权
4、临时状态(阻塞):具有执行资格,没有执行权。等待cpu的执行权
5、消亡状态:线程结束
根据线程的状态的执行原理是:创建一个线程A,调用start()方法,执行run()方法,当线程运行时,遇到sleep(time)或者wait()方法,线程就会进去冻结状态(放弃了执行资格),只有当sleep(time)或者执行了notify()方法,冻结状态就会变成临时状态(阻塞),这时临时状态的线程就会去同其他的线程一起抢夺cpu的执行权,一旦抢到cpu的执行权,该临时状态的线程就会运行(run),变成运行状态run,直到run()方法中的代码执行完,线程才会结束,变成最后的消亡状态。
同步代码块
较多情况下,Java在处理多线程的时候,会出现一些安全问题,而Java处理多线的安全问题的是方式,是使用同步代码块(synchronized(对象/this/类名.class))或者同步代码函数。
但是在解决多线程的安全问题时,使用同步代码块的前提是:
1、必须要有两个或者两个以上的线程
2、必须是多个线程使用同一个锁(如果存在多个类使用到同步,那么在这几个类中找唯一类,也就是找使用到同步快的共同类)
3、必须保证听你同步代码块中只能有一个线程在运行。
同步代码块的好处是解决了线程的安全问题,坏处是多个线程进去同步代码块的时候都要去判断,较为消耗资源。
在使用同步代码块的前提下,得确认线程是否存在安全性问题,如果找到线程的安全性问题;
1、明确哪些代码是多线程运行的代码
2、明确共享数据
3、明确多线程运行代码中哪些语句是操作的共享数据
同步代码块或者同步代码函数的运行原理:
1、将需要解决安全问题的代码放到同步代码块中
2、当多个线程运行时,第一个线程A获得cpu的执行权,首先会来判断真假是否持有锁,如果有,线程A就会进入到同步代码块中,并且进入到里面的第一件时间就是将同步锁给关闭。
3、然后线程A在同步代码块中,执行同步代码块中的代码。
4、在此之前,其他线程即使获得cpu的执行权也无法进入到同步代码块中,因为没有获得锁。
5、当线程A将同步代码块中的代码执行完毕时,最后一步是打开同步锁,然后退出。
6、这时,其他线程就可以获得cpu的执行权后并且获得同步锁,进入到同步代码块中,执行操作。
public class MyThread implements Runnable{ private static int i = 100; //多个线程共享的数据
boolean flag = true;
public void printVal(){
if(flag){
while(true){
synchronized(this){
if(i>0){
System.out.println("-----code"+i--);
}
}
}
}else{
while(true){
printStaticVal();
}
}
} public static void printStaticVal(){
while(true){
synchronized(MyThread.class){
if(i>0){
System.out.println("-----Static"+i--);
}
}
}
} public void run(){
printVal();
printStaticVal();
} }
class TestSync { public static void main(String[] args) {
TestSync t = new TestSync();
Thread my1 = new Thread(t);
Thread my2 = new Thread(t);
my1.start();
my2.start();
}
}
上述代码解析的结果是:有两个线程my1和my2,当my1首先抢到cpu的执行权,进去到run方法中,run方法中调用了printVal(val)方法。在这个方法上使用到了同步(synchronized),这样就相当于一个同步函数。当my1进来访问时,首先进行判断是否有锁,如果有,就会进去printVal(val)方法,然后首先就将锁给关闭,然后执行for循环,打印v的值。当for循环结束时,线程my1就会开启同步锁,然后出去。这时线程my2可能就会抢到cpu的执行权,然后进入到同步函数中。
如果printVal(val)方法不是同步方法,那么就会出现当线程my1抢到执行权进入run方法,执行for循环,当i=1时,执行到if(i>0)完,cpu的执行权被线程my2抢到了,这时它也来判断i = 1,i>0,满足,判断完后,这时cpu的执行权又被my1抢到了,这时,i被打印,然后i--,这时线程my2抢到了,它就不会去执行if判断了,它答应出来i的值就变了-1,这时就出现了多线程的安全问题。
出现多线程的原因是:当多条语句在操作同一个线程共享数据时,一个线程对多余语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。如果一个让一个线程进入方法将语句全部执行完再让其他的线程进来执行,那么线程的安全性的问题就会被解决。因此,我们在方法体上添加了同步(synchronized)。
在上述代码中,会发现同步代码块(synchronized)后面带的参数(锁)有所不同,函数需要被对象调用,那么函数都有一个对象引用this,所以,一般方法里,同步使用的锁是this。但是如果同步函数是个静态的,而静态方法中不可以定义this,静态在进内存时,内存中没有本类的对象,但是一定有该类对应的字节码对象(类名.Class)——该对象的类型是Class。因此静态同步方法使用的锁是该同步方法所在类的字节码对象(类名.Class)。
在Java中,还有一种机制:等待-唤醒机制(wait-notify,notifyAll)
线程在运行时,线程会开启一个线程池,等待线程就存在线程池中,而且唤醒的是唤醒线程池中第一个等待的线程。
这种机制用于同步中,因此这种机制在操作同步中的线程时,都必须要标识它们说操作线程持有的锁,只有同一个锁上被等待的线程,才能被痛一个锁给唤醒(notify),不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在object中。
停止线程(run方法结束)
开启多线程运行,运行代码通常是循环结构的,只要控制循环就可以让run方法结束,也就是线程结束。
Java基础之多线程详细分析的更多相关文章
- 基于Java 生产者消费者模式(详细分析)
Java 生产者消费者模式详细分析 本文目录:1.等待.唤醒机制的原理2.Lock和Condition3.单生产者单消费者模式4.使用Lock和Condition实现单生产单消费模式5.多生产多消费模 ...
- Java基础技术多线程与并发面试【笔记】
Java基础技术多线程与并发 什么是线程死锁? 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,我们就可以称 ...
- 关于java基础、多线程、JavaWeb基础、数据库、SSM、Springboot技术汇总
作者 : Stanley 罗昊 本人自行总结,纯手打,有疑问请在评论区留言 [转载请注明出处和署名,谢谢!] 一.java基础 1.多态有哪些体现形式? 重写.重载 2. Overriding的是什么 ...
- Java基础之多线程篇(线程创建与终止、互斥、通信、本地变量)
线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...
- Java基础之多线程框架
一.进程与线程的区别 1.定义: 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比 ...
- java线程基础巩固---多线程死锁分析,案例介绍
之前已经学习了关于同步锁的知识,但是在实际编写多线程程序时可能会存在死锁的情况,所以这次来模拟一下死锁,并且学会用一个命令来确认是否程序已经出现死锁了,下面开始: 首先新建两个类: 此时当然得到Oth ...
- 阿里一道Java并发面试题 (详细分析篇)
说明 前天分享了一篇关于阿里的"Java常见疑惑和陷阱"的文章,有人说这个很早就有了,可能我才注意到,看完之后发现内容非常不错,有几个我也是需要停顿下想想,如果后续有机会我录制一个 ...
- java.util.concurrent包详细分析--转
原文地址:http://blog.csdn.net/windsunmoon/article/details/36903901 概述 Java.util.concurrent 包含许多线程安全.测试良好 ...
- 面试【JAVA基础】多线程
本次整理的内容如下: 1.进程与线程的区别 进程是一个可执行的程序,是系统资源分配的基本单位:线程是进程内相对独立的可执行单元,是操作系统进行任务调度的基本单位. 2.进程间的通信方式 2.1.操作系 ...
随机推荐
- VS2013下使用cjson
想要在C++实现json文件的读取.因为中间也遇到过很简单的坑,为了增加记忆,对实现过程做一个记录. 本文采用的是静态链接库的方式: 1.先在github上下载源码, json源码下载地址 2.打开m ...
- android 企业级高性能图表库 SciChart (付费)
1.官网 https://www.scichart.com/ 2.特性 2.1 链接 https://www.scichart.com/android-chart-features/ https:// ...
- SpringCloud---API网关服务---Spring Cloud Zuul
1.概述 1.1 微服务架构出现的问题 及 解决: 1.1.1 前言 每个微服务应用都提供对外的Restful API服务,它通过F5.Nginx等网络设备或工具软件实现对各个微服务的路由与负载 ...
- decode 和 encode 区别
字符串在Python内部的表示是unicode编码,因此,在做编码转换时,通常需要以unicode作为中间编码,即先将其他编码的字符串解码(decode)成unicode,再从unicode编码(en ...
- 3dsmax2015卸载/安装失败/如何彻底卸载清除干净3dsmax2015注册表和文件的方法
3dsmax2015提示安装未完成,某些产品无法安装该怎样解决呢?一些朋友在win7或者win10系统下安装3dsmax2015失败提示3dsmax2015安装未完成,某些产品无法安装,也有时候想重新 ...
- selenium+Python(alert 、confirm 、prompt 的处理)
alert\confirm\prompt 弹出框操作主要方法有: text 返回 alert/confirm/prompt 中的文字信息 accept 点击确认按钮 dismiss 点击取消按钮, ...
- EntityFramework6 版本更变产生的错误
LINQ to Entities does not recognize the method 'System.Nullable`1[System.Int32] DiffMinutes(System.N ...
- Jquery执行效率提高的方法
1.选择器性能排行:$("#ID") > $("Tag") > $(".Class") > $("[attrib ...
- vue打包后运行在本地/非服务器端环境的访问路径
vue打包前的配置: 项目目录下--> config文件夹---> index.js: build: { assetsPublickPath: './', // 设置成相对路径 ...
- jquery.form.js ie 下下载文件已经ie8失效问题解决方案
https://github.com/malsup/form/blob/master/jquery.form.js在使用这个插件时遇到的问题1.ie下会变成下载文件,解决方案是在后端返回时设置'Con ...