✨你好啊,我是“ 罗师傅”,是一名程序猿哦。
🌍主页链接:楚门的世界 - 一个热爱学习和运动的程序猿
☀️博文主更方向为:分享自己的快乐 briup-jp3-ing
❤️一个“不想让我曾没有做好的也成为你的遗憾”的博主。
💪很高兴与你相遇,一起加油!

前言

目标:Java高级编程,灵活运用反射,线程,IO和网络等进行编程

进程线程

  • 进程

进程是指一个内存中运行的应用程序,它是资源分配的最小单位

一个程序从创建、运行到消亡,这样整个过程就是一个进程

一个操作系统中可以同时运行多个进程,每个进程运行时,系统都会为其分配独立的内存空间

在操作系统中,启动一个应用程序的时候,会有一个或多个进程同时被创 建,这些进程其实就表示了当前这个应用程序,在系统中的资源使用情况 以及程序运行的情况。如果关闭这个进程,那么对应的应用程序也就关闭 了。

  • 线程

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中也可以有多个线程,此时这个应用程序就可以称之为多线程程序它是CPU调度的最小单位

结论:一个程序运行后至少有一个进程,一个进程中可以包含一个(main线程)或多个线程!

当一个进程中启动了多个线程去分别执行代码(同时完成多个功能)的时 候,这个程序就是多线程程序,内存等资源使用情况如下:

思考:JVM是多线程吗?

是,JVM可以在运行进程的同时,进行GC垃圾回收,同一时刻做不同事情

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 Test01_JVM {
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i < 1000000; i++) {
new Test01(i);
}
Thread.sleep(3000);
System.out.println("main end ...");
}
}

class Test01 {
int n;

public Test01(int n) {
this.n = n;
}

// 当GC进行垃圾回收指定对象的时候对象的finalize方法会被自动调用
@Override
protected void finalize() throws Throwable {
System.out.println("Test被销毁, n: " + n);
super.finalize();
}
}

并发并行

  • 并发:指两个或多个事件在同一时间段内发生

线程的并发执行,是指在一个时间段内(微观),两个或多个线程,使用同一个CPU交替运行。

  • 并行:指两个或多个事件在同一时刻发生(同时发生

线程的并行执行,是指在同一时刻,两个或多个线程,各自使用一个CPU同时运行

如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来 执行代码

如果计算机是多核CPU的话,那么同一时刻有可能是俩个线程同时使用 不同的CPU执行代码

补充内容:

如果我们的计算机是多核的,在程序中编写了俩个线程,然后启动并 运行它们,计算机会用一个CPU运行还是两个CPU去运行,我们无法知 道也无法控制,因为计算机内核中有专门的资源调度算法负责资源的分 配,我们从应用程序的层面无法干涉。

线程调度

  • 时间片

并发多线程只有一个CPU,某个微观时刻,当指定线程拥有CPU的使用权,则该线程代码就可以执行,而其他线程阻塞等待。

一个线程不可能一直拥有CPU的使用权,不可能一直执行下去,它拥有CPU执行的时间是很短的,微秒纳秒级别,这个时间段我们就称之为CPU时间片。

线程执行时如果一个时间片结束了,则该线程就会停止运行,并交出CPU的使用权,然后等待下一个CPU的时间片的分配。

在宏观上,一段时间内,我们感觉俩个线程在同时运行代码,其实在微观中,这两个线程在使用一个CPU的时候,它们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使用权,只是它们俩个交替运行的速度太快了,给我们的感觉,好像是它们俩个线程在同时运行。

  • 调度方式
    • 时间片轮转
      • 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
    • 抢占式调度
      • 系统会让优先级高的线程优先使用CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片

JVM中的线程,使用的为抢占式调度。

线程的创建

java.lang.Thread 是java中的线程类,所有线程对象都必须是Thread 类或其子类的实例。 每个线程的作用,就是完成我们给它指定的任务,实际上就是执行一段我 们指定的代码。我们只需要在 Thread 类的子类中重写 run 方法,完成相应 的功能。

Java中通过继承Thread类来创建并启动一个新的线程的步骤如下:

  • 定义 Thread 类的子类,重写 run()方法,run() 方法中的代码就是线程的执行任务
  • 创建Thread子类对象(可以是匿名内部类对象),这个对象就代表一个要独立运行的新线程
  • 调用线程对象的**start()**方法来启动该线程。
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
public class Test04_Thread {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread2();
t.start();
for (int i = 0; i < 50; i++) {
System.out.println("in main, hello");
// 当前执行代码的线程睡眠500毫秒
Thread.sleep(500);
}
}
}

