一、线程锁
1. Synchronized
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
2.Lock
Lock锁与synchronized一样,都是可以用来控制同步访问的。 相比于synchronized,Lock需要手动的获取锁与释放锁。Lock是一个接口,常用实现类和方法有:
ReentrantLock
ReentrantReadWriteLock
- ReadLock
- WriteLock
方法:
- lock()
- unlock()
3.读锁和写锁
读锁是一种共享锁。可以被多个线程同时,同时访问数据,可以提高访问数据的并发性能。写锁是一种排他锁,只能被一个线程获得。
4.Lock 底层原理
原子操作工具
AtomicInteger
- AtomicLong
- AtomicReference
- ...
CAS
- Compare And Swap
- CAS算法,用非阻塞的方式,来获得锁
自旋锁
- 占用cpu资源
- 用循环,用CAS来获得锁
二、悲观锁和乐观锁
1.悲观锁
像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
2.乐观锁
就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
三、两种常见的锁
1. Synchronized 互斥锁(悲观锁,有罪假设)
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
public class TicketThread extends Thread{
//总票数,多个线程共享这个变量,能修改 ticket–
private int ticket = 10;
//执行业务,重写父类run方法
@Override
public void run() {
//业务处理,卖票:票–
while(true) { //线程非常多,我想尽量给我资源
synchronized (this) { //对象锁
//判断一个条件,出去条件
if(ticket<=0) { //多线程可能ticket=-1
break; //退出死循环
}
//不出现,线程run方法执行太快,不会发生线程冲突
try { //不能抛出异常,抛出就不是重写run方法
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“窗口:” + Thread.currentThread().getName()
+”, 剩余票数:” + ticket-- );
}
}
}
//3个窗口都买这一个票
public static void main(String[] args) {
//目标
Thread target = new TicketThread();
for(int i=0; i<3; i++) {
new Thread(target).start(); //3个线程共同作用一个target
}
}
}
2.ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)
ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入) Read(读)Write(写)Lock(锁),我们下面称它为读写锁。
读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
public class TicketLock implements Runnable {
private int ticket = 10;
//jdk1.6前,性能差异很大,1.6后synchronized底层实现类似Lock,性能伯仲之间
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while (true) {
try {
lock.writeLock().lock();
if (ticket <= 0) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“线程:” + Thread.currentThread().getName() + “,还剩票数:” + ticket--);
} catch (Exception e) {
// TODO: handle exception
} finally {
lock.writeLock().unlock(); //防止死锁,会自动释放,而synchronized不会释放
}
}
}
public static void main(String[] args) {
Runnable target = new TicketLock ();
int windows = 3; // 窗口数量
for (int I = 1; I < windows + 1; i++) {
new Thread(target, “窗口” + i).start();
}
}
}
3.两种方式的区别
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。