Java并发编程基础详解
2020-11-23 15:50:05 15 举报
AI智能生成
Java并发编程基础详解
作者其他创作
大纲/内容
什么是线程安全
线程安全是一个模糊的概念,没有正式的定义
当一个进程中多个线程同时运行,他们的运行结果始终能表现出正确的行为,那么就是线程安全
而当一个类在多线程中始终能表现出正确的行为,那么就称这个类是线程安全的
为什么会出现线程安全问题
简单通俗的讲,主要原因还是在多个线程 共享 同一个变量造成的,当两个线程同时修改和访问一个变量,如果没有适当的机制加以控制,那么一个线程的修改可能对另一个线程的运行结果产生影响,严重的话还会造成程序崩溃
如何解决线程安全问题
简单讲,使用适当的机制控制多线程对于 共享变量的访问,确保他们以预期的方式使用该共享变量
常见问题
volatile 关键字的作用是什么? 修饰了volatile的Filed线程安全了吗?
volatile
被 volatile修饰的共享变量的修改值对所有多线程可见,并且不会对该操作与其他内存操作一起重排序
注意: 访问volatile变量并不会执行加锁操作,如果修改后的变量值依赖于修改之前的变量值,建议对代码进行加锁
在方法体上加synchronized,他获取到的内置锁是什么?
是 this 对象,也就是当前对象实例
某个类的一个方法被synchronized修饰,使用这个类创建两个实例对象,两个实例对象在两个线程中分别代用该方法,代码块是同步的吗?
不同步,因为他们持有的内置锁是this,是两个不同的实例,也就是说,他们不是一把锁,当然毫无关联,所以不同步
最佳实践
++ 运算符不是线程安全的操作,他在执行过程中会分为两步,在使用++运算符时要格外注意
并发控制遵循最小并发代码块控制,尽量同步代码块的范围缩小
一个状态只由一种锁控制,一个状态由多个锁控制会导致并发问题;同时,一个状态的所有改变也应该在一种锁的控制下完成
为先检查后执行的操作加上同步代码块,或者使用线程安全类直接实现
比如在某个代码块中,先检查某个变量是不是为空,如果为空则初始化
在多线程中,可能多个线程重复初始化该变量,第一个线程判断是不是空,发现为空,立即初始化,在初始化之前,另一个线程也运行到此处,发现也为空,所以也初始化,就导致后面的线程初始化的对象覆盖了前面初始化的对象,导致出错
正确的做法是对检查为空和初始化放在一个同步代码块中一起执行,那么在判空和初始化后的过程中,就不会有其他线程进入从而影响结果了
线程安全的几个基本概念
竞态条件
竞态条件是一个概念,多个线程同时运行一个代码块,可能会产生并发错误,那么这个代码块就产生了竞态条件
原子性
将某个或者某一系列操作复合一个操作,在操作完这一系列操作之前,其他线程无法获得锁进入操作
在Java中,同步代码块中的所有操作即是一个原子操作
可见性
在Java中运行线程的过程中,会将那些共享变量复制一份副本到当前线程的工作内存,这就导致了其他线程修改原变量的值,而副本变量没有被修改,导致数据过期,修改数据不会被其他线程获取到
可见性的概念就是在某个线程修改共享变量后,对于其他线程是否可见
在Java中,被volatile修饰的变量对所有线程可见
有序性
在Java中,JVM为了高效地执行代码,有时候会对某些代码顺序进行调整,以达到更高效的运行
但在多线程中,这可能导致严重的问题,有序性指的是这些代码的执行顺序是否按照预定的顺序执行
在Java中,为了保证线程不出错,同步代码块中的顺序是有序的
发布 和 逸出
发布是一个概念,当前变量在多线程当中被共享,即为发布
发布需要注意如何安全发布,后续有详解
当一个变量发布后没有被正确地控制线程安全而暴露在外时,即为逸出
线程封闭
将某个变量封闭在某个单一线程中,仅在当前线程才能使用该变量,这就是线程封闭
实现线程封闭的几种方法
栈封闭
局部变量才能访问到的对象就是栈封闭的,通俗的将,方法体内new的对象就是栈封闭的,他不可能被外部访问,所以他是线程安全的
ThreadLocal
这个类能使线程中的某个值与保存值的对象关联起来
某种程度上,可以将ThreadLocal视为 Map<Thread,T>
使用ThreadLocal时,应多考虑一些设计模式,不要太过依赖ThreadLocal
Java实现线程安全的原理
实现代码同步的基本原理就是加互斥锁,在进入某个代码块之前必须使用定义的锁对该代码块加锁,锁只有一把,谁先加锁谁就可以独占该代码块进行运行,其他线程运行到该代码块发现被定义的锁锁上了,只能阻塞在那里等待,当之前加锁的程序退出该代码块后会归还那把锁,下一个人就可以拿到锁对代码块进行加锁独占。
内置锁
每个Java对象都可以用作一个实现同步的锁,这些锁称为内置锁
在Java中,每个Java对象都可以被看做上述描述中的唯一一把锁,当线程想要进入代码块是,首先要做的就是使用该对象对代码块进行加锁,其他线程运行到该代码块时,发现代码已经被该锁锁上,自然只能在原地等待
synchronized 关键字
synchronized 关键字的作用就是为某个代码块添加内置锁
当 synchronized 被修饰在方法体上时,他的内置锁就是 this 对象,也就是该实例本身
synchronzied (Class类)也是合法的
内置锁是可重入锁
重入参考节点 Java并发编程 - 线程安全性 - Java实现线程安全的原理 - 重入锁
重入锁
当持有该锁的线程又运行到需要加同样锁的代码块的时候,如果能进入,那么就是可重入的,如果被阻塞,那么就是不可重入的
内置锁是重入锁
重入意味着锁的粒度是线程而不是调用
volatile
被 volatile修饰的共享变量的修改值对所有多线程可见,并且不会对该操作与其他内存操作一起重排序
注意: 访问volatile变量并不会执行加锁操作,如果修改后的变量值依赖于修改之前的变量值,建议对代码进行加锁,而不是使用volatile
同步代码块的特性
同步代码块中的代码能保证变量的可见性、有序性、原子性,在同步代码块中,不存在竞态条件
也就是说,同步代码块中的变量不需要担心数据失效,不需要使用volatile修饰这些变量就可以实现可见性;同时,JVM也不会重排序这些语句执行的顺序
还有,被同步代码块修改状态的变量,使用未同步的代码获取该变量,也可能获取到失效值(比如set方法被synchronized修饰,但是get没有被修饰,那么set修改后的数据,get到的数据很可能是失效的),正确的发布方式是将状态的所有读取和修改都控制在同一种锁下进行控制
如何实现线程安全
如何实现线程安全,首先我们要搞清楚当前代码到底需要控制并发,控制到什么程度
并非所有的数据都需要加锁保护,只有被多个线程同时访问的可变数据才需要通过锁来保护
以下几种情况不需要控制并发
单线程环境
如果是单线程的应用程序,根本不会产生并发问题,也就多线程会带来的安全问题
这句话看似废话,却阐述了线程安全问题产生的根本原因,同时也说明一件事情,在单线程中,使用类似StringBuffer,HashTable等线程安全类是毫无意义的,反而是对资源的一种浪费,看到此处,那些未真正了解Java并发的开发者是不是该好好地审查一下自己的代码是不是也有同样问题
真的是单线程吗?
Java中没有真正意义的单线程,就算你没有在代码中显式的创建线程,Java照样是多线程在运行,无论何时,GC就在后台线程中默默地帮你回收垃圾
在代码开发中,除非你是从main函数开始写,否则你一旦使用了某些框架,你的应用程序仍然是多线程在运行,因为框架中仍封装了很多多线程代码
服务端开发框架基本都是并发的,因为系统不可能串行处理请求,一定是一个请求一个线程来处理,才能保证系统高效运行
使用无状态的对象
简单讲无状态对象就是该对象中没有任何Filed,只提供了一些方法类,这样多线程仅仅是使用了该对象,并不会该对象做出改变,自然谈不上某个线程对该对象的Filed做出修改而影响其他线程
所以无状态的对象一定是线程安全的
使用不可变的对象
如果多个线程访问一个不可变的对象,既然是只读变量,那也谈不上修改会对其他线程产生影响
什么是不可变对象
对象创建后就不可被修改,且他所有Filed也都不可被修改
对象创建期间,this引用没有逸出
使用 final 来修饰不可变对象以及他所有的Filed
以下情况只需要保证可见性和有序性即可
当某个变量不需要控制并发,但是要保证可见性,比如某个条件状态,使用 volatile 就可以满足
适当场景才使用volatile变量
volatile没有那么好理解,满足以下条件时,才应该使用volatile变量
对变量的修改不依赖变量的当前值,一般用作状态控制条件循环等
在访问变量时没有必要加锁
该变量可能被修改
以下情况需要进行控制并发
多个线程之间共享变量,并且多个线程都会访问和修改该共享变量
使用 synchronized 来保证可见性,有序性以及操作的原子性
加了同步就安全了吗?
格外注意: 两个线程安全的操作组合在一起并不代码他就是线程安全的
正确地发布
在一个对象没有被完成构造完成之前,不要急于发布,否则其他线程可能获得一个没有构造完成的对象,导致出错
产生这个问题的常见错误时,在构造函数中启动另一个线程,这个线程持有了当前对象并使用他的非静态Filed或者Method,而该对象没有完全构造完成,导致另一个线程获取该对象时出错
正确的做法是在初始化函数中再启动新的线程
一个状态只能由一种锁控制,并且该状态所有改变也应该在一种锁的控制下完成,一般会将共享状态的所有操作都封装在一个类里面,而不是依赖其他类去完成操作,比如修改一个类的某个Field,只能使用该类封装的几个同步方法才能读取和修改该Field
在发布一个对象时,可能会隐式地发布其他相关变量,这时要对这些隐式发布的对象进行控制
比如发布的一个对象Filed引用了另一个对象,那么我们就可以在不受控制的情况下对这个Filed引用的对象随意进行操作,这样是很危险的
线程安全带来的性能问题
每次只能有一个线程执行代码块,而其他代码块只能等待,这显然违背了多线程的初衷
在实际开发过程中,我们要尽量减少同步代码块的作用范围
在一个同步代码块中,其他不必要的同步操作会带来额外的开销,但实际没有什么作用,这种操作我们可以换一种实现方式
比如在一个同步代码块中,使用HashTable避免产生并发问题,这是不必要的,正确的做法是将HashTable类型变为volatile 修饰的 HashMap即可满足要求
总之,线程安全和性能两者不可兼得,开发者应该按照实际情况决定如何编写代码
0 条评论
下一页