class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("in run hello method");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

可以看出,main线程在执行main方法的过程中,创建并启动了t线程, 并且t线程启动后,和main线程就没有关系了,这时候main线程和t线程 都是自己独立的运行,并且他们俩个是要争夺CUP的时间片(使用权) 的

attention1: 栈区,又被称为方法调用栈,是线程专门执行方法中代码的地方,并且每一个线程,都有自己都灵的栈空间,和别的线程互不影响。

attention2: 最先启动的线程是主线程(main线程),因为它要执行程序的入口main方法,在主线程中,创建并且启动了t线程,启动之后main线程和t线程各独立运行,并且争夺CPU的时间片

attention3:线程启动之后(调用start方法),会开始争夺CPU的时间片,然后自动执行run如果子类重写了,那么就调用到重写后的run方法。

attention4:堆区是对所有线程的共享的,每个线程中如果创建了对象,那么对象就会存放到堆区中

attention5:线程对象t被创建出来的时候,它还只是一个普通的对象,但是当调用饿了t.start()方法之后,线程对象t可以说才真正的“现出原形”:开辟了单独的栈空间,供线程t调用方法使用

  • 提高程序的执行效率:多线程可以让程序同时处理多个任务,从而提高程序的执行效率。尤其是在涉及到大量数据处理或耗时的操作时,多线程可以极大地缩短程序的执行时间。
  • 提高系统资源利用率:多线程可以充分利用计算机的多核处理器,同时利用计算机的内存、网 络、磁盘等资源,从而提高系统资源的利用率。
  • 提高程序的响应速度:多线程可以使程序在处理任务时不会被阻塞,从而提高程序的响应速度, 保证程序的实时性。
  • 实现复杂的交互操作:多线程可以在程序中实现复杂的交互操作,例如同时响应多个用户的请 求,同时执行多个任务等。

匿名内部类

实际开发中,这样的方式更常见,书写简洁,推荐使用

线程名称

  • 默认线程名:

不管是主线程,还是我们创建的子线程,都是有名字的。默认情况下,主线程的名字为main,main线程中创建出的子线程,它们名字命名规则如下:

1
2
3
4
//JavaAPI-Thread构造器源码
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

其中, “Thread-“ + nextThreadNum() 就是在拼接出这个线程默认的名 字,比如第一个子线程Thread-0,第二个为Thread-1,第三个为Thread-2, 以此类推。

  • 获取当前线程对象
1
2
3
public static native Thread currentThread();
// 当前线程 === 执行当前方法的线程
// 也就是看目前是对在调用当前方法。
  • 获取线程名
1
public final String getName();
  • 常见用法
1
String name = Thread.currentThread().getName();

注意,一定要记得,start方法启动线程后,线程会自动执行run方法

千万不要直接调用run方法,这样就不是启动线程执行任务,而是普通的方法调用,和调用sayHello没区别

设置线程名

  • 通过线程对象设置线程名
1
public final synchronized void setName(String name);
  • 创建对象时,设置线程名
1
2
public Thread(String name);
public Thread(Runnable target, String name);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
Thread thread = Thread.currentThread();
//第一种设置方式
thread.setName("MAIN线程");
System.out.println("线程名称: " + thread.getName());
//第二种设置方式
Thread t = new Thread("子线程t") {
@Override
public void run() {
System.out.println("in run, 线程名称:" + Thread.currentThread().getName());
}
};
t.start();
}

