一、Synchronized
Synchronized 是什么
synchronized
是 Java 中的一个关键字,用于实现线程同步,它可以应用于代码块或方法,以确保多个线程对共享资源的安全访问。
在 JDK1.6 之前,它有另一个名称叫做:重量级锁。但是从 1.6 版本起,它就在不断被优化。在此之后,锁便拥有了 4 种状态,根据锁的级别从低到高可分为:无锁、偏向锁、 轻量级锁、重量级锁。
无锁
没有对共享资源进行任何锁定,所有线程都可以去访问并修改同一资源,但同时只能有一个线程修改成功,其他线程不断尝试直至成功,并会将原内容覆盖。
偏向锁
偏向锁,指的就是偏向第一个加锁线程,对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。
轻量级锁
轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
synchronized 锁升级的过程
-
首先,在锁对象的对象头里面有一个 threadid 字段,未访问时 threadid 为空,这时是无锁状态,任何线程都可竞争获取共享资源;
-
先得到共享资源的线程,其线程 ID 会被记录到 Mark Word 中,此时锁状态为偏向锁;
-
当后续还有线程去获取共享资源时,会先判断 threadid 是否与其线程 id 一致。如果一致则可以直接使用此对象;如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁;
-
自旋的线程在自旋过程中,成功获得资源 (即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败;
-
进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
二、Synchronized 用法
同步代码块
使用 synchronized
关键字修饰的代码块,通过指定一个对象来确定同步范围。例如:
synchronized (obj) {
// 需要同步的代码
}
在这个例子中,obj
是一个对象引用,同步块中的代码只有当线程获取了 obj
对象的锁时才能执行,确保了对共享资源的安全访问。
同步方法
使用 synchronized
关键字修饰的方法,整个方法体被视为同步代码块,锁定的对象是方法所属的对象。例如:
public synchronized void someMethod() {
// 需要同步的代码
}
静态同步方法
对于静态方法,可以使用 synchronized
关键字来实现同步,此时锁定的对象是类对象。例如:
public static synchronized void someStaticMethod() {
// 需要同步的代码
}
synchronized
的规则和细节:
- 当一个线程访问一个对象的
synchronized
代码块时,它获得的是括号中指定对象的锁。 - 当一个线程访问一个对象的
synchronized
实例方法时,它获得的是该对象的锁。 - 当一个线程访问一个类的
synchronized
静态方法时,它获得的是该类的锁。
虽然 synchronized
关键字可以简化多线程编程中的同步问题,但是因为锁的问题过度使用 synchronized
也可能导致性能问题,因为它可能会引入锁竞争和线程阻塞。
三、类锁
类锁是针对类的锁,它是针对类的所有实例的锁。当一个线程访问一个类的静态 synchronized
方法或代码块时,它获得了该类的锁,其他线程必须等待直到锁被释放。这意味着无论该类的实例有多少个,它们都共享同一个类锁。
换句话说,当一个线程获取了类锁时,其他线程无法同时访问这个类中的任何 synchronized
方法,无论是静态方法还是非静态方法。这是因为类锁是针对整个类的,而不是针对类的实例的。
四、对象锁
对象锁是针对类的实例(对象)的锁。每个 Java 对象都可以作为一个同步锁,通过 synchronized
关键字修饰的方法或代码块来获取对象锁。
当一个线程访问一个对象的 synchronized
方法或代码块时,它获得了该对象的锁,其他线程必须等待直到锁被释放。不同对象的对象锁是相互独立的,因此它们不会相互影响。
注意:不同对象的对象锁是相互独立的,因此它们不会相互影响。
既然不同对象的对象锁是相互独立的,不会相互影响。那么当不同的对象同时对共享资源进行修改时,如果没有适当的同步机制,就会存在线程安全问题。
下面看个简单的例子:
无锁的方法
public static class SharedResource {
private static int COUNT;
public void increment() {
COUNT++;
System.out.println(COUNT);
}
}
public static void sync1 (){
SharedResource obj = new SharedResource();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment();
}).start();
}
}
累加结果出现了错误:
然后我们把 increment
方法加上锁:
public static class SharedResource {
private static int COUNT;
public synchronized void increment() {
COUNT++;
System.out.println(COUNT);
}
}
public static void sync1 (){
SharedResource obj = new SharedResource();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment();
}).start();
}
}
累加结果正常:
我们使用不同对象去调用 increment
方法:
public static class SharedResource {
private static int COUNT;
public synchronized void increment() {
COUNT++;
System.out.println(COUNT);
}
}
public static void sync1 (){
for (int i = 0; i < 100; i++) {
SharedResource obj = new SharedResource();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment();
}).start();
}
}
累加结果又出错了:
从以上的测试可以看,即使使用了 synchronized
关键字,也是会存在线程不安全的情况。
简单来说要保证线程安全,需要保证获取的锁是同一个锁。
要解决这个问题,可以使用 synchronized
关键字来锁定某个共享的对象,或者使用 java.util.concurrent
包中的锁和原子类来确保对共享变量的安全访问。
使用 synchronized
关键字来锁定某个共享的对象:
public static class SharedResource {
private static int COUNT = 0;
public void increment(Object lock) {
synchronized (lock){
COUNT++;
System.out.println(COUNT);
}
}
}
public static void sync1 (){
//创建一个公共对象作为锁对象
Object lock = new Object();
for (int i = 0; i < 100; i++) {
SharedResource obj = new SharedResource();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment(lock);
}).start();
}
}
结果:
使用 java.util.concurrent.locks
包中的 ReentrantLock
锁实现安全访问:
public static class SharedResource {
private static int COUNT = 0;
public void increment(ReentrantLock lock) {
lock.lock();
try {
COUNT++;
System.out.println(COUNT);
} finally {
lock.unlock();
}
}
}
public static void sync1 (){
//创建一个公共锁对象
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 100; i++) {
SharedResource obj = new SharedResource();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment(lock);
}).start();
}
}
结果:
使用 java.util.concurrent.atomic
包中提供的 AtomicInteger
实现安全访问:
public static class SharedResource {
private static AtomicInteger count = new AtomicInteger(0);
public void increment() {
System.out.println(count.getAndIncrement());
}
}
public static void sync1 (){
for (int i = 0; i < 100; i++) {
SharedResource obj = new SharedResource();
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
obj.increment();
}).start();
}
}
结果(虽然 AtomicInteger
能够保证每次对整数的操作是原子的,但是线程的调度和执行顺序是不确定的):
五、java.util.concurrent 包
java.util.concurrent
是 Java 标准库中提供的用于并发编程的工具包。这个包提供了许多用于处理并发编程问题的类和接口,包括线程池、并发集合、同步器、原子变量等等。这些工具旨在简化并发编程,并提供更高效和更安全的并发操作。
以下是 java.util.concurrent
包中一些常用的类和接口:
-
线程池(ThreadPool):
Executor
框架及其实现类,如ExecutorService
、ThreadPoolExecutor
等,用于管理和调度线程的执行。线程池可以有效地重用线程、管理线程数量,并提供任务执行的调度和控制能力。 -
并发集合(Concurrent Collections):
java.util.concurrent
包提供了一系列并发安全的集合类,如ConcurrentHashMap
、ConcurrentLinkedQueue
等,用于在多线程环境下安全地操作集合数据。 -
同步器(Synchronizers):包括
CountDownLatch
、Semaphore
、CyclicBarrier
等,用于协调多个线程之间的操作,实现线程之间的同步。 -
原子变量(Atomic Variables):
java.util.concurrent.atomic
包提供了一系列原子变量类,如AtomicInteger
、AtomicLong
等,用于在多线程环境下进行原子操作,避免使用锁的情况下进行线程安全的操作。 -
并发工具类(Concurrent Utilities):包括
CopyOnWriteArrayList
、LinkedTransferQueue
等,提供了一些高效的并发工具,用于解决特定的并发编程问题。 -
locks:
java.util.concurrent.locks
包提供了一系列用于多线程同步的锁机制,这些锁机制相较于传统的synchronized
关键字提供了更灵活的锁定和解锁操作。这个包中最重要的接口是Lock
,它有几个常见的实现类,比如ReentrantLock
、ReentrantReadWriteLock.ReadLock
、ReentrantReadWriteLock.WriteLock
等。
java.util.concurrent
包中的类和接口提供了一套强大的工具,用于简化并发编程、提高性能,并降低编写线程安全代码的复杂性。