java多线程编程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

进程与线程

Java进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。

可以把进程简单的理解为正在操作系统中运行的一个程序。

Java线程

线程(thread)是进程的一个执行单元。

一个线程就是进程中一个单一顺序的控制流, 进程的一个执行分支。

进程是线程的容器,一个进程至少有一个线程.一个进程中也可以有多个线程。

在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等. 每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。

Java主线程与子线程

JVM启动时会创建一个主线程,该主线程负责执行main方法 . 主线程就是运行main方法的线程。

Java中的线程不孤立的,线程之间存在一些联系. 如果在A线程中创建了B线程, 称B线程为A线程的子线程, 相应的A线程就是B线程的父线程。

setPriority()

thread.setPriority( num ); 设置线程的优先级。

java线程的优先级取值范围是 1 ~ 10 , 如果超出这个范围会抛出异常IllegalArgumentException。

在操作系统中,优先级较高的线程获得CPU的资源越多。

线程优先级本质上是只是给线程调度器一个提示信息,以便于调度器决定先调度哪些线程. 注意不能保证优先级高的线程先运行。

Java优先级设置不当或者滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿。

线程的优先级并不是设置的越高越好,一般情况下使用普通的优先级即可,即在开发时不必设置线程的优先级。

线程的优先级具有继承性, 在A线程中创建了B线程,则B线程的优先级与A线程是一样的。

interrupt()

中断线程。

注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。

setDaemon()

Java中的线程分为用户线程与守护线程。

守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。

守护线程不能单独运行, 当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁, JVM会退出。

线程生命周期

线程的生命周期是线程对象的生老病死,即线程的状态。

线程生命周期可以通过getState()方法获得, 线程的状态是Thread.State枚举类型定义的, 由以下几种:

● NEW,新建状态. 创建了线程对象,在调用start()启动之前的状态。

● RUNNABLE,可运行状态. 它是一个复合状态,包含:READY和RUNNING两个状态. READY状态该线程可以被线程调度器进行调度使它处于RUNNING状态, RUNING状态表示该线程正在执行. Thread.yield()方法可以把线程由RUNNING状态转换为READY状态。

● BLOCKED阻塞状态.线程发起阻塞的I/O操作,或者申请由其他线程占用的独占资源,线程会转换为BLOCKED阻塞状态. 处于阻塞状态的线程不会占用CPU资源. 当阻塞I/O操作执行完,或者线程获得了其申请的资源,线程可以转换为RUNNABLE。

● WAITING等待状态. 线程执行了object.wait(), thread.join()方法会把线程转换为WAITING等待状态, 执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态。

● TIMED_WAITING状态,与WAITING状态类似,都是等待状态.区别在于处于该状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,该线程自动转换为RUNNABLE。

● TERMINATED终止状态,线程结束处于终止状态
在这里插入图片描述

Java多线程编程具有以下优势:

1、提高系统的吞吐率(Throughout). 多线程编程可以使一个进程有多个并发(concurrent,即同时进行的)的操作。

2、提高响应性(Responsiveness).Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。

3、充分利用多核(Multicore)处理器资源. 通过多线程可以充分的利用CPU资源。

Java多线程编程存在的问题与风险:

1、线程安全(Thread safe)问题.多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据), 如丢失数据更新。

2、线程活性(thread liveness)问题.由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:

● 死锁(Deadlock). 类似鹬蚌相争

● 锁死(Lockout), 类似于睡美人故事中王子挂了

● 活锁(Livelock). 类似于小猫咬自己尾巴

● 饥饿(Starvation).类似于健壮的雏鸟总是从母鸟嘴中抢到食物

3、上下文切换(Context Switch). 处理器从执行一个线程切换到执行另外一个线程。

4、可靠性. 可能会由一个线程导致JVM意外终止,其他的线程也无法执行。

java线程模型

在这里插入图片描述

java原子性 可见性

原子性

原子(Atomic)就是不可分割的意思. 原子操作的不可分割有两层含义:

● 访问(读,写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生, 即其他线程年示到当前操作的中间结果。

● 访问同一组共享变量的原子操作是不能够交错的。

如现实生活中从ATM机取款, 对于用户来说,要么操作成功,用户拿到钱, 余额减少了,增加了一条交易记录; 要么没拿到钱,相当于取款操作没有发生。

Java有两种方式实现原子性:

一种是使用锁; 另一种利用处理器的CAS(Compare and Swap)指令。

锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。

CAS指令直接在硬件(处理器和内存)层次上实现,看作是硬件锁。

可见性

在多线程环境中, 一个线程对某个共享变量进行更新之后 , 后续其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问题的另外一种形式: 可见性(visibility)。

如果一个线程对共享变量更新后, 后续访问该变量的其他线程可以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见, 否则称这个线程对共享变量的更新对其他线程不可见。

多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)。

Java有序性

有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的 内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order)。

乱序是指内存访问操作的顺序看起来发生了变化。

重排序

在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:

编译器可能会改变两个操作的先后顺序;

处理器也可能不会按照目标代码的顺序执行;

这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序。

重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能.但是,可能 对多线程程序的正确性产生影响,即可能导致线程安全问题。

重排序与可见性问题类似,不是必然出现的。

与内存操作顺序有关的几个概念:

源代码顺序, 就是源码中指定的内存访问顺序。

程序顺序, 处理器上运行的目标代码所指定的内存访问顺序。

执行顺序,内存访问操作在处理器上的实际执行顺序。

感知顺序,给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序。

可以把重排序分为指令重排序与存储子系统重排序两种:

指令重排序主要是由JIT编译器,处理器引起的, 指程序顺序与执行顺序不一样。

存储子系统重排序是由高速缓存,写缓冲器引起的, 感知顺序与执行顺序 不一致。

指令重排序

在源码顺序与程序顺序不一致,或者 程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)。

指令重排是一种动作,确实对指令的顺序做了调整, 重排序的对象指令。

javac编译器一般不会执行指令重排序, 而JIT编译器可能执行指令重排序。

处理器也可能执行指令重排序, 使得执行顺序与程序顺序不一致。

指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预期的结果。

存储子系统重排序

存储子系统是指写缓冲器与高速缓存。

高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的一个高速缓存。

写缓冲器(Store buffer, Write buffer)用来提高写高速缓存操作的效率。

即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下, 其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的顺序顺序看起来像是发生了变化, 这种现象称为存储子系统重排序。

存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的现象。

存储子系统重排序对象是内存操作的结果。

从处理器角度来看, 读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作; 写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作.内存重排序有以下四种可能:

● LoadLoad重排序,一个处理器先后执行两个读操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2->L1。

● StoreStore重排序,一个处理器先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是W2->W1。

● LoadStore重排序,一个处理器先执行读内存操作L1再执行写内存操作W1, 其他处理器对两个内存操作的感知顺序可能是W1->L1。

● StoreLoad重排序,一个处理器先执行写内存操作W1再执行读内存操作L1, 其他处理器对两个内存操作的感知顺序可能是L1->W1。

内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同。

内存重排序可能会导致线程安全问题.假设有两个共享变量int data = 0; boolean ready = false;

处理器1

处理器2

data = 1; //S1

ready = true; //S2

while( !ready){} //L3

sout( data ); //L4

貌似串行语义

JIT编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序, 给单线程程序造成一种假象----指令是按照源码的顺序执行的.这种假象称为貌似串行语义. 并不能保证多线程环境程序的正确性。

为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序.如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)。

如:

x = 1; y = x + 1; 后一条语句的操作数包含前一条语句的执行结果;
y = x; x = 1; 先读取x变量,再更新x变量的值;
x = 1; x = 2; 两条语句同时对一个变量进行写操作

如果不存在数据依赖关系则可能重排序,如:

double price = 45.8;
int quantity = 10;
double sum = price * quantity;

存在控制依赖关系的语句允许重排.一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系(Control Dependency). 如在if语句中允许重排,可能存在处理器先执行if代码块,再判断if条件是否成立。

保证内存访问的顺序性volatile关键字, synchronized关键字

可以使用volatile关键字, synchronized关键字实现有序性。
在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原子操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。


总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值