多线程

多线程

并发:同一时刻,多个任务交替执行,造成一种貌似同时,的错觉,单核cpu实现多任务是并发

并行:同一时刻,多个任务同时执行,多核cpu可以实现并行。并发和并行可能同时存在

多线程实现方式(三种)

  1. 继承Thread类(实现简单,可以直接使用父类方法,可拓展性差,不能继承其他类)
  2. 实现Runnable接口(拓展性强,不能直接使用Thread类里面的方法)
  3. 使用FutureTask创建线程,可以获取多线程执行结果

线程常用的方法

interrupt:线程中断,打断线程执行

isInterrupted() :判断线程是否被打断

  • 处于阻塞状态下面的线程(sleep , wait , join),此时被打断,打断标记会置为false
  • 打断处于正常运行的线程,打断标记会置为true
  • 还可以打断park状态下面的线程,park方法在JUC并发包LockSupport下面,被interrupt打断后,后续再使用park方法不在起作用,使用interrupted方法将打断标记置为假,才可以是park生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Interrupt {
public static void main(String[] args) {
Interrupt_ interrupt_ = new Interrupt_();
Thread thread = new Thread(interrupt_);
thread.start();
thread.interrupt();
}

}
class Interrupt_ implements Runnable{
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断了");
}
}
}
}

yield: 线程礼让,让出cpu,让其他的线程先执行,但是礼让的事件不确定,所以不一定成功

join:线程插队,插队的线程一旦插队成功,则肯定先执行完插入线程的所有的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Thread_Join_Yield {
public static void main(String[] args) throws InterruptedException {
T_Join01 t_join = new T_Join01();
Thread thread = new Thread(t_join);
thread.start();
for (int i = 0;i<20;i++){
Thread.sleep(1000);
System.out.println("MAIN ==> 00");
if (i==5){
//子线程执行完毕,主线程接着执行
thread.join();
}
}
}
}
class T_Join01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join==>01");
}
}
}

有时效的join(Long n) 等待线程运行结束,最多等待n毫秒

​ 会根据等待时间判断,如果等待时间小于线程执行时间,就不会再等待,如果等待时间大于线程执行时间,就会随着线程结束而停止等待

不推荐的方法

有一些不推荐使用的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁

stop ——停止线程运行

suspend ——-挂起(暂停)线程运行

resume ——-恢复线程运行

线程优先级

  • 线程优先级会提示任务调度器优先调度该线程,但是仅仅是一个提示,调度器可以忽略它

  • 如果cpu比较忙,那么优先级高的线程将会获得更多的时间片,cpu闲置时几乎没作用

  • yield和线程优先级都很难对线程进行控制,具体还是依赖于操作系统的任务调度器

守护线程

  1. 用户线程: 也叫工作线程,当线程的任务执行完或通知方式结束

  2. setDaemon() 设置为true,该线程就变为守护线程,守护线程会随着所有用户线程的结束而结束

  3. 常见的守护线程:垃圾回收机器,GC

线程的生命周期

线程的五种状态

初始状态 —-未拿到系统资源

可运行状态 —-未获得系统时间片,当获取时间片时,转换为运行状态

运行状态 —-获得系统时间片,当时间片使用完毕,转换为可运行状态

终止状态 —–表示线程执行完毕,生命周期结束

阻塞状态 —–调用阻塞API,放弃CPU使用权,启用上下文切换

线程的六种状态

按照java中枚举类来说的,线程有六种状态

synchronized解决方案

应用互斥

为了避免临界区竞态条件发生,有多种手段可以达到目的

  • 阻塞式解决方案:synchronized,lock

  • 非阻塞式解决方案:原子变量

    synchronized俗称对象锁,它采用互斥方式让同一时刻,只能由一个线程持有对象锁,其他线程想要获取这个锁只能阻塞,这样就可以保证拥有锁的线程可以安全执行临界区代码,不需要担心上下文切换

    注意

    虽然java中互斥和同步都可以使用synchronized关键字完成,但还是有区别的

    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    • 同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到每个点

    synchronized实际上是保证了临界区代码的完整性,临界区代码对外是不可以分割的,不会被线程切换所打断

线程同步机制(synchronized)

  1. 在多线程编程,一些敏感数据不能被多个线程同时访问,此时就使用同步访问技术,在保证数据在任何时刻,最多有一个线程访问,保证数据的完整性
  2. 线程同步,当有一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到线程完成操作,其他线程才能对该内存地址进行操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Thread_Ticket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket).start();
new Thread(ticket).start();
new Thread(ticket).start();
}
}
class Ticket implements Runnable{
private static int ticketNum = 50;//多个线程共享票数
@Override
public void run() {
while (ticketNum > 0){
sold();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void sold(){
if (ticketNum <= 0){
System.out.println("火车票买完了");
return;
}
System.out.println(Thread.currentThread().getName()+"卖出一张票"+"剩余票数"+(--ticketNum));

}
}

FutureTask

  • futuretask: futuretask里的get方法会造成线程阻塞,工作中不建议使用

  • 改进后的get方法可以在get里面设置超时时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FutureTask_ {

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<Integer> integerFutureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName() + "执行futureTask");
TimeUnit.SECONDS.sleep(5);
return 1024;
});

Thread a = new Thread(integerFutureTask, "a");
a.start();

System.out.println(integerFutureTask.get(2L,TimeUnit.SECONDS));
System.out.println("执行主线程");
}
}
  • 使用轮询方式来缓解线程阻塞

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class FutureTask_ {

    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    FutureTask<Integer> integerFutureTask = new FutureTask<>(() -> {
    System.out.println(Thread.currentThread().getName() + "执行futureTask");
    TimeUnit.SECONDS.sleep(5);
    return 1024;
    });

    Thread a = new Thread(integerFutureTask, "a");
    a.start();

    while (true){
    if (integerFutureTask.isDone()){
    System.out.println("futuretask完毕");
    break;
    }else {
    TimeUnit.SECONDS.sleep(2);
    System.out.println("未执行完成");
    }
    }
    System.out.println("执行主线程");
    }
    }

线程安全问题

由于分时系统造成的线程切换,导致的线程安全问题

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果没有共享,则线程安全
  • 如果被共享,根据状态是否被改变,又分为两种情况
    • 如果只有读操作,线程安全
    • 如果有写操作,这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但是局部变量引用的对象未必是安全的
    • 如果该对象没有逃离方法的作用访问,他是线程安全的
    • 如果该对象逃离方法的作用范围,则需要考虑线程安全

并发编程_模式

两阶段终止模式

错误思路

  • 使用线程对象的stop方法停止线程

    • ​ stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么杀死后就再也没有机会释放锁,其他线程将无法获取锁
  • 使用System.exit(int)方法停止线程

    • ​ 目的仅是停止一个线程,但是这种方法会将程序终止,虚拟机直接退出

两阶段终止模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TwoPathTermination {
private Thread monitor;
public void start(){
monitor = new Thread(()->
{
while (true){
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()){
System.out.println("执行线程结束前需要处理的事情");
break;
}
try {
currentThread.sleep(1000);
System.out.println("监视器启动监视");
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记,由于在睡眠时被打断,打断标记为false
currentThread.interrupt();
}
}
});
monitor.start();
}
public void stop(){
monitor.interrupt();
}
}
class Test{
public static void main(String[] args) throws InterruptedException {
TwoPathTermination twoPathTermination = new TwoPathTermination();
twoPathTermination.start();
Thread.sleep(5000);
twoPathTermination.stop();
}
}