文章目录
什么是Lock
官网介绍:
虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您
需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然
后获取节点B,然后释放A并获取C,然后释放B并获得D等。所述的实施方式中L0Ck接口通过允许获得并在不同的范围释放的锁,并
允许获得并以任何顺序释放多个锁使得能够使用这样的技术。
随着这种增加的灵活性,额外的责任。没有块结构化锁定会删除使用synchronized)方法和语句发生的锁的自动释放。在大多数情
况下,应使用以下惯用语:
Lock l =…1.lock();try /access the resource protected by this lock finally l.unlock();
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁
定。
Lock实现提供了使用synch ronized方法和语句的附加功能,通过提供非阻塞尝试来获取锁(tryLock()),尝试获取可被中断的
锁(lockInterruptibly()),以及尝试获取可以超时(tryLock(Long,TimeUnit))。
一个Lock类还可以提供与隐式监视锁定的行为和语义完全不同的行为和语义,例如保证排序,非重入使用或死锁检测。如果一个实
现提供了这样的专门的语义,那么实现必须记录这些语义。
请注意,Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。获取Lock实例的监视器锁与调用该实例的任何
Lock(防法没有特定关系。建议为避免混淆,您不要以这种方式使用L0ck实例,除了在自己的实现中。
除非另有说明,传递任何参数的nuLl值将导致NullPointerException被抛出。
Lock是一个接口,有三个实现类:ReentrantLock(可重入锁)、ReentrantReadWriteLock.ReadLock(读锁),ReentrantReadWriteLock.writeLock(写锁)
synchronized加锁和Lock加锁代码示例
这里统一用一个买票的例子,多个线程实现不同的窗口进行买票
synchronized
public class TicketSale {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源丢入线程
Ticket1 ticket = new Ticket1();
//@FunctionalInterface 函数式接口,jkd1.8 lambda 表达式(参数)->{代码}
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类OOP
class Ticket1{
//属性 方法
private int number = 30;
//卖票的方式
public synchronized void sale(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余"+number);
}
}
}
使用Lock加锁
public class SaleTicketDemo1 {
public static void main(String[] args) {
Ticket ticket=new Ticket();
new Thread(()->{for (int i = 0; i < 40; i++) ticket.sale();},"A").start();
new Thread(()->{for (int i = 0; i < 40; i++) ticket.sale();},"B").start();
new Thread(()->{for (int i = 0; i < 40; i++) ticket.sale();},"C").start();
}
}
//资源类
class Ticket{
//属性
private int number=50;
Lock lock=new ReentrantLock();
public void sale(){
lock.lock();//加锁
try {
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();//解锁
}
}
}
公平锁和非公平锁
公平锁和非公平锁是在多线程环境下用于同步访问共享资源的机制。它们之间的区别在于线程获取锁的顺序不同。
公平锁:
公平锁是指多个线程按照申请锁的顺序来获取锁,即先来先得的原则。当一个线程释放锁后,等待时间最长的线程会获得锁。
公平锁的优点是保证了资源的公平性,避免了饥饿现象,所有线程都有机会获取到资源。
公平锁的缺点是需要维护一个有序的等待队列,增加了系统开销,降低了并发性能。
非公平锁:
非公平锁是指多个线程获取锁的顺序是不确定的,有可能新申请锁的线程会在等待队列中插队,先于等待时间更长的线程获取到锁。
非公平锁的优点是相对于公平锁,减少了等待时间,提高了吞吐量。
非公平锁的缺点是可能会导致某些线程长时间等待,产生饥饿现象,不公平性可能会造成一些线程无法获得资源。
选择使用公平锁还是非公平锁,取决于具体的业务场景和需求:
如果对资源的访问顺序要求比较高,希望保证公平性,可以选择公平锁。
如果对吞吐量要求比较高,对于资源访问的顺序没有特别的要求,可以选择非公平锁。
需要注意的是,Java中的ReentrantLock默认是非公平锁,但可以通过构造函数参数来指定为公平锁。而synchronized关键字是一种非公平锁。在使用锁的时候,需要根据具体情况选择适合的锁机制。
在ReentrantLock中,默认是非公平锁,如果需要使用公平锁可以通过传入boolean类型的参数进行转换,true为公平锁
Lock和Synchronized的区别
1、Synchronized内置的ava关键字,Lock是一个ava类
2、Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
3、Synchronized会自动释放锁,lock必须要手动释放锁!如果不释放锁,死锁
4、Synchronized线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去(tryLock方法);
5、Synchronized可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断锁,非公平(可以自己设置);
6、Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码!
synchronized 版的生产者和消费者
生产者和消费者之间通过线程通信问题,生产者和消费者问题 等待唤醒 ,通知唤醒
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//等待 业务 通知
class Data{//数字 资源类
private int number = 0;
//加操作
public synchronized void increment() throws InterruptedException {
if(number != 0) {
//等待
this.wait();//在哪里睡就在哪里醒
}
number++;
//通知其他线程,我+1完毕了
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();
}
//减操作
public synchronized void decrement() throws InterruptedException {
if(number == 0) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"-->"+number);
this.notifyAll();
}
}
上面这段代码是4个线程进行交替。4个线程出现的问题在下文会有讲解。正常可以使用2个线程线程交替
Lock 版的生产者和消费者
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//等待 业务 通知
class Data2 {//数字 资源类
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// condition.await();//等待
// condition.signalAll();//唤醒全部
public void increment() throws InterruptedException {
lock.lock();
try {
//业务代码
if(number != 0) {
//等待
condition.await();
}
number++;
//通知其他线程,我+1完毕了
System.out.println(Thread.currentThread().getName() + "-->" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
if(number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "-->" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
生产者和消费者出现的问题
在通过上面的代码中,如果两个线程进行的话,可以实现生产者和消费者进行0、1的交替通信,那么如果再增加两个线程的话,就有可能出现问题,并不是0、1进行交替,而是可能出现8、9这样的数值。如图:
那这种情况就是虚假唤醒,也就是在不该醒的地方,线程醒了并执行了后续的操作。
虚假唤醒出现的原因:
如上图:当A线程进入阻塞的时候,其他三个线程就会竞争资源,这个使用可能c会获取资源,那么当它执行加操作的时候,会发现不符合条件就会进行阻塞,那么A、B、D进行争抢资源,当到A之后,发现不符合还是会进入阻塞,重复上面的操作,那么可能C抢到资源,这里注意,在刚刚的步骤中,C已经进行了一次阻塞,这里使用的wait(),那么wait是在哪里睡就在哪里醒,这里c就醒了,并执行了后续的加操作。所以才会出现加到7、8这种数值的情况
那么如果解决呢?可以把if判断更换为while判断。
Condition精准通知和唤醒线程
Condition因素出Object监视器方法(wait notify和notifyAll)成不同的对象,以得到具有多个等待集的每个对象,通过
将它们与使用任意的组合的效果Lock个实现。'Lock替换synchronized方法和语句的使用
Condition取代了对象监视器方法的使用。
条件(也称为条件队/域条件变量)为一个线程暂停执行(“等待”)提供了一种方法,直到另一个线程通知某些状态现在可能为真。
因为访问此共享状态信息发生在不同的线程中,所以它必须被保护,因此某种形式的锁与该条件相关联。等待条件的关键属性是它原
在这里插入代码片
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printA();
}
}, “A”).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printB();
}
}, “B”).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printC();
}
}, “C”).start();
}
}
class Data3 {// 资源类 Lock
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();//监视器
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1;//number为1的时候A执行,2的时候B执行,3的时候C执行
public void printA() {
lock.lock();
try {
//业务,判断->执行->通知
while (number != 1) {
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + “—>A”);
//唤醒指定的人:B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
//业务,判断->执行->通知
while (number != 2) {
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + “—>B”);
//唤醒指定的人:C
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
//业务,判断->执行->通知
while (number!=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+“—>C”);
number=1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
上面代码是通过设置线程执行完之后,后面需要执行那个线程的