【狂神说Java】多线程详解笔记

线程状态

image-20220203145906268

java类的方式

  • 外部类: 在主函数所在的类的外部定义的类, 若是在同一文件, 直接new; 若是不同文件, 导包再new

  • 静态内部类: 在主函数所在的类的内部且在主函数外部定义的类, 直接new

    因为main是static, 所以他也要是static

  • 局部类: 在主函数内定义的类, 直接new

  • 匿名内部类: Interface o = new Interface(){类的定义}, 需先定义interface

  • lambda:

    1
    2
    3
    4
    Runnable as = () -> {
    System.out.println("as");
    };
    as.run();
    1
    new Thread(()->System.out.println("as")).start();

线程休眠sleep

Thread.sleep(1000);单位为毫秒

线程礼让yield

  • 调用方式: 在线程内Thread.yield();

  • 将线程由运行态转为就绪态, 此时重新决定哪个线程获取cpu, 可能还是刚才的进程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.zq;

    public class yield {
    public static void main(String[] args) {
    myYield y = new myYield();
    new Thread(y, "a").start();
    new Thread(y, "b").start();
    }
    }

    class myYield implements Runnable {
    @Override
    public void run() {
    System.out.println(Thread.currentThread().getName() + "->start");
    Thread.yield();
    System.out.println(Thread.currentThread().getName() + "->end");
    }
    }

join

  • 阻塞其他线程, 强制该线程运行, 类似插队

  • 调用: thread.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
    29
    30
    31
    32
    33
    34
    package com.zq;

    public class join implements Runnable {

    public static void main(String[] args) throws InterruptedException {
    join j = new join();
    Thread thread = new Thread(j);
    thread.start();

    for (int i = 0; i < 10; i++) {
    if (i == 5) {
    thread.join();
    }
    System.out.println("main->" + i);
    }


    }

    @Override
    public void run() {
    for (int i = 0; i < 20; i++) {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("vip->" + i);
    }

    }
    }


线程优先级

image-20220203150103757

默认为5, main线程优先级为5

守护线程

image-20220207134005679

  • 用户线程执行完后,虚拟机即关闭,尽管守护线程没有执行完毕
  • 使用: thread.setDaemon("true");默认为false

线程同步

image-20220203152040944

image-20220203152609118

  • 线程同步形成条件: ==队列+锁==
  • sleep不会释放锁

synchronized

image-20220203175658674

image-20220203175715082

  • 同步方法默认锁的对象是this
  • 可以用同步块完全替代同步方法

死锁 活锁 饥饿

死锁: 两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁

  • 形象的例子: 两个小朋友分别拿着对方喜欢的玩具, 而且谁都不愿先把手中的玩具先给对方
  • 死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候
  • 死锁会让你的程序挂起无法完成任务
  • 解决方法: 只能通过中止并重启的方式来让程序重新执行
  • 在程序中, 双方不会协商, 只会一直僵持, 程序一直阻塞

死锁的四个必要条件:

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

预防死锁–破坏死锁的四个必要条件

破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。

破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。

破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。

破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。

活锁: 活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败, 线程为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进

  • 形象的例子: 两条车道上, 两人在同一车道相向行走, 发现对方阻碍自己的道路时, 两人都向另一条车道偏移, 结果还是会阻碍对方的道路(传说中的神默契), 这样一直僵持
  • 活锁有可能自行解开
  • 活锁可以认为是一种特殊的饥饿

饥饿: 是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况

  • 饥饿可以通过先来先服务资源分配策略来避免
  • 优先级高的线程抢占资源, 导致优先级低的线程一直得不到资源
  • 某个线程长期占用资源, 导致其他线程得不到资源

lock

1
2
3
4
5
6
7
8
private ReentrantLock lock=new ReentrantLock();
lock.lock();
try {
//放并发操作代码
System.out.println(Thread.currentThread().getName() + "-->抢到第" + ticketNum-- + "张票");
} finally {
lock.unlock();
}
  • 性能比ssynchronized好
  • 一般放在try-finally中, 不然容易出问题

