阿里面试官:说说多线程并发问题
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将缓存的修改操作立即写到主内存
写操作会导致其它 CPU 中的缓存行失效,写之后,其它线程的读操作会从主内存读。
1.5 volatile 的局限性
volatile 只能保证可见性,不能保证原子性写操作对其它线程可见,但是不能解决多个线程同时写的问题。
二、Synchroniz
ed
2.1 Synchronized 使用场景
多个线程同时写一个变量。
例如售票,余票是 100 张,窗口 A 和窗口 B 同时各卖出一张票, 假如余票变量用 volatile 修饰,是有问题的。
A 窗口获取余票是 100,B 窗口获取余票也是 100,A 卖出一张变成 99,刷新回主内存,同时 B 卖出一张变成 99,也刷新回主内存,会导致最终主内存余票是 99 而不是 98。
前面说到 volatile 的局限性,就是多个线程同时写的情况,这种情况一般可以使用 Synchronized。
Synchronized 可以保证同一时刻,只有一个线程可执行某个方法或某个代码块。
2.2 Synchronized 原理
public class SynchronizedTest {
public static void main(String[] args) {
synchronized (SynchronizedTest.class) {
System.out.println("123");
}
method();
}
private static void method() {
}
}
复制代码
将这段代码先用javac
命令编译,再java p -v SynchronizedTest.class
命令查看字节码,部分字节码如下
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/lanshifu/opengldemo/test/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String 123
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: invokestatic #6 // Method method:()V
26: return
复制代码
可以看到 4: monitorenter
和 14: monitorexit
,中间是打印的语句。
执行同步代码块,首先会执行monitorenter
指令,然后执行同步代码块中的代码,退出同步代码块的时候会执行monitorexit
指令 。
使用 Synchronized 进行同步,其关键就是必须要对对象的监视器 monitor 进行获取,当线程获取 monitor 后才能继续往下执行,否则就进入同步队列,线程状态变成 BLOCK,同一时刻只有一个线程能够获取到 monitor,当监听到 monitorexit 被调用,队列里就有一个线程出队,获取 monitor。详情参考:www.jianshu.com/p/d53bf830f…
每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一,所以只要这个锁的计数器大于 0,其它线程访问就只能等待。
2.3 Synchronized 锁的升级
大家对 Synchronized 的理解可能就是重量级锁,但是 Java1.6 对 Synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
偏向锁: 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程 A 访问加了同步锁的代码块时,会在对象头中存 储当前线程的 id,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。
轻量级锁: 在偏向锁情况下,如果线程 B 也访问了同步代码块,比较对象头的线程 id 不一样,会升级为轻量级锁,并且通过自旋的方式来获取轻量级锁。
重量级锁: 如果线程 A 和线程 B 同时访问同步代码块,则轻量级锁会升级为重量级锁,线程 A 获取到重量级锁的情况下,线程 B 只能入队等待,进入 BLOCK 状态。
2.4 Synchronized 缺点
不能设置锁超时时间
不能通过代码释放锁
容易造成死锁
三、ReentrantLock
上面说到Synchronized
的缺点,不能设置锁超时时间和不能通过代码释放锁,ReentranLock
就可以解决这个问题。
在多个条件变量和高度竞争锁的地方,用 ReentrantLock 更合适,ReentrantLock 还提供了Condition
,对线程的等待和唤醒等操作更加灵活,一个 ReentrantLock 可以有多个 Condition 实例,所以更有扩展性。
3.1 ReentrantLock 的使用
lock 和 unlock
ReentrantLock reentrantLock = new ReentrantLock();
System.out.println("reentrantLock->lock");
reentrantLock.lock();
try {
System.out.println("睡眠 2 秒...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
System.out.println("reentrantLock->unlock");
}
复制代码
实现可定时的锁请求:tryLock
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Thread thread1 = new Thread_tryLock(reentrantLock);
thread1.setName("thread1");
thread1.start();
Thread thread2 = new Thread_tryLock(reentrantLock);
thread2.setName("thread2");
thread2.start();
}
static class Thread_tryLock extends Thread {
ReentrantLock reentrantLock;
public Thread_tryLock(ReentrantLock reentrantLock) {
this.reentrantLock = reentrantLock;
}
@Override
public void run() {
try {
System.out.println("try lock:" + Thread.currentThread().getName());
boolean tryLock = reentrantLock.tryLock(3, TimeUnit.SECONDS);
if (tryLock) {
System.out.println("try lock success :" + Thread.currentThread().getName());
System.out.println("睡眠一下:" + Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("醒了:" + Thread.currentThread().getName());
} else {
System.out.println("try lock 超时 :" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("unlock:" + Thread.currentThread().getName());
reentrantLock.unlock();
}
}
}
复制代码
打印的日志:
try lock:thread1
try lock:thread2
try lock success :thread2
睡眠一下:thread2
try lock 超时 :thread1
unlock:thread1
Exception in thread "thread1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.lanshifu.demo_module.test.lock.ReentranLockTest$Thread_tryLock.run(ReentranLockTest.java:60)
醒了:thread2
unlock:thread2
复制代码
上面演示了trtLock
的使用,trtLock
设置获取锁的等待时间,超过 3 秒直接返回失败,可以从日志中看到结果。 有异常是因为 thread1 获取锁失败,不应该调用 unlock。
3.2 Condition 条件
public static void main(String[] args) {
Thread_Condition thread_condition = new Thread_Condition();
thread_condition.setName("测试 Condition 的线程");
thread_condition.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread_condition.singal();
}
static class Thread_Condition extends Thread {
@Override
public void run() {
await();
}
private ReentrantLock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
try {
System.out.println("lock");
lock.lock();
System.out.println(Thread.currentThread().getName() + ":我在等待通知的到来...");
condition.await();//await 和 signal 对应
//condition.await(2, TimeUnit.SECONDS); //设置等待超时时间
System.out.println(Thread.currentThread().getName() + ":等到通知了,我继续执行>>>");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("unlock");
lock.unlock();
}
}
public void singal() {
try {
System.out.println("lock");
lock.lock();
System.out.println("我要通知在等待的线程,condition.signal()");
condition.signal();//await 和 signal 对应
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("unlock");
lock.unlock();
}
}
}
复制代码
运行打印日志
lock
测试 Condition 的线程:我在等待通知的到来...
lock
我要通知在等待的线程,condition.signal()
unlock
测试 Condition 的线程:等到通知了,我继续执行>>>
unlock
复制代码
上面演示了Condition的 await 和 signal
使用,前提要先 lock。
3.3 公平锁与非公平锁
ReentrantLock 构造函数传 true 表示公平锁。
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的顺序。而非公平锁就是一种锁的抢占机制,是随机获得锁的,可能会导致某些线程一致拿不到锁,所以是不公平的。
3.4 ReentrantLock 注意点
ReentrantLock 使用 lock 和 unlock 来获得锁和释放锁
unlock 要放在 finally 中,这样正常运行或者异常都会释放锁
使用 condition 的 await 和 signal 方法之前,必须调用 lock 方法获得对象监视器
四、并发包
通过上面分析,并发严重的情况下,使用锁显然效率低下,因为同一时刻只能有一个线程可以获得锁,其它线程只能乖乖等待。
Java 提供了并发包解决这个问题,接下来介绍并发包里一些常用的数据结构。
4.1 ConcurrentHashMap
我们都知道 HashMap 是线程不安全的数据结构,HashTable 则在 HashMap 基础上,get 方法和 put 方法加上 Synchronized 修饰变成线程安全,不过在高并发情况下效率底下,最终被ConcurrentHashMap
替代。
ConcurrentHashMap 采用分段锁,内部默认有 16 个桶,get 和 put 操作,首先将 key 计算 hashcode,然后跟 16 取余,落到 16 个桶中的一个,然后每个桶中都加了锁(ReentrantLock),桶中是 HashMap 结构(数组加链表,链表过长转红黑树)。
所以理论上最多支持 16 个线程同时访问。
4.2 LinkBlockingQueue
链表结构的阻塞队列,内部使用多个 ReentrantLock
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
/**
Signals a waiting put. Called only from take/poll.
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
复制代码
源码不贴太多,简单说一下LinkBlockingQueue
的逻辑:
从队列获取数据,如果队列中没有数据,会调用
notEmpty.await();
进入等待。
在放数据进去队列的时候会调用
notEmpty.signal();
,通知消费者,1 中的等待结束,唤醒继续执行。
从队列里取到数据的时候会调用
notFull.signal();
,通知生产者继续生产。
在 put 数据进入队列的时候,如果判断队列中的数据达到最大值,那么会调用
notFull.await();
,等待消费者消费掉,也就是等待 3 去取数据并且发出notFull.signal();
,这时候生产者才能继续生产。
LinkBlockingQueue
是典型的生产者消费者模式,源码细节就不多说。
评论