java主流锁:
乐观锁和悲观锁:
悲观锁:对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
乐观锁:认为在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
在Java中乐观锁并没有确定的方法,或者关键字,它只是一个处理的流程、策略或者说是一种业务方案。
乐观锁在java中通过无锁来实现,主要通过cas算法,java原子类中的递增操作就是通过cas自旋实现。例如AtomicInteger的incrementAndGet()方法。
悲观锁适合写多读少的场景,先加锁可以确保写操作时数据正确。
乐观锁适合读多写少的场景,即冲突很少发生的时候,可以省去锁的开销,加大系统的吞吐量;但如果经常发生冲突,不断进行重试,这样反倒降低了性能。
乐观锁和悲观锁的讲解也可以看下这篇博客,说的比较详细。 https://zhuanlan.zhihu.com/p/337497522
CAS(Compare And Swap,比较与交换)算法:
一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
CAS设计的三个操作数:1.需要读写的内存值V 2.进行比较的原值A 3.要写入的新值B。
如果内存值V与需要比较的原值A相等(没有其他线程修改该内存值),那么将该值设置为新值B。否则不做更新。
CAS存在的三大问题:
1.ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。 ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。或者使用juc的原子包中的AtomicStampedReference类。
2.循环时间长开销大
看源码可知,Atomic类设置值的时候会进入一个无限循环,只要不成功,就会不停的循环再次尝试。在高并发时,如果大量线程频繁修改同一个值,可能会导致大量线程执行compareAndSet()方法时需要循环N次才能设置成功,即大量线程执行一个重复的空循环(自旋),造成大量开销。
3.只能保证一个共享变量的原子操作
只能保证一个共享变量的原子操作。一般的Atomic类,只能保证一个变量的原子性,但如果是多个变量呢?可以用AtomicReference,这个是封装自定义对象的,多个变量可以放一个自定义对象里,然后他会检查这个对象的引用是不是同一个。如果多个线程同时对一个对象变量的引用进行赋值,用AtomicReference的CAS操作可以解决并发冲突问题。
自旋锁和适应性自旋锁:
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁VS轻量级锁VS重量级锁:
这几种锁只是一种状态,并不是说有对应的关键字或者类。
目前锁一共有4中状态,级别从低到高分别是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
synchronized锁升级:
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。如果抢到锁了,然后线程将当前锁的持有者信息修改为自己。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。
如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
公平锁和非公平锁:
公平锁:在有锁竞争的情况下,先排队的线程先拿到锁,后排队的锁后拿到锁。
非公平锁:在有锁竞争的情况下,先排队的线程不一定先拿到锁,谁先抢到锁谁就先执行,可能后入队的队列先执行也有可能一个线程刚进来,还没有进入到排队队列中,这时锁刚好释放,它就抢到了,这样相当于它没有排队,不需要CPU进行线程唤醒,增加了系统的吞吐效率。
synchronized是非公平锁。
ReetrantLock默认使用的是非公平锁,也可以指定为公平锁。
可重入锁:
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
独占锁和共享锁:
独享锁:该锁每一次只能被一个线程所持有。独占锁也是悲观锁,synchronized和ReentrantLock。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
synchronized
作用于静态方法和非静态方法的区别:
非静态方法或synchronized(this):给该对象加锁(可以理解为给这个对象的内存上锁,注意 只是这块内存,其他同类对象都会有各自的内存锁),这时候在其他线程中执行该对象的这个同步方法(注意:是该对象)就会产生互斥。
静态方法或synchronized(类.class):相当于在类上加锁(*.class,静态方法位于静态区域,这个类产生的对象公用这个静态方法,所以这块内存,N个对象来竞争),这时候,只要是这个类产生的对象,在调用这个静态方法时都会产生互斥。即该类所有的对象都共享一把锁。
死锁:
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。这种就是最简答的死锁形式。
死锁解决办法:
- 定义好锁的顺序。不要出现上诉中线程A先获取锁L再获取锁M,而线程B先获取锁M再获取锁L。
2.未完待续。
Java实现线程安全的方式:
使用synchronized关键字。
使用java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger。
使用 java.util.concurrent.locks 包中的锁。
使用线程安全的集合 ConcurrentHashMap。
使用 volatile 关键字,保证变量可见性。