main线程

1
2
3
4
5
6
7
public static void main(String[] args) {
//获取执行当前方法的线程对象
Thread currentThread = Thread.currentThread();
System.out.println("执行当前方法的线程名字为:"+currentThread.getName());
}
//运行结果:
//执行当前方法的线程名字为:main

上面代码使用java命令运行的过程是:

  • 使用java命令运行Test类,会先启动JVM
  • 应用类加载器通过CLASSPATH环境遍历配置的路径,找到Test.class文件,并加载到方法区
    • 注意:这里会同时产生一个Class类型对象,来代表这个Test类型,并且会优先处理类中的静态代码(静态属性、静态方法、静态代码块
  • JVM创建并启动一个名字叫做main的线程
  • main线程将Test中的main方法加载到栈区中
  • 在栈里面,main线程就可以一行行的执行方法中的代码了
  • 如果在执行代码中,遇到了方法调用,那么线程会继续把被调用的方法,加载到栈中(压栈操作),然后执行栈顶这个最新添加进来的方法,栈顶方法执行完,就释放(出栈操作),然后在执行当前最新的栈顶方法
  • 代码执行过程输出执行结果
  • 当前是单线程程序,main线程结束了,JVM就停止了
  • 如果是多线程程序,那么JVM要等于所有线程都结束了才会停止

Runnable

前面的课程中,我们通过Thread的子类创建线程。

现在我们学习第二种创建线程对象的方式:借助Runnable接口的实现类完 成

java.lang.Runnable ,该接口中只有一个抽象方法 run

其实 Thread 类也是 Runnable 接口的实现类,其代码结构大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//JavaAPI-Thread类源码分析
public class Thread implements Runnable {
/* What will be run. */
private Runnable target;
public Thread() {
//...
}
public Thread(Runnable target) {
this.target = target;
//..
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}

可以看出,子类重写Thread中的run方法,这个run方法其实也来自于 Runnable接口

通过以上的代码结构可知,我们可以借助构造器 public Thread(Runnable target) 直接创建线程对象,该构造器需要传一个 Runnable 接口的实现类对象。

当线程对象创建成功后,调用线程对象 run 方法,默认会调用Runnable实 现类重写的run方法!

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 Test08_Runnable {
public static void main(String[] args) {
// 3.实例化对象
Runnable r = new MyRunnable2();
// r.run();
// 4.创建Thread对象
Thread th = new Thread(r);
th.setName("child-thread1");
// 5.启动线程
th.start();
// 匿名内部类方式 获取Runnable实现类对象
Runnable r2 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 80; i >= 30; i--)
System.out.println("in thread: " + name + " i: " + i);
}
};
Thread th2 = new Thread(r2, "子线程2");
th2.start();
}

}

//1.创建Runnable实现类
class MyRunnable2 implements Runnable {
//2.重写run方法
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 20; i <= 70; i++)
System.out.println("in thread: " + name + " i:" + i);
}
}

两种线程实现方式对比:

  • 继承Thread类
    • 好处:编程比较简单,可以直接使用Thread类中的方法
    • 缺点:可以扩展性较差,不能再继承其他的类
  • 实现Runnable接口
    • 好处:扩展性强,实现该接口的同时还可以继承其他的类
    • 缺点:编程相对复杂,不能直接使用Tread类中的方法

守护线程

Java中,线程可以分为两类:

  • 前台线程 = 执行线程 = 用户线程

这种线程专门用来执行用户编写的代码,地位较高,JVM是否会停止运行,就是要看当前是否还有前台线程没有执行完,如果还剩下任意一个前台线程没有“死亡”,那么JVM就不能停止!(结束立即停止)

例如,执行程序入口的主线程(main),就是一个前台线程,在单线程程 序中,main方法执行完,就代表main线程执行完了,这时候JVM就停止了