线程通信

image-20220205203121807

线程池

image-20220207113051618

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
package com.zq;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class pool {


public static void main(String[] args) {
MyThread mt = new MyThread();
ExecutorService service = Executors.newFixedThreadPool(2);
long startTime = System.currentTimeMillis(); //获取开始时间(毫秒)
for (int i = 0; i < 50; i++) {
service.execute(mt);
}
service.shutdown();
long endTime = System.currentTimeMillis(); //获取结束时间(毫秒)
System.out.println("程序运行时间: " + (endTime - startTime) + "ms");

}
}

class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}

如果线程池大小小于开启的线程数, 则等待之前的线程执行完毕释放, 再执行新线程

summary

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
36
37
38
39
40
41
42
43
44
45
46
47
package com.zq;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Summary {

public static void main(String[] args) {

new MyThread1().start();
new Thread(new MyThread2()).start();
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread3());
new Thread(ft).start();
try {
Integer integer = ft.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("1继承Thread");
}
}

class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("2实现Runnable");
}
}

class MyThread3 implements Callable<Integer> {

@Override
public Integer call() throws Exception {
System.out.println("3实现callable");
return 520;
}
}

一些扩展

  • java中的锁Lock就是基于AbstractQueuedSynchronizer来实现的
  • 在大多数情况下,我们写并发代码使用synchronized就足够了,而且使用synchronized也是首选
  • 但是lock更加灵活
  • image-20220207104526155
  • lockInterruptibly方法可以响应中断,lock方法会阻塞线程直到获取到锁,而tryLock方法则会立刻返回,返回true代表获取锁成功,而返回false则说明获取不到锁
  • newCondition方法返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。多个线程可以等待在同一个条件变量上,一些线程会在某些情况下通知等待在条件变量上的线程,而有些变量在某些情况下会加入到条件变量上的等待队列中去。
  • 独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁
  • 共享锁则可以允许多个线程获取到锁

ReentrantLock

  • 是lock的子类
  • 可重入性: 同一个线程可以多次获得锁,而不同线程依然不可多次获得锁
  • 划分:
    • 公平锁: 保证等待时间最长的线程将优先获得锁
    • 非公平锁: 并不会保证多个线程获得锁的顺序,并发性能表现更好,ReentrantLock默认使用非公平锁

CopyOnWriteArrayList

  • ArrayList的线程安全版本
  • CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。CopyOnWriteArrayList适用于读多写少的并发场景
  • CopyOnWriteArraySet是线程安全版本的Set实现,它的内部通过一个CopyOnWriteArrayList来代理读写等操作,使得CopyOnWriteArraySet表现出了和CopyOnWriteArrayList一致的并发行为
  • 使用了ReentrantLock来支持并发操作

多线程中的三大特性

  • 原子性: 一个或多个操作,要么全部执行完成,要么就都不执行
  • 可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程就能够立即看到修改的值
  • 有序性: 编译器可以对指令进行重排, 对单线程无影响, 但可能会影响多线程

wait notify

  • 必须在synchronized 中执行
  • wait 必须暂停当前正在执行的线程,并释放资源锁,让其他线程可以有机会运行
  • notify/notifyall:唤醒锁池中的线程,使之运行
  • 调用wait方法后, 线程会放弃对象锁, 进入等待此对象的等待锁定池, 只有再次调用此对象的notify方法, 本线程才会进入对象锁池准备, 才有可能获取对象锁进入运行状态

Volatile

在 java 中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是 CPU 缓存上进行的,之后才会同步到主存中,而加了 volatile 修饰符的变量则是直接读写主存

volatile 虽然具有可见性但是并不能保证原子性, 所以不能替代Synchronize

Volatile在某些情况下性能优于Synchronize

参考

Java CopyOnWriteArrayList详解 - 简书 (jianshu.com)

Java可重入锁详解 - 简书 (jianshu.com)

Java可重入锁详解 - 简书 (jianshu.com)