项目开发目标:高可用、高性能、高并发
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换开销大 | 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器PC,线程的切换开销小(部分寄存器) |
所处环境 | OS中能同时运行多个任务/程序 | 同一个应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行时会为每个进程分配不同的内存区域 | 除了CPU外,不会为线程分配内存,只能共享那个所在线程的资源,拥有相同的地址空间 |
包含关系 | 没有线程的进程可以被视为单线程的,如果一个进程拥有多个线程,则执行过程不是一条直线的,而是多条线共同完成 | 线程是进程的一部分,所以线程被称为轻权或轻量级进程 |
注意:大多线程是模拟出来的(感官上的多线程同步),真正的多线程指的是有多个CPU/核。
核心概念:
继承Thread类(implements Runnable) 实现Runnable接口(abstract run()) 实现Callable接口(JUC并发包) 注意:Java中少用继承 extends 多用实现 implements,避免单继承的局限性。一个类仅在他们需要被加强或者修改时才会被继承。 start()方法能够异步地调用普通方法run(),才能达到多线程的目的。
//方法一:继承Thread
class PrimeThread extends Thread{
long value;
public PrimeThread(long value) {
this.value = value;
}
@Override
public void run() {
System.out.println("通过继承Thread创建了进程");
}
}
//方法二:实现Runnable
class PrimeRun implements Runnable{
long value;
public PrimeRun(long value) {
this.value = value;
}
@Override
public void run() {
System.out.println("通过实现Runnable创建了进程");
}
}
public class TestThread {
public static void main(String[] args) {
PrimeThread primeThread = new PrimeThread(123);
primeThread.start();//创建一个进程,Thread类里有run方法,交给CPU并不保证运行
PrimeRun primeRun = new PrimeRun(123);
Thread thread = new Thread(primeRun);//借助Tread代理对象
thread.start();
//如果只使用一次,可以使用匿名创建
new Thread(new PrimeThread(123)).start();
//为了区分,可以在Thread中添加名称
new Thread(new PrimeRun(123), "new Thread").start();
//输出进程名,放在这里是main进程
System.out.println(Thread.currentThread().getName());
}
}
方法三:
继承Callable,加上泛型,重写call()方法,可能抛出异常。
调用时:用到了服务和线程池
1. 创建目标对象:Class class = new Class();
2. 创建执行服务:ExecutorService see = Executors.newFixedThreadPool(1);//固定大小的线程池,可控制最大并发量,还有Cached可缓存、Single单线程化FIFO、Scheduled大小无限周期性的ThreadPool
3. 提交执行:Future< Boolean> result1 = ser.submit(class);
4. 获取结果:boolean r1 = result1.get();
5. 关闭服务:ser.shutdownNow();
当一个资源有多个代理处理时,可能存在网络延时,存在并发问题,需要保存线程安全。
为什么要用线程池: 可重用
新生状态:线程对象创建 就绪状态:调用start()方法;阻塞解除;运行时调用yield()方法(没有其他等待线程,当前线程立即恢复执行);JVM切换进程 运行状态:线程真正执行线程体的代码块 阻塞状态:调用sleep()仍占用资源、wait()释放现有资源、join()函数等到另一个线程执行完再继续执行;I/O进行read()/write() 死亡状态:线程体的代码执行完毕或终端执行,一旦进入死亡状态,不能再调用start()再次启动
setDaemon(Boolean b):将指定的线程(线程启动前)设置成后台线程/守护线程,创建用户线程的线程结束时,守护线程也随之消亡; setPriority(int newPriority)、getPriority():线程优先级代表的是概率,范围从1到10,默认为5; stop():停止线程,不推荐使用。
线程分为用户线程和守护线程(Daemon)。 虚拟机必须保证用户线程执行完毕,而不用等待守护线程(如后台记录操作日志、监控内存等)执行完毕。线程默认都是用户进程。
Thread t = new Thread();
t.setDaemon(true);//设置为守护进程
t.start();//必须在线程执行前设置
对于同一个资源,多个线程同时访问。 解决办法:队列(queue)+锁(synchronized),还有一种Lock方法
锁操作的关键:目标对、效率高 锁机制存在的问题: 一个线程持有锁会导致其他需要此资源的线程挂起; 再多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题; 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
思路:通过private关键字来保证数据对象只能被方法访问,然后利用synchronized机制(synchronized方法和synchronized块)。 缺陷:若将一个大的方法声明为synchronized将会大大影响效率。
//synchronized方法,锁的是对象的资源
public synchronized void test(){
...
}
//synchronized块,obj是共享资源的对象
synchronized(obj){
...
}
//同步方法中不需要制定同步监视器obj,因为同步方法的监视器就是this,即该对象本身或Class。
普通块/局部块、构造块、静态块、同步块 同步块目标更明确,同步方法锁的是this。提高性能:在同步块之前添加一些特殊情况的判断,避免全都等待。
过多的同步可能造成互相不释放资源,从而相互等待。一般发生在同步中持有多个对象的锁。 避免:不要在同一个锁块中嵌套锁。
避免死锁的算法:银行家算法
1. 程序、进程和线程
整个outlook应用程序代码是一个程序;打开一个outlook是一个进程,打开一个word是另一个进程;而发邮件是outlook进程的一个线程,收邮件又是另一个线程。
2. 多线程和多进程
性能:多进程的程序要比多线程的程序健壮。 注意:Linux中以“未分配资源的进程描述线程”: 实际上,从内核的角度来看,Linux并没有线程的概念;是否共享地址空间几乎是进程与线程之间的本质的唯一区别。
3. 并行和并发
多线程并行还是并发,取决于分配到的CPU资源,如果只有一个需要线程抢夺就是并发;如果有多个线程分配到就是并行。
4. 多线程同步方式 (1)synchronized关键字(JVM托管) 方法和块。 (2)wait方法和notify方法 wait方法释放对象锁,进入等待状态,调用notify方法通知正在等待的线程。 (3)Lock(代码实现) lock()以阻塞方式获得锁,且阻塞态会忽略interrupt方法; tryLock()以非阻塞方式获得锁,可以加时间参数; lockInterruptibly()不同于lock()不会忽略interrupt方法。
5. 进程停止运行(停止:4,5)方法 (1)sleep() Thread类的静态方法,线程控制自身流程,不释放锁不通信,位置任意,需要捕获异常interrypt。 (2)wait() Object类的方法,用于线程间通信,释放锁,放在同步块中,无异常。 (3)yield() 给相同优先级或更高优先级的线程让出锁,自己进入可执行状态,无异常。 (4)stop() 释放已经锁定的所有监视资源。 (5)suspend() 挂起不会释放锁,会发生死锁。 (6)join() 参数为时间,等待后执行(在执行完run之后)。