我们的应用程序都是运行在多线程的环境下的,在多线程环境下的许多问题我们都了解吗?
- 线程间如何进行数据交换?
- 线程间如何进行通信与协作?
- 共享一个资源时如何保证线程安全?
线程数据交换
线程之间无法直接访问对方工作内存中的变量,必须通过主内存进行变量的传递。例如,线程A、B共享一个变量C,当A在工作内存中更新了C的值,并同步到主内存后,线程B才能从主内存中获取到变量C的最新值。
内存模型
要理解线程间的数据访问,首先得知道Java中的内存模型,其主要目的是定义程序中各种变量的访问规则。类似于计算机中的高速缓存和主内存,Java中的内存模型将内存划分为主内存和工作内存。
其中,主内存是所有线程共享的,每个线程拥有一个专属的工作内存。
- 主内存:用于存储所有的变量
- 工作内存:用于存储线程所使用变量的主内存副本
线程对变量的所有操作(读取、赋值等)都在工作内存中进行。例如,当线程赋值给字段时,会先在工作内存中进行更改,然后择机将更改同步到主内存中,这个时机由JVM决定。
线程安全
线程安全就是在多个线程间共享一个资源时,保证每个线程都能读取到正确状态的数据。简单来说就是同一时间只能有一个线程具有该共享资源更改或者访问权限。
那如何保证线程安全呢?实现策略无非就两种
- 一是基于悲观锁的阻塞同步
- 二是基于乐观锁的非阻塞同步
阻塞同步
阻塞同步,即同一时间,只能有一个线程有访问权限。java中阻塞同步的方式有两种,一是使用synchronized关键词、二是使用ReentrantLock。在访问被synchronized或ReentrantLock锁定的区域时,需要先获取对应的锁资源,并且同时只允许一个线程持有锁。在一个线程持有锁后,其他线程需要阻塞等待,直到可获取时被手动或者被动唤醒。
在被锁定的区域中(临界区),如果存在对共享变量的更改,在锁释放后,这个更改后立即刷新到主内存中,其他线程可以立即看到,这个也叫做可见性。
synchronized
synchronized是一个关键字,JVM级别的锁。可以修饰在方法上,表示方法为同步方法,也可以修饰在代码块上。但需要明白的是synchronized是锁在一个对象上的,修饰方法时,代表锁定的是当前实例。
那如果修饰在静态方法或者静态代码块上,锁定的是什么对象?因为与实例无关,肯定不是锁定在实例对象上,而是锁定在一个Class对象上,Class对象在内存中的表示就是一个字节码对象。通过反射机制获取类的字段、方法,就是操作的字节码对象。
如果一个类中多个方法都被synchronized修饰,那么同一时间,只能有一个方法被执行。
public synchronized String getConfig() {
return "hello";
}
如果在一个类中存在多个竞争资源且无关联时,需要为不同的资源设置不同的锁对象。如果都使用当前对象,可能对系统性能造成负面影响。
private final Object LOCK_CONFIG= new Object();
public String getConfig() {
Object o = new Object();
synchronized (LOCK_CONFIG) {
return "hello";
}
}
ReentrantLock
与synchronized不同的是,ReentrantLock本身就是一个锁对象,创建后直接调用lock、unlock即可加解锁。ReentrantLock基于同步器AQS(AbstractQueuedSynchronizer)实现。
AQS内部维护了一个双向链表(CLH同步队列),用于保存等待获取锁的线程。这个队列采用FIFO(先进先出)的顺序,可以保证线程获取锁的公平性。
AQS中通过一个整型的volatile变量来表示同步状态,支持独占式同步(独占锁)和共享式同步(共享锁)。
ReentrantLock、Semaphore、CountDownLatch均是基于AQS实现。
简单使用
private final ReentrantLock reentrantLock = new ReentrantLock();
public String getConfig() {
reentrantLock.lock();
try {
return "hello";
} finally {
reentrantLock.unlock();
}
}
高级应用
相较于synchronized,ReentrantLock功能更加强大,可以实现公平锁(默认为非公平锁实现,公平锁影响性能),可以通过newCondition()方法创建一个Condition对象,用于在某些条件下挂起和唤醒线程。
Condition接口提供了以下几个重要方法:
- await():当前线程等待,同时释放锁,直到其他线程调用signal()或signalAll()方法唤醒它。
- signal():唤醒一个等待在该条件上的线程。
- signalAll():唤醒所有等待在该条件上的线程。
在生产者-消费者模式下的阻塞队列中,使用ReentrantLock和Condition,可以更加灵活的控制线程间的协作。详见java.util.concurrent.LinkedBlockingQueue实现。
非阻塞同步
非阻塞同步是一种乐观策略,假设不存在并发冲突,先进行操作。这时有两种情况,一是没有并发冲突,那很完美,直接执行;如果存在并发冲突,再进行补偿操作。整个过程不需要加锁控制,所以被称为无锁编程。
非阻塞同步要求操作和冲突检测这两个步骤具备原子性和共享变量的可见性。
- 原子性:指一个操作是不可中断的,要么全部成功,要么全部失败,在多线程执行时,一个线程原子操作开始,不会被其他线程所干扰。Java中基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子性协定),synchronized关键字也实现了原子性。
- 可见性:可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
同步阻塞的加锁也保证了原子性(只有一个线程操作,不会被其他线程干扰)和可见性(释放锁时立即刷新到主存)。
CAS
java中是CAS(Compare-and-Swap,比较并交换)操作保证了原子性,执行时需要三个参数:
- 一是变量的内存地址,以V代指
- 二是旧值,以A代指
- 三是新值,以B代指
在更新时,只有A值等于V值时,才将V值更新为B值,否则不进行更新操作。
在sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供了CAS操作。
volatile
java中,使用volatile修饰的变量,可以保证其可见性。
线程在变量读取前会先从主内存刷新变量值,无论是普通变量还是volatile变量都是如此。但区别是,volatile变量保证了值在工作内存中更改后立即同步到主内存,而普通变量的更改则由JVM选择择机同步到主内存。
使用实例
java提供了一个java.util.concurrent.atomic包,这个包中的类都是线程安全的,实现方式为非阻塞的无锁实现。其中原子性通过CAS操作保证,而可见性通过volatile关键字实现。
- 在多线程环境下,使用AtomicInteger进行计数
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private static AtomicInteger counter = new AtomicInteger(0);
private static final int NUM_THREADS = 10;
private static final int NUM_INCREMENTS = 1000;
public static void main(String[] args) {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
counter.incrementAndGet();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (int i = 0; i < NUM_THREADS; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final counter value: " + counter.get());
}
}
线程协作/通信
在阻塞同步时,多个线程竞争一个锁资源,在第一个线程获取到锁后,其余线程全被阻塞。在第一个线程后执行完成后,可以唤醒阻塞在该锁上的线程。
每一个Object都可以作为一个锁,锁对象中提供了一些方法可用于线程间通信
wait()
使当前线程进入等待状态,并释放所持有的锁。
在没有设置timeout时,需要等待其他线程调用了相同对象上的notify()或notifyAll()方法来唤醒它;在设置timeout后,时间到了之后由VM唤醒。需要注意的是,被唤醒并重新获取锁后,其实是从wait方法处返回,继续往下执行同步代码块中的代码。
notify()
用于唤醒等待该对象锁的线程集合中的一个线程(将其从阻塞状态转换为就绪状态),具体是哪一个线程被唤醒是不确定的。
调用notify()后,当前线程 不会释放所持有的锁 ,而是需要执行完同步代码块后才释放。被唤醒的线程会尝试重新获取对象的锁,一旦获取到锁,它就会从wait()方法返回,继续执行。
notifyAll()
用于唤醒等待该对象锁的线程集合中的所有线程。
标签:同步,Java,变量,synchronized,编程,阻塞,线程,内存,多线程 From: https://www.cnblogs.com/cd-along/p/18211133