JMM基础
原子性:
原子性是指一个操作是不可中断的,即使在多个线程同时执行的情况下,一个操作一旦开始,就不会被其他线程干扰。对于基本数据类型(如int),在大多数情况下,它们的读写操作是原子性的。然而,对于long型数据,在32位JVM系统中,其读写操作不是原子性的,因为long有64位。这意味着,如果两个线程同时对long进行写入(或读取)操作,它们之间的结果可能会受到干扰。
为了避免这种情况,可以使用volatile关键字或者java.util.concurrent.atomic包中的原子类(如AtomicLong)来确保long型数据的读写操作具有原子性
可见性
对于一个线程A的修改变量t,其他线程B可以及时看到最新的t的值。但是由于以下情况,B无法及时看到最新修改的t的值:
- 线程A修改值没有及时写入内存
- 线程B从cpu cache中读值
- 指令重排导致
对于3,举个例子
public class Example {
static int a = 0;
static boolean flag = false;
public static void writer(){
a += 1;
flag = true;
}
public static void reader(){
if(flag){
System.out.println(a);
}
}
}
假设有线程A执行wirter,B执行reader, 由于第6-7行可能发生指令重排,所以B线程在12行打印出的a不能保证一定是1,也可能是0;
有序性
对于一个线程来说,它看到的指令执行顺序一定是一致的 (否则的话我们的应用根本无法正常工作)。也就是说指令重排是有一个基本前提的, 就是保证串行语义的一致性。指令重排不会使串行的语义逻辑发生问题。因此,在串行代码中, 大可不必担心。
注意:指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
需要关注的关键字/API
Object对象的wait和notify
Object.wait()和 Thread.sleep()方法都可以让线程等待若干时间。除了 wait()可以被唤醒外,另外一个主要区别就是 wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。
思考:
1.为什么java中wait方法需要在synchronized的方法中调用?
答:调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。
2.为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?
答:程调用notify(),notifyAll()方法是将等待队列的线程转移到入口队列,然后让他们竞争锁,所以这个调用线程本身必须拥有锁
详见:面试突击24:为什么wait和notify必须放在synchronized中? - 磊哥|www.javacn.site - 博客园 (cnblogs.com)
Volatile
不保证原子性。详见:www.cnblogs.com/zhengbin/p/…
ReentrantLock
思考:
1.condition的await是否会释放线程占有的reentrantLock? 答:会的。
2.为什么condition的await方法建议被try,catch处理?
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Runnable task = ()->{
lock.lock();
System.out.printf("Thread.currentThread().getName()"+ Thread.currentThread().getName());
try {
condition.await();
} catch (InterruptedException e) {
// 程序在运行8行时发生中断异常,则不会释放lock的锁。
// 详见condition.await()方法源码。故需要主动释放锁
lock.unlock();
throw new RuntimeException(e);
}
};
Thread t1 = new Thread(task,"t1");
Thread t2 = new Thread(task,"t2");
t1.start();
// 主线程休眠2s,可以看到t2线程也可以成功运行第6行,说明t1线程在第8行释放锁了
Thread.sleep(2000);
t2.start();
}
必看经典用法,ArrayBlockingQueue源码。
信号量:允许多个线程访问一个资源
读写锁:允许读读并行的重入锁
CountDownLaunch:等待所有线程执行完成再执行主线程;CyclicBarry:支持循环多次使用。每次达到计算器个数后执行任务。
线程阻塞工具:LockSupprot
LockSupport 可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况。和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出 InterruptedException 异常.
好处:先unpark()操作再park()操作,也能保证线程不会被阻塞。因为LockSupport 类使用类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么park()函数会立即返回,并且消费这个许可(也就是将许可变为不可用),如果许可不可用,就会阻塞。而unpark()则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。
所以,线程阻塞优先使用LockSupprot.park,unpark
下期更新系列2:进阶用法,主要介绍线程池和并发容器