一、线程、进程、多线程
1、线程:CPU调度和执行的单位。
2、进程:程序的一次执行过程,是系统资源分配的单位。
3、一个进程至少有一个线程,也可以包含多个线程。
二、并发和并行
1、并行:两个或者多个事件在
同一时刻
发生。是在不同实体
上的多个事件
小明在A座位吃饭,小王在B座位吃饭
2、并发:两个或者多个事件在同一时间段
发生。是在同一实体
上的多个事件
小明和小王都要在A座位吃饭,小明先吃,小王等小明吃完后再坐上去吃。
三、线程的四种创建方式
1.继承Thread类
1)实现过程
public class Thread implements Runnable {}
创建线程的方式1:
1、继承Thread类
2、重写run()方法
3、创建线程对象,调用start()开启线程
2)run()和start()的区别
1、run(),是Runnable接口中的方法,接口中只有这也一个方法。start()是Tread类中的方法。
2、start()方法是初始化线程的方法
。run()只是在当前线程中执行任务
。
程序中一般会有两部分线程,一部分在run()
方法中,一部分在main()
方法(称为主线程
)中
main()方法里,直接调用run(),那就只是相当于调用了一个普通的run()方法,不涉及多线程
main()方法里,调用start(),代表开启线程,run()方法中的线程和main()方法中的主线程交替执行
2.实现Runnable接口
创建线程的方式2(推荐使用,因为接口可以多继承):
1、实现Runnable接口
2、重写run()方法
3、创建实现类对象
4、创建线程对象,调用start()方法
3.实现Callable接口
- 创建线程的方式3(
优点:可以定义返回值;可以抛出异常
):
1、实现Callable接口
2、重写call()方法
1)Runnable和Callable的区别
- Runnable接口的源码
Runnable接口是一个函数式接口,里面只有一个抽象方法void run(),意思是无返回值
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }
- Callable接口的源码
Callable接口也是一个函数式接口,同时也是一个泛型接口。里面只有一个抽象方法V call(),其返回值类型就是泛型V
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
2)关于实现Callable接口创建线程的方式
第一种方式是配合ExecutorService使用,在ExecutorService接口中声明了若干个submit方法的重载版本。这个方法还未提及,在线程池里会具体讲解,一般情况下都会用这种方式。
第二种方式是配合Thread使用,通过创建FutureTask类型的对象,将此对象传入Thread(),再调用start()方法来开启线程。此方式会在3)中讲解。
3)Future接口和FutureTask实现类的解析
- 先拿到前面实现Callable接口创建线程的过程截图。
FuturTask
1.我们先了解一下Future接口
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞
直到任务返回结果。
2.FutureTask实现类
1)FutureTask类实现了RunnableFuture接口
;RunnableFuture接口继承了Runnable接口和Future接口
2)FutureTask类中有两个构造方法。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
3)使用FutureTask + Thread()创建线程时,可以把FutureTask理解成一个包装器
。使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
4.创建线程池
提前创建多个线程,丢入线程池中,使用时直接获取,使用完放回池中。避免频繁地创建和销毁,实现重复利用。
1)Excutors
常用的线程池工具类。里面有若干个静态方法。
public class Excutors { public static ExecutorService newFixedThreadPool(int nThreads) { //ThreadPoolExecutor的这个构造方法中返回了传入的所有参数 /* *public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } */ return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newCachedThreadPool() {} public static ExecutorService newSingleThreadExecutor() {} //public interface ScheduledExecutorService extends ExecutorService {} public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {} }
newFixedThreadPool(int nThreads):创建一个
定长线程池
,可控制线程最大并发数
,超出的线程会在队列中等待。
newCachedThreadPool:创建一个可缓存线程池
,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程
)
newSingleThreadExecutor:创建一个单线程化的线程池
,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newScheduledThreadPool(int corePoolSize):创建一个定长线程池
,支持定时及周期性任务执行。
2)线程池的核心参数
public class ThreadPoolExecutor extends AbstractExecutorService { public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } }
corePoolSize:
核心线程数
maximumPoolSize:最大线程数
keepAliveTime:空闲线程存活时间
unit:时间单位
workQueue:任务队列
defaultThreadFactory:线程工厂
defaultHandler:饱和拒绝策略
3)ExecutorService
public interface Executor { void execute(Runnable command); } public interface ExecutorService extends Executor { }
execute(Runnable command):履行Ruannable类型的任务。
(Executor接口中的抽象方法)
submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
shutdown():在完成已提交的任务后封闭办事,不再接管新任务,
shutdownNow():停止所有正在履行的任务并封闭办事。
isTerminated():测试是否所有任务都履行完毕了。,
isShutdown():测试是否该ExecutorService已被关闭
4)线程池的实现
四、静态代理模式
1.什么是静态代理
参考我的另一篇文章代理模式总结
2.多线程中的静态代理模式
五、线程状态
1.五大状态
2.线程方法
3.停止线程
推荐线程自己停下来。建议使用一个标志位进行终止变量
当flag==false,则终止线程运行。
4.线程休眠(sleep)
- public static native void
sleep(long millis)
throwsInterruptedException
;
sleep(long millis)指定当前线程阻塞的毫秒数
存在异常InterruptedException- sleep时间到达后,线程进入
就绪状态
- sleep可以模拟网络延时,倒计时等
- sleep
不会释放锁
5.线程礼让(yield)
- public static native void
yield()
;- 让当前正在执行的线程暂停,
不阻塞
,从运行状态
直接转为就绪状态
让cpu重新调度,礼让不一定成功,看cpu心情
可以理解成,让当前运行的线程停止,重新与其他线程竞争
礼让失败
礼让成功
6.线程强制执行(join)
- public final void
join()
throwsInterruptedException
- 可以理解成让某些线程
插队
7.观测线程状态
1)BLOCKED和WAITING的区别
- BLOCKED是
锁竞争
失败后被动触发的状态,WAITING是人为主动触发的状态 - BLOCKED后的唤醒是自动的,WAITING的唤醒需要通过特定的方法主动唤醒
六、线程优先级
线程调度器按照优先级决定应该调度哪个线程,但也不一定,只能是概率大。具体由CPU调度。
线程的优先级范围从1~10
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
public final int getPriority() {
return priority;
}
七、守护线程(daemon)
- 线程分为
用户线程
和守护线程
- 虚拟机必须确保用户线程执行完毕(
main()
)- 虚拟机不用等待守护线程执行完毕(
gc()
),即用户线程结束后,程序结束,守护线程可能还在执行
Thread thread = new Thread(daemonThread);
//默认是false,表示用户线程,正常的线程都是用户线程
thread.setDaemon(true);
八、线程同步
- 并发:
同一个对象被多个线程同时操作
- 线程同步可以理解成一个
等待机制
- 线程同步的形成条件:
队列
+锁(synchronized)
- 加锁后存在两个问题
1)加锁的目的是保护线程安全,但是性能会降低
2)会导致优先级倒置(优先级高的线程需要等待优先级低的线程释放锁)
- 线程不安全的例子
public
synchronized
void method(int args){}
- 同步方法:由于private关键字用来保证
数据对象只能被方法访问
,所以用synchronized方法
控制对对象
的访问。每个对象
对应一把锁
,每个synchronized方法都必须获得调用该方法的对象的锁
才能执行,否则线程会阻塞。方法一旦执行,就独占该锁
,直到该方法返回
才释放锁
,后面被阻塞的线程才能获得这个锁并继续执行- 缺陷:
若将一个大的方法申明为synchronized将会影响效率
。比如一个方法中有两部分,只有一部分需要修改才用到锁,而这种时候两部分都添加了锁,会造成资源浪费。synchronized(Obj){}
- 同步代码块
Obj
:同步监视器
。Obj可以是任何对象,但推荐使用共享资源
作为同步监视器。同步方法中无需指定同步监视器
,因为同步方法的同步监视器就是this(对象本身)。- Obj必须是
变化
的量。比如说增、删、改。
CopyOnWriteArrayList。线程安全的集合
九、死锁
多个线程各自占有对方需要的资源,形成僵持。
产生死锁的四个必要条件
1、互斥条件:一个资源每次只能被一个进程使用。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
十、Lock锁
1.概念
通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
可以用可重入锁(ReentrantLock)
实现显示加锁、释放锁:ReentrantLock类实现Lock接口
public interface Lock {}
public class ReentrantLock implements Lock, java.io.Serializable {}
public class LockDemo {
public static void main(String[] args) {
LockTest lt = new LockTest();
new Thread(lt).start();
new Thread(lt).start();
new Thread(lt).start();
}
}
class LockTest implements Runnable {
private int ticketsNums = 10;
//定义ReentrantLock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//加锁
lock.lock();
//线程运行
if (ticketsNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketsNums--);
} else {
break;
}
} finally {
//解锁
lock.unlock();
}
}
}
}
2.Lock与synchronized的对比
1、Lock是一个接口;synchronized是一个关键字。
2、Lock是显示锁(需要手动开启和关闭);synchronized是隐式锁,出了作用域自动释放
3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。具有更好的扩展性(提供更多的子类)
4、使用顺序:Lock > 同步代码块 > 同步方法
十一、线程协作
1.生产者消费者模式
不是23中设计模式之一,是一个问题!!
- 应用场景
1、假设仓库中只能存放一件商品。生产者生产商品,放入仓库,消费者取走商品。
2、若仓库中没有商品。生产者放入一件商品后,停止生产并等待,消费者取走商品。
3、若仓库中有商品。消费者取走商品并消费,否则停止消费并等待。- 问题分析
1、这是一个同步问题,生产者和消费者共享同一个资源
,并且二者相互依赖,互为条件
。
2、对于生产者。生产商品后,通知消费者
,如果容器满了,生产者等待
。
3、对于消费者。消费后,通知生产者
,如果容器空了,消费者等待
。- 结论
在生产者消费者问题中,仅有synchronized
是不够的的。synchronized仅能解决同步,但不能实现不同线程间的通信
。
2.线程通信
均为Object类的方法,只能在同步方法或同步方法快中只用,否则会抛出异常
3.解决生产者消费者问题
1)管程法
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
2)信号灯法
添加一个标志位flag,通过flag为true或false来切换线程