多线程
以下代码均使用IntelliJ IDEA 2020.1.4 x64运行
多线程知识梳理
一、多线程基础概述
线程与进程
关系:
1.一个进程最少有一个线程。
2.线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
多线程为了让多条执行路径能均分,能更合理的交替执行。
线程调度
分别为分时调度和抢占式调度。
Java使用的调用机制是抢占式调度。
多线程并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
同步与异步
同步:排队执行,效率低但是安全。
异步:同时执行,效率高但是数据不安全。
并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时执行)。
两者不是一个概念,同一时间段和同一时刻有很大的区别。
二、继承Thread
原理看如下代码:
public static void main(String[] args) {
//Thread
MyThread myThread = new MyThread();
myThread.start();//与下边的代码并发执行,抢占式分配,输出顺序会不一样
for (int i = 0; i <10 ; i++) {
System.out.println("汗滴禾下土"+i);
}
}
public class MyThread extends Thread{
/**
* run方法就是线程要执行的任务方法
*
* 每个线程都拥有自己的栈空间,共用一份堆内存
*/
@Override
public void run() {
//这里的代码就是一条新的执行路径
//这个执行路径的处罚方式,不是调用run方法,而是通过thread对象的start()来启动任务
for (int i = 0; i <10 ; i++) {
System.out.println("锄禾日当午"+i);
}
}
}
run()方法就是自己编写的想让线程执行的任务方法,他与main函数中的方法并发执行,由于是抢占式分配,所以输出顺序会不一样。
Thread常用方法
1.start(开启线程,start是通过线程来调用run方法)
2.run 此run非彼run (不是在run方法实现线程的逻辑,而是thread.run(),这个run方法是直接调用了线程中的run)
3.yield(暂停当前线程,并执行其他线程)
4.sleep(使当前线程由运行状态变成阻塞状态,若睡眠时其他线程调用了interrupt方法,会导致sleep抛出异常InterruptException)
5.join(保证当前线程在其他线程开始时会结束)(如下,A线程想运行的话,必须等B线程结束才能运行(将处于阻塞状态))
6.interrupt(中断线程)
7.wait/notify(从Object类继承下来的方法)
8.setPriority(设置线程优先级(只能在线程开始前设置))
三、实现Runnable
原理看如下代码:
public static void main(String[] args) {
//实现Runnable
//1.创建一个任务对象
MyRunnable r = new MyRunnable();
//2.创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
//3.执行这个线程
t.start();
for (int i = 0; i <10 ; i++) {
System.out.println("疑是地上霜"+i);
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程的任务
for (int i = 0; i <10 ; i++) {
System.out.println("床前明月光"+i);
}
}
}
与继承Thread不同,Thread只可以单继承,而Runnable可以多实现。
实现Runnable相比于继承Thread的好处
- 是通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。
- 可以避免单继承所带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 后续学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程。
使用线程还可以使用匿名内部类的方式,具体代码如下:
public static void main(String[] args) {
//匿名内部类
new Thread(){
@Override
public void run() {
for (int i = 0; i <10 ; i++) {
System.out.println("一二三四五"+i);
}
}
}.start();
for (int i = 0; i <10 ; i++) {
System.out.println("六七八九十"+i);
}
}
线程名称的获取以及线程休眠
1、线程名称获取
- new Thread(new MyRunnable(), “锄禾日当午”).start():给线程命名
- Thread.currentThread().getName():获取当前运行线程的名称,如果未命名,则获取默认的命名。
2、线程休眠
Thread.sleep(1000):线程休眠1秒,如果在循环中的话,则一秒之后再次循环。
四、线程中断
interrupt():中断标记,添加之后当线程运行到中断标记时就会停止运行。
原理代码如下:
public static void main(String[] args) {
//线程阻塞-所有比较消耗时间的操作
//线程中断
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;
}
}
}
}
由上边的代码可以看到,当main函数循环完毕之后,代码运行到中断标记,run()方法发现中断标记,线程自杀,线程运行结束。
用户线程和守护线程
用户线程:当一个进程不包含任何的存活的用户线程时,进行结束。
守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。
五、线程安全问题
举个栗子
有一个活动的售票点要准备开始售票,有三个售票窗口,总共要售出10张票,用代码实现。
首先第一种方法:
public static void main(String[] args) {
//线程不安全
//解决方案1,同步代码块
//格式: synchronized(锁对象){
//
// }
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;
@Override
public void run() {
while (true) {
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;
}
}
}
}
运行结果如下:
可以看到余票在最后出现了负数情况,单看代码逻辑是永远不会出现负数的,因为循环只有在票数大于0才会运行,但是依旧出了问题,这是为什么呢?
假设三个售票口分别为A,B,C,余票只剩一张,当A进入循环时,他在循环里可能费了点时间,在运行过程中丢失了时间片,这个时候B抢到了时间片,也进入了循环,这个时候票数还没有减,因为A可能还在count–前,同样的情况,这个时候C也抢到了时间片进入了循环,三个逻辑同时都处在循环中。这个时候A 拿到时间片,运行count–,这个时候count变成了0;然后B拿到时间片,运行count–,count变成了-1;最后C拿到时间片,运行count–,count变成了-2。这就是线程不安全问题。
1、线程安全1-同步代码块
格式:
private static Object o = new Object();
synchronized(锁对象:o){
}
作用:加锁,可以使线程排队执行。
static class Ticket implements Runnable{
//票数
private int count = 10;
private Object o = new Object();//多个线程只能看同一把锁
@Override
public void run() {
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、线程安全2-同步方法
原理代码如下:
public static void main(String[] args) {
//线程不安全
//解决方案1,同步方法
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;
@Override
public void run() {
while (true) {
boolean flag = sale();
if (!flag){
break;
}
}
}
public synchronized boolean sale(){
//this 锁
//静态:类名,class 锁
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;
}
}
3、线程安全3-显示锁 Lock 子类 ReentrantLock
原理代码如下:
public static void main(String[] args) {
//线程不安全
//解决方案3,显示锁 Lock 子类 ReentrantLock
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;
//显示锁 :参数为true,就表示是公平锁
private Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
lock.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;
}
lock.unlock();
}
}
}
公平锁:排队
不公平锁:不排队,抢
六、显示锁Lock和隐式锁synchronized的区别
1、层面不同
synchronized:Java中的关键字,是由JVM来维护的,时JVM层面的锁
Lock:使用lock是调用对应的API,是API层面的锁
2、使用方式不同
synchronized:程序能够自动获取锁和释放锁。非逻辑问题的话,不会出现死锁。
Lock:需要手动获取锁和释放锁。不释放锁,就可能导致死锁
3、等待是否可中断
synchronized:不可中断,除非抛出异常或者正常运行完成
Lock:可以中断
中断方式:
- 调用设置超时方法tryLock(long timeout ,timeUnit unit)
- 调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断
4、加锁的时候是否可以设置成公平锁
synchronized:只能为非公平锁
Lock:两者都可以,默认是非公平锁。
在其构造方法的时候可以传入Boolean值。true:公平锁、false:非公平锁
5、锁绑定多个条件来condition
- synchronized
不能精确唤醒线程。要么随机唤醒一个线程;要么是唤醒所有等待的线程。 - Lock
用来实现分组唤醒需要唤醒的线程,可以精确的唤醒。
6、性能区别
- synchronized
托管给JVM执行,Java1.5中,由于需要调用操作接口,可能导致加锁消耗时间过长,与Lock性比性能低。1.6以后,语义定义更加清晰,有适应自旋、锁粗化、锁消除、轻量级锁、偏向锁等,可进行许多优化,性能提高了,与Lock差不多。 - Lock
java写的控制锁的代码,性能高。
七、线程死锁
举个栗子
两个顾客在听一家卖衣服的商店,商店中有两个试衣间,A顾客进入第一个试衣间之后锁上了门,B顾客进入第二个试衣间之后锁上了门,两位顾客都进入试衣间之后,A顾客突然发现他进的试衣间没灯,没灯怎么换衣服,于是他想换一间试衣间,但是他进这个试衣间之前看到B顾客进入了另外一个试衣间,于是他决定等B顾客出来之后他再换试衣间;同一时间,B顾客发现他进的试衣间全是水,根本没法换衣服,于是他也想换一个试衣间,但是他进试衣间之前看到A顾客进入到另一个试衣间,于是他也决定等A顾客出来之后他再换试衣间,他们等啊等…等啊等…
线程死锁问题也就像这个栗子一样,示例代码如下:
public static void main(String[] args) {
//线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}
static class MyThread extends Thread{
private Culprit c;
private Police p;
public MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
//罪犯
static class Culprit{
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放了人质");
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放走了,罪犯也放了人质");
}
}
//警察
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放过你");
c.fun();
}
public synchronized void fun(){
System.out.println("警察救到了人质,但是罪犯跑了");
}
}
运行结果如下:
从上图可以看到,罪犯和警察的fun()方法都没有执行,这就是线程死锁造成的。
八、线程的状态
new:表示线程刚被创建,但是未被启动
Runnable:在Java虚拟机中执行的线程处于此状态
Blocked:排队时的线程的状态
Waiting:无限休眠状态,等待唤醒
TimedWaiting:没有被唤醒,等到时间到了会自己醒的状态
Trminated:已退出(已死亡)的线程的状态
九、第三种线程的创建方式-带返回值的线程Callable
Callable: 返回结果并且可能抛出异常的任务。
优点:
- 可以获得任务执行返回值;
- 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。
原理代码如下:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
Integer j = task.get();
System.out.println("返回值为:"+j);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
static class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
//Thread.sleep(3000);
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
return 100;
}
}
运行结果如下:
Callable获取返回值:
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
Runnable与Callable的异同
Runnable 与 Callable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
十、线程池
1、缓存线程池
* (长度无限制)
* 任务加入后的执行流程
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池,然后使用
2、定长线程池
*(长度是指定的数值)
* 任务加入后的执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
3、单线程线程池
* 执行流程:
* 1. 判断线程池的那个线程是否空闲
* 2. 空闲则使用
* 3. 不空闲,则等待 池中的单个线程空闲后使用
4、周期性任务定长线程池
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*
* 周期任务执行时:
* 定时执行,当某个时机触发时,自动执行某任务
* 1.定时执行一次
* 参数1。定时执行任务
* 参数2.时长数字
* 参数3.时长数字的时间单位,TimeUnit的常量指定
代码如下:
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日当午");
}
},5, TimeUnit.SECONDS);
* 周期性执行任务
* 参数1.任务
* 参数2.延迟时长数字(第一次执行在什么时间以后)
* 参数3.周期时长数字(每隔多久执行一次)
* 参数4.时长数字的单位
代码如下:
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("汗滴禾下土");
}
},5,1, TimeUnit.SECONDS);
十一、Lambda表达式
原理代码如下:
public static void main(String[] args) {
print((int x, int y) -> {
return x+y;
},100,200);
}
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
static interface MyMath{
int sum(int x,int y);
}
运行结果如下:
总结
- 公平锁与非公平锁的使用;
private Lock lock = new ReentrantLock(true);
括号内为true即为公平锁,否则为非公平锁。
- 1.什么是线程?线程和进程的区别?
线程:线程是CPU调度的最小单位,也是程序执行的最小单位。没有单独地址空间,线程属于进程,不能独立执行,每个进程至少要有一个线程,称为主线程。
进程:进程是系统进行资源分配的基本单位,有独立的内存地址空间;
- 2.描述CPU和多线程的关系
第一阶段,单CPU时代,单CPU在同一时间点,只能执行单一线程。
第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,就好像多个任务在同时运行。
第三阶段,多CPU多任务阶段,真正实现的,在同一时间点运行多个线程。具体到哪个线程在哪个CPU执行,这就跟操作系统和CPU本身的设计有关了。
- 3.什么是线程安全/线程不安全?
线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。
线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据