在 Java 程序中,存储数据的内存空间分为共享内存和本地内存。线程在读写主存的共享变量时,会先将该变量拷贝一份副本到自己的本地内存,然后在自己的本地内存中对该变量进行操作,完成操作之后再将结果同步至主内存。
类型 | 存储介质 | 数据 | 特征 |
---|---|---|---|
共享内存 | 主内存 | 存放变量 | 多线程共享 |
本地内存 | CPU 高速缓存、缓冲区、寄存器以及其它硬件优化 | 临时存放线程使用的变量副本 | 使用期间其它线程无法访问 |
public class ThreadDemo {
public static void main(String[] args) {
MyThread t = new MyThread();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
class MyThread implements Runnable {
private int x = 0; // 对象中的数据由线程共享
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
x++;
}
System.out.println("final x: " + x); // 最后输出的数据不一定为 20000
}
}Copy to clipboardErrorCopied
JMM 定义了共享内存系统中多线程程序读写操作行为的规范,用来保证共享内存的原子性、可见性、有序性。
原子性是指一个操作,要么全部执行并且执行过程不会被打断,要么就都不执行。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性是指程序执行的顺序按照代码的先后顺序执行。
阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会被直接挂起。直到其他线程释放锁,由操作系统激活线程。
适用于锁使用者保持锁时间比较长的情况,线程挂起后不再消耗 CPU 资源。
非阻塞锁。当线程需要获取的锁已经被其他线程占用时,该线程会不断地消耗 CPU 的时间去试图获取锁。
适用于锁使用者保持锁时间比较短的情况,没有用户态和内核态调度、上下文切换的开销和损耗。
每次读写资源时都会给资源上锁,其他线程想获取该资源时会被阻塞,直到其释放锁。
适用于写频繁的应用场景,写资源请求不会被一直驳回。synchronized 和 ReentrantLock 等独占锁都是悲观锁。
读资源时不会给资源上锁,多个线程可以同时读取资源。写资源时会比对数据检查其他线程有没有更新过该资源,如果未更新就写入资源并更新版本号,否则写资源请求被驳回,重新读取并写资源。
适用于读频繁的应用场景,多线程同时读取能有效提高吞吐量。CAS 算法和版本号机制都是乐观锁,悲观锁的抢占也会利用 CAS 算法。
加入到队列中等待唤醒,先到者先拿到锁。
公平锁不会出现线程饥饿,迟迟无法获取锁的情况。ReentrantLock 可以实现公平锁。
当线程要获取锁时通过两次 CAS 操作去抢锁,如果没抢到加入到队列中等待唤醒。
非公平锁的性能更好。synchronized 是非公平锁,ReentrantLock 默认情况下也是非公平锁。
允许一个线程对同一对象多次上锁。由 JVM 记录对象被线程加锁次数,只有当线程释放掉所有锁(加锁次数为0)时,其他线程才获准进入。
synchronized 和 ReentrantLock 等锁结构都是可重入锁。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。