文章目录
- 一、多线程概述
- 1、进程与线程
- 2、进程与线程的关系
- 二、多线程并发的实现
- 1、线程的实现方式一
- 2、线程的实现方式二
- 三、线程的生命周期
- 1、线程的五个生命周期
- 2、常用方法
- 3、线程的sleep
- 4、终止线程的睡眠状态
- 5、强行终止线程的执行
- 6、合理终止一个线程的执行
- 四、线程的调度
- 1、线程调度的模型
- 2、线程调度的方法---优先级
- 3、线程调度---让位
- 4、线程调度--线程合并
- 五、线程安全
- 1、同步与异步
- 2、同步机制synchronized
- 3、有线程安全的变量
- 4、synchronized出现在实例方法上
- 5、synchronized的三种写法
- 六、死锁
- 1、原理
- 2、代码实现
- 七、线程守护
- 1、线程守护的概述
- 2、实现守护线程
- 3、定时器
- 4、实现定时器
- 5、实现线程的第三种方式
- 八、wait和notify
- 1、概述
- 2、生产者和消费者模式
一、多线程概述
1、进程与线程
- 进程是一个应用程序(一个进程是一个软件)
- 线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程
举个例子:
DOS窗口运行java HelloWorld,先启动JVM,JVM是一个进程,JVM启动一个主线程调用main方法,同时再启动一个垃圾回收线程来负责看护、回收垃圾。(也就是说Java程序至少两线程并发,main方法对应的主线程+GC)
2、进程与线程的关系
把进程看作是现实生活中的公司,如京东。线程则可看作是其下的某一个职能部门,负责完成某任务,如开发部门。
- 进程A和进程B的内存独立不共享
- Java中,线程A和线程B,堆内存和方法区内存共享,但栈内存独立,一个线程一个栈
如启动了10个线程,就会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行各自的,这就是多线程并发。
🍁Java中的多线程机制,目的就是为了提高程序的处理效率, 如火车站看成是一个进程,则每个售票小窗口就是一个个线程,甲在窗口1买票,乙在窗口2买票,谁也不用等谁 一个个售票窗口就像一个个栈,有自己独立的空间。售票大厅这个共用空间就像堆和方法区。
引入多线程以后,main方法的结束,不再意味着程序的结束。main方法结束了,主栈空了,其他的栈(线程)可能还在压栈弹栈
二、多线程并发的实现
真正的多线程并发是:t1线程执行t1,t2线程执行t2,t1不影响t2,t2不影响t1。4核的CPU,在同一时间点,可以真正的有4个进程并发执行,单核的CPU,在某一个时间点上实际只能处理一件事情,但由于CPU的处理速度极快,多个线程之间频繁切换,从而造成了多个事情在同时处理的视觉假象。
public class Thread1 {
public static void main(String[] args) {
m1();
}
public static void m1(){
m2();
}
public static void m2(){
m3();
}
public static void m3(){
System.out.println("m3 excute");
}
}
分析以上:只有一个主线程,主栈,没有分支线程被启动
1、线程的实现方式一
编写一个类,直接继承java.lang.Thread,重写run方法
class MyThread extends Thread{
//这段代码运行在分支线程中
public void run(){
for(int i=0;i<100;i++){
System.out.println("分支线程" + i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
//创建分支线程对象
MyThread myThread = new MyThread();
//启动分支线程
myThread.start();
//这里仍然运行在主线程当中
for(int i=0;i<100;i++){
System.out.println("主线程" + i);
}
}
}
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这个任务完成后,这段代码就瞬间执行结束了
启动成功后的线程会自动调用我重写的run方法,并且run方法在分支栈的栈底部(main方法在主栈的底部,故run和main是平级的)
myThread.run();
如果直接调用我重写的run方法,则不会启动线程,不会分配新的分支栈,此时,就是单线程了。
运行结果:
2、线程的实现方式二
编写一个类,实现java.lang.Runable接口,重写run方法
public class ThreadTest {
public static void main(String[] args) {
//创建一个可运行的对象
MyRunnable r = new MyRunnable();
//给Thread类的构造方法传入Runnable类的对象
//将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
t.start();
for(int i=0;i<100;i++){
System.out.println("主线程"+ i);
}
}
}
//这不是线程,仅仅是一个可运行的类
class MyRunnable implements Runnable{
public void run(){
for(int i=0;i<100;i++){
System.out.println("分支线程"+i);
}
}
}
总结线程的实现:
🍁编写一个类继承Thread类并重写run方法
public class MyThread extends Thread{
public void run(){
}
}
MyThread t = new MyThread();
t.start();
🍁编写一个类,实现Runnable接口并重写run方法
public class MyRunnbale implements Runnable{
public void run(){
}
}
Thread t = new Thread(new MyRunnable());
t.start();
由于Java的单继承,第一种方式中,不能再继承别的类了,而第二种可以,面向接口编程更优。
//方式二的匿名内部类写法:
Thread t2 = new Thread(new MyRunnable(){
public void run(){
for(int i=0;i<100;i++){
System.out.println(i);
}
}
});
run()方法中要是有异常,也只能捕捉,不能上抛,因为run方法在父类中没有抛出任何异常,做为子类,重写时不能比父类抛出更多的异常。
三、线程的生命周期
1、线程的五个生命周期
-
🍁 新建状态:
刚new出来的线程对象 -
🍁就绪状态:
又叫做可运行状态,表示当前线程具有抢夺CPU时间片的能力(CPU时间片就是执行权)当一个线程抢夺到CPU时间片后,开始执行run方法,run方法的执行标志着线程进入运行状态。 -
🍁运行状态:
run方法开始执行,线程进入运行状态,当之前占有的CPU时间片用完之后,重新回到就绪状态继续抢夺CPU时间片,待抢到后,重新进入run方法上次执行的地方继续执行 -
🍁死亡状态:
run方法执行结束,线程到达死亡状态 -
🍁阻塞状态:
当一个线程遇到阻塞事件,如接收用户键盘输入,sleep方法,则进入阻塞状态,此时线程会放弃之前抢占到的CPU时间片
2、常用方法
获取线程的名字getName()
MyThread myThread = new MyThread();
String tName = myThread.getName();
//Thread-0
System.out.println(tName);
//更改
myThread.setName("code-9527 's Thread");
System.out.println(myThread.getName());
获取当前线程对象currentThread()
//静态方法获取当前线程对象,返回一个Thread类型对象
Thread currentThread = Thread.currentThread();
//返回线程名
System.out.println(currentThread.getName());
3、线程的sleep
//毫秒
static void sleep(Long millis);
作用是让当前线程进入休眠,进入阻塞状态,放弃占有CPU时间片,让给其他线程去使用,出现的地方,对应的线程休眠
public static void main(String[] args) {
try{
Thread.sleep(1000*5); //让当前线程(main)休眠五秒
}catch(InterruptedException e){
e.printStackTrace();
}
//五秒后被输出
System.out.println("sleep结束");
}
实现间隔特定的时间去执行一段特定的代码
sleep是静态方法,若上面的Thread.sleep改成:
Thread t = new MyThread();
...
t.sleep(1000*5);
执行的时候,t.sleep(1000*5);还是会被当作Thread.sleep(1000*5);,被休眠的也还是main线程,而不是t线程。
4、终止线程的睡眠状态
t.interrupt();
干扰,即中断t线程的睡眠,执行后sleep()出现异常,即catch(InterruptedException e),这种中断睡眠状态的方式,依靠的时Java的异常处理机制。
class MyThread extends Thread{
public void run(){
try{
Thread.sleep(1000*5);
}catch(InterruptedException e){
e.printStackTrace();
}
for(int i=0;i<100;i++){
System.out.println("分支线程" + i);
}
}
}
class ThreadTest1{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
//线程myThread计划sleep5秒
//扔出interrupt后就提前醒来了
myThread.interrupt();
}
}
5、强行终止线程的执行
线程对象的引用.stop()
stop方法已过时,容易丢数据。这种方式是直接将线程杀死了,线程没有保存的数据会丢失
MyThread myThread = new MyThread();
myThread.start();
//终止
myThread.stop();
6、合理终止一个线程的执行
- 编写的类中加入run属性:boolean run = true;
- 重写run方法的时候,if(run)…
- else中写终止线程之前要保存的数据和操作+return;
- 以后则只需改线程对象的run属性即可终止线程
class MyRun implements Runnable{
boolean run = true;
public void run() {
for (int i = 0; i < 100; i++) {
if (this.run) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
System.out.println(this.run);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("这是一些终止线程前要做的事");
System.out.println("保存数据中..终止线程成功!");
return;
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
MyRun r = new MyRun();
Thread t = new Thread(r);
t.start();
//sleep主线程三秒
try{
Thread.sleep(3000);
}catch(InterruptedException e){
e.printStackTrace();
}
//终止,改run属性为false
r.run = false;
}
}
运行效果:
四、线程的调度
1、线程调度的模型
-
🍁抢占式调度模型:
哪个线程的优先级比较高,抢到CPU时间片的概率就高一些/多一些。Java中采用的就是抢占式调度模型。 -
🍁均分式调度模型:
平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样
2、线程调度的方法—优先级
- 设置线程的优先级
void setPriority(int newPriority)
- 获取线程的优先级
int getPriority()
//最低优先级为1
static int MIN_PRIORITY
//默认优先级为5
static int NORM_PRIORITY
//最高优先级为10
static int MAX_PRIORITY
System.out.println(Thread.MAX_PRIORITY);
举例:
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName()+"的优先级是:"+ currentThread.getPriority());
currentThread.setPriority(9);
3、线程调度—让位
//静态方法
static void yield()
暂停当前正在执行的线程对象,去执行其他线程,yield方法的执行会让当前线程从运行状态进入就绪状态 ,注意不是阻塞状态。
class MyRunnable2{
public void run(){
for(int i=0;i<101;i++){
//每循环10次,让当前线程暂停让位一下
if(i%10 == 0){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + i);
}
}
}
4、线程调度–线程合并
实例方法void join(),注意线程合并不是栈的合并
MyThread t = new MyThread();
//让当前线程阻塞,t线程执行,直到t线程执行结束,当前线程才执行
t.join();
五、线程安全
线程安全问题的产生条件:
- 多线程并发
- 有贡献数据
- 共享数据有修改行为
线程安全问题的解决–线程同步机制:
线程同步即线程排队执行,不能并发了(可能会牺牲一部分效率,但数据安全是一切的前提)
1、同步与异步
-
🍁异步模型:
异步就是并发,线程t1和线程t2各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,即多线程并发,效率较高。 -
🍁同步模型:
线程t1和t2,t1执行的时候,必须等待t2执行结束,效率较低。
❀❀❀账户安全问题的代码模拟:
/**
* 账户类
*/
public class Account {
private String actno;
private double balance;
public Account(){
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 取款方法
* @param money
*/
public void withdraw(double money){
double before = this.getBalance();
double after = before - money;
//别立即更新余额,使用休眠模拟网络延迟
try{
Thread.sleep(1000*5);
}catch(InterruptedException e){
e.printStackTrace();
}
this.setBalance(after);
}
}
public class AccountThread extends Thread{
//该类的对象有Account属性
//某人“有一个账户”
private Account account;
//通过构造方法传递账户对象
public AccountThread(Account account){
this.account = account;
}
public void run(){
double money = 5000;
account.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对账户:" + account.getActno() +
"取款:" + money + ",余额:"+ account.getBalance());
}
}
class Test{
public static void main(String[] args) {
Account account = new Account("act-001",10000);
//两个线程共用一个账户对象
AccountThread t1 = new AccountThread(account);
AccountThread t2 = new AccountThread(account);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
运行结果:
2、同步机制synchronized
🍁语法:
synchronized(){
…
线程同步代码块
…}
小括号中传的是多个线程共享的对象,若有t1、t2、t3、t4、t5线程,t1、t2、t3需要排队,t4、t5不用,则()中是一个t1、t2、t3共享的对象,而这个对象t4、t5不共享
由此,上面例题中的取款方法变为:
public void withdraw(double money){
synchronized(this) {
double before = this.getBalance();
double after = before - money;
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
简单的说就是synchronized内部放的是要排队执行的代码块
🍁对象锁:
在Java中,任何一个对象都有“一把锁”,这把锁其本质是一个标记,一个对象一把锁。
当运行状态的线程遇到synchronized关键字:
- 在锁池lockpool中找共享对象的对象锁。线程进入锁池找共享对象的对象锁之前,会释放之前占有的CPU时间片
- 若找到了,则进入就绪状态继续抢夺CPU时间片,若没找到,则在锁池中等待
🍺例:
当t1和t2线程并发:
t1先遇到synchronized,自动找所共享对象的对象锁,找到之后,占有这把锁,然后执行同步代码块中的代码, 直到同步代码块执行结束,这个锁才释放
—>>>
t1占有对象锁后,t2线程若也遇到了synchronized,在找对象锁时,发现被t1占有,则t2在同步代码块外等待t1结束并释放对象锁后,再占有对象锁、执行同步代码块
3、有线程安全的变量
存在于堆区中的实例变量、存在于方法区中的静态变量,因为堆和方法区均只有一个,且是多线程共享的,有可能存在安全问题。
局部变量存在于栈区中,永远不会有线程安全问题,因为一个线程一个栈。
4、synchronized出现在实例方法上
改为:
public synchronized void withdraw(double money){
double before = this.getBalance();
double after = before - money;
try{
Thread.sleep(1000*5);
}catch(InterruptedException e){
e.printStackTrace();
}
this.setBalance(after);
}
这样写的缺点:
- synchronized出现在实例方法上,表示整个方法都需要同步(实际只有其中一部分代码需要同步),这样就扩大了同步的范围,导致程序的执行效率变低
- synchronized出现在实例方法上,锁的就一定是this,就不能是其他对象了
5、synchronized的三种写法
🍁
synchronized(线程共享对象){
同步代码块;
}
🍁
在实例方法中使用synchronized,表示共享的对象一定是this,并且同步的代码块是整个方法体
🍁
在静态方法中使用synchronized,表示找类锁,类锁永远只有1把(对象锁是100个对象就有100个对象锁)
六、死锁
1、原理
t1线程执行某同步代码块,用到了对象1和2,即t1线程需要先锁对象1,再锁对象2,全锁以后,算同步代码块执行结束,然后一下释放两个对象锁
t2线程执行另一个同步代码块,需要先锁对象2,再锁对象1才算这个同步代码块执行结束,然后释放两个对象锁。
如此:
t1锁到对象2的时候,发现已被锁,则等待,而另一边:t2锁到对象1的时候,发现对象1已被锁,两个线程同时陷入无休止的等待…尬住了
2、代码实现
class MyThread1 extends Thread{
Object o1;
Object o2;
public MyThread1(Object o1, Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
//同步代码块开始
synchronized(o1){
try{
//别着急锁o2,为了保证死锁必现,这里等两秒
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
//synchronized的嵌套
//从而实现:对象o1和o2都锁了才算同步代码块结束
synchronized(o2){
}
}
//同步代码块结束
}
}
class MyThread2 extends Thread{
Object o1;
Object o2;
public MyThread2(Object o1,Object o2){
this.o1 = o1;
this.o2 = o2;
}
public void run(){
synchronized (o2){
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
synchronized(o1){
}
}
}
}
public class DeadLock{
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
//启动两个线程,共用对象o1和o2
MyThread1 t1 = new MyThread1(o1,o2);
MyThread2 t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
}
}
运行结果:
死锁发生后,程序不出异常,也不报错,会一直僵持着,不易发现并调试。
关于synchronized死锁和线程安全的优化:
synchronized会让程序执行效率变低,系统吞吐量降低,用户体验变差。解决线程安全,可考虑:
- 使用局部变量代替实例变量和静态变量
- 若必须使用实例变量,考虑多创建几个对象,别对象共享了也就没有安全问题了
- 若以上两条都做不到,则用synchronized
七、线程守护
1、线程守护的概述
Java中,线程分为两大类:
- 用户线程,如主线程main线程
- 守护线程,如经典的垃圾回收线程
守护线程的特点是:
一般守护线程是一个死循环,所有用户线程结束的时候,守护线程自动结束
2、实现守护线程
通过实例方法setDaemon:
public class DaemonTest {
public static void main(String[] args) {
Thread t = new BackupThread();
t.setName("备份守护线程");
//传入true,则普通线程变守护线程
t.setDaemon(true);
t.start();
}
}
class BackupThread extends Thread{
public void run(){
//要进行的守护动作写在run方法中即可
int i = 0;
while(true){
//即使是死循环,但做为守护线程,当所有线程都结束的时候,也会自动结束
}
}
}
3、定时器
🍁作用:
间隔特定的时间,执行特定的程序
🍁应用场景:
如每周进行银行账户的总账操作,每天进行数据库的备份操作
🍁实现方式:
- 用sleep方法,设置线程睡眠,睡到某时刻醒来执行代码完成任务
- 用java.util.Timer
- 用Spring框架中的SpringTask框架(底层还是java.util.Timer)
4、实现定时器
用到的java.util.Timer类中的方法
- Time类的无参构造方法,创建定时器对象
Timer timer = new Timer();
- Timer类的有参构造
//传入true,表示以守护线程的方式
Timer timer = new Timer(true);
- schedule方法
/**安排任务从指定时间开始进行重复固定的延迟执行
* TimerTask是一个抽象类
* Date firstTime即第一次执行的时间
* Long period 即间隔多少毫秒
*/
void schedule(TimerTask task, Date firstTime , Long period)
//安排任务在指定时间执行任务task
void schedule(TimerTask task, Date time)
❀代码实现–编写一个定时器任务类记录日志
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class LogTimerTask extends TimerTask {
/**
* 重写父类TimerTask中的抽象方法run
* TimerTask类实现了Runnable接口,所以有run方法
*/
public void run(){
//这里写要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(new Date());
System.out.println(strTime + "日志记录成功");
}
}
class TimerTest{
public static void main(String[] args) {
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date firstTime = sdf.parse("2022-12-06 08:10:06");
timer.schedule(new LogTimerTask(),firstTime,1000*5);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
运行效果:
5、实现线程的第三种方式
JDK8新特性,可实现Callable接口。这种方式实现的线程可以获得线程的返回值。前两种实现方式不能获取返回值,因为run方法的返回值类型是void。
优点:
可获取到线程的执行结果
缺点:
效率较低,在获取t线程执行的结果时,当前线程需要等待,等拿到返回值以后再往下执行其余code
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest0 {
public static void main(String[] args) {
//创建一个”未来任务类“的对象,传参为Callable接口实现类的对象
FutureTask task = new FutureTask(new Callable(){ //匿名内部类
//call方法相当于run方法,不过其有返回值
public Object call() throws Exception{
System.out.println("call method begin!");
Thread.sleep(1000*6);
System.out.println("call method end");
int a = 100;
int b = 200;
//线程执行一个任务,执行完可能有返回结果
//这里自动装箱
return a+b;
}
});
Thread t = new Thread(task);
t.start();
try {
//当前为主线程,获取t线程的执行结果
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
//此处get方法要拿另一个线程的执行结果,可能要很久
//但这导致了下面main线程要等待get执行结束。
//这就导致了”当前线程的阻塞“
System.out.println("这里要等get拿到线程的返回值才能执行");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
运行结果:
八、wait和notify
1、概述
wait 和notify方法是Java中任何一个Java对象都有的方法,因为这两个方法是Object类自带的。不是给线程对象用的,所以别t.wait();
Object o = new Object();
o.wait();
以上让正在o对象上活动的线程进入等待状态,且为无限等待,直到被唤醒为止
T线程在o对象上活动,o.wait()后,T线程进入无限期等待,并且释放o对象的对象锁
o.notify()让正在o对象上等待的线程被唤醒
notifyAll()方法是唤醒o对象上处于等待的所有线程
2、生产者和消费者模式
🍁代码实现:
import java.util.ArrayList;
import java.util.List;
/**
* 生产线程
*/
class Producer implements Runnable{
/**
* 仓库
*/
private List list;
public Producer(List list){
this.list = list;
}
public void run(){
while(true){
synchronized(list){
if(list.size()>0){ //仓库有东西,则生产线程wait
try {
list.wait(); //进入等待状态,释放之前占有的list的对象锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//能到这说明仓库空了,开始生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
list.notifyAll(); //生产完了以后唤醒消费线程来消费
}
}
}
}
/**
* 消费线程
*/
class Consumer implements Runnable{
private List list;
public Consumer(List list){
this.list = list;
}
public void run(){
while(true){
synchronized(list){
if(list.size() == 0){ //仓库已空,暂停消费
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "-->" + obj);
//唤醒生产线程来生产
list.notifyAll();
}
}
}
}
/**
* 测试
*/
public class PC_Moudle {
public static void main(String[] args) {
//创建一个仓库,生产和消费线程共享
List list = new ArrayList();
Thread t1 = new Thread(new Producer(list));
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
}
}
运行效果: