文章目录
1.公平锁和非公平锁
公平锁就是公平的锁,非公平锁就是非公平的锁。。。
公平锁:一切都是按照顺序来就像排队做核酸谁也不能插队,美女也不行,此时你是直男。
非公平锁:还是排队但是可以打破规则,比如你大冷天在那排队做核酸检测马上到你了,这时候突然来了一个沉鱼落雁闭月羞花的美女,娇滴滴的对你说大哥我有急事插个队吧,你于心不忍让她插队了,这就是非公平锁。此时你有可能一直被插队你的优先级大小取决于插你队的美女的颜值,虽然你拜托了直男的帽子但是这就发生了优先级倒转,同时在美女大并发的情况下你有可能一直做不了核酸(饥饿)。
ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认就是非公平锁。非公平锁的优点在于吞吐量比公平锁大,对于synchronized而言也是一种非公平锁。
2.可重入锁(递归锁)
可重入锁指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码。
例:
public synchronized method1(){
method2();
}
public synchronized method2(){
}
比如说我已经拿到了method1的锁了进入到方法里面我就不用再拿method2的锁了,因为就像你家大门有一把锁这时候你再进你的卧室就不用钥匙了。
ReentrantLock/Synchronized就是一个典型的非公平(默认),可重入锁;下面请看示例分别用ReentrantLock和Synchronized演示可重入锁。sendSMS()和sendEmail()分别打印出一句话如果这两句话同时打印说明拿到外层函数的锁就可以直接执行内层函数了。get,set同理。
class Phone implements Runnable{
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS");
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t ############invoked sendEmail");
}
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS");
set();
} finally {
lock.unlock();
}
}
public void set(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t ############invoked sendEmail");
} finally {
lock.unlock();
}
}
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
}
public class ReenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"t2").start();
System.out.println();
System.out.println();
System.out.println();
System.out.println();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t3 = new Thread(phone,"t3");
Thread t4 = new Thread(phone, "t4");
t3.start();
t4.start();
}
}
最后结果
结果表明同一个线程在拿到外层函数的锁之后执行完sendSMS之后不用再拿到内层函数的锁可以直接执行内层函数。
3.独占锁(写)和共享锁(读)
独占锁:该锁一次只能被一个线程所持有。对ReetrantLock和Synchronized而言都是独占锁。
共享锁:该锁可以被多个线程持有。
对于ReetrantReadWriteLock其读锁是共享锁,写锁是独占锁。读锁的共享锁可保证并发读是非常高效读写,写读,写写的过程是互斥的。也就是说多个线程同时去读取某类资源是没问题的,但是一旦涉及到了写操作就不能和其他线程共享。
我们来看一下读写锁的实例:
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
public void write(String key,Object value){
rwlock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成:");
}catch (Exception e){
e.printStackTrace();
}finally {
rwlock.writeLock().unlock();
}
}
public void read(String key){
rwlock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取成功:" + result);
}catch (Exception e){
e.printStackTrace();
}finally {
rwlock.readLock().unlock();
}
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(()->{
myCache.write(tempInt+"",tempInt + "");
},String.valueOf(i)).start();
}
for(int i = 1;i<=5;i++){
final int tempInt = i;
new Thread(()->{
myCache.read(tempInt + "");
},String.valueOf(i)).start();
}
}
}
设计思想及预期结果,首先写一个缓存类可以对其进行读写,所以有两个方法read,write。这是我们分别给他们加上读锁和写锁。主函数各自开启5个线程负责读5个线程负责写。预期效果是在一个线程读的时候可以有其他线程过来读不能有其他线程来写,再加上写锁之后不能有其他线程对其进行读写。
结果如下
从中我们可以看出在进行写入的时候中间没有其他线程进来插队,只有当一个线程写入完成之后才能有下一个线程进行写入或读取。读取时可以有其他线程过来读取。
4.自旋锁
自旋锁是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少上下文切换的消耗,缺点是循环会消耗CPU。
看下面的实例:
public class SpinkLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void mylock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\tCome in");
while(!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(thread.getName() + "\tinvoked Lock");
}
public static void main(String[] args) {
SpinkLockDemo spinkLockDemo = new SpinkLockDemo();
new Thread(()->{
spinkLockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------------------------5s钟之后----------------------------");
spinkLockDemo.myUnlock();
},"AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinkLockDemo.mylock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinkLockDemo.myUnlock();
},"BB").start();
}
}
代码设计思想首先建立一个原子引用这个引用的是线程自己写一个加锁和解锁的方法,一开始这个原子引用即主物理内存一定是空的,加锁的时候需要把这个线程放进去采用CAS的思想比较并交换,一旦返回false就不断循环自旋。解锁就是把主物理内存的thread改成null,类似于释放的一个过程。创建两个线程AA,BB。AA上来先加锁然后停5s解锁,BB停1s解锁,中间主线程要暂停1s中为了确保AA一定先执行。AA先执行加锁中控制台打出AA进来了加锁然后停5s,在这个过程中BB可能进来加锁打出BB进来了但是由于期望值为thread不是null所以compareAndSet返回false整体为true所以不断循环自旋等5s后,AA解锁,BB才能加锁然后BB解锁。
来看一下结果是不是和我们分析的一样。
这就说明了自旋锁尝试获取锁的过程中如果没拿到锁不会阻塞而是不断循环以尝试获得锁。
一定先执行。AA先执行加锁中控制台打出AA进来了加锁然后停5s,在这个过程中BB可能进来加锁打出BB进来了但是由于期望值为thread不是null所以compareAndSet返回false整体为true所以不断循环自旋等5s后,AA解锁,BB才能加锁然后BB解锁。