注意:我们在主线程创建并启动的新线程,默认情况下就是一个前台线程

  • 后台线程 = 守护线程 = 精灵线程

这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。(不关心)

例如,垃圾回收器,其实就一个后台线程,它一直在背后默默的执行着负 责垃圾回收的代码,为我们前台线程在执行用户代码的时候,提供一个良 好的内存环境。

设置后台(守护)线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//JavaAPI-Thread类守护线程设置源码分析
public class Thread implements Runnable {
//...省略
/* Whether or not the thread is a daemon thread. */
private boolean daemon = false;
//...省略
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}
}

优先级

线程类Thread中,有一个属性表示线程的优先级,取值1-10,默认为5。线程的优先级越高,越容易获得CPU时间片而执行

源码分析:

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
//JavaAPI-Thread类线程优先级设置源码
public class Thread implements Runnable {
private int priority;
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
public final int getPriority() {
return priority;
}
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority <
MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
//核心代码,底层借助该方法实现
setPriority0(priority = newPriority);
}
}
private native void setPriority0(int newPriority);
}

​ 可以看出,最终设置线程优先级的方法,是一个native方法,并不是Java语言实现的

多个线程争夺CPU时间片:

  • 优先级相同,获得CPU使用权的概率相同
  • 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权

优先级是建议性的,而非强制,可能有效,也可能无效

线程组

Java中使用 java.lang.ThreadGroup类来表示线程组,它可以对一批线程进行管理,对线程组进行操作,同时也会对线程组里面的这一批线程操作。

java.lang.ThreadGroup :

1
2
3
4
5
6
7
8
public class ThreadGroup{
public ThreadGroup(String name){
//..
}
public ThreadGroup(ThreadGroup parent, String name){
//..
}
}

创建线程组的时候,需要指定该线程组的名字。

也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。

案例1:

1
2
3
4
5
6
7
8
9
10
11
public class Test11_ThreadGroup {
public static void main(String[] args) {
// 获取当前线程
Thread currentThread = Thread.currentThread();
// 获取当前线程的线程组 main 线程组就是它自身
ThreadGroup currenThreadGroup = currentThread.getThreadGroup();
System.out.println(currenThreadGroup);
}
}
// java.lang.ThreadGroup[name=main,maxpri=10]
// 可以看出,当前线程组的名字为main,并且线程组中的线程最大优先级可以设置为10

案例2: 用户在主线程中创建的线程,属于默认线程组(名字叫”main”的线程组)

