Lock(锁):
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
- 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
API中的介绍:
public interface Lock
Lock
实现提供比使用synchronized
方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition
。锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如
ReadWriteLock
的读锁。使用
synchronized
方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。虽然
synchronized
方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。 例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。 所述的实施方式中Lock
接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。随着这种增加的灵活性,额外的责任。 没有块结构化锁定会删除使用
synchronized
方法和语句发生的锁的自动释放。 在大多数情况下,应使用以下惯用语:当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
Lock
实现提供了使用synchronized
方法和语句的附加功能,通过提供非阻塞尝试来获取锁(tryLock()
),尝试获取可被中断的锁(lockInterruptibly()
) ,以及尝试获取可以超时(tryLock(long, TimeUnit)
)。一个
Lock
类还可以提供与隐式监视锁定的行为和语义完全不同的行为和语义,例如保证排序,非重入使用或死锁检测。 如果一个实现提供了这样的专门的语义,那么实现必须记录这些语义。请注意,
Lock
实例只是普通对象,它们本身可以用作synchronized
语句中的目标。 获取Lock
实例的监视器锁与调用该实例的任何lock()
方法没有特定关系。 建议为避免混淆,您不要以这种方式使用Lock
实例,除了在自己的实现中。
格式:锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行
class A{
private final ReentrantLock lock = new ReentrantLock();
public void fun(){
lock.lock();
try {
//保证线程安全的代码
}finally {
lock.unlock();
}
}
}
synchronized 与Lock的对比:
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) <--------------> synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁 <--------------> synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
例:三个人去抢票
package com.kuang.thread.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName TestLock
* @Description lock锁
* @Author 麻虾
* @Date 2021/6/1 14:55 55
* @Version 1.0
*/
//lock锁:
//1.定义锁: private final ReentrantLock lock = new ReentrantLock();
//2.获得锁: l.lock();
//3.释放锁: l.unlock();
//锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行
public class TestLock {
public static void main(String[] args){
//创建三个线程去买票
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2,"Marry").start();
new Thread(testLock2,"Jack").start();
new Thread(testLock2,"Davi").start();
}
}
class TestLock2 implements Runnable{
//票数
int ticketNums = 10;
//定义Lock锁
private final ReentrantLock l = new ReentrantLock();
@Override
public void run() {
//买票
while (true){
//加锁
l.lock();
try {
if (ticketNums > 0) {
//sleep放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "买到了票:" + ticketNums--);
}else {
//无票退出
break;
}
}finally{
//别忘了释放锁
l.unlock();
}
}
}
}
但是执行后发现了问题,运行多次,都是一个人拿到了票,于是增加了票数看看是不是因为运行太快,发现都是一个人结束另一个人才开始。
Marry买到了票:10
Marry买到了票:9
Marry买到了票:8
Marry买到了票:7
Marry买到了票:6
Marry买到了票:5
Marry买到了票:4
Marry买到了票:3
Marry买到了票:2
Marry买到了票:1
Jack买到了票:10
Jack买到了票:9
Jack买到了票:8
Jack买到了票:7
Jack买到了票:6
Jack买到了票:5
Jack买到了票:4
Jack买到了票:3
Jack买到了票:2
Jack买到了票:1
Marry买到了票:100
Marry买到了票:99
Marry买到了票:98
Marry买到了票:97
Marry买到了票:96
Marry买到了票:95
Marry买到了票:94
Marry买到了票:93
Marry买到了票:92
Marry买到了票:91
Marry买到了票:90
Marry买到了票:89
Marry买到了票:88
Marry买到了票:87
Marry买到了票:86
Marry买到了票:85
Marry买到了票:84
Marry买到了票:83
Marry买到了票:82
Marry买到了票:81
Marry买到了票:80
Marry买到了票:79
Marry买到了票:78
Marry买到了票:77
Marry买到了票:76
Marry买到了票:75
Marry买到了票:74
Marry买到了票:73
Marry买到了票:72
Marry买到了票:71
Marry买到了票:70
Marry买到了票:69
Marry买到了票:68
Marry买到了票:67
Marry买到了票:66
Marry买到了票:65
Marry买到了票:64
Marry买到了票:63
Marry买到了票:62
Marry买到了票:61
Marry买到了票:60
Marry买到了票:59
Marry买到了票:58
Marry买到了票:57
Marry买到了票:56
Marry买到了票:55
Marry买到了票:54
Marry买到了票:53
Marry买到了票:52
Marry买到了票:51
Jack买到了票:50
Jack买到了票:49
Jack买到了票:48
Jack买到了票:47
Jack买到了票:46
Jack买到了票:45
Jack买到了票:44
Jack买到了票:43
Jack买到了票:42
Jack买到了票:41
Jack买到了票:40
Jack买到了票:39
Jack买到了票:38
Jack买到了票:37
Jack买到了票:36
Jack买到了票:35
Jack买到了票:34
Jack买到了票:33
Jack买到了票:32
Jack买到了票:31
Jack买到了票:30
Davi买到了票:29
Davi买到了票:28
Davi买到了票:27
Davi买到了票:26
Davi买到了票:25
Davi买到了票:24
Davi买到了票:23
Davi买到了票:22
Davi买到了票:21
Davi买到了票:20
Davi买到了票:19
Davi买到了票:18
Davi买到了票:17
Davi买到了票:16
Davi买到了票:15
Davi买到了票:14
Davi买到了票:13
Davi买到了票:12
Davi买到了票:11
Davi买到了票:10
Davi买到了票:9
Davi买到了票:8
Davi买到了票:7
Davi买到了票:6
Davi买到了票:5
Davi买到了票:4
Davi买到了票:3
Davi买到了票:2
Davi买到了票:1
问题所在:
Thread类中的静态方法sleep(),当一个执行中的线程调用了Thread的sleep()方法后,调用线程会暂时让出时间的执行权,这期间不参与cpu的调度,但是该线程持有的锁是不让出的。时间到了会正常返回,线程处于就绪状态,然后参与cpu调度,获取到cpu资源之后就可以运行。
原来的代码里使用了sleep,这里要注意!!sleep是不会释放锁的,我们把sleep放在了锁里面,所以无论执行多少次,都是先输出A再输出B,或者先输出B再输出A,反正不会出现交叉输出的情况。因为A获取到这个共享对象的锁之后,执行到了sleep,sleep不会释放锁,所以后面的线程B就获取不到这个共享对象的锁,也就无法执行。
修改代码,将sleep放到锁外面:
public void run() {
while (true){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
l.lock();
try {
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票:" + ticketNums--);
}else {
break;
}
}finally{
l.unlock();
}
}
}
再运行,则正常输出,线程安全:
Jack买到了票:10
Davi买到了票:9
Marry买到了票:8
Marry买到了票:7
Davi买到了票:6
Jack买到了票:5
Davi买到了票:4
Jack买到了票:3
Marry买到了票:2
Davi买到了票:1