引用的四种类型(栈引用堆内存)
强引用: 不管内存是否够用,或者发生gc操作,都不会被回收,只有所在类被回收,或者主动设置为null,分配的内存才会被回收。
软引用: 只有当内存不足的时候才会被回收,当gc的时候,如果内存足够,也不会被回收。
弱引用:不管内存是否够用,当发生gc的时候,都会被回收。
虚引用:没有使用
线程和进程的定义
进程是一个应用程序,线程是进程的分配内存的最小单位。一个进程可以有多个线程。
线程的启动方式
两种启动线程的方式:Thread类和Runnable接口
区别:Thread是对线程的抽象,Runnable是对任务和业务逻辑的抽象
Thread的知识点
1.线程start()和run()区别
thread.start()是真正开启一个线程,调用native的start0(),并且只能调用一次,否则会抛出异常
thread.run(),就是执行 Thread类 抽象函数run() 方法体,在主线程中运行的。
2.join()让线程之间的执行变成串行执行
3.线程的优先级,默认为5,设置区间为1-10
4.线程分为用户线程和守护线程
用户线程就是我们自己new出来的线程,通过setDaemon() 可以把用户线程,设置为守护线程。
守护线程是系统线程,当用户线程和守护线程都执行结束,系统就退出了。
线程安全
线程共享
多线程是共享进程的资源和内存,当多个线程访问同一份资源进行读写操作时候,可能会导致数据的错乱。
JMM(Java内存模型)是围绕并发过程中如何处理可见性、原子性和有序性这3个特征建立起来的
1)可见性
可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。
在 Java 中 volatile、synchronized 和 final 都可以实现可见性。
2)原子性
原子性指的是某个线程正在执行某个操作时,中间不可以被加塞或分割,要么整体成功,要么整体失败。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
3)有序性
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行
保证线程安全有三种方式:
1.synchronized 同步关键字,内置锁,可以保证在同一时刻只有一个线程对锁住的资源进行读写操作,保证线程的安全性。可以保证数据的原子性,可见性,有序性。
synchronized 可以对方法函数上锁,也可以对代码块加锁。
对代码块加锁的时候,需要指定加锁的范围,如果是synchronized(this){...}表示对当前对象进行加锁,同一时刻,只有一个线程能对当前对象函数或者变量进行操作。但是当锁同一个类的两个不同实例时,也会报错。
2.volatile关键字,是轻量级的同步机制,能保证数据的可见性,有序性,不能保证原子性,所以不保证线程安全。
可见性,就是当一个变量被volatile修饰时,一个线程对数据进行操作,其他的线程能立刻看到最新的数据。不能取代synchronized,不保证线程安全性,适用多个线程读取操作,一个线程写操作。
3.ThreadLocal,能够屏蔽线程,创建线程的副本。但是容易产生内存泄露,造成线程不安全,具体实现待.....
怎么安全的停止线程
stop,suspend,resume,destory 都是停止线程的操作,但是不建议使用。因为这种操作会立即停止线程,有可能造成已分配的资源未被回收,造成内存泄露,或者已分配的锁不能回回收,造成死锁。操作方式比较野蛮,已经被弃用的。
interrupt 中断标志位,不会立即停止线程,只是先打个招呼,把停止的操作交给开发者自己决定。
Thread 中断标志位涉及三个函数:
interrupt() 设置中断标志位,
isInterrupted()返回中断标志位的数据,默认为false;
静态方法 interrupted() 也是返回中断标志位数据,多了一步把标志位清空的操作,就是重新重置为false
阻塞操作,sleep,wait等,抛出InterruptedException
当run函数中执行阻塞操作,用户调用了interrupt(),会被try-catch捕获,这时候返回的interrupted标志位数据为false,需要再次调用interrupt(),才能把标志位变成tru。
自定义标志位,中断线程运行 ,不建议使用
正常的操作可以捕获这种中断,但是当线程中执行阻塞操作时,就不会捕获自定义的中断标志位,必须等阻塞操作结束,才可以中断线程执行。
阻塞队列
生产者和消费者,队列,指定大小,先进先出。生产者往队列里面放产品put(),如果队列满了,那么生产者就阻塞。消费者从队列里面取产品take(),如果队列空了,那么阻塞消费者.
JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue :一个由数组结构组成的**有界**阻塞队列。 先进先出,不知道空数据,进队操作采用了加锁的方式保证并发安全
LinkedBlockingQueue :一个由链表结构组成的**有界**阻塞队列。内部基于链表来存放元素。添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。如果不指定队列的容量大小,也就是使用默认的Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出
PriorityBlockingQueue :一个支持优先级排序的**无界**阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个**不存储元素**的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的**无界**阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的**双向阻塞**队列。
线程池
为什么要使用线程池
1.降低资源消耗
2.提高响应速度
3.提高线程的可管理型
线程池的工作机制
1.当加入一个任务,会先创建核心线程执行任务,当核心线程已经是最大了,就把加入的任务添加到阻塞队列中。
2.当阻塞队列也已经满了,就会创建新的空闲线程(数量=最大线程数量-核心线程数量),用空闲线程去执行任务。
3.当空闲线程也已经最大了,此时就执行饱和策略
4.系统内置4种饱和策略,默认的是AbortPolice,就是抛出异常
线程池的基本参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
1.corePoolSize 核心线程数量
2.maximumPoolSize 最大线程数量
3.keepAliveTime 空闲线程存活时间
4.unit 时间单位
5.workQueue 阻塞队列
6.threadFactory 自定义一些name啥的
7.RejectedExecutionHandler 饱和策略
饱和策略
线程池的饱和策略执行:当核心线程和空闲线程都是最大值,并且正在工作,而且阻塞队列也已经处于饱和的状态,这时候来了新的任务,就会执行相应的饱和策略。
线程池内部提供四种饱和策略:
- AbortPolicy .也是默认饱和策略,新任务提交时直接抛出异常RejectedExecutionException,该异常可由调用者捕获。
2.CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。谁发送的任务谁来执行。
3.Discard策略:新提交的任务被抛弃。
4.DiscardOldest策略:把阻塞队列中最后一个任务扔掉,加入新进来的任务。
5.自定义饱和处理策略
系统内置的线程池
CacheThreadPool 没有核心线程,最大线程数量不限制,空闲线程存活时间是60s,使用SynchronousQueue队列,不能存放元素,默认饱和策略。
所以缓存线程池,只能使用空闲线程处理任务,
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
newSingleThreadExecutor核心线程数量和最大数量都是1,采用有边界的链式阻塞队列。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
newSingleThreadScheduledExecutor 对ScheduledThreadPoolExecutor 一个封装,只有一个核心线程
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
newFixedThreadPool 核心线程数量和最大数量一致,没有保活时间,采用链式有边界阻塞队列
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
终止线程池
shutdown() ---> 不再接受新的任务,并且把当前没有执行任务的线程执行中断操作,正在执行任务的线程还会继续运行。
shutdownNow() --->不再接受新的任务,并且把所有的线程都执行中断操作,不管是否正在执行任务。
合理分配线程池
从应用的三个方面考虑:
1.Cpu密集型, 执行大量的计算操作,核心线程的数量,等于 cpu的核心数
2.IO密集型, 执行网络操作和磁盘操作,核心线程的数量,等于 cpu核心数*2
3.混合型, 随便配置吧
并发编程 面试题
synchronized 修饰普通函数和修饰静态函数的区别?
对象锁和类锁的区别,修饰普通函数,是锁住的整个实例对象。类锁 锁住的是class类。
volatile能否保证线程安全,在DCL上的作用是什么
不能保证线程的安全,volatile是轻量级的同步机制,只能保证数据可见性和排序性,不能保证原子性,所以保证不了线程安全。
在DCL(单例的双重检测)上的作用是 防止指令重排,保证创建对象的执行顺序。
1.分配内存,2实例化对象instance,3把实例化的对象instance 的引用指向已分配的内存空间,这样instance就有了内存地址,不会再为null。
volatile能保证instance要么为null,要不已经初始化结束。
sleep wait yield 的区别,wait线程如何唤醒。
sleep :让当前线程暂停,进入阻塞状态,不会释放当前线程所持有的锁
wait :通常用于线程间的交互,会释放当前线程持有的锁,当被唤醒后,需要重新竞争锁。
yield:当前线程让出cpu的占有,但是不会释放持有的锁。
wait线程如何唤醒:notify,notifyall
线程的生命周期
线程有6中状态:开始,就绪,阻塞,运行,等待,结束
ThreadLocal是什么
ThreadLocal 是线程的一个本地变量,是一个特殊变量,为每个线程提供一个变量的副本,使得每一个线程在同一时间访问到的都是不同的对象,这样就隔离了线程之间对数据的一个共享访问。在内部实现上,每个threaLocal内部都有一个ThreadLocalMap,用来保存每一个线程所拥有的变量副本。
线程间的通信方式
1.handle通信
2.runOnUiThread方法
3.AsyncTask
4.第三方库 eventBus,sharepreference,磁盘,数据库