目录
一、悲观锁
自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。使用synchronized关键字和Lock的实现类都是悲观锁。
适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
二、乐观锁
认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重新枪锁等。
判断规则:1、版本号机制Version 2、最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。乐观锁则直接去操作同步资源,是一种无锁算法。
三、synchronized关键字
1、作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁。
2、作用于代码块,对括号里配置的对象加锁。
3、作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁。
四、公平锁与非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 ReentrantLock lock = new ReentrantLock(true);//这里的true表示公平锁
非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
ReentrantLock lock = new ReentrantLock();默认就是非公平锁。
为什么有公平锁与非公平锁的设计,为什么默认使用非公平锁?
1、恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
2、使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平?什么时候用非公平?
为了更高的吞吐量,显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了,否则就使用公平锁,大家公平使用。
public class SaleTicket {
public static void main(String[] args) {
//创建对象
LTicket lTicket = new LTicket();
//创建线程
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"aa").start();
//创建线程
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"bb").start();
//创建线程
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"cc").start();
}
}
/**
* 创建资源类
*/
class LTicket{
//票数
private int number = 30;
ReentrantLock lock = new ReentrantLock(true);
//卖票
public void sale(){
try {
//上锁
lock.lock();
//判断是否有票
if(number > 0 ){
System.out.println(Thread.currentThread().getName()+"卖出:"+(number--) +"剩下:"+number);
}
} finally { //不管是否有异常都会执行解锁
//解锁
lock.unlock();
}
}
}
五、可重入锁(递归锁)
指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(锁对象是同一个对象),不会因为之前已经获取过还没有释放而阻塞。
在Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度上避免死锁。
5.1、synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1,计算器为零代表锁已被释放。