常见问题
1)synchronize的原理
2)谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
3)static synchronized 方法的多线程访问和作用
4)如何控制某个方法允许并发访问线程的个数?
5)两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
6)线程间操作List
7)什么导致线程阻塞?
8)同一个类里面两个synchronized方法,两个线程同时访问的问题
9)volatile的原理
10)lock的原理
11)死锁的四个必要条件?怎么避免死锁?
12)什么是线程池,如何使用?
13)ReentrantLock的内部实现
14)ReentrantLock 、synchronized和volatile比较
15)synchronized 与 Lock的区别
16)对象锁和类锁是否会互相影响?
17)synchronized 和volatile 关键字的区别
一. 线程
线程的状态
NEW(新建):创建后尚未启动的线程的状态
RUNNABLE(运行):包含Running和Ready
BLOCKED:等待获取排它锁
WAITING(无限期等待):不会被分配CPU执行时间,需要显式被唤醒
没有设置 Timeout 参数的 Object.wait()方法
没有设置 Timeout 参数的 Thread.join()方法
LockSupport.park()方法
TIMED_WAITING(限期等待):在一定时间后会由系统自动唤醒
Thread.sleep()
设置了Object.wait()方法
设置了Timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUnit()方法
TERMINATED:
已终止线程的状态,线程已经结束执行。
如何实现处理线程的返回值
- 主线程等待法
- 使用Thread类的join() 阻塞当前线程以等待子线程处理完毕
-
通过Callable接口实现:通过FutureTask Or 线程池获取
sleep 和 wait的区别
- sleep 是Thread的方法,wait是Object的方法
- sleep 方法可以在任何地方使用
- wait方法只能在synchronized方法或synchronized块中执行
最主要的本质区别 - Thread.sleep 只会让出CPU,不会导致锁行为的改变
- Object.wait 不仅让出CPU,还会释放已经占有的同步资源锁
notify和notifyAll的区别
首先两个概念
- 锁池 EntryList
- 等待池 WaitSet
锁池
:假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。
等待池
:假设线程 A 调用了某个对象的 wait() 方法,线程 A 就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
- notifyAll 会让所有出入等待池中的线程全部进入锁池去竞争获取锁的机会。
- notify 只会随机选取一个处于等待吃中的线程进入锁池去竞争获取所的机会。
yield
概念:当调用Thread.yield() 函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度可能会忽略这个暗示。
如何中断线程
-
调用interrupt(),通知线程应该中断了。
1)如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
2)如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。
线程状态图
二. 并发工具类
J.U.Ctools
- 闭锁 CountDownLatch
- 栅栏 CyclicBarrier
- 信号量 Semaphore
- 交换器 Exchanger
locks
atomic
collections
executor
CountDownLatch
:让主线程等待一组事件发生后继续执行。
事件指的是CountDownLatch里的 countDown() 方法
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
new CountDownLatchDemo().go();
}
private void go() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(new Task(countDownLatch), "Thread1").start();
Thread.sleep(1000);
new Thread(new Task(countDownLatch), "Thread2").start();
Thread.sleep(1000);
new Thread(new Task(countDownLatch), "Thread3").start();
countDownLatch.await();
System.out.println("所有线程已到达,主线程开始执行" + System.currentTimeMillis());
}
class Task implements Runnable {
private CountDownLatch countDownLatch;
public Task(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已经到达+" + System.currentTimeMillis());
countDownLatch.countDown();
}
}
}
运行结果
线程Thread1已经到达+1563158946909
线程Thread2已经到达+1563158947913
线程Thread3已经到达+1563158948917
所有线程已到达,主线程开始执行1563158948918
CyclicBarrier
:阻塞当前线程,等待其他线程。
- 所有线程必须同时到达栅栏位置后,才能继续执行;
- 所有线程到达栅栏处,可以触发执行另外一个预先设置的线程。
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
new CyclicBarrierDemo().go();
}
private void go() throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
new Thread(new Task(cyclicBarrier), "Thread1").start();
Thread.sleep(1000);
new Thread(new Task(cyclicBarrier), "Thread2").start();
Thread.sleep(1000);
new Thread(new Task(cyclicBarrier), "Thread3").start();
}
class Task implements Runnable {
private CyclicBarrier cyclicBarrier;
public Task(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已经到达" + System.currentTimeMillis());
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "开始处理" + System.currentTimeMillis());
}
}
}
运行结果
线程Thread1已经到达1563160000457
线程Thread2已经到达1563160001460
线程Thread3已经到达1563160002465
线程Thread3开始处理1563160002465
线程Thread1开始处理1563160002465
线程Thread2开始处理1563160002466
Semaphore
:控制某个资源可被同时访问的线程个数
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
@Override
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing:" + NO);
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
exec.shutdown();
}
}
Exchanger:两个线程到达同步点后,相互交换数据。
Exchanger
BolockingQueue:提供可阻塞的入队和出队操作。
BlockingQueue
BlockingQueue
主要用于生产者-消费者模式中,在多线程场景时生产者线程在队列尾部添加元素,而消费者线程则在队列头部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。
1、ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列;
2、LinkedBlockingQueue:一个由链表结构组成的有界/无界阻塞队列;
3、PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列;
4、DelayQueue:一个使用优先级队列实现的无界阻塞队列;
5、SynchronousQueue:一个不存储元素的阻塞队列;
6、LinkedTransferQueue:一个由链表结构组成的无界阻塞队列;
7、LinkedBlockingQueue:一个由链表组成的双向阻塞队列;
三. 线程安全
线程安全的主要诱因
- 存在共享数据(也称临界资源)
- 存在多线程共同操作这些共享数据
解决问题的根本方法:同一时刻有且只有一个线程在操作共享数据,其它线程必须等到该线程处理完数据后再对共享数据进行操作。
synchronized
互斥锁的特性:
互斥性
:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。
可见性
:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized
锁的不是代码,锁的都是对象。
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种方法:
1、同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号() 中的实例对象
2、同步非静态方法(synchronized method),锁是当前对象的实例对象。
获取类锁的两种方法:
1、同步代码块(synchronized(类.class)),锁是小括号()中的类对象(Class对象)
2、同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)
对象锁和类锁的总结
1、有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
2、若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的代码会被阻塞;
3、若锁住的是同一个对象,一个对象在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
4、若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程也会被阻塞,反之依然;
5、同一个类的不同对象的对象锁互不干扰;
6、类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
7、类锁和对象锁互补干扰;
synchronized底层实现原理
对象在内存中的布局
- 对象头
- 实例数据
- 对齐填充
什么是重入?
从互斥锁的设计上说,当一个线程试图操作一个由其它线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。
synchronized太重吗? - 早期版本中,synchronized 属于重量级锁,依赖于Mutex Lock实现
- 线程之间的切换需要从用户态转换到核心态,开销较大
- Java6以后,synchronized 性能得到了很大提升。Adaptive Spinning、Lightweight Locking、Lock Eliminate、Biased Locking、Lock Coarsening...
自旋锁与自适应自旋锁
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU
- 缺点:若锁被其它线程长时间占用,会带来许多性能上的开销
自适应自旋锁
- 自旋的次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
更彻底的优化
JIT编译时,对运行上下文进行扫描,取出不可能存在竞争的锁
锁粗化
通过扩大加锁的范围,避免反复加锁和解锁。
synchronized的四种状态
- 无锁、偏向锁、轻量级锁、重量级锁
锁膨胀方向:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁:减少统一线程获取锁的代价 CAS(compare and swap)
-
大多情况下,锁不存在多线程竞争,总是由统一线程多次获得
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于 Mark Word的ThreadID 即可,这样就省去了大量有关锁申请的操作。
不适用与锁竞争比较激烈的多线程场合。
A操作的结果需要对B操作可见,则A与B存在happens-before关系。
网友评论