悲观锁
悲观锁:
介绍:每次拿数据都要上锁,别人想要拿数据就会堵塞直到拿到锁
常见:关系型数据库(MySQL)的行、表锁,synchronized(独占锁,也是一种悲观锁),lock
场景:资源竞争严重,线程冲突严重
两种方式创建锁
- 传统方式synchronized
- Locked(有三种形式:ReentrantLock(常用的可重复锁)、ReentrantReadWriteLock.ReadLock(读锁)、ReentrantReadWriteLock.writeLock(写锁)
Lock锁
- 常用的是ReentrantLock创建锁,属于排他锁。换句话说就是一次只允许执行一个线程,有序进行
- 读写锁(readWriteLock)。特点是
- 读取的时候允许多个线程访问共享资源,同时读取,属于共享锁(允许多个线程同时占有)。
- 写入的时候只允许一个线程写入。相当于写入是独占锁(一次只能被一个线程占有)。
- 读-读:可以共存
- 读-写:不可以共存,先写后读
- 写-写:不可以共存,一次只执行一个线程
代码实现:
//创建一个读写多,更加细粒化,性能更高。读取的时候需要多个线程访问共享资源 private ReadWriteLock writeLock = new ReentrantReadWriteLock(); private HashMap<String,String> map = new HashMap<>(); //读取 public void get(String key){ writeLock.readLock().lock(); System.out.println("正在读取"+Thread.currentThread().getName()+"线程"); map.get(key); System.out.println("读取完毕"+Thread.currentThread().getName()+"线程"); writeLock.readLock().unlock(); } //写入 public void put(String key, String value){ writeLock.writeLock().lock(); System.out.println("正在写入"+Thread.currentThread().getName()+"线程"); map.put(key,value); System.out.println("写入完毕"+Thread.currentThread().getName()+"线程"); writeLock.writeLock().unlock(); }
Synchronized和locked锁的区别?
- Synchronized 的内置的java关键字,lock是一个java类
- Synchronized 无法判断获取锁的状态,lock可以判断是否获取到锁。用trylock()
- Synchronized 会自动释放锁,lock必须要手动释放锁!如果不释放锁,死锁
- Synchronized 会一直等待,lock锁就不一定会等待下去
- Synchronized 可重入锁、不可以中断、非公平,lock,可重入锁,可以判断锁、非公平(可以自己设置)
- Synchronized 适合锁少量的代码同步问题,lock锁适合大量的代码同步问题。
补充:
- 第三点:为了保证一定能释放锁,一般会把lock.unLock()放在finally{}里面
- 简单来说,也就是手动和全自动的区别,手动更明显适合于生产环境,因为可以自己控制。就像rabbitMQ里面的一块,返回ack是手动还是自动。
- 非公平锁:
// 默认都是非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 可以手动修改,传递一个true开启就是公平锁 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
- 可重入锁:也叫递归锁。可以避免死锁。某一线程已经获得某个锁,可以再次获取锁而不会出现死锁。简单来讲,就是获取了可以开房门的钥匙也就可以进入到屋子里了上。
public synchronized void repeatLock(){ System.out.println("房门"); repeatLockAgain(); } public synchronized void repeatLockAgain(){ System.out.println("获取了房门的钥匙也就可以进入屋子"); }lock锁也同理:
Lock lock = new ReentrantLock(); public void repeatLock(){ lock.lock(); try { System.out.println("房门"); repeatLockAgain(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public void repeatLockAgain(){ lock.lock(); try { System.out.println("获取了房门的钥匙也就可以进入屋子"); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } }main主函数调用repeatLock即可,体现了可重复锁。
LockData lockData = new LockData(); synchronizedData synchronizedData = new synchronizedData(); // lock锁 new Thread(() -> { lockData.repeatLock(); }).start(); TimeUnit.SECONDS.sleep(2); // synchronized锁 new Thread(() -> { synchronizedData.repeatLock(); }).start();
有关于synchronized(同步方法) 和static synchronized(静态同步方法) 和 普通方法的区别
synchronized:是方法锁
- 只new一个对象的时候,并且都是同步方法,就会有先后顺序等待。无论TimeUnit.SECONDS.sleep(1);睡眠加在哪里。都是写在第一个线程先执行,也就是说,第一个线程调用的方法无论等待多长时间都等,等第一个线程执行完毕第二个线程才接着执行。因为第一个线程先捕获到,方法就锁住了,所以分先后。
- 两个对象的话并且都是同步方法就分开执行。也就是说,正常也是先执行第一个再执行第二个。但是只要第一个线程的方法加上睡眠延时,第二个线程就不会傻傻的等待,而是继续执行。因为是两个对象,加的是方法锁,互不干扰。
static synchronized:就是类锁,所以无论创建多少个对象。都执行的是同一个模板
- 简单来说,就是锁住的是类,跟创建多少个对象没有关系根本。就是根据哪个线程最先捕获静态同步方法,谁就最先执行。另一个等待,第一个执行完毕再执行。一下两种方法的都跟两个对象的同步方法是一个效果,都不会傻傻的等待。
synchronized + 普通方法
- new创建一个对象,一个同步方法一个普通方法,跟两个对象都是同步方法,一个效果。因为只是普通方法,并没有上锁,正常执行,都不会傻傻的等。
static synchronized + 普通方法
- 静态同步方法加上普通方法。跟两个对象的同步方法是一个效果。主要原因就是普通方法并没有上锁,可以正常执行。不需要等待第一个线程执行完毕才执行。
static synchronized + synchronized
- 跟两个对象的同步方法是一个效果。主要原因就是同步方法并不是类锁,可以正常执行。不需要等待第一个线程执行完毕才执行。
集合类加锁
换句话说,如果遇到集合的时候用CopyOnWriteArrayList<>()、new CopyOnWriteArraySet<>()、concurrentHashMap<>()这三种方式创建相应的集合
List:
- Vector<>():这个只不过比ArrayList多了一个synchronized关键字。出现很早
- Collections.synchronizedList():用顶层的Collections类实现,效率慢
- CopyOnWriteArrayList<>():用JUC的工具包实现,最优解。
Set:
- Collections.synchronizedSet();用顶层的Collections类实现,效率慢
- new CopyOnWriteArraySet<>();用JUC的工具包实现,最优解
补充:hashset底层是hashmap。实现原理:数组+链表/红黑树。Set.add()方法的底层实现就是map.put()方法,也就是由于key具有唯一性,所以set具有唯一性。
Map:
- Hashtable<>():老方法、感觉处境跟Vector差不多。
- Collections.synchronizedMap():用顶层的Collections类实现,效率慢
- concurrentHashMap<>():JUC的工具类的,效率更高最优解
这几个总结一下吧,线程安全对于list、set、map都一定会有Collections里面对应用synchronized修饰的方法以及JUC包下的方法。对于list、map会多一个老方法,实质也是多加了一个synchronized修饰。Vector()、HashTable().
补充:
CopyOnWriteArrayList<>()采用了一种写入时复制的思想。简称COW、是计算机程序设计领域中的一种通用优化策略。就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。换句话说,只有更新的加锁(复制一份数据来执行更新操作、结束后再覆盖原数据)、读取的时候不加锁,这就在读取数据的时候比Vector读写和更新都加锁性能要好很多。但是只是保证数据的最终一致性,读取的时候可能会出现假数据。适用于读多写少的数据,例如:配置、黑名单、物流地址等变化非常少的数据。帮助实现更高的并发。