1
2
3
4
5
6
7
public static void main(String[] args) {
Thread t = new Thread();
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
// 运行结果:
// java.lang.ThreadGroup[name=main,maxpri=10]

案例3:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("我的线程组");
//指定线程所属的线程组
Thread t = new Thread(group,"t线程");
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
// 运行结果:
// java.lang.ThreadGroup[name=我的线程组,maxpri=10]

案例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注意,启动后,三个线程都会进行休眠,等run方法运行完就“死亡”了
t1.start();
t2.start();
t3.start();
//返回当前线程组中还没有“死亡”的线程个数
System.out.println("线程组中还在存活的线程个数
为:"+group.activeCount());
//准备好数组,保存线程组中还存活的线程
Thread[] arr = new Thread[group.activeCount()];
//将存活的线程集中存放到指定数组中,并返回本次存放到数组的存活
线程个数
System.out.println("arr数组中存放的线程个数
为:"+group.enumerate(arr));
//输出数组中的内容
System.out.println("arr数组中的内容为:"+Arrays.toString(arr));

注意:只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变它所属的线程组

线程状态

java.lang.Thread.State 枚举类型中(内部类形式),定义了线程的几种状态,其代码结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Thread{
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
//返回线程当前所处的状态
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
}

状态解释:

线程状态变化的情况如下:

一个线程从创建到启动、到运行、到死亡,以及期间可能出现的情况。

分析:

  • 刚创建好的线程对象,就是出于 NEW的状态

  • 线程启动后,会处于RUNNABLE状态,其中包含俩种情况

    • 就绪状态,此时这个线程没有运行,因为没有抢到CPU的执行权
    • 运行状态,此时这个线程正在运行中,因为抢到CPU的执行权
    • JavaAPI中没有定义就绪状态和运行状态,而是统一叫做RUNNABLE(可运行状态)
    • 线程多次抢到CPU执行权,“断断续续”把 run 方法执行完之后,就变成了TERMINATED状态(死亡)

    之所以“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢夺下一个时间片并且中间还有可能抢不到的情况。

sleep方法

线程类Thread中的sleep方法:

1
2
3
4
5
6
//JavaAPI-Thread源码
public class Thread implements Runnable {
// 该静态方法可以让当前执行的线程暂时休眠制定的毫秒数
public static native void sleep(long millis) throws
InterruptedException;
}

注意事项:

  • 线程执行了sleep方法后,会从RUNNABLE状态进入到TIMED_WAITING状态
  • TIMED_WAITING阻塞结束后,线程会自动回到RUNNABLE状态

join方法

Thread类中 join 方法:

1
2
3
4
5
6
7
8
9
10
11
//JavaAPI-Thread源码
public class Thread implements Runnable {
//
public final synchronized void join(long millis) throws InterruptedException{
//... 省略
}
//
public final void join() throws InterruptedException{
//... 省略
}
}

作用:

join方法,可以让当前线程阻塞,等另一个指定线程运行结束后,当前线程才可以继续运行。

  • join() 一直等待到线程结束,死等
  • join(long millis) 只等待参数毫秒,时间到了后,继续运行

状态转换:

线程执行了join()方法后,会从RUNNABLE状态进入到WAITING(无限期等待)状态

线程执行了join(long million)方法后,会从RUNNABLE进入到TIMED_WAITING(有限期等待)状态

join方法状态图:

线程安全

如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量(含写入操作),那么最终可能会出现数据和预期的结果不符的情况,这种情况就是线程安全问题。

案例演示: 模拟电影院售票业务,准备50张电影票,让多个窗口一起售卖。

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
public class Test15_ThreadSafe {
public static void main(String[] args) {
// TicketRunnable r = new TicketRunnable();
TicketRunnable2 r = new TicketRunnable2();
Thread t1 = new Thread(r, "1号窗口");
Thread t2 = new Thread(r, "2号窗口");
Thread t3 = new Thread(r, "3号窗口");
t1.start();
t2.start();
t3.start();
}

}

class TicketRunnable implements Runnable {
private int num = 50;

@Override
public void run() {
while (true) {
if (num <= 0) {
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + " 正在卖票,编号:" + num);
num--;
}
}
}

线程安全问题都是由全局变量及静态变量引起的

若每个线程中队全局、静态变量只有读操作,而无写操作,一般来说,线程是安全

若多个线程同时执行写操作,就很可能出现线程安全问题,此时需要考虑线程同步技术

线程同步

Java中提供了线程同步的机制,来解决上述的线程安全问题。

Java中实现线程同步,主要借助 synchronized 关键字实现。

同步代码块

1
2
3
4
5
// Object类及其子类对象都可以作为 线程同步锁对象
synchronized(mutex锁对象) {
//需要同步操作的代码
//...
}

解决买电影票出现的问题(加锁):

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
class TicketRunnable2 implements Runnable {
//待售票数量
private int num = 50;
//准备锁对象【多个线程必须使用相同锁对象】
Object mutex = new Object();
@Override
public void run() {
while(true) {
//同步代码块:固定书写格式,需要使用同一把锁
//线程执行流程:
// 1.线程成功抢占到共享资源mutex(上锁成功),才能进入代码块执行
// 其他抢占资源失败的线程,则进入阻塞状态
// 2.同步代码执行完成,该线程自动释放共享资源(解锁)
// 其他线程由阻塞转入就绪状态,重新抢占资源(上锁)
synchronized (mutex) {
//如果待售数量 小于0,跳出循环,线程结束
if(num <= 0)
break;
//每隔50ms 销售 一张票
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出信息:模拟卖票
String name =
Thread.currentThread().getName();
System.out.println(name + " 正在卖票,编号:" + num);
//编号自减
num--;
}
}
}
}

注意,要实现线程同步,必须满足下面两个条件:

  • 所有线程都需要参与线程同步
  • 所有线程必须使用同一个锁对象

线程同步理解:

1
2
3
4
5
6
// 线程只有竞争到锁,才能执行同步代码块中代码(上锁)
synchronized(mutex锁对象) {
// 需要同步操作的代码
// 程序执行流程离开该代码块,则自动释放锁(解锁)
// 离开含多种情况,正常出右大括号、break、return或遇到异常跳出
}

线程同步可理解成一个规则(进卫生间必须竞争到钥匙开门,离开卫生间必须锁门交出钥匙)

  • 所有的线程都遵循这种规则,才能同步成功
  • 如果个别线程不遵循规则(不用钥匙,破门而入),则无法实现同步

锁对象可以理解成保证线程同步的重要因素(打开卫生间的唯一钥匙)

  • 多个线程使用同一把锁(用唯一的钥匙开门),才能保证线程同步
  • 如果使用不同的锁(不同的人用不同的钥匙去开门),则也无法实现线程同步效果

同步方法

使用synchronized修饰的方法,就叫做同步方法,固定格式如下:

1
2
3
public [static] synchronized 返回值类型 同步方法() {
可能会产生线程安全问题的代码
}

注意事项:

  • 同步方法可以是不同成员方法,也可以是static静态方法
  • 普通成员同步方法,默认锁对象为this,即当前方法的调用对象
  • static静态同步方法,默认锁对象是当前类的字节码对象(一个类右且只有一个

类的字节码对象:类名.class,固定用法

线程通信

概念理解

  • 线程间通信

多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

  • 等待唤醒机制

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个下次你能有效的利用资源。

wait和notify

Object类中有三个方法:wait()、notify()、notifyAll

当一个对象,在线程同步的代码中,充当锁对象的时候,在synchronized同步的代码块中,就可以调用这个锁对象的这三个方法了。

三个核心点:

  • 任何对象中都一定有这三个方法
  • 只有对象作为锁对象的时候,才可以调用。
  • 只有在同步的代码块中,才可以调用

其他情况下,调用一个对象的这三个方法,都会报错!

  • 等待唤醒机制:

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。

就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完它们的指定代码过后在将其唤醒(notify());在有多个下次你进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。

wait/notify就是线程间的一种协作机制。

  • 方法详解:

等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义 如下:

  • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还 要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个 对象上等待的线程从wait set 中释放出来,重新进入到调度队列 (ready queue)
  • notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆 有空位置后,等候就餐最久的顾客最先入座。
  • notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:

哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初终端的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从WAITING状态–>RUNNABLE状态
  • 否则,从wait set处理,又进入entry set,线程就从WAITING –> BLOCKED

注意事项:

  • wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用wait方法后的线程。
  • wait方法与notify方法是属于Object类的方法。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
  • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因 为:必须要通过锁对象调用这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
31
32
33
34
35
36
37
38
39
40
//包子类
class Bum {
//包子数量
int num = 0;
//包子存在标识
boolean flag = false;
}
//生产者
class Producer extends Thread {
private Bum bum;
public Producer(String name, Bum bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//同步
synchronized (bum){
//根据flag判断包子是否存在,如果存在则 线程进行
等待
if(bum.flag){
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产包子
System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
bum.num++;
System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
//生产完成,修改flag存在标识为true
bum.flag = true;
//通知 消费者线程吃包子
bum.notify();
}
}
}
}
  • 消费者线程类:
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
class Customer extends Thread {
private Bum bum;
public Customer(String name, Bum bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//同步
synchronized (bum){
//根据flag判断包子是否存在,如果不存在则线程等待
if(bum.flag == false){
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("第" + i + "次," + this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子数量: " + bum.num + ",快生产吧!");
//消费完成,修改flag存在标识为false
bum.flag = false;
//通知 消费者线程吃包子
bum.notify();
}
}
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建两个线程,一个生产包子,另一个消费包子,要求线程按照
public class Test17_TwoCommunication {
public static void main(String[] args) {
//准备共享对象
Bum bum = new Bum();
//生产者线程
Thread th1 = new Producer("打工人",bum);
//消费者线程
Thread th2 = new Customer("吃货",bum);
//启动线程
th1.start();
th2.start();
}
}

注意事项:

  • 2个线程通信主要借助wait()、notify()和flag标值完成
1
2
3
if(flag判断){
执行wait()等待;
}
  • wait()可以让线程进入等待状体
  • notify()可以通知等待的某个线程,让其转入就绪状态

多线程同通信

创建3个线程,第1个是生产者线程,每次蒸2只包子,第2个是消费者线 程,吃1个包子,第3个也是消费者线程,吃1个包子,要求3个线程轮流执 行(线程1生产,线程2消费,线程3消费)

案例实现:

  • 生产者线程类:
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
//包子类
class Bum2 {
// 包子数量
int num = 0;
// 线程执行标识: 0表示线程1执行 1表示线程2执行 2表示线程3执行
int flag = 0;
}
//生产者
class Producer1 extends Thread {
private Bum2 bum;
public Producer1(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果存在则 线程进
行等待
// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 0) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产包子
System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
// 每次生产2个包子
bum.num += 2;
System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
// 生产完成,修改flag存在标识为true
bum.flag = 1;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}

  • 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Customer2 extends Thread {
private Bum2 bum;
public Customer2(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果不存在则线程等

// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 1) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子剩余数量: "
+ bum.num);
// 消费完成,修改flag存在标识为false
bum.flag = 2;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}
class Customer3 extends Thread {
private Bum2 bum;
public Customer3(String name, Bum2 bum) {
super(name);
this.bum = bum;
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
// 同步
synchronized (bum) {
// 根据flag判断包子是否存在,如果不存在则线程等待
// 注意,此处必须改为while,用if无法实现功能
while (bum.flag != 2) {
try {
bum.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 开始吃包子...");
bum.num--;
System.out.println("消费完成,包子剩余数量: " + bum.num);
// 消费完成,修改flag存在标识为false
bum.flag = 0;
// 通知 其他所有线程转入运行
bum.notifyAll();
}
}
}
}
  • 测试类:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test17_MoreCommunication {
public static void main(String[] args) {
Bum2 bum = new Bum2();
// 生产者线程
Thread th1 = new Producer1("打工人", bum);
// 消费者线程
Thread th2 = new Customer2("1号吃货", bum);
Thread th3 = new Customer3("2号吃货", bum);
th1.start();
th2.start();
th3.start();
}
}

注意事项:

  • 多个(3个及以上)线程通信主要借助wait()、notifyAll()和flag标识完成
  • notifyAll()可以通知所有等待的某个线程,让其转入就绪状态
  • flag的判断必须使用while,如果使用if则无法完成功能
1
2
3
4
5
6
7
8
9
//notifyAll()会唤醒所有wait线程
//但第一个醒来并上锁成功的那个线程,很可能不是我们想要的
//所以需要使用while再做一次状态判断
//从而保证,只有我们期望的线程 能够成功醒来并上锁成功,往下执行
while(flag判断) {
执行wait()等待;
}
//成功醒来并上锁成功,往下执行代码
doNext...

死锁

程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的 运行,然后查找并修改产生死锁的问题代码

简单的描述死锁就是:俩个线程t1和t2,t1拿着t2需要等待的锁不释放,而 t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。

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
public class Test18_DeadLock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();

Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj1) {
System.out.println("t1拿到左边筷子");
synchronized (obj2) {
System.out.println("t1拿到右边筷子");
System.out.println("t1终于吃到心心念念的螺狮粉啦!");
}
}
}
}
});

Thread t2 = new Thread(new Runnable() {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (obj2) {
System.out.println("t2拿到右边筷子");
System.out.println("t2拿到左边筷子");
synchronized (obj1) {
System.out.println("t2拿到左边筷子");
System.out.println("t2终于吃到心心念念的螺狮粉啦!");
}
}
}
}
});
t1.start();
t2.start();
}
}

