文章目录
前言
这个只是很简单的介绍,但是可以有一个大概的了解了!等后续通过《Java并发编程艺术》这个书的学习,再加深。
2.1 synchronizd
2.1.1 synchronizd关键字基础
synchronized
是 Java 中的关键字,Lock
是一种同步锁(本质上是一种监视器monitor
)。它修饰的对象有以下几种:
- 修饰方法
被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
tips:
虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了
- 修饰类
其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象 。
- 修饰代码库
修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰静态方法
其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
2.1.2 多线程编程步骤-1
- 创建资源类,在资源类创建属性和操作方法。
- 创建多个线程,调用资源类的操作方法。
2.1.3 synchronizd案例(售票)
/**
* @author LWJ
* @date 2023/6/17
*/
class Tickets{
private int numbers = 30;
public synchronized void sale(){
if(numbers > 0){
System.out.println(Thread.currentThread().getName()
+ "售出1张票,剩余:" + --numbers + "张票" );
}
}
}
public class SaleTickets {
public static void main(String[] args) {
Tickets tickets = new Tickets();
//线程A
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}
},"thread-A").start();
//线程B
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}
},"thread-B").start();
//线程C
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}
},"thread-C").start();
}
}
这里可能会有疑惑,“三个线程的票难道不都是独立的吗?”
当然不是,Runnable接口和Thread都可以正确的实现资源的共享。
对于Thread和Runnable资源共享问题,参考:
Thread和Runnable关于共享资源的对比_runnable资源共享_Hadoop_Liang的博客-CSDN博客
2.1.4 分析售票案例存在的问题
如果一个代码块被 synchronized
修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2. 线程执行发生异常,此时 JVM 会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。
2.2 Lock
Lock
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 Lock
提供了比 synchronized
更多的功能。
2.2.1 Lock接口
public interface Lock {
/*
获取锁
如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
实现注意事项:
Lock 实现可能能够检测到锁的错误使用,比如会导致死锁的调用,在那种环境下还可能
抛出一个 (unchecked) 异常。Lock实现必须对环境和异常类型进行记录。
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中
进行,以保证锁一定被被释放,防止死锁的发生。
通常使用 Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock()
}
*/
void lock(); //获取锁
/*
如果当前线程未被中断,则获取锁。
如果锁可用,则获取锁,并立即返回。
如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直
处于休眠状态:
1.锁由当前线程获得;
2.其他某个线程中断当前线程,并且支持对锁获取的中断。
如果当前线程:
1.在进入此方法时已经设置了该线程的中断状态;
2.在获取锁时被中断,并且支持对锁获取的中断,
则将抛出 InterruptedException,并清除当前线程的已中断状态。
实现注意事项:
1.在某些实现中可能无法中断锁获取,即使可能,该操作的开销也很大。
程序员应该知道可能会发生这种情况。在这种情况下,该实现应该对此进行记录。
2.相对于普通方法返回而言,实现可能更喜欢响应某个中断。
3.Lock实现可能可以检测锁的错误用法,例如,某个调用可能导致死锁,
在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。
抛出:
InterruptedException - 如果在获取锁时,当前线程被中断(并且支持对锁获取的中断)。
*/
void lockInterruptibly() throws InterruptedException;//如果当前线程未被中断,则获取锁。
boolean tryLock(); //获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//获取锁,有时间限制
void unlock(); //释放锁
/*
返回绑定到此 Lock 实例的新 Condition 实例。
在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,
并在等待返回前重新获取锁。
实现注意事项:
Condition 实例的具体操作依赖于 Lock 实现,并且该实现必须对此加以记录。
返回:
用于此 Lock 实例的新 Condition 实例
抛出:
UnsupportedOperationException - 如果此 Lock 实现不支持条件
*/
Condition newCondition();
}
2.2.2 Lock 与的 Synchronized 区别
Lock
不是 Java 语言内置的,synchronized
是Java 语言的关键字,因此是内置特性。Lock
是一个类,通过这个类可以实现同步访问;Lock
和synchronized
有一点非常大的不同,采用synchronized
不需要用户去手动释放锁,当synchronized
方法或者synchronized
代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock
则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
2.2.3 用Lock实现售票案例
import java.util.concurrent.locks.ReentrantLock;
/**
* @author LWJ
* @date 2023/6/17
*/
//创建资源,和资源的属性与操作
class LTickets {
private int numbers = 30;
ReentrantLock lock = new ReentrantLock();
public synchronized void sale() {
lock.lock();
try {
if (numbers > 0) {
System.out.println(Thread.currentThread().getName() + "售出1张票,剩余:" + --numbers + "张票");
}
} finally {
lock.unlock();
}
}
}
public class LSaleTickets {
//创建线程操作资源
public static void main(String[] args) {
Tickets tickets = new Tickets();
//线程A
new Thread(() -> {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}, "thread-A").start();
//线程B
new Thread(() -> {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}, "thread-B").start();
//线程C
new Thread(() -> {
for (int i = 0; i < 50; i++) { //假设一个比30大的数就行,举例而已
tickets.sale();
}
}, "thread-C").start();
}
}
2.2.4 多线程编程步骤
- 创建资源类,在资源类创建属性和操作方法。
- 在资源类操作方法
- 判断
- 干活
- 通知
- 创建多个线程,调用资源类的操作方法。
2.3 总结
Lock
和 synchronized
有以下几点不同:
Lock
是一个接口,而synchronized
是 Java 中的关键字,synchronized
是内置的语言实现。synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally
块中释放锁;
3.Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;- 通过
Lock
可以知道有没有成功获取锁,而synchronized
却无法办到。 Lock
可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock
的性能要远远优于synchronized
。
Lock
为什么比synchronized
效率高?
Lock
(锁)与 synchronized
(同步)是 Java 中用于实现多线程同步的两种机制,它们在实现线程互斥和协作的方式上有一些不同,从而导致 Lock
相对于 synchronized
具有更高的效率。
-
粒度更细:
Lock
提供了更细粒度的锁控制。在synchronized
中,锁的粒度是以整个方法或代码块为单位的,而Lock
允许开发者更细粒度地控制锁的范围,可以仅对关键部分进行加锁。这样可以减少线程之间的竞争,提高并发性能。 -
可中断性:
Lock
提供了可中断的获取锁的方式。在某些场景下,如果一个线程在等待锁的过程中可以被中断,那么它可以根据实际情况来决定是否继续等待,而synchronized
则无法实现这种中断操作。可中断的锁对于处理复杂的线程交互和超时控制非常有用。 -
公平性:
Lock
可以提供更好的公平性。在synchronized
中,无法控制线程获得锁的顺序,而Lock
可以通过公平锁和非公平锁的选择来决定线程获取锁的顺序。公平锁会按照线程请求锁的顺序进行处理,避免了饥饿现象。 -
高度可定制性:
Lock
提供了更高度的可定制性和灵活性。它支持多种锁的实现方式,比如 ReentrantLock
、StampedLock
等,并且可以通过参数来设置锁的特性,如超时等待、尝试获取锁等。这使得开发者能够根据具体需求进行优化和调整,从而提高效率。
尽管 Lock
相对于 synchronized
具有更高的效率,但它的使用也更加复杂和容易出错。在使用 Lock
时需要手动进行锁的获取和释放,而 synchronized
则由 JVM 自动管理锁的获取和释放。因此,在编写多线程代码时,如果不需要 Lock
特有的功能,使用 synchronized
是更简单和安全的选择。只有在需要更高级的线程同步机制时,才需要使用 Lock
。