目录
[1.2 什么是进程?](#1.2 什么是进程?)
[1.4 进程与线程的区别](#1.4 进程与线程的区别)
[2. 线程基本概念](#2. 线程基本概念)
[3. 线程的创建与启动](#3. 线程的创建与启动)
[4. 线程的创建方式](#4. 线程的创建方式)
[5. 线程的命名](#5. 线程的命名)
[6. 线程的休眠(暂停)](#6. 线程的休眠(暂停))
[二. 线程的状态及常用方法](#二. 线程的状态及常用方法)
[2. 线程的插队:join( )方法](#2. 线程的插队:join( )方法)
[3. join( )方法和sleep( )方法的区别](#3. join( )方法和sleep( )方法的区别)
[4. 线程的中断:interrupt( )方法](#4. 线程的中断:interrupt( )方法)
[5. 线程的让出:yield( )方法](#5. 线程的让出:yield( )方法)
[5.1 yield( )方法的作用](#5.1 yield( )方法的作用)
[6. 守护线程(Daemon Thread)](#6. 守护线程(Daemon Thread))
[6.1 用户线程与守护线程的区别](#6.1 用户线程与守护线程的区别)
[6.2 设置守护线程](#6.2 设置守护线程)
[2.synchronized 关键字的用法](#2.synchronized 关键字的用法)
[6.synchronized 关键字的补充](#6.synchronized 关键字的补充)
一.多线程基础
概述:
现代操作系统(Windows
,macOS
,Linux
)都可以执行多任务。多任务就是同时运行多个任务。例如:播放音乐的同时,浏览器可以进行文件下载,同时可以进行QQ消息的收发。
1.进程与线程
1.1什么是程序?
程序是含有指令和数据的文件,被存储在磁盘或其他数据存储设备中,可以理解为程序是包含静态代码的文件,例如:浏览器软件,音乐器播放器。
1.2 什么是进程?
进程是程序的一次执行过程,是系统运行程序的基本单位。在windows系统中,每一个正在执行的exe文件或后台服务,都是一个进程,由操作系统统一管理并分配资源,因此进程是动态的。
例如:正在运行的浏览器就是一个进程。
1.3什么是线程?
某些进程内部还需要同时执行多个子任务。例如:我们在使用WPS
时,WPS
可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行自动保存和上传云文档,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个主线程。
┌──────────┐
│Process │
│┌────────┐│
┌──────────┐││ Thread ││┌──────────┐
│Process ││└────────┘││Process │
│┌────────┐││┌────────┐││┌────────┐│
┌──────────┐││ Thread ││││ Thread ││││ Thread ││
│Process ││└────────┘││└────────┘││└────────┘│
│┌────────┐││┌────────┐││┌────────┐││┌────────┐│
││ Thread ││││ Thread ││││ Thread ││││ Thread ││
│└────────┘││└────────┘││└────────┘││└────────┘│
└──────────┘└──────────┘└──────────┘└──────────┘
┌──────────────────────────────────────────────┐
│ Operating System │
└──────────────────────────────────────────────┘
线程是一个比进程更小的执行单位(CPU
的最小执行单位)。一个进程在其执行的过程中可以产生多个线程。与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源, 所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
1.4 进程与线程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 资源开销:每个进程都有独立的代码副本和数据空间,进程之间的切换,资源开销较大;线程可以看作是轻量级的进程,每个线程都有自己独立的运行栈和程序计数器,线程之间的切换,资源开销小。
- 包含关系:一个进程内包含有多个线程,在执行过程中,线程的执行不是线性串行的,而是多条线程并行共同完成;
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响;一个线程崩溃,会导致整个进程退出。所以多进程要比多线程健壮;
- 执行过程:每个独立的进程有程序运行的入口和程序出口。但是线程不能独立执行,必须依存在应用程序(进程)中,由应用程序提供多个线程执行控制;
// 程序:程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,可以理解为程序是包含静态代码的文件 // 多进程:进程是程序的一次执行过程,是系统运行程序的基本单位。 // 多线程:线程是操作系统运行时,能够调度的最小单位,它被包含在进程中,时进程在运行过程中的一个单位 // 多线程的应用场景: // 软件中的耗时操作,拷贝和迁移文件,加载大量的资源的时候 // 所有的后台服务器 // 所有的聊天软件
2. 线程基本概念
单线程:单线程就是进程中只有一个线程。
public class SingleThread {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
System.out.print(i + " ");
}
}
}
多线程:由一个以上的线程组成的程序称为多线程程序。Java中,一定是从主线程开始执行(main方法),然后在主线程的某个位置创建并启动新的线程。
public class MultiThread {
public static void main(String[] args) {
// 创建2个线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("线程1:" + i + " ");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("线程2:" + i + " ");
}
}
});
// 启动2个线程
t1.start();
t2.start();
}
}
3. 线程的创建与启动
-
通过创建Thread实例,完成线程的创建。
-
- 线程的内部实现可以通过继承
Thread
类、实现Runnable
接口等方式进行封装。
- 线程的内部实现可以通过继承
-
通过调用
Thread
实例的start()
方法启动新线程。 -
查看
Thread
类的源代码,会看到start()
方法内部调用了一个private native void start0()
方法,native
修饰符表示这个方法是由JVM
虚拟机内部的C
代码实现的本地方法,由JVM
根据当前操作系统进行本地实现。package thread;
public class Demo01 {
public static void main(String[] args) {for (int i = 0; i < 100; i++){ System.out.println("main线程:" + i); } ThreadOne t1 = new ThreadOne(); t1.start();
// for (int i = 0; i < 100; i++){
// System.out.println("main线程:" + i);
// }}
}
注意:直接调用Thread
实例的run()
方法是无效的,因为直接调用run()
方法,相当于调用了一个普通的Java
方法,当前线程并没有任何改变,也不会启动新线程。
4. 线程的创建方式
方式1:
通过继承Thread,重写Thread类中run()方法。main主线程中new一个Thread的子类,然后调用start()方法。
package thread;
public class ThreadOne extends Thread{
public ThreadOne(String name){
super(name);
}
public ThreadOne(){
super();
}
public void run(){
for(int i = 0; i < 100; i++){
System.out.println(getName() + ":" + i);
}
}
}
main
ThreadOne t1 = new ThreadOne();
t1.start();
方式2:java.lang.Runnable 接口实现多线程,创建一个实现Runnable接口的实现类,重写run方法
创建Runable的实现类的对象r1,创建Thread类对象,构造方法中传递Runnable接口的实现类对象
调用start方法的启动线程。
public static void main(String[] args) {
ThreadTwo t2 = new ThreadTwo();
Thread t = new Thread(t2);
t.start();
for (int i=0; i < 100; i++){
System.out.println("main线程:" + i);
}
}
}
class ThreadTwo implements Runnable{
public void run(){
for(int i = 0; i < 100; i++){
System.out.println("线程1:" + i);
}
}
}
方式3:实现 java.util.concurrent.Callable
接口,允许子线程返回结果、抛出异常。
// 实现子线程
public class SubThread implements Callable<Integer>{
private int begin,end;
public SubThread(int begin,int end){
this.begin = begin;
this.end = end;
}
@Override
public Integer call() throws Exception {
int result = 0;
for(int i=begin;i<=end;i++){
result+=i;
}
return result;
}
}
5. 线程的命名
-
调用父类的setName()方法或在构造方法中给线程名字赋值。
-
如果没有为线程命名,系统会默认指定线程名,命名规则是
Thread-N
的形式。package thread.method;
import thread.ThreadOne;
public class Demo01 {
// 给线程设置名字
// setName()方式设置线程名
// 调用有参构造设置线程名
public static void main(String[] args) {
ThreadOne t1 = new ThreadOne();
t1.setName("线程1");
t1.start();
// 使用构造方法设置线程名
ThreadOne t2 = new ThreadOne("线程2");
t2.start();
// Runable作为参数
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程正在运行");
}
};
Thread t3 = new Thread(r,"西红柿");
t3.start();
}
}
6. 线程的休眠(暂停)
可以调用Thread.sleep(long millis),强迫当前相乘按照毫秒值休眠。
package thread.method;
import java.sql.SQLOutput;
public class Demo08 {
public static void main(String[] args) throws InterruptedException {
System.out.println("main线程进入");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 获取当前的系统时间
long start = System.currentTimeMillis();
System.out.println("进入t1线程");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("中断t1线程,消耗时间为:"+(System.currentTimeMillis()-start)+"毫秒");
e.printStackTrace();
return;
}
System.out.println("结束t1线程,消耗时间为:"+(System.currentTimeMillis()-start)+"毫秒");
}
},"t1线程");
t1.start();
// 让主线程休眠
Thread.sleep(1000*3);
System.out.println("main线程结束");
// main主线程修改t1线程中断状态=true
// t1线程检测中断状态=true,则抛出InterruptedException异常, 子线程执行结束
// t1.interrupt();
}
}
7.线程的优先级
- 在线程中通过setPriorty(int n)设置线程优先级,范围为1-10,默认为5.
- 优先级高的线程被操作系统调度的优先级较高。
提示:并不能代表,通过设置优先级来确保高优先级的线程一定会先执行。
8.小结
- Java用
Thread
对象表示一个线程,通过调用start()
启动一个新线程; - 一个线程对象只能调用一次
start()
方法; - 线程的执行代码写在
run()
方法中; - 线程调度由操作系统决定,程序本身无法决定调度顺序;
二. 线程的状态及常用方法
1.线程的状态
在Java
程序中,一个线程对象通过调用start()
方法启动线程,并且在线程获取CPU
时,自动执行run()
方法。run()
方法执行完毕,代表线程的生命周期结束。

- 线程终止的原因有:
-
- 线程正常终止:run()方法执行到return语句返回;
- 线程意外终止:**run()**方法因为未捕获的异常导致线程终止;
- 对某个线程的Thread实例调用**stop()**方法强制终止(宇宙超级无敌强烈不推荐);
2. 线程的插队:join( )方法
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程Main:开始执行,即将创建并调用子线程");
// 创建并启动子线程
MyThread myThread = new MyThread();
myThread.start();
// 主线程调用myThread子线程的join()方法
myThread.join(); // 子线程插队,插入到当前线程main的执行序列前
System.out.println("主线程Main:当子线程myThread执行完毕后,主线程Main再执行");
}
}
-
- 综上所述:
join()
方法实际上是通过调用wait()
方法, 来实现同步的效果的。
- 综上所述:
-
-
- 例如:A线程 中调用了B线程 的
join()
方法,则相当于A线程 调用了B线程 的wait()
方法,在调用了B线程 的wait()
方法后,A线程 就会进入WAITING
或者TIMED_WAITING
等待状态,因为它相当于放弃了CPU
的使用权。
- 例如:A线程 中调用了B线程 的
-
-
-
注意:
join(0)
的意思不是A线程 等待B线程 0秒,而是A线程 等待B线程 无限时间,直到B线程 执行完毕:即join(0)
=join()
;public class Thread implements Runnable
public final void join() throws InterruptedException {
join(0);
}public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { // 无限等待 wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } // 计时等待 wait(delay); now = System.currentTimeMillis() - base; } } }
}
-
3. join( )方法和sleep( )方法的区别
-
- 两个方法都可以实现类似"线程等待"的效果,但是仍然有区别;
join()
是通过在内部使用synchronized + wait()
方法来实现的,所以join()
方法调用结束后,会释放锁;sleep()
休眠没有结束前,不会释放锁;
4. 线程的中断:interrupt( )方法
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()
方法,使得自身线程能立刻结束运行。
注意事项:
- 线程被
Object.wait()
,Thread.join()
和Thread.sleep()
三种方法阻塞或等待,此时调用该线程的interrupt()
方法,那么该线程将抛出一个InterruptedException
中断异常,从而提前终结被阻塞状态。 - 如果线程没有被阻塞或等待,调用
interrupt()
将不起作用,直到执行到wait()
,sleep()
,join()
等方法进入阻塞或等待时,才会抛出InterruptedException
异常;
5. 线程的让出:yield( )方法
5.1 yield( )方法的作用
-
- 线程通过调用
yield()
方法告诉JVM
的线程调度,当前线程愿意让出CPU
给其他线程使用。 - 至于系统是否采纳,取决于
JVM
的线程调度模型:分时调度模型 和抢占式调度模型
- 线程通过调用
-
-
- 分时调度模型 :所有的线程轮流获得
cpu
的使用权,并且平均分配每个线程占用的CPU
时间片; - 抢占式调度模型 :优先让可运行池中优先级高的线程占用
CPU
,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU
。(JVM
虚拟机采用的是抢占式调度模型 )
- 分时调度模型 :所有的线程轮流获得
-
6. 守护线程(Daemon Thread)
6.1 用户线程与守护线程的区别
-
- 用户线程:我们平常创建的普通线程;
- 守护线程 :用来服务于用户线程的线程,在
JVM
中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;而守护线程执行结束后,虚拟机不会自动退出。
6.2 设置守护线程
-
在调用
start()
方法前,调用setDaemon(true)
把该线程标记为守护线程Thread myThread = new Thread();
myThread.setDaemon(true);
myThread.start();public class Main {
public static void main(String[] args) { long startTime = System.currentTimeMillis(); // 创建并启动子线程 new Thread() { @Override public void run() { //子线程休眠10秒钟 try { Thread.sleep(10*1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("普通用户线程,运行耗时" + (System.currentTimeMillis() - startTime)); } }.start(); //主线程休眠3秒,确保在子线程之前结束休眠 try { Thread.sleep(3*1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main主线程,运行耗时 " + (System.currentTimeMillis() - startTime)); }
}
运行结果分析:普通用户线程,在没有完成打印内容的时候,JVM
是不会被结束。
三.Synchronized同步锁
1.什么是Synchronized同步锁?
Synchronized
同步锁,简单来说,使用Synchronized
关键字将一段代码逻辑,用一把锁给锁起来,只有获得了这把锁的线程才访问。并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全.
2.synchronized 关键字的用法
- 修饰实例方法:
synchronized
修饰实例方法, 则用到的锁,默认为this
当前方法调用对象; - 修饰静态方法:
synchronized
修饰静态方法, 则其所用的锁,默认为Class
对象; - 修饰代码块:
synchronized
修饰代码块, 则其所用的锁,是某个指定Java
对象;
3.synchronized修饰实例方法
- 使用当前对象this 充当锁**,** 完成对当前方法的锁定,只有获取
this
锁的线程才能访问当前方法; - 并发过程中,同一时刻,可以有
N
个线程请求执行方法,但只有一个线程可以持有this
锁,才能执行; - 不同线程,持有的对象,必须相同;
当使用synchronized
修饰实例方法时, 以下两种写法作用和意义相同:
public class Foo {
// 实例方法
public synchronized void doSth1() {
// 获取this锁,才能执行该方法
}
// 实例方法
public void doSth2() {
synchronized(this) {
// 获取this锁,才能执行该代码块
}
}
}
public static void main(String[] args) {
// 实例化一个对象
Foo fa = new Foo();
// 创建不同的线程1
Thread thread01 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
fa.doSth1();
}
};
// 创建不同的线程2
Thread thread02 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
fa.doSth1();
}
};
// 启动线程
thread01.start();
thread02.start();
}
4.synchronized修饰静态方法
-
使用当前对象的
Class
对象充当锁,完成对当前方法的锁定,只有获取Class
锁的线程才能访问当前方法; -
不同线程,持有的对象,可以不同,但必须相同
class
类型;public class Foo {
// 静态方法
public synchronized static void doSth1() {
// 获取当前对象的Class对象锁,才能执行该方法
}// 实例方法 public static void doSth2() { synchronized(this.getClass()) { // 获取当前对象的Class对象锁,才能执行该代码块 } }
}
public static void main(String[] args) {
// 创建不同的对象(相同类型)
Foo fa = new Foo();
Foo fb = new Foo();// 创建不同线程1 Thread thread01 = new Thread() { public void run() { // 使用不同的对象访问synchronized方法 fa.doSth2(); } }; // 创建不同线程2 Thread thread02 = new Thread() { public void run() { // 使用不同的对象访问synchronized方法 fb.doSth2(); } }; // 启动线程 thread01.start(); thread02.start();
}
5.synchronized修饰代码块
synchronized(自定义对象) {
//临界区
}
6.synchronized 关键字的补充
- 当一个线程访问对象的一个
synchronized(this)
同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)
同步代码块。
, 所有的线程都可以自由地访问对象中的代码, 而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代码块应该越短小越好。
- 父类中
synchronized
修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用synchronized
修饰,则该方法不是线程安全的; - 在定义接口方法时,不能使用
synchronized
关键字; - 构造方法不能使用
synchronized
关键字,但可以使用synchronized
代码块来进行同步; - 离开
synchronized
代码块后,该线程所持有的锁,会自动释放;