锁
一、Lock锁
Lock是一个接口,有三个实现类,可重入锁是最常用的一个实现类。
- 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
public class Lock1 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
}
static class Ticket1{
private int ticket = 30;
Lock lock = new ReentrantLock(false);
//业务逻辑
private void sale(){
//加锁
lock.lock();
try {
if(ticket>0)
System.out.println(Thread.currentThread().getName()+"购买了一张票,"+"卖出了第"+(30-(--ticket))+"张票,还剩"+ticket+"张票");
}catch (Exception e){
}finally {
//无论如何都会释放锁
lock.unlock();
}
}
}
}
Thread-0购买了一张票,卖出了第1张票,还剩29张票
Thread-0购买了一张票,卖出了第2张票,还剩28张票
Thread-1购买了一张票,卖出了第3张票,还剩27张票
Thread-2购买了一张票,卖出了第4张票,还剩26张票
Thread-0购买了一张票,卖出了第5张票,还剩25张票
Thread-1购买了一张票,卖出了第6张票,还剩24张票
Thread-2购买了一张票,卖出了第7张票,还剩23张票
Thread-0购买了一张票,卖出了第8张票,还剩22张票
Thread-1购买了一张票,卖出了第9张票,还剩21张票
Thread-2购买了一张票,卖出了第10张票,还剩20张票
Thread-0购买了一张票,卖出了第11张票,还剩19张票
Thread-1购买了一张票,卖出了第12张票,还剩18张票
Thread-2购买了一张票,卖出了第13张票,还剩17张票
Thread-0购买了一张票,卖出了第14张票,还剩16张票
Thread-1购买了一张票,卖出了第15张票,还剩15张票
Thread-2购买了一张票,卖出了第16张票,还剩14张票
Thread-0购买了一张票,卖出了第17张票,还剩13张票
Thread-1购买了一张票,卖出了第18张票,还剩12张票
Thread-2购买了一张票,卖出了第19张票,还剩11张票
Thread-0购买了一张票,卖出了第20张票,还剩10张票
Thread-1购买了一张票,卖出了第21张票,还剩9张票
Thread-2购买了一张票,卖出了第22张票,还剩8张票
Thread-0购买了一张票,卖出了第23张票,还剩7张票
Thread-1购买了一张票,卖出了第24张票,还剩6张票
Thread-2购买了一张票,卖出了第25张票,还剩5张票
Thread-0购买了一张票,卖出了第26张票,还剩4张票
Thread-1购买了一张票,卖出了第27张票,还剩3张票
Thread-2购买了一张票,卖出了第28张票,还剩2张票
Thread-0购买了一张票,卖出了第29张票,还剩1张票
Thread-1购买了一张票,卖出了第30张票,还剩0张票
- 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
public class Lock1 {
public static void main(String[] args) {
Ticket1 ticket = new Ticket1();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
new Thread(()->{
for(int i=0;i<40;i++)
ticket.sale();
}).start();
}
static class Ticket1{
private int ticket = 30;
Lock lock = new ReentrantLock();
//业务逻辑
private void sale(){
//加锁
lock.lock();
try {
if(ticket>0)
System.out.println(Thread.currentThread().getName()+"购买了一张票,"+"卖出了第"+(30-(--ticket))+"张票,还剩"+ticket+"张票");
}catch (Exception e){
}finally {
//无论如何都会释放锁
lock.unlock();
}
}
}
}
Thread-0购买了一张票,卖出了第1张票,还剩29张票
Thread-0购买了一张票,卖出了第2张票,还剩28张票
Thread-0购买了一张票,卖出了第3张票,还剩27张票
Thread-0购买了一张票,卖出了第4张票,还剩26张票
Thread-0购买了一张票,卖出了第5张票,还剩25张票
Thread-0购买了一张票,卖出了第6张票,还剩24张票
Thread-0购买了一张票,卖出了第7张票,还剩23张票
Thread-0购买了一张票,卖出了第8张票,还剩22张票
Thread-0购买了一张票,卖出了第9张票,还剩21张票
Thread-0购买了一张票,卖出了第10张票,还剩20张票
Thread-0购买了一张票,卖出了第11张票,还剩19张票
Thread-0购买了一张票,卖出了第12张票,还剩18张票
Thread-0购买了一张票,卖出了第13张票,还剩17张票
Thread-0购买了一张票,卖出了第14张票,还剩16张票
Thread-0购买了一张票,卖出了第15张票,还剩15张票
Thread-0购买了一张票,卖出了第16张票,还剩14张票
Thread-0购买了一张票,卖出了第17张票,还剩13张票
Thread-0购买了一张票,卖出了第18张票,还剩12张票
Thread-0购买了一张票,卖出了第19张票,还剩11张票
Thread-0购买了一张票,卖出了第20张票,还剩10张票
Thread-0购买了一张票,卖出了第21张票,还剩9张票
Thread-0购买了一张票,卖出了第22张票,还剩8张票
Thread-0购买了一张票,卖出了第23张票,还剩7张票
Thread-0购买了一张票,卖出了第24张票,还剩6张票
Thread-0购买了一张票,卖出了第25张票,还剩5张票
Thread-0购买了一张票,卖出了第26张票,还剩4张票
Thread-0购买了一张票,卖出了第27张票,还剩3张票
Thread-0购买了一张票,卖出了第28张票,还剩2张票
Thread-0购买了一张票,卖出了第29张票,还剩1张票
Thread-0购买了一张票,卖出了第30张票,还剩0张票
1.1 synchronized和Lock的区别
- synchronized 是内置的Java关键字,Lock是一个Java类
- synchronized无法判断锁的状态,Lock可以判断是否获取锁
- synchronized会自动释放锁,lock必须手动释放锁,如果不释放锁,就会造成死锁
- 有两个线程使用synchronized,线程1获得锁后阻塞,线程2会一直等待;Lock锁不一定会一直等待下去
- synchronized 可重入锁,不可以中断,非公平; Lock,可重入锁,可中断,非公平(可以直接设置)
- synchronized 适合锁少量的代码,Lock适合锁大量的代码
1.2 使用synchronized,如何判断锁的是谁
- new this ,生成一个对象,锁的是具体的对象
- static,锁的是Class,唯一的一个模板,无论生成多少个对象都是一个锁
synchronized修饰的同步方法,锁的对象是方法的调用者,即new出来的phone对象
两个方法是同一个锁,谁先拿到谁先执行,另外的线程阻塞
1、标准情况下,两个线程先打印哪个,结果先打印,发短信
2、发短信延迟2s,两个线程先打印哪一个,结果是先打印,发短信
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
}).start();
}
}
class Phone{
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
有两个对象之后,就是两个锁,发短信线程先执行,但是有延迟,然后打电话跟它不是同个锁,执行到它就打印
3、增加一个普通方法后,先执行发短信还是hello,结果是hello
4、两个对象,两个同步方法,先发短信还是打电话,结果是打电话
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.hello();
}).start();
}
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
}).start();
}
}
class Phone2{
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("hello");
}
}
静态同步方法锁的Class模板,无论new多少个对象,它的模板都只有一个,也就是说都是同个锁
5、增加一个静态的同步方法,结果先发短信
6、在5的基础上再new 一个对象phone2,调用phone2的打电话,结果先发短信
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Phone3 phone = new Phone3();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
}).start();
Phone3 phone2 = new Phone3();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
}).start();
}
}
class Phone3{
//静态方法,类一加载就有了,只有唯一个Class模板
//同步静态方法锁的Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
两个对象的话,静态方法和普通同步方法的锁不是同一个,所以不会影响。
7、一个静态同步方法,一个普通同步方法,一个对象,结果是先打电话
8、一个静态同步方法,一个普通同步方法,两个对象,结果是先打电话
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Phone4 phone = new Phone4();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
}).start();
Phone4 phone2 = new Phone4();
new Thread(()->{
phone.sendSms();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
}).start();
}
}
class Phone4{
//静态方法,类一加载就有了,只有唯一个Class模板
//同步静态方法锁的Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
1.3 可重入锁
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁,下面是一个用synchronized实现的例子:
public class Test {
public synchronized void get(){
System.out.println(Thread.currentThread().getName()+"get");
set();
}
public synchronized void set(){
System.out.println(Thread.currentThread().getName()+"set");
}
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(()->{
test.get();
}).start();
}
}
}
输出结果,整个过程没有发生死锁
Thread-0get
Thread-0set
Thread-7get
Thread-7set
Thread-6get
Thread-6set
Thread-5get
Thread-5set
Thread-4get
Thread-4set
Thread-3get
Thread-3set
Thread-1get
Thread-1set
Thread-2get
Thread-2set
Thread-9get
Thread-9set
Thread-8get
Thread-8set
set()和get()同时输出了线程名称,表明即使递归使用synchronized也没有发生死锁,证明其是可重入的。
1.4 不可重入锁
不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。下次自己尝试设计一个不可重入锁。
public class UnreentrantLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
使用该锁
public class UnreentrantLockDemo {
private UnreentrantLock lock = new UnreentrantLock();
public void get() throws InterruptedException {
lock.lock();
System.out.println(Thread.currentThread().getName()+" get");
set();
lock.unlock();
}
public void set() throws InterruptedException {
lock.lock();
System.out.println(Thread.currentThread().getName()+"set");
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
UnreentrantLockDemo demo = new UnreentrantLockDemo();
demo.get();
}
}
当前线程执行get()方法首先获取lock,然后执行set()方法,进入set()方法后无法在获取锁而发生阻塞,必须先获取锁。这个例子证明该锁是不可重入的。
二、ReadWriteLock读写锁
共享锁(读锁)
独占锁(写锁)
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <=20 ; i++) {
final int temp = i;
if(i%2!=0){
new Thread(()->{
myCache.put(temp+"",temp+"");
}).start();
}else {
new Thread(()->{
myCache.get(temp-1+"");
}).start();
}
}
}
}
/**
* 自定义缓存
*/
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//写入,一次只能有一个线程
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入Ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
System.out.println(Thread.currentThread().getName()+"读取到数据"+map.get(key));
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
结果:
Thread-0写入1
Thread-0写入Ok
Thread-2写入3
Thread-2写入Ok
Thread-3读取3
Thread-3读取到数据3
Thread-5读取5
Thread-1读取1
Thread-5读取到数据null
Thread-1读取到数据1
Thread-10写入11
Thread-10写入Ok
Thread-4写入5
Thread-4写入Ok
Thread-12写入13
Thread-12写入Ok
Thread-6写入7
Thread-6写入Ok
Thread-7读取7
Thread-7读取到数据7
Thread-16写入17
Thread-16写入Ok
Thread-8写入9
Thread-8写入Ok
Thread-19读取19
Thread-13读取13
Thread-11读取11
Thread-9读取9
Thread-11读取到数据11
Thread-13读取到数据13
Thread-19读取到数据null
Thread-9读取到数据9
Thread-14写入15
Thread-14写入Ok
Thread-15读取15
Thread-17读取17
Thread-15读取到数据15
Thread-17读取到数据17
Thread-18写入19
Thread-18写入Ok
PS:读的时候其实不加锁也能实现多个线程同时读,但是可能在读的时候会有线程写入,造成幻读,加了读锁之后则一次读的操作没有完成前,不能写入