第一节
- 基本的锁的分类:
(1)重入锁(也叫作递归锁),为了避免我们递归时候发生的异常,我们常用的synchronized和lock都是递归锁。
(2)乐观锁:总是认为不会发生并发问题,每一次去取数据的时候不认为其他线程对数据进行修改,因此不会上锁,但是在更新的时候会判断其他线程在这之前是否对其进行修改,一般会用版本号机制或CAS操作实现。
– 2.1 version方式:一般是在数据表中加上一个数据版本号version子弹,表示数据被修改的次数,当数据被修改的时候,version值加1。当线程A要更新数据的时候,在读取数据的时候也会读取version的值,在提交更新的时候,若刚才读取到的version值为当前数据库中的version值相等的时候,才会更新,否则就一直进行重试,直到更新成功。
update table set x=x+1, version=version+1 where id= #{id} and version=#{version}
– 2.2 CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新的时候,判断当前内存中的内存值与之前取到的值是否相等,若相等,则更新新值,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
内存值:当前内存中读到的值,不管是否修改,就是当前变量的值(修改了则为修改后的)
预期值:我们预期的值,即预期是没有被任何其他线程修改,就是我们要更新前的值,理想下线程安全的值
新值:最新的数据,将要被更新的数据
(3)悲观锁:总是假设最坏的情况,每次取数据都认为其他线程会被修改,所以都会加锁,当其他线程想要访问数据的时候,都需要阻塞挂起。
(4)读写锁:如果两个线程同时读一个资源,没有任何写操作的时候,两个线程可以共同读取。但是只要有一个线程执行了写的操作的时候,就不应该有其他线程对其进行读或者写的操作。
(5)AQS锁
(6)自旋锁
(7)公平锁
(8)非公平锁
(9)排它锁和互斥锁:同一时刻只能允许一个线程进入
(10)CAS无锁
(11)布式锁
读锁:
ReentranReadWriterLock cwl = new ReentranReadWriterLock();
Lock r = cwl.readLock();
Lock w = cwl.writeLock();
- CAS无锁机制效率高于synchronized,且永远不可能发生死锁。
- 常用的原子类
(1)AtomicBoolean
(2)AtomicInteger
(3)AtomicLong
(4)AtomicReference
上面的常用的原子类都是用了无锁的概念,有的地方直接使用了CAS操作的线程安全。
第二节 Synchronized的重入锁
- 轻量级锁:lock锁
- 重量级锁:synchronzied
- 轻量级锁和重量级锁的区别:轻量级锁灵活性高,需要自己手动加锁和释放锁。
- 重入锁:又叫递归锁,有可重入性,目的就是为了避免死锁,A方法请求B方法,A,B方法都加同一把锁,如果一个线程进入了A方法,那么也可以调用A方法中B方法。具有锁的传递性,指的是同一个线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响,内层函数仍然可以获得该锁。
- Synchronized重入锁:
代码测试如下:
public class Test implements Runnable{
public synchronized void get(){
System.out.println("name: " + Thread.currentThread().getName() + " get()");
set();
}
public synchronized void set() {
System.out.println("name: " + Thread.currentThread().getName() + " set()");
}
public void run() {
get();
}
public static void main(String[] args) {
Test t = new Test();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
结果是:
name: Thread-0 get()
name: Thread-0 set()
name: Thread-3 get()
name: Thread-3 set()
name: Thread-2 get()
name: Thread-2 set()
name: Thread-1 get()
name: Thread-1 set()
- 重入锁什么时候释放?
外层函数执行完之后,方法执行完之后,锁释放。
第三节 ReentrantLock锁
- lock锁必须在try-catch-finally中,因为需要在finally中释放锁
锁是可以传递的,递归传递。外层函数可以将锁传递给内层函数。
可重入锁的目的就是为了让我们避免死锁现象
第四节 ReentrantReadWriteLock读写锁
- 如果读方法和写方法不加锁,启动两个线程分别对其进行读操作和写操作,此时,可能没有写完的时候,就开始读数据,造成脏读的现象。
- 读写锁,在写的时候,读的方法也不能进入,就是写的时候不能读,读的时候不能写。
- 读写锁的目的
都是读的操作,不会加锁,但凡有写的操作,就会加锁,只允许有一个线程进入。
代码如下:
package com.xiyou.mayi.thread5.duxiesuo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁
*/
public class Cache {
private static Map<String, Object> map = new HashMap<>();
// 定义读写锁
private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 定义读锁
private static Lock r = rwl.readLock();
// 定义写锁
private static Lock w = rwl.writeLock();
/**
* 获取一个key对应的value
* @param key
* @return
*/
public static final Object get(String key){
// 读加锁
r.lock();
try {
System.out.println("正在做读的操作, key: " + key + " 开始");
Thread.sleep(100);
Object object = map.get(key);
System.out.println("正在做读的操作,key:" + key + " 结束");
System.out.println();
return object;
}catch (Exception e){
e.printStackTrace();
}finally {
// 读锁的释放
r.unlock();
}
return key;
}
/**
* 赋值操作,写操作
* @param key
* @param value
* @return
*/
public static final Object put(String key, Object value) {
// 写加锁
w.lock();
try {
System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
Thread.sleep(100);
Object object = map.put(key, value);
System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
System.out.println();
return object;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
w.unlock();
}
return value;
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.put(i + "", i + "");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Cache.get(i + "");
}
}
}).start();
}
}
第五节 乐观锁
- 乐观锁的本质上其实是没有锁的,效率比较高,无阻塞,无等待,若乐观锁失败可以重试,直至成功获取锁。
- 在设计表的时候有一个version字段,该字段就是用来实现乐观锁的,判断version是否满足要求,因为每一次修改操作都会更新version字段的值。
update table set x=x+1, version=version+1 where id= #{id} and version=#{version}
- version字段若没有发生改变就可以更新,version字段发生了改变就不能更新,版本发生了改变,值已经被别人改了,此时应该重试进行更新。
第六节 悲观锁
- 悲观锁是什么?
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁,写锁,行锁等),当其他线程想要访问数据的时候,都需要阻塞挂起。可以依靠数据库实现,如行锁,读锁和写锁,都是在操作之前加锁,在java中,synchronized的思想也是悲观锁。
- 悲观锁是一个重量级锁,会发生阻塞。
- 乐观锁执行失败会进行重试,不会阻塞,不会等待。
- 乐观锁的效率会很高。
第七节 AtomicInteger的原子类
- 预期值?
指的是本地内存中的值。
举例说明:
两个线程同时操作一个全局变量,演示线程安全问题:
private static AtomicInteger atomic = new AtomicInteger();
// 加1 做自增操作
atomic.incrementAndGet();
- AtomicInteger天然的线程安全,用了无锁的概念,底层用了CAS无锁。完全不会阻塞,没有加锁。
第八节和第九节 CAS无锁机制
- 原子类的底层实现原理是CAS无锁技术。就是做比较。
- 什么是CAS的无锁技术?
CAS: Compare And Swap,即比较之后再进行交换
jdk5增加了并发包java,util,concurrent.*,其下面的类使用了CAS算法实现了区别于synchronized同步锁的一种乐观锁,jdk5之前用的是synchronized关键字来保证同步的,这是一种独占锁,也是悲观锁的一种。
- 原子性指的是线程安全,可见性指的是本地内存修改之后,立刻刷新到工作内存中,本地内存的值永远和工作内存一致。
- JAVA内存模型(JMM,定义了一个线程对另一个线程的可见性)
JMM将内存分为主内存(共享内存)和本地内存(各自线程中的内存),本地内存也叫作工作内存。
共享内存存放了一个我们的全局变量i,本地内存主要存放共享内存的副本,也就是i全局变量的副本,也就是每个本地内存都存了一个i = 0,假设有两个本地内存t1线程的和t2线程的。假设两个线程都做了i++操作,两个线程都执行了,此时各自本地内存中的值都是1,因为各自本地内存中的值i初始是0,这明显不对,做了两次对全局变量i的++操作,此时我们将本地内存都刷新到主内存后,主内存的值是1,那么这种情况怎么解决?此时我们可以使用volatile保证可见性(就是本地内存中的值一修改就进行刷新),保证了工作内存修改后的值,永远和主内存一致。或用synchronized关键字,保证了原子性和可见性,同一时刻只能一个线程操作,将本地内存刷新到主内存之后,才可以让另一个线程执行,那么有没有别的方法呢?有的,就是 使用CAS的无锁机制。
- CAS无锁机制
CAS的无锁机制,包含了三个参数,V, E, N
V:表示的是要去更新的变量。表示需要去更新的变量。表示主内存的值(对照着上图)
E:表示的是预期值。表示本地内存的值(对照着上图)
N:表示的是新值。表示变量的最新值。需要去修改成的值。
我们仅仅当V值等于E值的时候,才会将V值的值设置为N,如果V值和E值不同,则说明已经有线程对其作出了更新,则当前线程什么都不做,不停的重试,直至可以进行更新。
简单理解,就是主内存的值和本地内存的值一致的情况下,说明了没有人对变量做操作,这样就可以将主内存的值修改为最新的值,如果不一致,表示已经修改了,主内存的值应该重新备份到本地内存中,重新操作。
- CAS缺点:
ABA问题无法确定,即最先开始主内存的值为A,备份到工作内存中为A,将工作内存又修改成了B,最后又改成A,这时候判断的时候,主内存最先开始是A,此时还是A,可以修改,但是实际上我们已经将A修改过了一次
- 如何解决CAS的ABA问题?
JAVA并发包中提供了一个带有标记的原子引用类AtomicStampedReference,他可以通过控制变量值的版本来保证CAS的正确性。
自旋锁
- CAS已经用到了自旋锁,如果自旋锁失败了(即本地内存的值和主内存的值不一致,无法修改新值),则其会一直循环重试,直至成功。这里的不停的循环重试实际上就是已经使用了自旋锁的机制。
- 自旋锁和互斥锁的区别?
synchronized就是一个互斥锁,同一个时刻只能有一个线程获取锁,没有获取到锁的线程会进行等待
(1)互斥锁与自旋锁的区别就是,互斥锁中未获得锁的线程会进行等待,自旋锁中未获得锁的线程则不停的尝试获取,不会等待和阻塞
(2)互斥锁是属于悲观锁的一种,自旋锁属于乐观锁的一种。
(3)互斥锁的效率很低,因为有上下为的切换,CPU的抢占。
排它锁与共享锁
- 共享锁就相当于是读锁
- 排它锁相当于是写锁
公平与非公平锁
- 什么是公平锁和非公平锁?
简单的理解就是公平锁与请求顺序有关,先请求锁的先得到,而非公平锁,就是直接去争,不按照顺序进行。
补充知识点:
- lock的原理使用的是AQS。AQS使用的是一个双向链表。