JAVA基础知识系列---进程、线程安全
1 相关概念
1.1 临界区
保证在某一时刻只有一个线程能访问数据的简便方法,在任意时刻只允许一个线程对资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后,其他所有试图访问临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的
1.2 互斥量
互斥量和临界区很相似,只能拥有互斥对象的线程才能具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下次共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后可以访问资源。互斥量比临界区复杂,因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
1.3 管程/信号量
管程和信号量是同一个概念。指一个互斥独占锁定的对象或称为互斥体。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定,他必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待线程。一个拥有管程的线程如果愿意的话可以再次进入相同的管程(可重入性)
1.4 CAS操作
CAS操作(compare and swap)CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。
1.5 重排序
编译器和处理器为了提高性能,而在程序执行时会对程序进行重排序。他的出现是为了提高程序的并发度。从而提高性能;但是对于多线程程序,重排序可能会导致程序执行的结果不是我们需要的结果,重排序分为编译器和处理器俩个方面。而处理器重排序包括指令级重排序和内存重排序。
小节
在java中,所有的变量(实例字段,静态字段,构成数组的元素,不包括局部变量和方法参数)都存储在主内存中,内个线程都有自己的工作内存,线程的工作内存保存被线程使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,为不能直接读写主内存的变量。不同线程之间也不恩能够直接访问对方工作内存中的变量,线程间比变量值的传递通过主内存来完成。
JAVA中线程安全相关关键字及类
主要包括:synchronized,Volitile,ThreadLocal,Lock,Condition
2.1 Volitile
作用:
1)保证了心智能立即存储到主内存才,每次使用前立即从主内存中刷新
2)禁止指令重排序优化
Volitile关键字不能保证在多线程环境下对共享数据的操作的正确性,可以使用在自己状态改变之后需要立即通知所有线程的情况下,只保证可见性,不保证原子性。即通过刷新变量值确保可见性。
Java中synchronized和final也能保证可见性
synchronized:同步快通过变量锁定前必须清空工作内存中的变量值,重新从主内存中读取变量值,解锁前必须把变量值同步回主内存来确保可见性。
final:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传递进去,那么在其他线程中就能看见final字段的值,无需同步就可以被其他线程正确访问。
2.2 synchronized
把代码块声明为synchronized,有俩个作用,通常是指改代码具有原子性和可见性。如果没有同步机制提供的这种可见性,线程看到的共享比那里可能是修改前的值或不一致的值,这将引发许多严重问题。
原理:当对象获取锁是,他首先是自己的高速缓存无效,这样就可以保证直接从主内存中装入变量,同样在对象释放锁之前,他会刷新其高速缓存,强制使已做的任何更改都出现在主内存中,这样会保证在同一个锁上同步的俩个线程看到在synchronized块内修改的变量的相同值。
synchronized释放由JVM自己管理。
存在的问题:
1)无法中断一个正在等待获得锁的线程
2)无法通过投票得到锁,如果不想等待下去,也就没法得到锁
3)同步还需要锁的释放只能在与获得锁所在的堆栈帧相同的堆栈中进行,多数情况下,这没问题(而且与一场处理交互的很好),但是,确实存在一些非块结构的锁定更适合情况。
2.3 Lock
Lock是有JAVA编写而成的,在java这个层面是无关JVM实现的。包括:ReentrantLock,ReadWriteLock。其本质都依赖于AbstractQueueSynchronized类。Lock提供了很多锁的方式,尝试锁,中断锁等。释放锁的过程由JAVA开发人员自己管理。
就性能而言,对于资源冲突不多的情况下synchronized更加合理,但如果资源访问冲突多的情况下,synchronized的性能会快速下降,而Lock可以保持平衡。
2.4 condition
Condition将Object监视器方法(wait,notify,notifyall)分解成截然不同的对象,以便通过这些对象与任意Lock实现组合使用,为每个对象提供多个等待set(wait-set),,其中Lock替代了synchronized方法和语句的使用,condition替代了Object监视器方法的使用。Condition实例实质上被你绑定到一个锁上。要为特定Lock实例获得Condition实例,请使用其newCondition()方法。
2.5 ThreadLock
线程局部变量。
变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本,这种情况下TreadLocal就非常有用。
应用场景:当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始值的时候,最适合使用TreadLocal。
事实上,从本质上讲,就是每个线程都维持一个MAP,而这个map的key就是TreadLocal,而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那就肯定不存在线程安全的问题。总体来讲,TreadLocal这个变量的状态根本没有发生变化。它仅仅是充当了一个key的角色,另外提供给每一个线程一个初始值。如果允许的话,我们自己就能实现一个这样的功能,只不过恰好JDK就已经帮助我们做了这个事情。
使用TreadLocal维护变量时,TreadLocal为每个使用该变量的线程提供独立地变量副本,所以每一个线程都可以独立地改变自己的副本,而不会英语其他线程所对应的副本。从线程的角度看,目标变量对象是线程的本地变量,这也是类名中Local所需要表达的意思。
TreadLocal的四个方法:
void set(Object val),设置当前线程的线程局部变量的值
Object get()返回当前线程所对用的线程局部变量。
void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,线程结束后,局部变量自动被GC
Object initValue() 返回该线程局部变量的初始值,使用protected修饰,显然是为了让子类覆盖而设计的。
线程安全的实现方式
3.1 互斥同步
在多线程访问的时候,保证同一时间只有一条线程使用。
临界区,互斥量,管程都是同步的一种手段。
java中最基本的互斥同步手段是synchronized,编译之后会形成monitorenter和monitorexit这俩个字节码指令,这俩个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象,还有一个锁的计数器,来记录加锁的次数,加锁几次就要同样解锁几次才能恢复到无锁状态。
java的线程是映射到操作系统的原生线程之上的,不管阻塞还是唤醒都需要操作系统的帮助完成,都需要从用户态转换到核心态,这是很耗费时间的,是java语言中的一个重量级的操作,虽然虚拟机本身会做一点优化的操作,比如通知操作系统阻塞之前会加一段自旋等待的过程,避免频繁切换到核心态。
3.2 非阻塞同步
互斥和同步最主要的问题就是阻塞和唤醒所带来的性能的问题,所以这通常叫阻塞同步(悲观的并发策略).随着硬件指令集的发展,我们有另外的选择:基于冲突检测的乐观并发策略,通俗讲就是先操作,如果没有其他线程争用共享的数据,操作就成功,如果有,则进行其他的补偿(最常见的就是不断的重试)。这种乐观的并发策略许多实现都不需要把线程先挂起,这种同步操作被称为非阻塞同步。
3 无同步
部分代码天生就是线程安全的,不需要同步。
1)可重入代码:纯代码,具有不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等特征,它的返回结果是可以预测的。
2)线程本地存储:把共享数据的可见性范围限制在同一个线程之内,这样就无需同步也能保证线程之间不出现数据争用问题。可以通过java.lang.TreadLocal类来实现线程本地存储的功能。
JAVA基础知识系列---进程、线程安全的更多相关文章
- Java基础知识系列——String
最近晚上没有什么事(主要是不加班有单身),就复习了一下Java的基础知识.我复习Java基础知识主要是依据Java API和The Java™ Tutorials. 今天是第一篇,复习了一下Strin ...
- JAVA基础知识之多线程——线程池
线程池概念 操作系统或者JVM创建一个线程以及销毁一个线程都需要消耗CPU资源,如果创建或者销毁线程的消耗源远远小于执行一个线程的消耗,则可以忽略不计,但是基本相等或者大于执行线程的消耗,而且需要创建 ...
- JAVA基础知识之多线程——线程组和未处理异常
线程组 Java中的ThreadGroup类表示线程组,在创建新线程时,可以通过构造函数Thread(group...)来指定线程组. 线程组具有以下特征 如果没有显式指定线程组,则新线程属于默认线程 ...
- JAVA基础知识之多线程——线程通信
传统的线程通信 Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题. wait():释放当前线程的同步监视控制器,并 ...
- JAVA基础知识之多线程——线程同步
线程安全问题 多个线程同时访问同一资源的时候有可能会出现信息不一致的情况,这是线程安全问题,下面是一个例子, Account.class , 定义一个Account模型 package threads ...
- Java基础知识系列——Exception
异常在编程中使用频率非常非常的高,在Java中异常的基类是Exception. 下面就介绍一下Java中的异常: 1.结构 try{ //捕获try里的异常 }catch( Exception e){ ...
- Java基础知识系列——目录操作
Java对目录操作的许多方法与上一篇文件操作的方法很多是一样的. java.io.File file = new File( "D:\1\2\3\4"); 1.递归创建目录 fil ...
- Java基础知识系列——文件操作
对文件进行操作在编程中比较少用,但是我最近有一个任务需要用到对文件操作. 对文件有如下操作形式: 1.创建新的文件(夹) File fileName = new File("C:/myfil ...
- Java基础知识系列——日期
日期类型也是在编程中经常用到的一种数据类型. Java中的日期类型为Date. 另外需要记住三个类: java.text.SimpleDateFormat; java.util.Calendar; j ...
随机推荐
- PHP中file_exists与is_file、is_dir的区别,以及执行效率的比较
判断文件是否存在,有2个常用的PHP函数:is_file 和 file_exists, 判断文件夹是否存在,有2个常用PHP函数:is_dir 和 file_exists, 即 file_exists ...
- react中常用的一些方法
React.createClass:创建一个ReactClass(组件类),参数是一个对象且必须带有 render 属性方法,该方法必须返回一个封闭的容器(容器内可以有其它不限结构的容器)或 null ...
- TortoiseSVN使用简介
TortoiseSVN使用简介 2009-04-24 来源: dev.idv.tw 1.安装及下载client 端 2.什么是SVN(Subversion)? 3.为甚么要用SVN? 4.怎么样在Wi ...
- MySQL密码丢失,解决方法
我的MySQ安装路径是:D:\Program Files\MySQL 1.所以先cmd下切入盘 输入-> D: 输入->cd "D:\Program Files\MySQL\My ...
- LeetCode Database题解
175. Combine Two Tables 使用外连接即可. # Write your MySQL query statement below select FirstName, LastName ...
- java二维码生成-谷歌(Google.zxing)开源二维码生成学习及实例
java二维码生成-谷歌(Google.zxing)开源二维码生成的实例及介绍 我们使用比特矩阵(位矩阵)的QR码编码在缓冲图片上画出二维码 实例有以下一个传入参数 OutputStream ou ...
- CCNA网络工程师学习进程(8)访问控制列表ACL
前面几节我们介绍了路由器的路由配置,接下来几节我们将介绍路由器的高级配置应用,包括ACL.NAT.DHCP.PPP.VPN和远程连接等的配置. (1)ACL概述: ACL(Access C ...
- 《JAVASCRIPT高级程序设计》原生拖放和媒体元素
一.原生拖放 最早在网页中引入javascript拖放功能的是IE4,当时,网页中只有两种对象可以拖放:图像和某些文本.而现在,几乎网页中的任何元素都可以拖放以及作为放置目标.下面介绍一些与拖放相关的 ...
- WinForm DataGridView控件、duck布局
1.DataGridView控件 显示数据表 (1)后台数据绑定: List<xxx> list = new List<xxx>(); dataGridView1.DataSo ...
- 进程——wait与waitpid、僵尸进程与孤儿进程
僵尸进程:子进程终止了,但是父进程没有回收子进程的资源PCB.使其成为僵尸进程 孤儿进程:父进程先与子进程结束了,使得子进程失去了父进程,这个时候子进程会被1号进程init进程领养,成为孤儿进程 为了 ...