Java多线程知识点
基础概念
进程和线程的区别?多线程有什么好处?
进程是操作系统资源分配的最小单位,线程是进程的一个实体,是cpu调度和分配的基本单元,同一个进程的线程共享内存空间,一个线程的操作是会影响另一个线程的。
进程:正在进行中的程序(直译)。
线程:就是进程中一个负责程序执行的控制单元
- 一个进程中可以多执行路径,称之为多线程,一个进程中至少要有一个线程。
- 开启多个线程是为了同时运行多部分代码。 每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。
- 多线程好处:解决了多部分同时运行的问题。
- 什么时候使用多线程?当需要多部分代码同时执行的时候,可以使用。
编写多线程程序有几种实现方式?
一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。Runnable不是线程,是线程里运行的代码
class Demo implements Runnable// extends Fu
//准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行
{
public void run() {
show();
}
public void show() {
for (int x = 0; x < 20; x++) {
System.out.println(Thread.currentThread().getName() + "....." + x);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
为什么 stop ()方法被废弃而不被使用呢?
原因是 stop ()方法太过于暴力,会强行把执行一半的线程终止。这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下。
使用 boolean 类型的变量,来终止线程或者使用 interrupt
interrupt 其实不是终止,需要自己判断 isInterrupt
interrupt 对线程进行终断,但并不是真真正正的终止一个线程,而是发给线程的一个终断标志位,当调用这个方法的时候,其实就是给线程打了一个招呼,不代表这个线程就要立即停止工作,而且这个线程完全可以不理会这个终断请求(也就是说 JDK 中,线程是协作式的,而不是所谓的抢占式);
启动一个线程是调用 run ()还是 start ()方法?
启动一个线程是调用 start ()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,这并不意味着线程就会立即运行。run ()方法是线程启动后要进行回调(callback)的方法。
一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器(锁),那么这个对象监视器会被立即释放
守护进程
t1.start ();
t2.setDaemon (true); // setDameon 是守护线程,可以理解为后台线程,你停我也停。前台必须手动结束
t2.start ();
多次 start 一个线程会怎么样
会抛出 java. lang. IllegalThreadStateException 线程状态非法异常
Android 一个线程占用多大内存?
在 iava 中每个线程需要分配线程内存,用来存储自身的线程变量
在 idk 1.4 中每个线程是 256 K 的内存,在 idk 1.5 中每个线程是 1 M 的内存。在 java 中每 new 一个线程,jvm 都是向操作系统请求 new 一个本地线程,此时操作系统会使用剩余的内存空间来为线程分配内存,而不是使用 jvm 的内存。
当操作系统的可用内存越少,则 jvm 可用创建的新线程也就越少
状态转换
线程的基本状态以及状态之间的关系?
创建并运行线程:
- 新建状态(New Thread):在Java语言中使用new 操作符创建一个线程后,该线程仅仅是一个空对象,它具备类线程的一些特征,但此时系统没有为其分配资源,这时的线程处于创建状态。线程处于创建状态时,可通过Thread类的方法来设置各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。
- 就绪状态(Runnable):使用start()方法启动一个线程后,系统为该线程分配了除CPU外的所需资源,使该线程处于就绪状态。此外,如果某个线程执行了yield()方法,那么该线程会被暂时剥夺CPU资源,重新进入就绪状态。
- 运行状态(Running):Java运行系统通过调度选中一个处于就绪状态的线程,使其占有CPU并转为运行状态。此时,系统真正执行线程的run()方法。
可以通过Thread类的isAlive方法来判断线程是否处于就绪/运行/阻塞状态:当线程处于就绪/运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于阻塞状态,也可能处于停止状态。
-
阻塞和唤醒线程阻塞状态(Blocked):一个正在运行的线程因某些原因不能继续运行时,就进入阻塞 状态。这些原因包括:
a) 当执行了某个线程对象的sleep()、join方法等阻塞类型的方法时,该线程对象会被置入一个阻塞集内,等待超时而自动苏醒。
b) 当线程执行了某个对象的wait()方法时,线程会被置入该对象的等待集中,知道执行了该对象的notify()方法wait()/notify()方法的执行要求线程首先获得该对象的锁。 -
死亡状态 (Dead):线程在 run ()方法执行结束后进入死亡状态。此外,如果线程执行了 interrupt ()或 stop ()方法,那么它也会以异常退出的方式进入死亡状态。
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:
“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;
而“等待状态”则是在等待一段时间,或者唤醒动作的发生。
在程序等待进入同步区域的时候,线程将进入这种状态。
Wait
wait 和 sleep 区别?
sleep来自Thread类,和wait来自Object类
调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒
wait和notify
wait:使一个线程处于等待(阻塞/冻结)状态,并且释放所持有的对象的锁,让其他线程可以进入Synchronized 数据块,当前线程被放入对象等待池中;是 Object 的方法,必须与 synchronized 关键字一起使用
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
标准写法,打印前得先初始化:
用法
- 在未达到目标时 wait ()
- 用 while 循环检查
- 设置完成后 notifyAll ()
- wait () 和 notify () / notifyAll () 都需要放在同步代码块里
- wait是进入一个同步的方法中,释放锁,用完后唤醒,让另一个线程进行操作。跟锁有关系
join
join(插队):一种特殊的 wait,当前运行线程调用另一个线程的 join 方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。注意该方法也需要捕捉异常。
线程的 sleep ()方法和 yield ()方法有什么区别?
- sleep ()方法给其他线程运行机会时不考虑线程的优先级,因此可能会给低优先级的线程以运行的机会;yield ()方法只会给相同优先级或更高优先级的线程以运行的机会; Thread.setPriority (Thread. MAX_PRIORITY);
- 线程执行 sleep ()方法后转入阻塞(blocked)状态,而执行 yield ()方法后转入就绪(Runnable)状态。
- yield () 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield () 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
- sleep ()方法声明抛出 InterruptedException,而 yield ()方法没有声明任何异常
线程池
线程池的好处
- 因为不需要每次处理复杂逻辑耗时操作都创建一个线程,比如加载网络,避免了线程的创建和销毁所带来的性能开销和消耗的时间,能有效控制线程池的最大并发数,避免了大量线程间抢占资源而导致的阻塞现象
- 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能(ExecutorService 是安全的)
- 避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
线程池相关方法
- Java为我们提供了 ExecutorService 线程池来优化和管理线程的使用。
- Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,他们的内部其实是通过:ThreadPoolExecutor
- 既然线程池就是ThreadPoolExecutor,所以我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {...}
线程池的参数
-
核心线程数,核心线程会一直存活,及时没有任务需要执行,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;核心线程数默认是0,需要自己去实现。
-
表示在线程池中最多能创建多少个线程;
-
如果线程池没有要执行的任务 存活多久
-
存活时间的单位
-
如果 线程池里管理的线程都已经用了,剩下的任务 临时存到LinkedBlockingQueue对象中排队,先进先出
-
threadFactory
线程工厂
,如何去创建线程的,可以自定义线程创建的执行者,他们有适当的线程名称、优先级,甚至他们还可以守护进程。
Java 自己的线程池
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 。
- 如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
自定义线程池ThreadPoolExecutor
自定义ThreadManager类管理多线程,例如
- 请求网络数据线程交由长时间任务线程池执行
- 访问数据库交由短时间任务线程池执行
- 图片下载任务将由单任务线程池执行
- 20个图片都做一些操作,创建固定线程池
开启线程数一般是cpu的核数* 2+1
Executors弊端
Executors的4个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors的4个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
LinkedBlockingQueue
在 Java 多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列 (先进先出)。Java 提供的线程安全的 Queue 可以分为阻塞队列(安全)和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子 ConcurrentLinkedQueue(也是安全的,cas)。
LinkedBlockingQueue 是线程安全的队列,通过 ReentrantLock 保证的。(链表实现的队列)
由于 LinkedBlockingQueue 实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是 Integer. MAX_VALUE,其中主要用到 put 和 take 方法,put 方法在队列满的时候会阻塞直到有队列成员被消费,take 方法在队列空的时候会阻塞,直到有队列成员被放进来。
2 的 31 次方 - 1
有界无界
如果我们要实现一个无界队列,也就是说,队列的大小事先不确定,理论上可以支持无限大。这种情况下,我们适合选用链表来实现队列。因为链表支持快速地动态扩容。
如果我们要实现一个有界队列,也就是说,队列的大小事先确定,当队列中数据满了之后,生产者就需要等待。直到消费者消费了数据,队列有空闲位置的时候,生产者才能将数据放入。
代码
newFixedThreadPool
创建一个固定线程数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:控制线程最大并发数。
newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。
newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
应用场景:执行大量、耗时少的任务。
newScheduledThreadPool
创建一个可以定时或者周期性执行任务的线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
应用场景:执行定时或周期性的任务。
优化方案
内存优化方案1:
- 保持核心线程始终存活
- 将非核心线程的存活时间延长,避免频繁创建新的线程
可以避免频繁创建线程的内存分配负担和内存碎片问题
内存优化方案2:
- 核心线程也可以回收
- 非核心线程存活时间变短,及时回收资源
可以避免内存峰值过高的问题,及时回落
public static final int MAX_CPU_COUNT = CPU_COUNT * 2 + 1;
public static final long KEEP_ALIVE_TIME_IN_MINUTES = 30L;
降低线程数量并不是说不用线程或少用线程,而是让线程在空闲的时候自动销毁,基于这一理论,通过替换创建线程池的方法,或者直接修改线程池的参数:
将 corePoolSize 设置为 0
为 maxPoolSize 设置上限 MAX_CPU_COUNT
Callable、FutureTask
Java并发Concurrent包——Callable/Future/FutureTask解析
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
省了自己写回调了
public interface Callable<V> {
// 计算结果,如果无法计算结果,则抛出一个异常
V call() throws Exception;
}
FutureTask、RunnableFuture相比runnable有结果的返回
看看:AsyncTaskInstance
public static class CountTask implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("子线程开始计算");
Thread.sleep(5000);
System.out.println("子线程结束计算,共用时5秒");
return 100;
}
}
ExecutorService executor = Executors.newCachedThreadPool();
// Future的使用
//Future<Integer> result = executor.submit(new CountTask());
// FutureTask的使用
FutureTask<Integer> futureTask = new FutureTask<>(new CountTask());
executor.submit(futureTask);
executor.shutdown();
// Future获取结果
//Integer i = result.get();
// futureTask获取结果
Integer i = futureTask.get();
生产者消费者
多个线程在处理同一资源,但是任务却不同。
生产者和消费者在同一时间段内共用同一个存储空间,生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况:
存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品。互相等待,从而发生死锁。
wait()和notify()方法的实现
缓冲区满和为空时都调用wait()方法等待,当生产者生产了一个产品或者消费者消费了一个产品之后会唤醒所有线程。
public class Test1 {
private static Integer count = 0;
private static final Integer FULL = 10;
private static String LOCK = "lock";
public static void main(String[] args) {
Test1 test1 = new Test1();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
new Thread(test1.new Producer()).start();
new Thread(test1.new Consumer()).start();
}
class Producer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == FULL) {
try {
LOCK.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
count++;
System.out.println(Thread.currentThread().getName() + "生产者生产,目前总共有" + count);
LOCK.notifyAll();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
while (count == 0) {
try {
LOCK.wait();
} catch (Exception e) {
}
}
count--;
System.out.println(Thread.currentThread().getName() + "消费者消费,目前总共有" + count);
LOCK.notifyAll();
}
}
}
}
}
死锁
指多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。它们中的一个或者全部都在等待某个资源被释放。
public class DeadlockTest {
public static void main(String[] args) {
String str1 = new String("资源1");
String str2 = new String("资源2");
new Thread(new Lock(str1, str2), "线程1").start();
new Thread(new Lock(str2, str1), "线程2").start();
}
}
class Lock implements Runnable {
private String str1;
private String str2;
public Lock(String str1, String str2) {
super();
this.str1 = str1;
this.str2 = str2;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "运行");
synchronized (str1) {
System.out.println(Thread.currentThread().getName() + "锁住"+ str1);
Thread.sleep(1000);
synchronized (str2) {
// 执行不到这里
System.out.println(Thread.currentThread().getName()
+ "锁住" + str2);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
线程1运行
线程1锁住资源1
线程2运行
线程2锁住资源2
线程1运行线程1锁住资源1,线程2运行线程2锁住资源2,两个线程是同时执行的,线程1锁住了资源1,线程2锁住了资源2,线程1企图锁住资源2,但是资源2已经被线程2锁住了,线程2企图锁住资源1,但是资源1已经被线程1锁住了,然后就死锁了。
你的同步(锁)有我的同步,我的同步有你同步
要出现死锁问题需要满足以下条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决方法
- 如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;
- 打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;
- 进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;
- 避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。