java锁
- 一 多线程锁synchronized案例分析
- 1.1synchronized介绍
- 1.2 synchronized案例分析
- 1.2.1.标准访问,请问先打印邮件还是短信?
- 1.2.2.邮件⽅法暂停4秒钟,请问先打印邮件还是短信?
- 分析
- 1.2.3.新增⼀个普通⽅法hello(),请问先打印邮件还是hello?
- 1.2.4.两部⼿机,请问先打印邮件还是短信?
- 1.2.5.两个静态同步⽅法,同⼀部⼿机,请问先打印邮件还是短信?
- 1.2.6.两个静态同步⽅法,2部⼿机,请问先打印邮件还是短信?
- 1.2.7.1个普通同步⽅法,1个静态同步⽅法,1部⼿机,请问先打印邮件还是短信?
- 1.2.8.1个普通同步⽅法,1个静态同步⽅法,2部⼿机,请问先打印邮件还有短信?
- 1.2.8 总结
- 二⼈⼯窗⼝排队购票(回顾)
- 三 公平锁⾮公平锁 (⽕⻋站⼈⼯窗⼝排队购票)
- 四 可重⼊锁/递归锁
- 锁的配对
- 五 ⾃旋锁
- 六 读写锁/独占/共享
一 多线程锁synchronized案例分析
1.1synchronized介绍
关键字synchronized
可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized
可以保证一个线程的变化可见(可见性),即可以代替volatile
使用方法
1.普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2.静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3.同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
1.2 synchronized案例分析
使用8个案例来详细说明synchronized
的加锁用法:
1.标准访问,请问先打印邮件还是短信?
2.邮件⽅法暂停4秒钟,请问先打印邮件还是短信?
3.新增⼀个普通⽅法hello(),请问先打印邮件还是hello?
4.两部⼿机,请问先打印邮件还是短信?
5.两个静态同步⽅法,同⼀部⼿机,请问先打印邮件还是短信?
6.两个静态同步⽅法,2部⼿机,请问先打印邮件还是短信?
7.1个普通同步⽅法,1个静态同步⽅法,1部⼿机,请问先打印邮件还是短信?
8.1个普通同步⽅法,1个静态同步⽅法,2部⼿机,请问先打印邮件还有短信?
1.2.1.标准访问,请问先打印邮件还是短信?
答案:邮件
class Phone {
public synchronized void sendEmail() {
System.out.println("==========sendEmail");
}
public synchronized void sendMessage() {
System.out.println("======sendMessage");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendMessage();
}, "t2").start();
//==========sendEmail
//======sendMessage
}
}
1.2.2.邮件⽅法暂停4秒钟,请问先打印邮件还是短信?
答案:邮件
//2. 邮件方法暂停4秒钟,请问先打印邮件还是短信? 邮件
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public synchronized void sendMessage() {
System.out.println("======sendMessage");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendMessage();
}, "t2").start();
//==========sendEmail
//======sendMessage
}
}
分析
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
⼀个对象⾥⾯如果有多个synchronized⽅法,某⼀个时刻内,只要⼀个线程去调⽤其中的⼀个synchronized⽅法了,其他的线程都只能等待。换句话说,某⼀个时刻内,只能有唯⼀⼀个线程去访问这些 synchronized⽅法,锁的是当前对象this(new 的这个phone),被锁定后,其他的线程都不能进⼊到当前对象的其他的synchronized⽅法。
1.2.3.新增⼀个普通⽅法hello(),请问先打印邮件还是hello?
答案:hello
加个普通⽅法后发现和同步锁⽆关
//3. 新增一个普通方法hello(),请问先打印邮件还是hello?
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public synchronized void sendMessage() {
System.out.println("======sendMessage");
}
public void hello() {
System.out.println("sayHello");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendMessage();
}, "t2").start();
new Thread(() -> {
phone.hello();
}, "t3").start();
//sayHello
//==========sendEmail
//======sendMessage
}
}
1.2.4.两部⼿机,请问先打印邮件还是短信?
答案:邮件
分析:换成两个对象后,不是同⼀把锁了,情况⽴刻变化
//4. 两部手机,请问先打印邮件还是短信?
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public synchronized void sendMessage() {
System.out.println("======sendMessage");
}
public void hello() {
System.out.println("sayHello");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.sendMessage();
}, "t2").start();
//======sendMessage
//==========sendEmail
}
}
1.2.5.两个静态同步⽅法,同⼀部⼿机,请问先打印邮件还是短信?
答案:邮件
分析:见 1.2.6
//5. 两个静态同步方法,同一部手机,请问先打印邮件还是短信?
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public static synchronized void sendMessage() {
System.out.println("======sendMessage");
}
public void hello() {
System.out.println("sayHello");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendMessage();
}, "t2").start();
//==========sendEmail
//======sendMessage
}
}
1.2.6.两个静态同步⽅法,2部⼿机,请问先打印邮件还是短信?
答案: 邮件
分析: 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
全局锁
//6. 两个静态同步方法,2部手机,请问先打印邮件还是短信?
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public static synchronized void sendMessage() {
System.out.println("======sendMessage");
}
public void hello() {
System.out.println("sayHello");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.sendMessage();
}, "t2").start();
//==========sendEmail
//======sendMessage
}
}
1.2.7.1个普通同步⽅法,1个静态同步⽅法,1部⼿机,请问先打印邮件还是短信?
答案: 短信
分析:
//7. 1个普通同步方法,1个静态同步方法,1部手机,请问先打印邮件还是短信?
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========sendEmail");
}
public static synchronized void sendMessage() {
System.out.println("======sendMessage");
}
public void hello() {
System.out.println("sayHello");
}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendMessage();
}, "t2").start();
//======sendMessage
//==========sendEmail
}
}
1.2.8.1个普通同步⽅法,1个静态同步⽅法,2部⼿机,请问先打印邮件还有短信?
答案: 短信
分析:
1.2.8 总结
(1)当⼀个线程试图访问同步代码块时,它⾸先必须得到锁,退出或抛出异常时必须释放锁。 也就是说如果⼀个实例对象的普通同步⽅法获取锁后,该实例对象的其他普通同步⽅法必须等待获取锁的⽅法释放锁后才能获取锁,可是别的实例对象的普通同步⽅法因为跟该实例对象的普通同步⽅法⽤的是不同的锁,所以⽆需等待该实例对象已获取锁的普通同步⽅法释放锁就可以获取他们⾃⼰的锁。
(2)所有的静态同步⽅法⽤的也是同⼀把锁–类对象本身, 这两把锁(this/class)是两个不同的对象,所以静态同步⽅法与⾮静态同步⽅法之间是 不会有静态条件的。 但是⼀旦⼀个静态同步⽅法获取锁后,其他的静态同步⽅法都必须等待该⽅法释放锁后才能获取锁,⽽不管是同⼀个实例对象的静态同步⽅法之间,还是不同的实例对象的静态同步⽅法之间,只要它们同⼀个类的实例对象
二⼈⼯窗⼝排队购票(回顾)
题目:三个售票员 卖出 30张票
多个线程共抢一个资源
/**
* 题目:三个售票员 卖出 30张票
*
*/
class Ticket{//资源类
//票
private int number = 30;
public synchronized void saleTicket(){
if (number > 0) {
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t还剩下:"+number);
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{ for (int i = 1; i <= 30 ; i++) ticket.saleTicket(); }, "A").start();
new Thread(()->{ for (int i = 1; i <= 30 ; i++) ticket.saleTicket(); }, "B").start();
new Thread(()->{ for (int i = 1; i <= 30 ; i++) ticket.saleTicket(); }, "C").start();
}
}
我们用synchronized同步代码块的方式来解决。随着juc并发编程。我们有更加轻量级的锁方式来解决问题。
三 公平锁⾮公平锁 (⽕⻋站⼈⼯窗⼝排队购票)
lcok锁
Lock lock = new ReentrantLock();
概念:
公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。
⾮公平锁,则是多个线程抢夺锁,会导致优先级反转或饥饿现象。
区别:
- 公平锁在获取锁时先查看此锁维护的等待队列,为空或者当前线程是等待队列的队⾸,则直接占有锁,否则插⼊到等待队列,FIFO原则。
- ⾮公平锁⽐较粗鲁,上来直接先尝试占有锁,失败则采⽤公平锁⽅式。⾮公平锁的优点是吞吐量⽐公平锁更⼤。
synchronized
和 juc.ReentrantLock
默认都是⾮公平锁。 ReentrantLock
在构造的时候传⼊ true 则是公平锁
。
四 可重⼊锁/递归锁
可重⼊锁⼜叫递归锁,指的同⼀个线程在外层⽅法获得锁时,进⼊内层⽅法会⾃动获取锁。也就是说,线程可以进⼊任何⼀个它已经拥有锁的代码块。
⽐如method01⽅法⾥⾯有method02⽅法,两个⽅法都有同⼀把锁,得到了method01的锁。就⾃动得到了method02的锁,就像有了家⻔的锁,厕所、书房、厨房就为你敞开了⼀样。可重⼊锁可以避免死锁的问题。
/**
* 可重入锁/递归锁
*/
class PhonePlus implements Runnable{
//Synchronized Test
public synchronized void sendEmail(){
System.out.println(Thread.currentThread().getName()+"\t"+"sendEmail");
sendSMS();
}
public synchronized void sendSMS(){
System.out.println(Thread.currentThread().getName()+"\t"+"sendSMS");
}
//ReenTrantLock Test
Lock lock = new ReentrantLock();
public void method1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"method1");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t"+"method2");
} finally {
lock.unlock();
}
}
@Override
public void run() {
method1();
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
PhonePlus phonePlus = new PhonePlus();
new Thread(()->{
phonePlus.sendEmail();
}, "t1").start();
new Thread(()->{
phonePlus.sendEmail();
}, "t2").start();
Thread t3 = new Thread(phonePlus);
Thread t4 = new Thread(phonePlus);
t3.start();
t4.start();
}
}
锁的配对
锁之间要配对,加了⼏把锁,最后就得解开⼏把锁,下⾯的代码编译和运⾏都没有任何问题。但锁的数量不匹配会导致死循环。
lock.lock();
lock.lock();
try{
someAction();
}finally{
lock.unlock();
}
五 ⾃旋锁
所谓⾃旋锁,就是尝试获取锁的线程不会⽴即阻塞,⽽是采⽤循环的⽅式去尝试获取。⾃⼰在那⼉⼀直循环获取,就像“⾃旋”⼀样。这样的好处是减少线程切换的上下⽂开销,缺点是会消耗CPU。CAS底层的 getAndAddInt就是⾃旋锁思想。
//跟CAS类似,⼀直循环⽐较。
while (!atomicReference.compareAndSet(null, thread)) { }
/**
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取直到成功为止,没有类似wait的阻塞。
* <p>
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,
* B随后进来后发现当前有线程持有锁,不是null,所以只能通过自选等待,直到A释放锁后B随后抢到。
*/
public class SpinLockDemo {
//原子引用(线程)
AtomicReference<Thread> atomicReference = new AtomicReference<>(); //Thread ==> null
//获取锁
public void myLock() {
Thread currentThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t com in...");
while (!atomicReference.compareAndSet(null, currentThread)) {
}
}
//释放锁
public void myUnLock() {
Thread currentThread = Thread.currentThread();
atomicReference.compareAndSet(currentThread, null);
System.out.println(Thread.currentThread().getName() + "\t unlock....");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
}, "AA").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnLock();
}, "BB").start();
}
}
结果
过程分析
六 读写锁/独占/共享
读锁是共享的,写锁是独占的。 juc.ReentrantLock
和 synchronized
都是独占锁,独占锁就是⼀个锁只能被⼀个线程所持有。有的时候,需要读写分离,那么就要引⼊读写锁,即 juc.ReentrantReadWriteLock
。
独占锁:指该锁⼀次只能被⼀个线程所持有。对
ReentrantLock
和Synchronized
⽽⾔都是独占锁
共享锁:指该锁可被多个线程所持有
对ReenntrantReadWriteLock
其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是⾮常⾼效的,读写、写读、写写的过程是互斥的。
⽐如缓存,就需要读写锁来控制。缓存就是⼀个键值对,以下Demo模拟了缓存的读写操作,读的 get ⽅法使⽤了 ReentrantReadWriteLock.ReadLock()
,写的 put⽅法使⽤了ReentrantReadWriteLock.WriteLock()
。这样避免了写被打断,实现了多个线程同时读。