线程池

问题引入

我们使用线程的时候就去创建一个线程,使用完成线程自 动销毁,这样操作非常简便,但会产生问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率(频繁创建线程和销毁线程需要时间)。

那么有没有一种办法使得线程可以复用?即执行完一个任务,并不立即销毁,而是可以继续执行其他的任务。在Java中可以通过线程池来达到这样的效果

线程池概念

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

  • 线程池工作原理理解:
  • 线程池优点

    • 降低资源消耗

    减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

    • 提高响应速度

    当任务到达时,任务可以不需要的等到线程创建就能立即执行

    • 提高线程的可管理性

    可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过度的内存,而把服务器累趴下(每个线程需要大于1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

线程池使用

java里面线程池的顶级接口是 java.util.concurrent.Executor,但是严格意义上将 Executor 并不是一个线程池,而只是一个执行线程的工具类。真正的线程池接口是 java.util.concurrent.ExecutorService.

创建线程池的方法:

1
public static ExecutorService newFixedThreadPool(int nThreads);
  • 返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

使用线程池对象方法:

  • public Future<?> submit(Runnable task)
  • Future接口:用来记录线程任务执行完毕后产生的结果 (异步处理,不必等待结果完成,可以先去做别的事情,等到结果完成会自动过来取)
  • 获取线程池中的某一个线程对象,并执行

线程池操作步骤:

  • 创建线程池对象(ExecutorService类对象)
  • 创建Runnable接口子类对象
  • 提交Runnable接口子类对象(借助submit方法实现)
  • 关闭线程池(一般不做)

案例实现: Runnable实现类代码:

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 Test19_ThreadPool {
public static void main(String[] args) {
// 通过Executors来创建线程池
ExecutorService service = Executors.newFixedThreadPool(2);
SwimmingPool r = new SwimmingPool();
service.submit(r);
service.submit(r);
service.submit(r);
service.shutdown();
}

}

class SwimmingPool implements Runnable {

@Override
public void run() {
System.out.println("我要一个教练 游泳池共享");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "教练来啦");
System.out.println("服务完毕!");
}
}

