Java锁的认识

1.为什么要使用锁?

1.1首先说明两个定义:

  1. 进程:程序启动,进入内存,资源分配的基本单位
  2. 线程:程序执行的基本单元,程序执行的基本单位

1.2程序如何开始运行?

  1. Cpu读指令 pc(程序计数器,存储指令地址)
  2. 读数据 Register
  3. 计算,回写
  4. 下一条

1.3多线程带来的问题:

多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。一个进程可以包含多个线程,线程之间通过操作系统(linux线程调度器)来进行调度。

线程切换: Context Switch CPU保存线程,执行新线程,回复线程,继续执行原线程。

多个线程对同一资源同时访问时,如果不加锁,就有可能出现线程安全问题,举例:比如有两张票,但是有5个人进来买,买了一张票数就减1,在他们进门的时候会判断是否还有票,但是在他们进门的那一刻,票还一张都没有买走。但是他们都已经进门了,过了是否有票的校验了,所以最后票数为被减成负3,显然是不对的,因为票不能小于0,所以需要加一个锁,在同一时刻只能有一个人进门去买票,也就是同一个资源同一个时刻只能有一个线程进行操作,这样在第三个人进门的时候就能判断出票数已经卖完了,不会产生票数成负数的情况。

 2.synchronized

Java中的关键字,是一种同步锁,锁升级的过程:

 

 

偏向锁-->轻量级锁-->重量级锁

  • 偏向锁,严格意义来说不是一把锁,适用于一个线程,只要有第二个线程来竞争,升级为轻量级锁

  • 轻量级锁:不需要经过操作系统调度,例如自旋锁

    • cas (compare and swap)

      • 出现ABA问题,通过版本号解决

      • cas修改值时候的原子性问题, lock cmpxchg 指令,锁定总线

  • 重量级锁,需要OS调度

    jol-core jar包

3.ReentrantLock

java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,

也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别:

对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:

Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

ReenTrantLock独有的能力:

  1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ReenTrantLock提供了更多的加锁方式
    • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
    • tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁,当前线程处于休眠状态,直到获得锁,或者当前线程被别的线程中断一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

4.锁的类型

1.可重入锁 

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
    class MyClass {
        public synchronized void method1() {
            method2();
        }

        public synchronized void method2() {
      
        }
    }

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。lockInterruptibly()方法。

3.公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。 
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性: 
ReentrantLock lock = new ReentrantLock(true); 
如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。 
另外在ReentrantLock类中定义了很多方法,比如:
  1. isFair()//判断锁是否是公平锁
  2. isLocked()//判断锁是否被任何线程获取了
  3. isHeldByCurrentThread()//判断锁是否被当前线程获取了
  4. hasQueuedThreads()//判断是否有线程在等待该锁,读操作不会发生冲突。 
ReadWriteLock是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。 

 4.乐观锁

 十分乐观认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

 version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

例:线程1:  获取version = 1时;

update user set name='kerwin',version = version+1 where id = 111 and version =1;

线程2: 同时获取version = 1时,抢先执行完成,version = 2, 此时线程1执行失败。

update user set name='wj',version = version+1 where id = 111 and version =1;

 CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

5.悲观锁

 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

posted @ 2020-08-17 00:27  杰哥!  阅读(252)  评论(0编辑  收藏  举报
/*scroll to top 火箭按钮*/