乐观锁发生冲突了怎么办?
我们可以引入一个版本号来处理。假设我们需要多线程修改 “用户账户余额”,设当前余额为 100.,引入一个版本号 version, 初始值为 1.,并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额”
(具体图示见课件)
读写操作的线程安全分析:
当多个线程尝试读一个变量时,是不会有线程安全问题的; 但是当多个线程尝试修改一个变量时,或者一个线程写一个线程读时,就会有线程安全问题。
在有些场景中,写操作本来就少,主要以读操作为主。为了提高并发效率,我们就不能一味的加锁,应该根据不同的场景来给读和写分别加锁。
Synchronized锁是没有对读和写进行区分的,只要使用就一定互斥了。
Java中专门提供了一个类:
场景举例:
由上可知:在写少读多时,使用读写锁可以极大的减少冲突,冲突减少即阻塞等待的线程就减少了,这就极大的提高了程序执行的效率。
区分重量级锁和轻量级锁的主要依据就是:看加锁解锁开销大不大。
加锁解锁开销大就是代表频繁的开锁加锁。
重量级锁:加锁解锁的开销很大,往往需要内核态来完成。
轻量级锁:加锁解锁的开销不大,只需要在用户态就能完成
应用场景:
当一个线程在竞争一个锁时,没有竞争成功,但是过不了多久,这个锁就会被释放,所以这个线程没必要进入阻塞状态并且放弃这个CPU。
自旋锁的伪代码:
while (抢锁(lock) == 失败) {}
自旋锁 vs 挂起等待锁:
想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里, 女神可能已经换了好几个男票了).
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位.
自旋锁是一种轻量级锁的实现方式:
【重点】synchronized锁中的轻量级锁,大概率就是通过自旋锁的方式进行实现的。
注意:一般情况下的锁都是非公平锁,要想实现公平锁,就要加上一定的数据结构进行限制来达到约定的公平。
在Java中,CAS应用很多。
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
伪代码:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
Synchronized是一种自适应锁,经过上面对各种锁的认识,我们对Synchronized也有了更近一步的认识:
Callable是一个接口,它相当于把线程封装了一个返回值。
Runnable:只是描述一个过程,不关注结果,没有返回值。
Callable:也是描述一个过程,但是有返回值。
Callable中包含一个call方法,相当于Runnable中的run方法,都是用来描述一个具体的任务,不同的是:call方法是带有返回值的
思路:
- 对于主线程来说,其必须得等t线程执行完之后在进行打印,才符合要求得思路
- 在主线程中进行等待(wait),等到 t 执行完循环之后,再通知(notify)主线程
public class ThreadDemo28 {
static class Result{//设置静态内部类,就可以在实例化时,不需要创建外部类
public int sum=0;
public Object locker = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread(){
@Override
public void run() {
for(int i=1;i<=1000;i++){//这里存在优化,sum的值不能及时同步到内存当中
result.sum+=i;
}
synchronized (result.locker){
result.locker.notify();
}
}
};
t.start();
synchronized (result.locker){
while(result.sum==0){
//当在t线程中的求和运算结果没有出来时,内存中sum的值一直就是0,直到运算结束才会同步到内存
result.locker.wait();
}
}
System.out.println(result.sum);
}
}
public class ThreadDemo29 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个匿名内部类, 实现 Callable 接口
Callable<Integer> callable = new Callable<Integer>() {
@Override
//重写 Callable 的 call 方法, 完成累加的过程
public Integer call() throws Exception {
int sum=0;
for(int i=0;i<=1000;i++){
sum+=i;
}
return sum;
}
};
//把 callable 实例使用 FutureTask 包装一下
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
//call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
Thread t = new Thread(futureTask);
t.start();
//调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
int result = futureTask.get();
System.out.println(result);
}
}
对于Thread类来说,其构造方法为要求放入的是一个Runnable,所以不能直接把Callable作为参数传到Thread中,所以就需要将Callable转换成符合要求的参数。
因此可以通过FutureTask的构造方法,传入Callable接口的实例,构造FutureTask对象,由于FutureTask间接实现了Runnable接口,将FutureTask作为Thread的参数即可满足需要。
Callable<Integer> callable = new Callable<Integer>() {
@Override
//重写 Callable 的 call 方法, 完成累加的过程
public Integer call() throws Exception {
int sum=0;
for(int i=0;i<=1000;i++){
sum+=i;
}
return sum;
}
};
这里的 new 看似是对接口进行了实例化,其实并不是这样,接口并不能进行实例化,这只是匿名内部类的一种实现方式,所表达的意思就是一个匿名的类实现了Callable这个接口。
可重入互斥锁,其定位于与synchronized类似,都是用来达到互斥效果,保证现成安全。
ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
public class ThreadDemo30 {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();//加锁,如果获取不到锁就死等
reentrantLock.tryLock();//加锁,如果湖区不到锁,等待一段时间之后,就放弃加锁
reentrantLock.unlock();//解锁
}
}
信号量就是用来表示可用资源个数的一个计数器
理解信号量
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);//表示有四个可用资源
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("申请资源成功");
Thread.sleep(3000);
System.out.println("释放资源");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i=0;i<10;i++){
Thread t = new Thread(runnable);
t.start();
}
}
同时等待 N 个任务执行结束.
好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public static void main(String[] args) throws InterruptedException {
//表示有十个任务需要完成
CountDownLatch latch =new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
long time=(long) (Math.random() * 10000);
Thread.sleep(time);
System.out.println("比赛用时:"+time);
latch.countDown();//完成任务,任务数就减1
} catch (Exception e) {
e.printStackTrace();
}
}
};
for(int i=0;i<10;i++){
Thread t = new Thread(runnable);
t.start();
}
latch.await();
System.out.println("比赛结束");
}
以 juc 的 ReentrantLock 为例,
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.
原来的集合类, 大部分都不是线程安全的.
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.
HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用: