1.线程同步
当多线程执行,对于一个共享变量进行操作时,会产生一些安全问题,称为同步问题。
线程同步就是在多线程访问共享变量时,通过一定的机制,使得线程可以按照一定的顺序,每次只能有一个线程访问共享变量。
1.1解决线程同步
给程序枷锁 两种:
①.悲观锁:必须要上锁,有具体的上锁操作,悲观锁有两种,使用synchronized关键字,使用Lock对象
②.乐观锁:不是真正意义上的锁,没有具体的锁操作,但有一个数据状态。每次操作数据前,会获得数据状态。在操作数据时,如果发现状态发生了变化,基于新状态重新操作 JDK针对于数字计算,提供了原子类,底层使用的是CAS + 自旋机制
( synchronized )锁的使用:该关键字可以给方法上锁,也可以给代码段上锁
-
当线程调用同步方法或同步代码段时,就会获得一个对象锁
每一个对象创建时,都会有一个对象锁,本质是一个监视器Mointer
-
当synchronized关键字修饰的是方法时,调用该同步方法获得的是该方法所属对象的对象锁
而不是调用这个方法的线程对象的对象锁
当线程争抢对象锁时,如果对象所以被占用,线程将处于等待状态
当另一个线程执行完毕,释放了占有的锁, 这些等待的线程才能继续执行。
-
线程进入等待状态,本质就是加入了一个同步等待队列(是系统级别的)
方法
同步代码段:同步代码段所需要的锁,是通过传参指定的。只要保证传递对象是唯一的即可。
Lock锁的使用:Lock本身是一个接口,我们实际应用时使用其对应的子类ReentrantLock
Lock的使用过程
-
创建锁对象。 如果多个线程需要争抢一把锁,就创建一个锁对象。 需要多个锁,就创建多个锁对象
-
调用lock对象的
lock()
尝试争抢锁。 如果抢不到锁,线程进入等待状态。有一个从在方法
tryLock(10,TimeUnit.SECONDS)
尝试在指定的时间内获得锁。返回boolean -
调用lock对象的
unlock()
,释放锁。 其他等待锁的线程才能继续争抢。
在使用lock锁,建议将获得锁的代码写在try中,将释放锁的代码写在finally中
确保无论操作是否成功还是是否,都能释放锁。
Lock锁的底层机制
底层使用的是CAS + AQS
-
在lock底层有一个计数器,记录锁被获取的状态,起初为0 , 当被抢占的时候变为1
也就是说,当我们调用lock.lock()方法,就是将状态从0改为1的过程
当我们调用lock,unlock()方法时,就是将状态从1改为0的过程
当我们调用lock.lock方法时,如果发现状态是1,表示锁被占用,当前线程进入等待状态
-
当多个线程同时访问lock.lock()方法时,就想将状态从0改为1
每个线程都尝试着将状态从0改为1. 假设a线程最先完成状态改变,a线程获得了锁
此时b线程也尝试将0改为1,先获得原始值0,将0改为1,再将改好的1替换原始值0(赋值)
在替换前,会拿着之前的原始值与现在变量里的值进行比较,看看是否发生了变化
如果没有变化,说明这个过程中没有其他线程访问资源,将其改为1获得锁。
如结发生了变化,说明这个过程中被其他线程捷足先登了,其他的线程获得锁,当前线程等待
我们称这个过程为 CAS (compare and set)
-
当一个线程获得锁时,发现锁已经被占用了,当前这个线程就会处于等待状态
实际上,并不是线程对象有一个状态码,改为等待状态的值。
实际上是将需要等待的线程,存入了一个集合,并使其进入最终等待状态(jvm级别的等待状态)
当最开始线程执行完毕释放锁后,就会从这个集合中取出最开始的那个线程继续执行
这个集合我们就称为 AQS (抽象的)队列同步器
乐观锁的使用:乐观锁不是一种真正存在的锁,而是一种机制。 底层使用的是 CAS + 自旋 应用组合
-
乐观锁的使用更具有局限性, 适用于数字变量的计算,一般多是 ++ 和 --
-
JDK中提供了一个原子类 AtomicIntger,该类对象中提供了++和--的计算方法
当通过该类对象提供的++和--的方法计算时,可以确保线程同步。
AtomicInteger的常用方法
CAS特点是,在set设置新值之前,先用原始值与当前变量中的值做比较,相等说明这个过程中没有其他线程操作变量,也就相当于当前线程占有变量。不相等说明这个过程中有其他线程操作变量,也就是这个变量其他线程占有,我需要重新获得。
ABA问题:在比较时原始值与当前变量中的值相等,不能说明这个过程中变量没有被其他线程操作,因为有可能另一个线程将变量中的值,从A改成了B又改回成了A。
解决ABA:可以为数据增加一个版本号,只要改变过,版本号就+1
JDK提供了一个可以解决ABA问题的原子类