多线程并发必须掌握的相关概念:
多线程:Thread、ThreasPool
线程安全:volatile ,synchonized,lock,CAS,ThreadLocal
线程通信:synchonized+wait/notify;lock+condition;生产者与消费者
另外需要了解还有:JVM种锁的优化等
1、开启多线程的三种方式
- 继承Thread,重写run方法
- 实现Runnable接口,作为new Thread创建线程时作为线程的target
- 实现call接口,创建FutureTask包装call接口,把FutureTask作为线程的Target(FutureTask实现了Runnable接口)
https://blog.csdn.net/longshengguoji/article/details/41126119
2 Thread的run()和start()方法的区别
- 调用start会开启一个新的线程,并在新线程种执行run方法。
- 调用run不会开启新线程,就是一个普通方法,在原来线程种执行。
https://blog.csdn.net/longshengguoji/article/details/41126119
3、多线程不安全的原因
- 线程不安全的诱因有3个:
1.有共享变量
2.处在多线程环境下
3.对共享变量有修改操作(只有读操作不会造成线程不安全) - 结合JMM来说,变量存储在主内存,线程对变量的操作需要将变量从主内容复制到自己的工作内存,操作后再写回主内存。这样就会存在一个问题:线程A修改共享变量x的值,还未写回主内存,这是线程B也对x进行操作,但此时A线程中x对线程B时不可见的。
4、死锁
- 原因:两个线程持有锁,却又在等待对方释放锁。
- 解决方法: 在写代码时,要确保线程在获取多个锁时采用一致的顺序。
a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。
//可能发生静态锁顺序死锁的代码
class StaticLockOrderDeadLock {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void a() {
synchronized (lockA) {
synchronized (lockB) {
System.out.println("function a");
}
}
}
public void b() {
synchronized (lockB) {
synchronized (lockA) {
System.out.println("function b");
}
}
}
}
https://github.com/LRH1993/android_interview/blob/master/java/concurrence/deadlock.md
5、volatile
- 既然有操作系统底层保证了缓存一致性原则为什么还会导致内存不可见?
各个缓存会监视别的缓存中的数据,但是由于readBufer/loadBuffer的存在,数据在cpu/寄存器上修改后不会立即刷新到缓存/内存中,缓存中数据没有变化,所以导致内存不可见。 - 指令重排序
编译器和cpu都会进行指令重排序,在不影响单线程运行结果的情况下,对于没有依赖的数据的的读写操作会进行重排序。 - volatile的作用
使用volatile修饰变量相当于在插入了一个内存屏障,内存屏障能够保证2点:
1、禁止cpu和编译器进行指令重排,内存屏障前的指令不会在内存屏障后面执行。
2、强制把缓存中的数据刷新到主内存中,这个操作会导致其他缓存中的数据无效。 - volatile为什么不能保证线程安全(原子性)
以i++自增为例,volatile修饰变量i,可以保证内存可见性,但是i++并不是原子操作,该操作先是读值,再加1,然后在把输入刷新到内存中。 - volatile的使用场景
1、CAS,利用其可见性。
2、DCL,利用其禁止指令重排。
http://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/javazejian/article/details/72772461
https://www.jianshu.com/p/506c1e38a922
https://tech.meituan.com/java-memory-reordering.html
6、synchonized的作用及底层原理
- synchronized 关键字有三种应用方式:
1 修饰实例方法,作用与当前的实例加锁,进入同步代码钱要获得当前实例的锁。
2 修饰静态方法,作用于当前类的class对象加锁,进入同步代码前要获得当前类对象的锁
3 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - synchronized 底层原理
对象在内存中布局除了实例数据,还有对象头,对象头有一个指针,指向monitor对象。每个对象都存在一个monitor与之关联。线程进入同步方法或者同步代码块需要获取monitor的监视权。
使用synchronized 修饰代码块时会在代码块前面插入MonitorEnter指令,在代码块后面插入Monitor插入MonitorExit指令。MonitorEnter指令会去尝试获取monitor的持有权,monitor对象有个字段count,计数器,当count等于0时表示当前没有任何线程持有monitor,那么该线程就能成功获取锁,执行同步代码块。
同步方法的底层原理和同步代码块一致。
https://blog.csdn.net/javazejian/article/details/72828483#synchronized%E7%9A%84%E5%8F%AF%E9%87%8D%E5%85%A5%E6%80%A7
6、Lock
- ReentrantLock,可重入锁,可重入锁分为2种,公平锁和非公平锁,底层实现都是AQS。
AQS有个成员变量state,用来控制同步状态。当state=0,表示当前没有线程持有该锁,否则说明有线程持有该变量,其他尝试获取锁的线程的线程进入队列中,节点进入队列后会处于一个自旋过程(死循环过程),如果节点的前驱节点是头节点,则尝试获取锁,如果不是则挂起线程,符合队列先进先出。
公平锁与非公平锁的区别是公平锁先判断队列中是否有线程等待,有的话加入到队列尾部,没有才尝试获取锁,而非公平锁直接尝试获取锁。
https://blog.csdn.net/javazejian/article/details/75043422
https://blog.csdn.net/jiangjiajian2008/article/details/52226189
https://www.jianshu.com/p/e4301229f59e
7、ThreadLocal
- 线程局部变量实现线程安全的原理是:
线程Thread类中有个变量是ThreadLoaclMap,ThreadLocalMap可以看作是一个哈希表,key是ThreadLocal,value则是调用set设置进来的值。每个线程维护自己的ThreadLocal,实现线程数据的隔离,实现线程安全。 - ThreadLocalMap跟HashMap的区别:
1、处理Hash冲突的方法:ThreadLocal是线性探测法,即找到数组的下一个null位置,作为插入点。采用线性探测法的原因是:key的hash值分布均匀冲突比较少,能快速找到插入的位置。
2、ThreadLocalMap的key是ThreadLocal的弱引用对象,使用弱引用的原因是解绑Thread和ThreadLocal的生命周期,如果采用强引用的话则Thread存在的周期内,即使ThreadLocal已经没有使用到,ThreadLocal仍然无法垃圾回收。使用弱引用ThreadLocal会造成内存泄漏,因为ThreadLocal被回收,但是ThreadLocalMap到value存在引用,即使key为null,value也无法回收。为了避免这种情况,调用get,set等方法的过程,会去检查key是否为null,如果为null则回收该位置的Entry,另外一种方法避免内存泄漏的方法,使用完后手动调用remove方法。
https://www.cnblogs.com/micrari/p/6790229.html
https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html
8、CAS
- CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下:
CAS包括三个值:预期值,旧值,新值,如果预期值等于旧值,则把旧值改为新值。 - ABA问题:带上时间戳
https://blog.csdn.net/javazejian/article/details/72772470
9、锁的优化
首先偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。
为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
http://www.importnew.com/21353.html
10、线程池
- 原理
①如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
②如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
③由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
④如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。 - 4种线程池
(1)newSingleThreadExecutor
(2)newFixedThreadPool
这两种线程池核心线程固定(newSingleThreadExecutor是一个核心线程,newFixedThreadPool是n个),线程池个数等于核心线程个数,任务队列无限。
(3)newCachedThreadPool
没有核心线程,阻塞队列为0,线程个数无限,,即是提交一个任务则会开启一个线程执行,空闲 60s后销毁。
(4)newScheduledThreadPool
定时执行任务 - 创建线程池核心线程个数的选择
(1) CPU密集型任务:线程池中线程个数应尽量少,如配置N+1个线程的线程池。
(2) IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。
https://github.com/LRH1993/android_interview/blob/master/java/concurrence/thread-pool.md
11、生产者消费者模型
- wait/notify/notifyAll
static class ProduceThread implements Runnable {
Resource resource;
public ProduceThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while(true) {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
resource.produce(new Object());
}
}
}
static class ComsumeThread implements Runnable {
Resource resource;
public ComsumeThread(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while(true) {
try {
Thread.sleep((long) (1000 * Math.random()));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
resource.consume();
}
}
}
static class Resource {
private int num = 0;
private int size;
private Object[] list;
private ReentrantLock lock = new ReentrantLock();
private Condition produceCon = lock.newCondition();
private Condition comsumeCon = lock.newCondition();
public Resource(int size) {
this.size = size;
this.list = new Object[size];
}
public void produce(Object o) {
try {
lock.lock();
if (num < size) {
list[num++] = o;
System.out.println("produce " + num);
comsumeCon.signalAll();
} else {
produceCon.await();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
public Object consume() {
Object o = null;
try {
lock.lock();
if (num > 0) {
o = list[--num];
list[num] = null;
System.out.println("consume " + num);
produceCon.signalAll();
} else {
comsumeCon.await();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
return o;
}
}
//测试
public static void main(String[] args) {
Resource resource = new Resource(10);
Thread produceThread1 = new Thread(new ProduceThread(resource));
Thread produceThread2 = new Thread(new ProduceThread(resource));
Thread comsumeThread1 = new Thread(new ComsumeThread(resource));
Thread comsumeThread2 = new Thread(new ComsumeThread(resource));
produceThread1.start();
produceThread2.start();
comsumeThread1.start();
comsumeThread2.start();
}
11、线程执行相关
- AABB问题
多线程,5个线程内部打印hello和word,hello在前,要求提供一种方法使得5个线程先全部打印出hello后再打印5个word。
static class MyRunnable implements Runnable{
@Override
public void run() {
synchronized (o) {
System.out.println("hello");
nextHashCode.getAndAdd(1);
System.out.println(nextHashCode.get());
if(nextHashCode.get() < 5) {
try {
o.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
o.notifyAll();
}
System.out.println("world");
}
}
}
- ABCABC
建立三个线程A、B、C,A线程打印10次字母A,B线程打印10次字母B,C线程打印10次字母C,但是要求三个线程同时运行,并且实现交替打印,即按照ABCABCABC的顺序打印。
public class ABC_Condition {
private static Lock lock = new ReentrantLock();
private static Condition A = lock.newCondition();
private static Condition B = lock.newCondition();
private static Condition C = lock.newCondition();
private static int count = 0;
static class ThreadA extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 0)//注意这里是不等于0,也就是说在count % 3为0之前,当前线程一直阻塞状态
A.await(); // A释放lock锁
System.out.print("A");
count++;
B.signal(); // A执行完唤醒B线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 1)
B.await();// B释放lock锁,当前面A线程执行后会通过B.signal()唤醒该线程
System.out.print("B");
count++;
C.signal();// B执行完唤醒C线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 10; i++) {
while (count % 3 != 2)
C.await();// C释放lock锁
System.out.print("C");
count++;
A.signal();// C执行完唤醒A线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}
https://blog.csdn.net/xiaokang123456kao/article/details/77331878
- 多个线程如何顺序执行
https://blog.csdn.net/quliangmao/article/details/78666417