1公平锁、非公平锁
概念:所谓公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。而非公平锁,则是多个线程抢夺锁,会导致优先级反转或饥饿现象
区别:公平锁在获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的队列,则直接占有锁,否则插⼊到等待队列,FIFO原则。
非公平锁比较粗鲁,上来直接先尝试占有锁,失败则采⽤公平锁方式。非公平锁的优点是吞吐量比公平锁更大。
synchronized 和 juc.ReentrantLock 默认都是非公平锁。ReentrantLock 在构造的时候传入 true则是公平锁。
private final ReentrantLock lock = new ReentrantLock(true);//公平锁
private final ReentrantLock lock = new ReentrantLock(false);//非公平锁
查看ReentrantLock源码的构造方法 默认是非公平锁
使用方法为:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }
2可重入锁、递归锁
可重入锁又叫递归锁,指的同⼀个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进⼊任何⼀个它已经拥有锁的代码块。比如method01方法里面有 method02 方法,两个方法都有同⼀把锁,得到了method01的锁,就自动得到了method02的锁。就像有了家门的锁,厕所、书房、厨房就为你敞开了⼀样。可重⼊锁可以避免死锁的问题。
public synchronized void method1(){ method2(); } public synchronized void method2() { }
synchronized 和 ReentrantLock()默认支持可重入锁
2.1使用synchronized
/**
* 可重入锁
*/
class PhonePlus {
public synchronized void sendEmail(){
System.out.println(Thread.currentThread().getName()+"=====sendEmail.....");
sendMessage();
}
public synchronized void sendMessage(){
System.out.println(Thread.currentThread().getName()+"=====sendMessage....");
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
PhonePlus phonePlus = new PhonePlus();
new Thread(()->{
phonePlus.sendEmail();
}, "A").start();
new Thread(()->{
phonePlus.sendEmail();
}, "B").start();
}
}
2.2 使用ReentrantLock();
class PhonePlus {
Lock lock = new ReentrantLock();
public void method01(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"=====method01.....");
method02();
} finally {
lock.unlock();
}
}
public void method02(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"=====method02.....");
} finally {
lock.unlock();
}
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
PhonePlus phonePlus = new PhonePlus();
new Thread(()->{
phonePlus.method01();
}, "A").start();
new Thread(()->{
phonePlus.method01();
}, "B").start();
}
}
2.3验证内层方法是否获得锁
public void method02(){//去掉同步锁 System.out.println(Thread.currentThread().getName()+"=====method02....."); }for (int i = 0; i < 100; i++) {//创建100线程实现高并发 new Thread(()->{ phonePlus.method01(); }, i+"").start(); }
从运行结果看 method02()不加同步锁,也不会出现并发问题
3自旋锁
所谓自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采⽤循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”⼀样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。CAS底层的 getAndAddInt 就是自旋锁思想
//跟CAS类似,⼀直循环比较。
while (!atomicReference.compareAndSet(null, thread)) { }
3.1CAS 比较并交换
CAS的全称为Compare-And-Swap,比较并交换,是⼀种很重要的同步思想。它是⼀条CPU并发原语。
它的功能是判断主内存某个位置的值是否为跟期望值⼀样,相同就进行修改,否则⼀直重试,直到⼀致为止。这个过程是原子的。
public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5, 2000) +"\t 当前数据值:" + atomicInteger.get()); //修改 System.out.println(atomicInteger.compareAndSet(5, 1000) +"\t 当前数据值:" + atomicInteger.get()); }
参考以前博客代码:01volatile关键字 的2.2原子性例子
查看AtomicInteger.getAndIncrement()源码
getAndAddInt()采用的是自旋锁的方式,while()判断条件内会判断共享内存中是否跟期望值相同,相同则进入循环体
3.2自旋锁实例代码:
/**
* 自旋锁
*/
class WC {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//锁门
public void lock() {
Thread currentThread = Thread.currentThread();
//判断当前wc中 是否有人使用
while(!atomicReference.compareAndSet(null, currentThread)){
System.out.println(Thread.currentThread().getName()+"等待进入中===");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
}
System.out.println(Thread.currentThread().getName()+"====在卫生间中...");
}
//开门
public void unlock() {
Thread currentThread = Thread.currentThread();
atomicReference.compareAndSet(currentThread, null);
System.out.println(currentThread.getName()+"=====unlock...");
}
}
public class SpinLockDemo {
public static void main(String[] args) {
WC wc = new WC();
new Thread(()->{
wc.lock();//锁门
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) {e.printStackTrace(); }
wc.unlock();//开门
}, "AA").start();
new Thread(()->{
wc.lock();//锁门
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) {e.printStackTrace(); }
wc.unlock();//开门
}, "BB").start();
}
}
自旋锁好处:循环比较获取直到成功为⽌,没有类似wait的阻塞。
通过CAS操作完成⾃旋锁,A线程先进来调⽤lock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过⾃旋等待,直到A释放锁后B随后抢到。
4读写锁
读锁是共享的,写锁是独占的。 juc.ReentrantLock 和 synchronized 都是独占锁,独占锁就是⼀个
锁只能被⼀个线程所持有。有的时候,需要读写分离,那么就要引⼊读写锁,即 juc.ReentrantReadWriteLock 。
独占锁:指该锁⼀次只能被⼀个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有
对ReenntrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是⾮常⾼效的,读写、写读、写写的过程是互斥的。
比如缓存,就需要读写锁来控制。缓存就是⼀个键值对,以下Demo模拟了缓存的读写操作,读的 get⽅法使⽤了 ReentrantReadWriteLock.ReadLock() ,写的 put ⽅法使⽤了ReentrantReadWriteLock.WriteLock() 。这样避免了写被打断,实现了多个线程同时读。
实例代码:
/**
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
* 但是,如果有一个线程想去写共享资料,就不应该再有其他线程可以对该资源进行读或写
* 小总结:
* 读-读 能共存
* 读-写 不能共存
* 写-写 不能共存
*/
class Cache {
private Map<String, Object> map = new ConcurrentHashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 记录哪个线程来 签字
* @param key 线程名
* @param value 签字内容
*/
public void put(String key, Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(key+"====正在签字: "+ value);
//耗时操作
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
map.put(key, value);
System.out.println("===签字完成===");
} finally {
readWriteLock.writeLock().unlock();
}
}
/**
* 获取指定线程 对应的value
* @param key 线程名
* @return 签字内容
*/
public Object get(String key){
Object result = null;
readWriteLock.readLock().lock();
try {
System.out.println(key + "====正在看大家的签字:");
//耗时操作
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {e.printStackTrace(); }
result = map.get(key);
System.out.println(key+"===看完了===");
} finally {
readWriteLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
Cache cache = new Cache();
//写
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
cache.put("线程名"+temp, "数据"+temp);
}, String.valueOf(i)).start();
}
//读
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
cache.get("线程名"+temp);
}, String.valueOf(i)).start();
}
}
}
运行结果:写入数据环节是同步,阻塞访问
读取数据环节是支持并发访问的
可以运行看下效果,因不能上传运行结果视频 ,需要自己手动运行理解一下:读-读 能共存,读-写 不能共存,写-写 不能共存的概念