Java并发编程
多线程
Java支持多线程开发,多线程技术使得程序的响应速度更快,可以在进行其他工作的同时一直处于活动状态,程序性能得到提升。
性能提升的本质就是榨取硬件的剩余价值(硬件利用率)
多线程带来的问题
安全性(访问共享变量),性能(切换开销等)
并行执行与并发执行
单核cpu线程实际是串行执行的。操作系统种有一个组件叫做人物调度器,将cpu的时间片,分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换速度非常快,人类感觉是同时运行的。
微观串行,宏观并行,一般会将这种线程轮流使用cpu的做法叫做并发,concurrent
多核cpu下,每个核都可以调度运行线程,这时候线程是可以并行的。
用买咖啡作为例子
两队人排队在一个咖啡机上接咖啡,交替执行,这种是并发。
两台咖啡机都有人排队使用,是并行。
从严格意义上来说,并行的多任务是真的同时执行,而对于并发来说,这个过程只是交替的,一会执行任务A,一会执行任务B,系统会不停的在两者之间切换。
并发执行说的是在一个时间段内,多件事情在这个时间段内交替执行。
并行执行说的是多件事情在同一时刻同时发生。
并发编程的核心问题
不可见性
乱序性
非原子性
volatile关键字
该关键字修饰的变量,在一个工作内存中操作后,底层会将工作内存种的数据同步到其他线程的工作内存,使其立即可见,解决了不可见性的问题。
同时被修饰后还禁止进行指令重排序.
volatile不能保证对变量操作的原子性
如何保证原子性
锁
解决非原子性问题,可以通过加锁的方式实现
synchronized和ReentrantLock都可以实现
synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!
synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个
资源时,发现锁还未释放,所以只能在外面等待。
synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码
后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一
定能保证原子操作。
原子变量
java还提供了一种方案,在不加锁的情况下,实现++操作的原子性
就是原子类,AtomicInteger
在java.util.concurrent包下,定义了许多与并发编程相关的处理类,此包一般成为JUC
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(atomicInteger.incrementAndGet());
}
}.start();
}
}
CAS
采用CAS(比较并交换)思想,当多个线程对同一个内存数据库操作时
假设A线程把内存数据加载到自己工作内存中,这个工作内存中的值就是预期值.
然后在自己的工作内存种操作后,当写回住内存时,先判断预期值和住内存的值是否一致,如果一致则证明还没有其他线程对主内存进行修改,直接写回主内存.
若是预期值和主内存中的中的值不一样,说明有其他线程已经修改过了,线程A需要重新获取主内存中的值,重新操作,判断一直到预期值和主内存值相同才结束,否则自旋一直判断.
由于采用自旋方式实现,使得线程都不会阻塞,一直自旋,适合并发量低的情况
若是并发量过大,线程一直自旋会导致cpu开销过大
还会出现ABA问题:线程A拿到主内存值后,期间有其他线程已经多次修改内存数据,最终又将数据修改为和线程A拿到的值相同,这种就称之为A->B->A
可以通过带版本号的原子类(AtomicStampedReference类)避免ABA问题,每次对该原子值进行修改时,同时修改版本号,在进行CAS的过程中,若是版本号不一致也无法进行修改
Java中锁的分类
Java中很多锁的名词,但是并不是全是锁,有的指的是锁的特性,有的指锁的设计,有的指锁的状态
乐观锁/悲观锁
乐观锁与悲观锁指的是看待并发同步的角度
乐观锁
是一种不加锁的实现,例如原子类,认为不加锁,采用自选方式尝试修改共享数据,是不会有问题的.
乐观锁认为对于同一个数据的并发操作,是不会发生修改的.在更新数据的时候,会采用尝试更新,不断重新的方式更新数据
悲观锁
是一种加锁的实现方式,例如synchronized和ReentrantLock,悲观锁认为不加锁修改共享数据会出现问题
可重入锁
又名递归锁,是指在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁(内层方法与外层方法使用的是同一把锁)
对于java reentrantlock而言,他的名字就能看出是一个可重入锁,对于synchronized而言,也是一个可重入锁
可重入锁的好处是可以一定程度的避免死锁
下面的例子中,A方法已经进入了synchronized锁,此时方法A中想要调用方法B,但是方法B无法获得锁,此时A就无法结束,A无法结束B也就无法获得锁,就产生了死锁问题
public class Demo{
synchronized void setA()throws Exception{
System.out.print(“方法A”);
setB();
}
synchronized void setB()throws Exception{
System.out.print(“方法B”);
}
}
读写锁
读写锁的特点:
读读不互斥,读写互斥,写写互斥
加读锁是防止在另外的线程此时写入数据,防止读取脏数据
ReentrantReadWriteLock读写锁实现
private int data;// 共享数据
private ReadwriteLock rwl =new ReentrantReadwriteLock();
//写数据
public void set(int data){
rwl.writeLock().lock()://取到写锁
try {
System,out.printIn(Thread.currentThread().getName()+"准备写入数据");
this.data= data;
System.out.printIn(Thread,currentThread().getName()+"写入”+ this.data);
} finally {
rwl.writeLock().unlock();//释放写锁
}
//读数据
public void get() {
rw1.readLock().1ock()://取到读锁
try {
System.out.printIn(Thread,currentThread().getName()+"准备读取数据");
System,out.printIn(Thread,currentThread().getName()+"读取"+ this.data);
} finally {
rw1.readLock().unlock();//释放读锁
}
}
只要有写入操作,其他线程就不能写,也不能读,保证读不到脏数据,最大程度的保证读的效率
分段锁
将锁的颗粒度进一步细化,提高并发效率
Hashtable是现成安全的,方法上都加了锁,加入有两个线程同时读,也只能一个一个的读,并发效率低
ConcurrentHashMap没有给方法上锁,使用hash表中每个位置上的第一个对象作为锁对象,这样就可以多个线程对不同的位置进行操作,相互不影响,只有对同一个位置操作时,才互斥
有多把锁,提高了并发操作的效率
自旋锁
线程尝试不断地获取锁,当第一次获取不到时,线程不阻塞,尝试继续获取锁
有可能在几次尝试之后,有其他线程释放了锁,此时我们就可以获取走该锁
若是在一定次数之后还未获取到锁,则阻塞线程
所谓自旋锁其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到了锁就继续,要是抢不到就阻塞线程,说白了还是为了尽量不阻塞线程.
共享锁/独占锁
共享锁:
锁可以被多个线程共享(读写锁中的读锁就是共享锁)
独占锁:
一把锁只能被一个线程使用(读写锁中的写锁,synchronized,ReentrentLock)
公平锁/非公平锁
公平锁
按照请求锁的顺序分配,拥有稳定获得锁的机会
synchronized是一种非公平锁
非公平锁
不按照请求锁的顺序分配,不一定拥有获得锁的机会
ReentrantLock默认是非公平锁,但是底层可以通过AQS实现线程调度,可以编程公平锁
//默认
public ReentrantLock(){
sync = new NonfairSync();
}
//传入true or false
public ReentrantLock(Boolean fair){
sync = fair?new FairSync():new NonfairSync();
}
偏向锁/轻量级锁/重量级锁
锁分为四种状态:
无锁
偏向锁
轻量级锁
重量级锁
锁的状态是通过对象监视器在对象头中的字段来表明的
四种状态会随着竞争的情况逐渐升级
这四种状态都不是Java语言中的锁,而是JVM为了提高锁的获取与释放速率而做的优化(使用synchronized时)
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。
降低获取锁的代价。
轻量锁
轻量级锁是指当锁是偏向锁的时候,此时又有一个线程访问,偏向锁就会升级
为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一
直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁
膨胀为重量级锁。在高并发情况下,出现大量线程自旋获得锁,对 cpu 销毁较大, 升级为重量级锁后,获取不到锁的线程将阻塞,等待操作系统的调度.