Callable接口

实现多线程的第三种方式: 实现Callable接口,该方式使用不 多,大家了解即可。

  • 相关方法
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
public class Test20_Callable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 3、实例化Callable的实现类对象
MyCallable mc = new MyCallable();
// 4、创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
FutureTask<String> ft = new FutureTask<>(mc);
// 5.创建Thread对象,并传递ft对象作为构造器参数
Thread t1 = new Thread(ft);
// 6、开启线程
t1.start();

// 7、获取线程方法执行后返回的结果
String s = ft.get();
System.out.println(s);
}

}

// 1.创建Callable的实现类
class MyCallable implements Callable<String> {

// 2、重写call方法
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟girl唱告白气球第" + i + "天");
}
// 返回值就表示线程运行完毕之后的结果
return "I like you";
}
}

三种线程实现方式对比:

  • 实现Runnable、Callable接口
    • 好吃:扩展性强,实现该接口的同时还可以继承其他的类。Callable接口可以获取线程处理函数执行的结果
    • 缺点:编程相对复杂,不能直接使用Thread类中的方法
  • 继承Thread类
    • 好处:编程比较简单,可以直接使用Thread类中的方法
    • 缺点:可以扩展性较差,不能再继承其他的类

❤️❤️❤️忙碌的敲代码也不要忘了浪漫鸭!

穷则独善其身,达则兼济天下。💪