Java多线程
多线程概述
==多线程:栈空间独立,堆内存共享==
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是在进程的基础上进行的进一步划分。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,一个进程可能包含了多个同时执行的线程。
进程与线程
- 进程:
- 正在运行的应用程序:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,即每个进程都有着自己的堆、栈等且是互不共享的。
- 线程:
- 进程中的一个执行路径(一段程序从执行到结束的整个过程),共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
- 线程实际上是在进程的基础上进一步划分的,一个进程执行后,里面的若干执行路径又可以划分为若干个线程
线程调度
- 分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉很快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
同步与异步&并发与并行
同步:排队执行,效率低但安全
异步:同时执行,效率高但数据不安全
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
多线程的实现方式
继承Thread类
步骤:
创建一个自定义类并继承Thread类;
重写run()方法,创建新的执行任务(通过thread对象的start()方法启动任务,一般不直接调用run()方法)
创建自定义类对象实例,调用start(),让线程执行
代码如下:
1 | //MyThread.java |
运行结果:
可以看到顺序并不统一,两个线程在交替执行而且各自所占的时间不完全相同,这是线程在抢时间片,谁先抢到谁就执行。
时序图:
运行过程中子线程任务中调用的方法都在子线程中运行
在上述代码中。如果Thread对象只需要调用1次,也可以通过使用匿名内部类的方式进行简化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class ThreadTest {
public static void main(String[] args) {
new Thread(){
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("MyRunnable" + i);
}
}
}.start();
for (int i = 0; i < 5; i++) {
System.out.println("MainThread" + i);
}
}
}
实现Runnable接口
Runnable接口代码:
1 | public interface Runnable { |
步骤:
- 创建一个自定义类实现Runnable接口,并实现其抽象方法run(),编写线程要执行的任务
- 创建自定义类对象实例
- 用Thread类创建一个对象实例,并将第二步中的自定义类对象实例作为参数传给其构造函数
- 调用Thread类实例的start()方法执行线程。
1 | //MyRunnable.java |
上述代码也可以通过使用匿名内部类的方式进行简化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class RunnableTest {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("MyRunnable" + i);
}
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("MainRunnable" + i);
}
}
}
==上面两种方式的比较==
继承Thread类:
- 优点:直接使用Thread类中的方法,代码简单
- 弊端:如果已有父类,不可用(Java不可多继承)
实现Runnable接口(更常用):
与继承Threadl类相比具有以下优势:
- 通过创建任务,给线程分配任务实现多线程,更适合多个线程同时执行相同任务的情况
- 可以避免单继承带来的局限性(Java允许实现多个接口,但不能继承多个父类)
- 任务和线程分离,提高程序健壮性
- 后续学到的线程池技术,它只接收Runnable类型任务,不接收Thread类型线程
==Thread类API==
- 常用构造方法
构造器 描述 Thread()
分配新的 Thread
对象。Thread(Runnable target)
分配新的 Thread
对象。Thread(Runnable target, String name)
分配新的 Thread
对象。Thread(String name)
分配新的 Thread
对象。
- 常用其他方法
变量和类型 方法 描述 long
getId()
返回此Thread的标识符。 String
getName()
返回此线程的名称。 int
getPriority()
返回此线程的优先级。 void
setPriority(int newPriority)
更改此线程的优先级。 Thread.State
getState()
返回此线程的状态。 static Thread
currentThread()
返回对当前正在执行的线程对象的引用。 void
start()
导致此线程开始执行; Java虚拟机调用此线程的 run
方法。static void
sleep(long millis)
导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 static void
sleep(long millis, int nanos)
导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 void
setDaemon(boolean on)
将此线程标记为 daemon线程或用户线程。
- 特殊字段:控制线程抢到时间片的几率
变量和类型 字段 描述 static int
MAX_PRIORITY
线程可以拥有的最大优先级。 static int
MIN_PRIORITY
线程可以拥有的最低优先级。 static int
NORM_PRIORITY
分配给线程的默认优先级。 其他的可以参考Java的API手册
实现Callable接口
Callable接口代码:
1 | public interface Callable<V> { |
步骤:
创建一个自定义类实现Callable接口,并实现其抽象方法call(),编写线程要执行的任务
1
2
3
4
5
6class XXX implements Callable<T> {
public <T> call() throws Exception {
return T;
}
}创建FutureTask对象 , 并传入第一步编写的Callable类对象
1
FutureTask<Integer> future = new FutureTask<>(callable);
通过Thread,启动线程
1
new Thread(future).start();
1 | //MyCallable.java |
上述代码也可以通过使用匿名内部类的方式进行简化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
new Thread(new FutureTask<>(new Callable<String>() {
public String call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println("MyCallable:" + i);
}
return null;
}
})).start();
for (int i = 0; i < 5; i++) {
System.out.println("main" + i);
}
}
}
Runnable 与 Callable比较
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
行,如果不调用不会阻塞。
多线程的应用实例
设置和获取线程名称
currentThread()
可以获取当前正在执行的线程对象
1 | //MyRunnable.java |
执行结果:
线程休眠sleep
sleep(long millis)
是Thread类的静态方法,类名直接调用即可,单位ms。
1 | public class Demo1 { |
运行结果:每隔1秒打印一个数字。
线程阻塞:所有较耗时的操作都能称为阻塞。也叫耗时操作。
线程的中断
一个线程是一个独立的执行路径,它是否应该结束,由其自身决定。
因为线程执行过程会有很多资源需要使用或释放,如果干涉它的结束,很可能导致资源没能来得及释放,一直占用,从而产生无法回收的内存垃圾。
Java以前提供stop()方法可以结束线程,现在已经过时(不再使用)。现在出了新的方法,给线程打中断标记(interrupt
)来控制它的结束。
具体方法就是 调用interrupt()
方法,子线程执行时捕获中断异常,并在catch块中,添加处理释放资源的代码。
如下代码所示:main线程执行完后不管子线程是否执行完都中断掉它
1 | //MyRunnable.java |
运行结果:
守护线程
线程分为守护线程和用户线程
- 用户线程:当一个进程不包含任何存活的用户线程时,进行结束。
- 守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
直接创建的都是用户线程,
设置线程为守护线程:在启动之前设置 ,语法为:线程对象.setDaemon(true);
。
1 | //MyRunnable.java |
运行结果
线程安全问题
问题引入
我们先来看个例子:三个窗口(线程)同时卖5张票。
1 | public class Demo1 { |
运行结果部分截图:
我们看到余票出现了负数,显然这是不合理的,这就是线程不安全导致的。出现这种情况的原因:线程争抢,导致线程不安全。 多线程在进行同一卖票任务时,没人干涉,各个窗口疯狂买票,最终导致卖的票超出总票数,余票出现负数。
线程不安全的原因:
当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
多个线程争抢同一个数据,使得数据在判断和使用时出现不一致的情况。那如何解决呢?
解决方法:保证一段数据同时只能被一个线程使用(排队使用),也就是线程同步,给线程加锁(synchronized)
我们有以下三种方法解决线程不安全的问题:同步代码块、同步方法、显示锁
同步代码块
使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块
多个同步代码块如果使用相同的锁对象, 那么他们就是同步的
语法格式:synchronized(锁对象) {}
任何对象都可以作为锁对象存在。
还以上面卖票的代码为例,给卖票的线程加锁
同步方法
以方法为单位进行加锁。把synchronized关键字修饰在方法中。
还以上面卖票的代码为例,写一个synchronized修饰的方法sale()执行卖票任务,
显式锁
上面三种方法中,同步代码块和同步代码都是隐式锁
Lock l = new ReentrantLock()
:自己创建一把锁
lock()
:加锁 unlock()
:解锁
还以上面卖票的代码为例
显式锁和隐式锁的区别:
区别 synchronized lock 原始构成 Java关键字,由JVM维护,是JVM层面的锁 JDK1.5之后的类,使用lock是在调用API,是API层面的锁 使用方式 隐式锁,不需要手动获取和释放锁,只需要写synchronized,不用进行其他操作 显式锁,需要手动获取和释放锁,如果没有释放锁,可能会出现死锁 等待中断 不会中断,除非抛出异常或正常运行完成 可以中断,1:调用设置超时方法tryLock(long timeout ,timeUnit unit);2:调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断 加锁公平 非公平锁 可以是公平锁也可以是非公平锁,默认是非公平锁。可以在其构造方法传入Boolean值,true公平锁,false非公平锁 绑定多个条件 没有。不能精确唤醒线程,要么随机唤醒一个线程,要么唤醒所有等待线程 用来实现分组唤醒需要唤醒的线程,可以精确唤醒线程 性能 JDK1.5时性能较低,JDK1.6时性能优化,与lock相较无异 JDK1.5时性能更高,JDK1.6时synchronized优化赶上lock 加锁方式 线程获取独占锁(CPU悲观锁机制),只能依靠阻塞等待线程释放锁。在CPU转换线程阻塞时会引起线程上下文切换,当竞争锁的线程过多时,会引起CPU频繁上下文切换导致效率低下 使用乐观锁机制(CAS操作 Computer and Swap),假设不会发生冲突,一旦发生冲突失败就重试,直到成功为止。 公平锁:先来先得,排队执行
非公平锁:抢占式的,谁抢到是谁的
更多关于线程安全的问题可以看下面这篇文章
线程死锁
概述
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处于阻塞的假死状态,形成死锁。
举个例子,如下图所示,在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式。
死锁产生的条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
如何避免死锁
按顺序加锁:如果每个线程都按同一个的加锁顺序这样就不会出现死锁。
给锁加时限:每个线程获取锁的时候加上个时限,如果超过某个时间就放弃锁。
死锁检测:按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
更多关于线程死锁的问题可以看下面这篇文章,以上内容也是来自这篇文章:
多线程通信
Object方法中提供了一些线程间相互通信的方法
变量和类型 | 方法 | 描述 |
---|---|---|
void |
notify() |
唤醒正在此对象监视器上等待的单个线程。 |
void |
notifyAll() |
唤醒等待此对象监视器的所有线程。 |
void |
wait() |
导致当前线程等待它被唤醒,通常是 通知或 中断 。 |
void |
wait(long timeoutMillis) |
导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
void |
wait(long timeoutMillis, int nanos) |
导致当前线程等待它被唤醒,通常是 通知或 中断 ,或者直到经过一定量的实时。 |
什么时候需要通信
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,如果我们希望他们有规律的执行, 就可以使用通信。
生产者与消费者
看下面代码,有Cooker类,Waiter类,Food类
厨师cooker为生产者线程,服务员waiter为消费者线程,食物food为生产与消费的物品;
假设目前只有一个厨师,一个服务员,一个盘子。理想状态是:厨师生产一份饭菜,服务员端走一份,且饭菜的属性未发生错乱;
厨师可以制作两种口味的饭菜,制作100次;
服务员可以端走饭菜100次;
1 | public class Demo { |
运行结果
原因:我们在设定菜名和味道的setNameAndTaste
方法中,先设定名称,然后休眠一段时间,再设定的味道,中间休眠的那段时间很可能发生时间片丢失,使得菜属性产生混乱。
解决方式一:
为了防止在生产过程中setNameAndTaste出现时间片切换,可以用synchronized修饰此方法;
1 | public synchronized void setNameAndTaste(String name,String taste){ |
运行结果
可以看出,依然不符合实际情况,这是因为synchronized只是确保了方法内部不会发生线程切换,但并不能保证生产一个消费一个的逻辑关系
解决方式二:
在解决方案一的基础上,进行线程之间的通信
1 | private boolean flag = true; //默认为true,表示可以做饭 |
厨师做完饭后喊醒服务员,自己睡着。服务员送完饭后喊醒厨师,自己睡着;将Food类左如下修改
运行结果,做一道菜,端走一道。
线程的六种状态
Enum Thread.State描述了六种线程的状态,如下表所示
Enum Constant | 描述 |
---|---|
BLOCKED |
线程的线程状态被阻塞等待监视器锁定。(阻塞) |
NEW |
尚未启动的线程的线程状态。(创建) |
RUNNABLE |
可运行线程的线程状态。(就绪和运行) |
TERMINATED |
终止线程的线程状态。(消亡) |
TIMED_WAITING |
具有指定等待时间的等待线程的线程状态。(有限期等待) |
WAITING |
等待线程的线程状态。(无限期等待) |
线程池Executors
普通线程的执行流程:
创建线程 → 创建任务 → 执行任务 → 关闭线程
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建和销毁线程对象的操作,节省了大量的时间和资源。
线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
Java中有四种线程池(ExecutorService):缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池
缓存线程池
长度无限制
执行流程:
判断线程池是否存在空闲线程
存在则使用
不存在,则创建线程 并放入线程池, 然后使用
1 | ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池对象 |
定长线程池
长度是指定的数值
步骤:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
1 | ExecutorService service = Executors.newFixedThreadPool(2); |
单线程线程池
步骤:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲则等待池中的单个线程空闲后使用
1 | ExecutorService service = Executors.newSingleThreadExecutor(); |
周期性任务定长线程池
步骤:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程,并放入线程池后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务
1 | ScheduledExecutorService service = Executors.newScheduledThreadPool(2); |