day23-线程篇(一)
目录
一.多线程基础
概述:
1.进程与线程
1.1什么是程序?
1.2 什么是进程?
1.3什么是线程?
1.4 进程与线程的区别
2. 线程基本概念
3. 线程的创建与启动
4. 线程的创建方式
5. 线程的命名
6. 线程的休眠(暂停)
7.线程的优先级
8.小结
二. 线程的状态及常用方法
1.线程的状态
2. 线程的插队:join( )方法
3. join( )方法和sleep( )方法的区别
4. 线程的中断:interrupt( )方法
5. 线程的让出:yield( )方法
5.1 yield( )方法的作用
6. 守护线程(Daemon Thread)
6.1 用户线程与守护线程的区别
6.2 设置守护线程
三.Synchronized同步锁
1.什么是Synchronized同步锁?
2.synchronized 关键字的用法
3.synchronized修饰实例方法
4.synchronized修饰静态方法
5.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() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {System.out.println("线程1:" + i + " ");}}});Thread t2 = new Thread(new Runnable() {@Overridepublic 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);}}
}mainThreadOne 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;}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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 Runnablepublic 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() {@Overridepublic 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();// 创建不同的线程1Thread thread01 = new Thread() {public void run() {// 使用相同的对象访问synchronized方法fa.doSth1();}};// 创建不同的线程2Thread 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();// 创建不同线程1Thread thread01 = new Thread() {public void run() {// 使用不同的对象访问synchronized方法fa.doSth2();}};// 创建不同线程2Thread 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
代码块后,该线程所持有的锁,会自动释放;