程序设计之多线程 程序设计之多线程
1. 进程与线程的区别
进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。
2. 创建线程的三种方式
2.1. 继承Thread类创建线程类
- 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
- 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
2.2. 实现Runnable接口
- 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
2.3. 通过Callable创建线程
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
- call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
2.4. 线程的创建方式源码
3. 创建线程池的四种方式
3.1. newCachedThreadPool
-
创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程(参考1)
-
缺点:大家一般不用是因为newCachedThreadPool 可以无线的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值。
3.2. newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程
3.3. newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
Executors.newScheduledThreadPool(5);与Timer 对比:Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer
3.4. newSingleThreadExecutor
创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定
顺序(FIFO,LIFO,优先级)执行。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。
3.5. 为什么要使用线程池
-
new Thread 的弊端
- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
-
相比new Thread,Java提供的四种线程池的好处在于:
- 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
3.6. 线程数的规划
- CPU密集型任务(批量审核属于该类型任务)
一般配置线程数=CPU总核心数+1 (+1是为了利用等待空闲)。
要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。 - IO密集型任务
一般配置线程数=CPU总核心数 * 2 +1。
这类任务的CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。常见的大部分任务都是IO密集型任务,比如Web应用。对于IO密集型任务,任务越多,CPU效率越高(但也有限度)。
4. 线程安全及解决
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。做读操作是不会发生数据冲突问题。
使用线程同步或使用锁能解决线程安全问题,只能让当前一个线程进行执行。被包裹的代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
-
解决多线程同步问题:
(1) 同步代码块
(2) 同步函数
(3) 静态同步函数
-
同步代码块与同步函数区别?
同步代码使用自定锁(明锁)
同步函数使用this锁
-
同步函数与静态同步函数区别?
注意:
面试会这样问:例如现在一个静态方法和一个非静态方法怎么实现同步?(参考2)
同步函数使用this锁(实例对象本身)
静态同步函数使用字节码文件,也就是类.class(类对象本身)
5. 什么是多线程死锁
同步中嵌套同步,无法释放锁的资源。
解决办法:
同步中尽量不要嵌套同步
6. Wait()与Notify ()区别
Wait让当前线程有运行状态变为等待状态,和同步一起使用
Notify 唤醒现在正在等待的状态,和同步一起使用
7. Wait()与sleep()区别
sleep()方法,该方法是属于Thread类中的。wait()方法,则是属于Object类中的。
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
在调用sleep()方法的过程中,线程不会释放对象锁。sleep()方法导致了程序暂停执行指定的时间,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 。
理解:
sleep由线程自动唤醒,wait必须显示用代码唤醒。
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
8. start()和run()的区别
- start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
- run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
- start()方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run()方法。
9. 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
(参考3)
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
10. 说一下ThreadLocal
- ThreadLocal是Java提供的本地存储机制,利用这个机制可以将数据缓存在线程内部,在任何时候都可以获取使用。
- ThreadLocal底层是通过ThreadLocalMap来实现的,每一个Thread对象中都存在一个ThreadLocalMap,Map的Key是ThreadLocal对象,Map的Value是缓存的值。
- 如果在线程池中使用ThreadLocal会造成内存泄露的问题,需要手动进行remove。因为线程池中的线程不会回收,而是去执行下一个任务,线程不被回收,Entry对象就不会被回收,造成了内存泄露。
11. join 方法
可以将并行改为串行,在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("线程A");
ThreadJoinTest t2 = new ThreadJoinTest("线程B");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,
并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,
相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join();
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
》》程序结果是先打印完线程A线程,在打印线程B线程;
12. sleep()与yield()的区别
sleep:会强制让当前线程进入等待,即当前线程的状态为:等待、阻塞
yield:会先去判断是否有和当前线程相同优先级的线程,如果没有,则自己继续执行,如果有,则将CPU资源让给它,然后进入到就绪状态。