JUC-Lock接口的基本使用

1. Synchronized回顾

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象(锁的Class对象);
  4. 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。

synchronized的缺点:
如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以实现

2. Lock锁

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别
• Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个接口,通过这个接口的实现类可以实现同步访问;
• Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,有可能导致出现死锁现象。

2.1 Lock接口

public interface Lock {
void lock(); // 获取锁
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock(); // 释放锁
Condition newCondition(); // 返回 Condition 对象,实现等待/通知模式。
}

lock()方法用来获取锁。如果锁已被其他线程获取,则进行等待。采用 Lock,必须手动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用 Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
	//处理任务
}catch(Exception ex){
}finally{
	lock.unlock(); //释放锁
}
  • Condition对象
    关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 两个常用方法:
    • await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重
    新获得锁并继续执行。
    • signal()用于唤醒一个等待的线程。
    注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
  • ReentrantLock
    ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。下面通过一些实例看具体看一下如何使用。

2.2 可重入锁

可重入锁也叫做递归锁,是指同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。广义上的可重入锁,JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。不同点在于,synchronized 是隐式的可重入锁,ReentrantLock 是显式的可重入锁。

package com.lchtest.juc;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 可重入锁
 * synchronized是隐式的可重入锁,ReentrantLock是显式的可重入锁
 */
public class ReentrantLockTest2 {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "获取到了锁");
                } finally {
                    lock.unlock();
                    System.out.println("----");
                }
            } finally {
                lock.unlock();
                System.out.println("==");
            }
        },"线程a").start();
    }
}

2.3 分别使用synchronized和ReentrantLock模拟多线程卖票

  • 多线程编程步骤:
  1. 创建资源类,在资源类中封装属性和操作方法
  2. 如果是生产者消费者场景:在资源类的操作方法中,要完成 判断, 干活, 通知 操作
  3. 创建多个线程,调用资源类的操作方法
  4. 防止虚假唤醒,资源类操作方法中的判断要使用while循环来判断,而不是if判断

synchronized版本:创建资源类Ticket

package com.lchtest.juc.resource;
public class Ticket {
    private int ticketNum = 30;
    public synchronized void saleTicket(){
        if(ticketNum > 0){
            ticketNum --;
            System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余: " + ticketNum) ;
        }
    }
}
package com.lchtest.juc;

import com.lchtest.juc.resource.Ticket;

/**
 * Synchronized关键字的使用
 * synchronized修饰代码块  作用范围是{}中的代码块,作用的对象时调用这个代码块的对象
 * synchronized修饰一个方法: 作用范围是整个方法,作用的对象时调用这个方法的对象
 * 多线程编程步骤:
 * 1.创建资源类,在资源类中封装属性和操作方法
 * 2.创建多个线程,调用资源类的操作方法
 */
public class SynchronizedTest {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 40; i ++){
                    ticket.saleTicket();
                }
            }
        }, "售票员A").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i ++){
                    ticket.saleTicket();
                }
            }
        }, "售票员B").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 30; i ++){
                    ticket.saleTicket();
                }
            }
        }, "售票员C").start();

    }
}

使用ReentrantLock实现:资源类中实现对资源的操作,加锁-》 判断 -》 操作 -》 释放锁

package com.lchtest.juc.resource;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket1 {
    private int ticketNum = 30;
    private Lock lock = new ReentrantLock();
    public void saleTicket(){
        // 上锁
        lock.lock();
        // 资源类的操作,放到try中,防止出现异常后,finally中能构解锁
        try {
            if(ticketNum > 0){
                ticketNum --;
                System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余: " + ticketNum) ;
            }
        } finally {
            lock.unlock();
        }
    }
}

创建三个线程来调用资源类的操作方法:

package com.lchtest.juc;
import com.lchtest.juc.resource.Ticket1;
public class LockTest {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1();
        new Thread(() ->{
            for (int i = 0; i < 40; i++){
                ticket.saleTicket();
            }
        }, "线程a").start();

        new Thread(() ->{
            for (int i = 0; i < 40; i++){
                ticket.saleTicket();
            }
        }, "线程b").start();

        new Thread(() ->{
            for (int i = 0; i < 40; i++){
                ticket.saleTicket();
            }
        }, "线程c").start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值