悲观锁

悲观锁:

介绍:每次拿数据都要上锁,别人想要拿数据就会堵塞直到拿到锁
常见:关系型数据库(MySQL)的行、表锁,synchronized(独占锁,也是一种悲观锁),lock
场景:资源竞争严重,线程冲突严重


两种方式创建锁

  1.  传统方式synchronized
  2.  Locked(有三种形式:ReentrantLock(常用的可重复锁)、ReentrantReadWriteLock.ReadLock(读锁)、ReentrantReadWriteLock.writeLock(写锁)
 

Lock锁

  1. 常用的是ReentrantLock创建锁,属于排他锁。换句话说就是一次只允许执行一个线程,有序进行
  2. 读写锁(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锁的区别?

  1. Synchronized 的内置的java关键字,lock是一个java
  2. Synchronized 无法判断获取锁的状态,lock可以判断是否获取到锁。用trylock()
  3. Synchronized 会自动释放锁,lock必须要手动释放锁!如果不释放锁,死锁
  4. Synchronized 会一直等待,lock锁就不一定会等待下去
  5. Synchronized 可重入锁、不可以中断、非公平,lock,可重入锁,可以判断锁、非公平(可以自己设置)
  6. Synchronized 适合锁少量的代码同步问题,lock锁适合大量的代码同步问题。

补充:

  • 第三点:为了保证一定能释放锁,一般会把lock.unLock()放在finally{}里面
  • 简单来说,也就是手动和全自动的区别,手动更明显适合于生产环境,因为可以自己控制。就像rabbitMQ里面的一块,返回ack是手动还是自动。
  • 非公平锁
//  默认都是非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
}


//  可以手动修改,传递一个true开启就是公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}
  • 可重入锁:也叫递归锁。可以避免死锁。某一线程已经获得某个锁,可以再次获取锁而不会出现死锁。简单来讲,就是获取了可以开房门的钥匙也就可以进入到屋子里了上。
synchronized锁:
 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读写和更新都加锁性能要好很多。但是只是保证数据的最终一致性,读取的时候可能会出现假数据。适用于读多写少的数据,例如:配置、黑名单、物流地址等变化非常少的数据。帮助实现更高的并发。