目录
1、synchronized适用场景
2、synchronized的原理
3、synchronized的锁升级
4、synchronized的注意事项
5、总结
synchronized 是 Java 中用于实现线程同步的关键字。它可以在方法级别或代码块级别使用,以确保同一时刻只有一个线程可以访问被同步的代码段。synchronized 通过内部锁机制来实现线程间的互斥访问
synchronized 关键字可以在方法级和代码块级使用:
①可以将 synchronized 用在方法级别上,这种情况下锁定的对象是当前对象(this)
public class MyClass {
public synchronized void myMethod() {
// synchronized 代码
}
}
②也可以将 synchronized 关键字用在代码块级别上,这种情况下锁定的对象可以是当前对象(this),也可以是任意一个对象
public class MyClass {
private final Object lock = new Object(); // 定义一个对象作为锁
public void myMethod() {
synchronized (lock) {
// synchronized 代码
}
}
}
:多线程访问以下这段代码时,count结果会是多少呢?
public class SynchronizedTest implements Runnable{
public static int count = 0;
@Override
public void run() {
addCount();
}
public void addCount(){
int i = 0;
while (i++ < 1000) {
count++;
}
}
public static void main(String[] args) throws Exception{
SynchronizedTest obj = new SynchronizedTest();
Thread t1 = new Thread(obj);
Thread t2 = new Thread(obj);
t1.start();
t2.start();
t1.join(); // 等待 t1 线程执行完毕
t2.join(); // 等待 t2 线程执行完毕
System.out.println(count); // 打印 count 的值
}
}
逻辑很简单, 最终的count值一定是<2000,由于t1线程获取count的值为0,然后执行了+1操作,但还未同步至主内存,此时,t2获取主内存中count=0,然后也执行了+1操作,那最终的结果依然是count=1
为了保证count的值=2000,就需要用到synchronized关键字
当需要确保一段代码在多线程环境中只被一个线程执行时,可以使用 synchronized。
当需要保证多个变量之间的操作是一致的时候,可以使用 synchronized 来确保这些操作作为一个整体被执行。
虽然 synchronized 可能会导致死锁,但也可以通过合理的锁顺序和锁粒度设计来预防死锁。
synchronized 关键字还可以保证可见性和有序性,确保线程间的数据同步
上面都是一些很空泛的概念,有木有,因此还是需要结合来深入了解synchronized
掌声欢迎,登场!
在上文中Java并发—CAS的原理及应用场景-CSDN博客讲了利用CAS来实现抢票功能,没看过上文,也没关系,有兴趣可以看看,哈哈哈……
CAS操作多个变量时,会比较复杂,虽然保证多变量的一致性更新,但可能需要多次 CAS 操作和自旋重试,可能会消耗更多的 CPU 资源,那如果使用synchronized关键字实现上述功能呢
还是以之前的案例:假设线程A和线程B,都在尝试购买不同活动的门票
public class Ticket {
private int remainingTickets = 100; // 剩余票数
private int eventId = 1; // 活动 ID
public synchronized void buyTicket(int ticketsToBuy, int eventId) {
if (remainingTickets >= ticketsToBuy) {
this.eventId = eventId; // 更新活动 ID
System.out.println(Thread.currentThread().getName() + " 成功购买 " + ticketsToBuy + " 张票");
remainingTickets -= ticketsToBuy;
System.out.println("剩余票数: " + remainingTickets);
} else {
System.out.println(Thread.currentThread().getName() + " 购票失败,剩余票数不足");
}
}
}
模拟抢票
// 创建 TicketWithCAS 对象
TicketWithCAS ticketWithCAS = new TicketWithCAS();
// 创建线程模拟购票
Thread thread1 = new Thread(() -> { ticketWithCAS.buyTicket(10, 1);}, "Thread 1");
Thread thread2 = new Thread(() -> { ticketWithCAS.buyTicket(20, 2);}, "Thread 2");
Thread thread3 = new Thread(() -> { ticketWithCAS.buyTicket(30, 3);}, "Thread 3");
// 启动线程
thread1.start();
thread2.start();
thread3.start();
输出:
Thread 1 成功购买 10 张票
剩余票数: 90
Thread 2 成功购买 20 张票
剩余票数: 70
Thread 3 成功购买 30 张票
剩余票数: 40
最终剩余票数: 40
最终活动 ID: 3
Ticket 类中的 buyTicket 方法使用 synchronized 关键字来保证线程安全。这意味着在同一时刻只有一个线程可以执行 buyTicket 方法,从而保证了剩余票数和活动 ID 的一致性
使用 synchronized 更容易实现多变量的一致性,因为可以保证整个代码块的原子性
此处是用了synchronized锁住了buyTicket(),也可以synchronized锁住一个对象
public synchronized void buyTicket(int ticketsToBuy, int eventId) {}
但使用synchronized会有些潜在的问题
(1)锁粒度过大:
整个 buyTicket 方法都被同步了,这意味着即使在执行方法中的非关键部分时,其他线程也无法访问该方法。这可能导致不必要的等待。
(2)性能问题:
当多个线程试图同时访问同一个 synchronized 方法时,它们必须等待获得锁。这会导致线程阻塞,从而降低系统的整体吞吐量。
(3)不可扩展性:
当并发需求增加时,由于 synchronized 锁的排他性,性能可能会迅速下降
那么如何优化呢?
使用更细粒度的锁:只同步关键部分,而不是整个方法。这样可以减少锁的竞争,提高并发性能。
使用 java.util.concurrent 包中的并发工具,例如: java.util.concurrent.locks.ReentrantLock 或 java.util.concurrent.atomic.AtomicInteger 等工具来减少锁的使用
public class Ticket {
private final AtomicInteger remainingTickets = new AtomicInteger(100); // 剩余票数
private int eventId = 1; // 活动 ID
private final Lock lock = new ReentrantLock(); // 创建锁
public void buyTicket(int ticketsToBuy, int eventId) {
lock.lock();
try {
if (remainingTickets.get() >= ticketsToBuy) {
this.eventId = eventId; // 更新活动 ID
System.out.println(Thread.currentThread().getName() + " 成功购买 " + ticketsToBuy + " 张票");
remainingTickets.addAndGet(-ticketsToBuy); // 使用原子操作更新剩余票数
System.out.println("剩余票数: " + remainingTickets.get());
} else {
System.out.println(Thread.currentThread().getName() + " 购票失败,剩余票数不足");
}
} finally {
lock.unlock();
}
}
}
优化说明:
创建了一个 ReentrantLock 实例 lock,并在 buyTicket 方法中使用它来同步关键代码段。
使用 lock.lock() 和 lock.unlock() 来获取和释放锁。
使用 AtomicInteger 替换了原始的 int 类型变量 remainingTickets,以避免显式的锁使用。
使用 get() 和 addAndGet() 方法来获取和更新剩余票数
❓什么时候synchronized会释放锁?
synchronized 锁会在多种情况下被释放,包括正常执行结束、异常结束、显式返回、显式中断、线程被中断以及线程挂起。这些机制确保了线程安全性和资源的正确释放。
public class LockReleaseExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread thread1 = new Thread(() -> {
resource.increment();
});
Thread thread2 = new Thread(() -> {
resource.increment();
});
thread1.start();
thread2.start();
}
static class SharedResource {
private int counter = 0;
public synchronized void increment() {
try {
// 正常执行结束
counter++;
// 异常结束
if (counter == 5) {
throw new RuntimeException("An exception occurred.");
}
// 显式返回
if (counter == 3) {
return;
}
// 显式中断
if (counter == 4) {
break; // 这里不能直接使用 break,因为这不是循环。但在实际应用中,你可以使用其他逻辑来模拟这种情况。
}
// 线程被中断
if (counter == 2) {
Thread.sleep(1000); // 如果线程在这里被中断,锁会被释放
}
// 线程挂起
if (counter == 1) {
wait(); // 调用 wait() 会使线程释放锁并进入等待状态
}
} catch (InterruptedException e) {
// 处理中断异常
e.printStackTrace();
}
}
}
}
但是不会主动释放锁,就可能会造成死锁
死锁是指两个或多个线程互相等待对方持有的锁,从而导致所有线程都无法继续执行的状态
导致死锁的一些常见情况:
(1)嵌套锁:
如果一个线程持有了多个锁,并且在释放锁之前又尝试获取其他锁,而其他线程也在等待这些锁,就可能形成死锁
例如,线程 A 持有锁 X 并尝试获取锁 Y,而线程 B 持有锁 Y 并尝试获取锁 X
解决:尽量减少同时持有多个锁的情况,或者使用更高级的并发工具如 ReentrantLock 的 tryLock 方法来尝试获取锁,并设置超时时间
(2)锁顺序不一致:
如果多个线程按照不同的顺序获取锁,也可能导致死锁。
例如,线程 A 先获取锁 X 后获取锁 Y,而线程 B 先获取锁 Y 后获取锁 X。
解决:确保所有线程按照相同的顺序获取锁
(3)无限期等待:
如果线程在获取锁时没有设置超时时间,可能会导致无限期等待,进而引发死锁
解决:使用 tryLock 方法尝试获取锁,并设置超时时间,如果在指定时间内未能获取到锁,则放弃尝试,或者使用 Semaphore 或 CyclicBarrier 等并发工具类来管理锁的获取和释放
嵌套锁造成的死锁
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 & lock 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 & lock 1...");
}
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,thread1 先获取 lock1,然后尝试获取 lock2;而 thread2 则先获取 lock2,然后尝试获取 lock1。由于两个线程都在等待对方释放锁,所以形成了死锁
那使用 ReentrantLock 和 tryLock 方法,并设置了超时时间,这样即使无法获取锁,线程也不会陷入无限期等待,从而避免了死锁的发生
private static final ReentrantLock lock1 = new ReentrantLock();
boolean hasLock1 = lock1.tryLock(1, java.time.TimeUnit.SECONDS);
Synchronized的底层实现原理是完全依赖JVM虚拟机的
当对一段代码使用 synchronized 关键字修饰后,确实会绑定上对应锁对象的监视器对象。在 Java 中每个 Java 对象都对应一个监视器(Monitor)对象,该监视器对象中包含了一个计数器和等待队列。下面我将详细解释这一过程
监视器对象(Monitor Object)
监视器对象是 Java 中用于实现同步的关键组件。当一个线程试图进入一个由 synchronized 修饰的代码块或方法时,它会尝试获取该代码块或方法所绑定的对象的监视器锁。监视器对象中包含以下主要组成部分:
(1)计数器:
记录了当前持有锁的线程的数量。当线程首次获取锁时,计数器设为 1;如果同一个线程多次获取锁,则计数器递增
(2)等待队列:
存储了等待获取锁的所有线程。当线程无法立即获取锁时,会被加入到等待队列中,等待锁的释放
获取和释放锁的过程
当使用 synchronized 关键字修饰一段代码时,该代码块会绑定到一个监视器对象。监视器对象中包含一个计数器和等待队列,用于跟踪锁的持有情况和等待获取锁的线程。通过这种方式,synchronized 关键字确保了线程之间的互斥访问,从而保证了数据的一致性和线程的安全性
为了提高性能,Java 虚拟机对 synchronized 锁进行了优化,引入了锁升级的概念。
锁升级是指 synchronized 锁从一种状态升级到另一种状态的过程,以适应不同的并发需求,从而减少锁带来的性能开销
例如:在这个例子中,increment 和 getCount 方法都是同步的。当多个线程尝试同时访问这些方法时,JVM 会根据锁升级的策略来决定使用哪种类型的锁
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
锁升级的过程是从偏向锁到轻量级锁再到重量级锁。以下是锁升级的详细步骤:
无锁状态:
初始状态下,对象没有被任何线程锁定。
偏向锁:
轻量级锁:
重量级锁:
锁升级的好处
锁升级的限制
使用 synchronized 时,需要注意以下几点:
synchronized 关键字只能用于方法和代码块内部,不能用于类和接口。
synchronized 锁定的对象是当前对象(this)或指定的对象,要注意锁对象不应该是一个字符串或者数字等常量,因为这样可能导致死锁情况。
synchronized 的开销很大,每次加锁和释放锁都需要进行系统调用,需要注意性能问题。
synchronized 仅能解决单 JVM 内的线程同步问题,对于多线程分布式环境,需要考虑分布式锁的解决方案
使用synchronized锁时尽量缩小范围以保证性能。能不锁方法就不锁方法,推荐尽量使用synchronized
代码块来降低锁的范围
synchronized 关键字是 Java 中用来确保线程安全的基本机制,特别是在需要保证多个变量之间的一致性时。通过使用 synchronized,可以锁定一个对象,从而确保同一时刻只有一个线程可以访问该对象
可以将 synchronized 用于方法级别和代码块级别,要注意锁定的对象应该是一个合适的对象,不能是一个常量,使用 synchronized 可以简化并发控制逻辑,但需要注意其性能开销和潜在的死锁风险
下一篇:Java并发编程(五)—ReetrantLock详解及应用-CSDN博客