进程:一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:一条进程中的执行路径,每个进程至少有一个线程
线程是CPU调度的最小单位,进程是系统资源分配的最小单位
线程调度是指按照特定机制为多个线程分配CPU的使用权。
分时调度
抢占式调度(Java采用)
多线程程序并不能提高程序运行速度,但能够提高程序运行效率,让CPU的使用率更高
同步:排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
并发:两个或多个事件在同一 时段内发生
并行:两个或多个事件在同一 时刻发生(同时发生)
继承Thread类
重写run()方法
创建子类对象
调用start()启动线程
优点:可以写成匿名内部类,简化代码
定义一个任务(实现Runnable接口,重写run()方法)
创建一个任务对象
创建一个线程,并为其分配一个任务
调用start()启动线程
与继承Thread类相比,实现Runnable的优点 |
---|
1. 避免单继承带来的局限性 |
2. 任务与线程分离,提高程序的健壮性 |
3. 通过给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况 |
4. 线程池接受Runnable类型的任务 |
定义一个实现类,实现Callable接口,重写call()方法
创建实现类对象
创建FutureTask对象,传入实现类对象
创建Thread对象,传入FutureTask对象
调用start()启动线程
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
Callable callable = new Callable();
FutureTask<Integer> future = new FutureTask<>(callable);
new Thread(future).start();
Runnable | Callable |
---|---|
没有返回值 | 可以返回执行结果 |
run()方法不能抛出异常 | call方法允许抛出异常 |
相同点 |
---|
都是可以编写多线程程序的接口 |
都采用Thread.start()启动线程 |
Callable获取返回值
Callable接口支持返回执行结果,需要调用 FutureTask.get() 得到
此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
FutureTask其他常用方法: isDone() cancel()
线程状态 | 描述 |
---|---|
NEW | 新建态: 表示线程被创建,还未启动 |
RUNNABLE | 可运行态:线程就绪,或已在虚拟机中执行 |
BLOCKED | 阻塞态:线程被阻塞,等待除CPU以外资源(排队状态) |
WAITING | 等待态:线程无限等待其他执行特定操作的线程唤醒 |
TIMED_WAITING | 限时等待态:线程等待其他执行最多指定时间操作的线程唤醒 |
TERMINATED | 死亡态:线程运行结束,或程序强制退出 |
图片作者:程明东 图片来源:https://my.oschina.net/mingdongcheng/blog/139263
1 线程信息
getName() setname() getPriority() setPriority()… :设置、获得线程名称、优先级
如果没有给线程命名,系统会给线程设置默认的名字:Thread-X
Thread.currentThread().getName() :获取当前执行线程的名称
守护线程:依附用户线程,所有用户线程死亡后,守护线程才会死亡,进程结束
2 线程启动和死亡
start() :线程启动,进入就绪状态,等待CPU调度
stop() :强制停止线程,已过时,不能用此停止线程,极有可能会使线程来不及释放所占用的资源,就死亡了
一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定
应该用做标记的方式,通知线程停止,例如执行return语句使run方法结束
3 线程休眠
Thread.sleep(long millis):线程休眠指定的毫秒数,此时进入等待态
4 线程阻塞
耗时操作导致线程阻塞,也就是需要排队(比如读取文件等)
5 线程中断
线程中断异常 InterruptedException
try{
Thread.sleep(1000);
}catch(InterrupterdException e){
e.printStackTrace();
}
给线程t1添加中断标记,告知线程你该死亡啦
t1.interrupt();
但是到底死不死亡,是被告知该死亡的线程自己决定的
以下操作触发中断:wait、sleep、interrupt、interrupted…
6 守护线程
用户线程:当一个进程不包含任何存活的用户线程时,进程结束
守护线程:守护用户线程时,当最后一个用户线程结束时,所有的守护线程自动死亡
原子性:一个或多个操作要么全部执行成功要么全部执行失败
可见性:一个线程对共享变量的修改,其他线程能够立刻看到
有序性:程序执行的顺序按照代码的先后顺序执行
一个线程不安全的情况如下:
三个线程卖票,只有票数大于0才能买票,但是程序执行结果中,余票出现负数
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况,反之,就是线程不安全
这是因为在三个线程并发执行中,每个线程的执行都要经过检票-卖票,但它们的推进顺序是不同的,这个执行过程不是一个原子操作、票也没有设置为互斥资源,无法保证线程安全的三要素:原子性、可见性、有序性
1 同步代码块
格式:synchronized(锁对象 o){}
当有线程获得了锁对象,就可以执行同步代码块,在jvm底层会给这个对象打上锁标记
那么当其他线程想要获得这个锁对象o,就会发现对象已经打上标记,该线程只能等待
当获得锁对象的线程执行完同步代码块里的代码,就会释放锁,对象o的锁标记就取消掉了
其他线程就竞争这个锁对象,谁抢到了,就再给对象o打上锁标记,并执行代码块
Ps:同步代码块在循环里面,谁先抢到,那么重复抢到的几率比较高(刚刚解锁,反手又拿了锁)
任何对象都能作为锁,但多线程中多个线程争夺的必须是同一把锁
缺点: 效率变低了
2 同步方法
public synchronized
boolean sale(){…}
那么想要执行这个同步方法,要获得的锁对象是谁呢?
Runnable run = new Ticket(); // 创建了一个任务对象
如果同步方法是在静态类run方法中调用的,那么它的锁是:静态任务类.class
如果同步方法是非静态类的run方法调用的,那么它的锁是:任务对象的this
3 显示锁Lock
隐式锁:同步代码块、同步方法,代码块或方法执行结束,由JVM来释放锁
显式锁:需要自己手动加锁、手动解锁
Lock l = new ReentrantLock();
l.lock();
l.unlock();
什么是公平锁?
公平锁:先来先得
显式锁:fair参数为true 就是公平锁
private Lock l = new ReentrantLock(true)
非公平:抢 !
三种线程安全方法都是:非公平锁
互斥资源
占有并请求
循环等待
wait notify notify
厨师做饭,服务员睡觉
厨师做好饭,服务员被唤醒
服务员取走饭,厨师睡觉
服务员来取饭,厨师被唤醒
厨师做饭…
我们在使用同步机制解决线程不安全问题时,容易引发生产者与消费者问题
这是因为synchroinzed用的是非公平锁
这时候,线程之间通信是非常有必要的!(等待->唤醒)
对于生产者消费者问题,必须生产者生产后,唤醒服务员,自己再休眠才可
否则,对于非公平锁,若生产者生产后,不唤醒服务员,而生产者又抢到了锁,又生产了,导致与服务员线程无法同步
(假设这时候他们的缓冲区容量为1)
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间
线程池 是一个容纳多个线程的容器,池中的线程可以反复使用,省去频繁创建线程对象的操作,节省大量的时间和资源
线程池的好处:
降低资源损耗
提高响应速度
提高线程的可管理型
1 缓存线程池
缓存线程池:长度无限制
执行流程:
判断线程池是否存在空闲线程
存在则使用
不存在,则创建线程,并放入线程池,然后使用
ExecutorService service = Executors.newCachedThreadPool();
// 向线程池中,加入新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
2 定长线程池
定长线程池:长度是指定的数组
执行流程:
判断线程池是否存在空闲的线程
存在则使用
不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池,然后使用
不存在空闲线程,且线程池已经满的情况下,则等待线程池存在空闲线程
ExecutorService service = Executors.newFixedThreadPool(2)
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
3 单线程线程池
效果与定长线程池 创建时传入数组1效果一致
执行流程:
判断线程池 的那个线程 是否空闲
空闲则使用
不空闲,则等待 池中的单个线程空闲后 使用
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currnrThread);
}
}
4 周期性任务定长线程池
执行流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池,,然后使用
4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时:
定时执行,当某个时机触发时,自动执行某任务
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/*** 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
service.schedule(new Runnable() {
@Override public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
/*** 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
例子:冗余的Runnable代码
写法一: 实现Runnable接口,创建任务对象,创建线程对象,把任务对象传给线程对象,启动线程
写法二:把实现的Runnable任务对象写成匿名对象
将匿名内部类(原来要实现接口的实现类) 省略 类名、方法名,保留参数、方法过程
Thread t = new Thread(() -> System.out.println("这是一个任务"));
t.start();