文章目录
前言
多线程,是指从软件或者硬件上实现多个线程并发执行的技术。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 。一、多线程技术概述
1.线程与进程
进程
- 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
- 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
2.线程调度
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而CPU在多个线程间的切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3.同步与异步
同步
- 排队执行 , 效率低但是安全。
异步
- 同时执行 , 效率高但是数据不安全。
4.并发与并行
并发
- 指两个或多个事件在同一个时间段内发生。
并行
- 指两个或多个事件在同一时刻发生(同时发生)。
二、多线程的使用
1.继承Thread
- 创建一个MyThread类继承Thread类(extends Thread),Thread类中有个run方法;
- run()方法就是线程要执行的任务方法;
- run方法里的代码就是一条新的执行路径;
- 这个执行路径的触发方式不是调用run方法,而是通过Thread对象的start()来启动任务。
每个线程都拥有自己的栈空间,共用一份堆内存。
由一个线程所调用的方法,这个方法会执行在这个线程里。
2.实现Runnable
创建一个MyRunnable类实现Runnable类(implements Runnable),Runnable中有个run方法。
public static void main(String[] args) {
//实现runnable
//1 创建一个任务对象
MyRunnable r = new MyRunnable();
//创建一个线程并给他一个任务
Thread t = new Thread(r);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
3.实现Runnable相比于继承Thread的好处
- 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况
- 可以避免单继承所带来的局限性
- 任务与线程本身是分离的,提高了程序的健壮性
- 后续学习的线程池技术,接受Runnable类型的任务,而不接受Thread类型的线程
实现Runnable比继承Thread用的多,不过Thread也有一个很简单的用法——匿名内部类写法:
4.线程阻塞
所有比较消耗时间的操作都可以称为线程阻塞,也称为耗时操作
例如:
- 文件读取:会导致线程等待在那个位置,代码不会继续往下执行,除非文件读完
- 接收用户输入:线程就会堵在那个位置等用户输入,用户不输入代码就不会往下执行
5.线程中断
一个线程是一个独立的执行路径,他是否应该结束,应该由其自身决定
以前的stop方法是从外部掐死线程,线程是结束了,但是其所占用的资源无法释放或者GC,所以stop过时了。
现在的处理方法:给线程打一个中断标记,线程运行过程中会关注这个标记,如果发现有需要中断的标记,线程就会触发异常,程序员通过try+catch来处理,在catch块中决定如何处理(关闭或不关闭)。
public static void main(String[] args) {
//线程中断
//y一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给线程t1添加中断标记
t1.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;
}
}
}
}
6.守护线程
线程分为守护线程和用户线程
- 用户线程:当一个进程不包含任何存活的用户进程时,进程结束。
- 守护线程:守护用户线程的,当最后一个用户线程结束时,所以守护线程自动死亡。
t1.setDaemon(true);//设置t1为守护线程
三、线程安全问题
以上代码是线程不安全的,运行结果会出现-1和-2。
线程安全1:同步代码块
线程同步:synchronized
同步代码块格式:
synchronized(锁对象){
//相关操作语句块
}
排队的时候一定要看同一个锁对象,不然等于没排队。
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}
}
}
}
线程安全2——同步方法
把卖票操作抽成一个方法sale(){}
在定义方法的时候,在返回值的前面用synchronized修饰。
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
同步方法如果是static,那么锁对象就是:Ticket.class(类名.class)字节码文件;
如果不是static,那么锁对象就是this,就是调用这个方法的对象,即主函数里的run。
如果创建了三个run对象同时开始任务卖票,那么这三个对象卖票是不排队的,因为有三把锁。
线程安全3——显式锁Lock
同步代码块和同步方法都属于隐式锁
显式锁 Lock 子类 ReentrantLock
创建一个ReentrantLock对象,在卖票钱把锁对象锁上,卖票结束把锁对象解锁。
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
l.unlock();
}
}
}
显式锁是真正的创建了一个锁对象,加锁,解锁,更符合面向对象的思想。
公平锁和非公平锁
公平锁就是锁解开的时候大家按照先来后到的原则排队用锁;非公平锁就是锁解开了大家抢锁,谁抢到谁用。(只是决定锁谁来用,真正用锁还是一个一个来)
ReentrantLock的构造方法有个boolean参数fair,true就表示公平锁,默认是false
线程死锁
所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
避免死锁的方法:
1、加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
避免嵌套封锁:这是死锁最主要的原因的,如果你已经有一个资源了就要避免封锁另一个资源。如果你运行时只有一个对象封锁,那是几乎不可能出现一个死锁局面的。
2、加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行干点其它事情,再回头来重复
3、死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。之前加锁的逻辑)。
四、线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
1.线程池的好处
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性。
2.java中的四种线程池
缓存线程池
(长度无限制)
- 执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程 并放入线程池, 然后使用
定长线程池
(长度是指定的数值)
- 执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
单线程线程池
效果与定长线程池 创建时传入数值1 效果一致。
- 执行流程:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲,则等待池中的单个线程空闲后使用
周期性任务定长线程池
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时,定时执行, 当某个时机触发时, 自动执行某任务。
(完)