多线程并发可能遇到的问题及Runable和Thread之间的关系
一、多线程并发可能遇到的问题
多线程并发执行可能会导致一些问题:
安全性问题:在单线程系统上正常运行的代码,在多线程环境中可能会出现意料之外的结果。
活跃性问题:不正确的加锁、解锁方式可能会导致死锁或者活锁问题。
性能问题:多线程并发即多个线程切换运行,线程切换会有一定的消耗并且不正确的加锁。
1. 安全性问题
多线程的三大特性:原子性、可见性、有序性。如果不满足这三大特性,就可能产生线程安全问题。
案例:需求现有100张火车票,两个窗口同时售卖火车票,请用多线程模拟抢票效果。
class ThreadTrain1 implements Runnable{private int train1Count = 100;//火车票总数 总共100张@Overridepublic void run() {//为了能够模拟程序一直在抢票while (train1Count > 0){try {//线程从运行状态 -> 休眠状态 -> CPU执行权让给其他线程//有两个线程同时释放了CPU执行权 两个线程又会同时从就绪到运行状态 去做train1Count -- 冲突的概率非常大Thread.sleep(30);} catch (InterruptedException e) {e.printStackTrace();}//出售车票System.out.println(Thread.currentThread().getName()+"出售第"+(100-train1Count +1)+"票");train1Count -- ;}}
}
public static void main(String[] args) {ThreadTrain1 threadTrain1 = new ThreadTrain1();Thread t1 = new Thread(threadTrain1,"窗口1");//两个线程共享一个数据Thread t2 = new Thread(threadTrain1,"窗口2");t1.start();t2.start();
}
执行结果:
当多个线程同时共享同一个全局变量,做写的操作时可能会受到其他线程的干扰,导致数据误差。这种现象即线程安全问题。
如上图,每个线程都有自己的私有工作内存,工作内存和主内存之间需要通过load/store进行交互。线程1可能在自己的工作内存中进行了计算操作,但还没有及时将新值刷新到主内存,这样线程2再操作同一个变量时就会产生问题。
除非使用volatile
或利用锁机制
显示的告诉处理器需要确保线程之间的可见性。
解决思路
- 不在线程之间对全局共享数据做操作
即限制变量同一时刻只能在单个线程中访问。实现方式:
- 线程封闭
保证变量只能被一个线程可以访问到。可以通过Executors.newSingleThreadExecutor()
实现。- 栈封闭
栈封闭即使用局部变量
。局部变量只会存在于本地方法栈
中,不能被其他线程访问,因此也就不会出现并发问题。所以如果可以使用局部变量就优先使用局部变量
。- ThreadLocal封闭
ThreadLocal是Java提供的实现线程封闭
的一种方式,ThreadLocal内部维护了一个Map,Map的key是各个线程,而Map的值就是要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal
利用Map实现了对象的线程封闭。
- 多线程之间同步
synchronized
或使用锁(lock
)
内置锁:jdk自带的锁—synchronized
synchronized(对象){
可能会发生线程冲突的代码;
}
显示锁:人为添加的锁–Lock锁
2. 活跃性问题
活跃性问题包括但不限于死锁、活锁、饥饿等。
死锁:死锁发生在一个线程需要获取多个资源的时候,这时由于两个线程互相等待对方的资源而被阻塞,死锁是最常见的活跃性问题。
活锁:当多个线程都拿到资源却又相互释放不执行,出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁——任何一个线程都无法继续执行。
饥饿:线程无法访问到它需要的资源而不能继续执行时,就是处于饥饿状态。
常见有几种场景:
- 高优先级的线程一直在运行消耗CPU,所有的低优先级线程一直处于等待;
- 一些线程被永久堵塞在一个等待进入同步块的状态,而其他线程总是能在它之前持续地对该同步块进行访问;
3. 性能问题
前面讲到了线程安全和死锁、活锁这些问题会影响多线程执行过程,如果这些都没有发生,也并不是只要用多线程性能就高,主要因为多线程有创建线程
和线程上下文切换
的开销。
线程的创建和销毁都需要时间,操作系统需要给它分配内存、列入调度等,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失。并且大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,对移动端来讲,最大的影响就是造成页面卡顿。
线程创建完之后,还会遇到线程上下文切换。
CPU是很宝贵的资源速度也非常快,为了保证雨露均沾,通常为给不同的线程分配时间片,当CPU从执行一个线程切换到执行另一个线程时,CPU需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个开关被称为『上下文切换』。
解决办法:有效利用现有的处理资源,重用已有的线程,从而减少线程的创建和销毁(这就需要使用线程池,线程池的基本作用就是进行线程的复用)。
3. 线程问题的深层原因
线程调度模型
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
JAVA内存模型
Java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型分为主内存和工作内存。主内存所有线程共享,工作内存每个线程单独拥有,不共享。
指令重排
在我们编程过程中,习惯性程序思维认为程序是按我们写的代码顺序执行的,举个例子来说,某个程序中有三行代码:
int a = 1; // 1 int b = 2; // 2 int c = a + b; // 3
从程序员角度执行顺序应该是1 -> 2 -> 3,实际经过编译器和CPU的优化很有可能执行顺序会变成 2 -> 1 -> 3(注意这样的优化重排并没有改变最终的结果)。类似这种不影响单线程语义的乱序执行我们称为指令重排。(后面讲Java内存模型也会讲到这部分)
目标:提高运行速度。
起因:只要程序的最终结果与严格串行环境中执行的结果相同,那么所有操作都是允许的。
重排序的问题是一个单独的主题,常见的重排序有3个层面:
- 编译级别的重排序,比如编译器的优化—编译器指令重排
- 指令级重排序,比如CPU指令执行的重排序—CPU指令重排
- 内存系统的重排序,比如缓存和读写缓冲区导致的重排序—内存系统重排
二、Thread和Runable
1 创建线程的两种方式
通过继承Thread实现
package com.yt.multithreading.thread;/*** @Author YT* @Date 2022/3/25 15:36**/
public class MyFirstThread {private static class FirstThread extends Thread{//这个run方法定义了我们自己的线程具体运行的代码@Overridepublic void run() {System.out.println("当前运行的线程是:"+Thread.currentThread().getName());}}public static void main(String[] args) {FirstThread firstThread = new FirstThread();firstThread.run();System.out.println("main方法中的线程:"+Thread.currentThread().getName());}
}
private static class FirstThread extends Thread{@Overridepublic void run() {System.out.println("当前运行的线程是:"+Thread.currentThread().getName());}}public static void main(String[] args) {Thread t1 = new FirstThread();t1.setName("T1");//将此线程的名字改为指定参数FirstThread t2 = new FirstThread();t2.setName("T2");FirstThread t3 = new FirstThread();t3.setName("T3");t1.start();t2.start();t3.start();}
每一次运行的顺序都不一样,表明线程的运行具有随机性:
- 调用
start()
方法后,程序通知JVM——“我已经准备好了,可以开始运行了” - JVM异步的调用线程对应的
run()
方法
start()方法的调用顺序不代表线程的run()方法运行顺序。
用Runable接口的方式实现多线程
package com.yt.multithreading.thread;/*** @Author YT* @Date 2022/3/25 19:24**/
public class UseRunableThread {private static class UseRunableTest implements Runnable{@Overridepublic void run() {System.out.println("当前运行的线程是:"+Thread.currentThread().getName());}}public static void main(String[] args) {Thread thread = new Thread(new UseRunableTest());//Thread thread = new Thread(() -> System.out.println("当前运行的线程是:"+Thread.currentThread().getName()));thread.start();System.out.println("main方法中的线程:"+Thread.currentThread().getName());}
}
2 Thread类和Runable接口的关系
对于继承Thread
类方式创建的线程,启动线程本质上执行过程是经过start()
—>start0()
—>run()
这样一个执行过程。
对于实现Runable接口的方式,创建线程对象时首先调用init()
方法,Thread类中的target初始化为实现业务逻辑的Runable实现。启动线程本质上经历的过程:start(
) --> start0()
--> run()
,但是再执行run
方法的时候会判断tartget
是否为空,决定执行的run
方法到底是谁的run
方法。
Thread
类实现了Runnable
接口,Runnable
接口里只有一个抽象的run()
方法。说明Runnable
不具备多线程的特性。Runnable
依赖Thread
类的start
方法创建一个子线程,再在这个子线程里调用run()
方法,才能Runnable
接口具备多线程的特性。
无论继承Thread类还是实现Runnable接口的方式,最终线程执行时都是调用你所覆写的run方法。实质上线程自身的逻辑都在Thread类中,Runable实现类只是线程执行流程中的一个步骤。
多线程并发可能遇到的问题及Runable和Thread之间的关系相关推荐
- Java学习笔记---多线程并发
Java学习笔记---多线程并发 (一)认识线程和进程 (二)java中实现多线程的三种手段 [1]在java中实现多线程操作有三种手段: [2]为什么更推荐使用Runnable接口? [3][补充知 ...
- java线程钥匙_Java多线程并发编程/锁的理解
一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等 ...
- dateformat java 并发_java.text.DateFormat 多线程并发问题
在日常开发中,java.text.DateFormat 应该算是使用频率比较高的一个工具类,经常会使用它 将 Date 对象转换成字符串日期,或者将字符串日期转化成 Date 对象.先来看一段眼熟的代 ...
- python多线程并发_Python进阶记录之基础篇(二十四)
回顾 在Python进阶记录之基础篇(二十三)中,我们介绍了进程的基本概念以及Python中多进程的基本使用方法.其中,需要重点掌握多进程的创建方法.进程池和进程间的通信.今天我们讲一下Python中 ...
- Selenium 2 WebDriver 多线程 并发
我用的是Selenium2,至于它的背景和历史就不赘述了.Selenium2也叫WebDriver.下面讲个例子,用WebDriver+java来写个自动化测试的程序.(如果能用firefox去测试的 ...
- Java多线程并发技术
Java多线程并发技术 参考文献: http://blog.csdn.net/aboy123/article/details/38307539 http://blog.csdn.net/ghsau/a ...
- 【Python之旅】第五篇(三):Python Socket多线程并发
1.多线程模块 主要是socketserver模块,如下图示: 2.多线程原理 如下图示说明: 3.SockteServer例子说明 服务器端: 客户端: 4.演示 还是以前面例子,对代码进行修改,作 ...
- java多线程抽奖_java 线程池、多线程并发实战(生产者消费者模型 1 vs 10) 附案例源码...
导读 前二天写了一篇<Java 多线程并发编程>点我直达,放国庆,在家闲着没事,继续写剩下的东西,开干! 线程池 为什么要使用线程池 例如web服务器.数据库服务器.文件服务器或邮件服务器 ...
- JAVE SE 学习day_09:sleep线程阻塞方法、守护线程、join协调线程同步方法、synchronized关键字解决多线程并发安全问题
一.sleep线程阻塞方法 static void sleep(long ms) Thread提供的静态方法sleep可以让运行该方法的线程阻塞指定毫秒,超时后线程会自动回到RUNNABLE状态,等待 ...
最新文章
- Elixir: 多太(Polymorphism)
- 小程序组件 Vant Weapp 安装
- python过滤后缀
- CF17E:Palisection——题解
- 在Linux中安装R语言包,遇到无法验证下列签名的错误
- List实现类的特点和性能分析
- 如何设置CentOS 7开机自动获取IP地址详解
- 2016计算机二级c语言题库,计算机二级c语言题库2016精选
- Nagel-Schreckenberg(交通流)模型——python实现
- windows xp sp3
- C++实现http客户端连接服务端及客户端json数据的解析
- 【跟我学Puppet】1.5 Puppet 3.7 使用Hiera定义配置
- ThinkPad E450 最新macOS BigSur黑苹果安装教程(OpenCore引导)
- unity3d 批量替换模型材质的脚本 一键替换模型及子物体材质
- Excel如何快速将图片插入到批注中?
- 爬虫例子——多页、函数模板
- centos7源码搭建lnmp环境
- 450g吐司烘烤温度_解决这24个问题,吐司面包想失败都难!
- 核心解读 - 2022版智慧城市数字孪生标准化白皮书
- AHD同轴摄像头接入电脑USB录制视频的方法,AHD转USB,AI图像算法(ADAS\DMS\360环视\BSD\人脸识别),图像接入电脑处理