Java多线程(超详细版!!)
Java多线程(超详细版!!)
文章目录
- Java多线程(超详细版!!)
- 1. 线程 进程 多线程
- 2.线程实现
- 2.1线程创建
- 2.1.1 继承Thread类
- 2.1.2 实现runnable接口
- 2.1.2.1 思考:为什么推荐使用runnable接口?
- 2.1.2.1.1 更高的灵活性(避免单继承限制)
- 2.1.2.1.2 任务与线程分离(更好的设计理念)
- 2.1.3 实现callable接口
- 2.1.4 线程并发问题:买票案例
- 2.1.5 龟兔赛跑
- 2.2多线程详解
- 2.2.1 静态代理模式
- 2.3 Lamba表达式
- 2.3.1Lambda表达式的推导:
- 3.线程状态
- 3.1 停止线程
- 3.2 线程休眠
- 3.3 线程礼让
- 3.4 线程强制执行
- 3.5 观察测试线程的状态
- 3.6 线程的优先级
- 3.6.1线程优先级的设置和获取
- 3.7 守护线程
- 4.线程同步
- 4.1 三大不安全案例
- 4.1.1 不安全的买票:
- 4.1.2 线程不安全的例子:
- 4.2 同步方法
- 4.2.1 synchronized方法
- 4.2.2 synchronized块
1. 线程 进程 多线程
线程(thread):是cpu调度和执行的单位。
进程(precess):是执行程序的一次执行过程。一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕。
- 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运
行的含必,是一个静恋的概念。 - 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单元
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。
注意:很多多线程是模拟出来的,真正的多线程是指有多个Cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个Cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
普通方法调用和多线程的区别如图:
2.线程实现
2.1线程创建
2.1.1 继承Thread类
在这类方法中,我们利用createThread01继承Thread类,通过.start()方法实现线程启动。继承 Thread 后,必须重写 run() 方法,因为 run() 方法定义了线程执行的具体任务。
public class createThread01 extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("hello "+i);}}public static void main(String[] args) {createThread01 t1 = new createThread01();t1.start();for (int i = 0; i < 100; i++) {System.out.println("你好 "+i);// 穿插进行}}
}
程序启动后,主线程运行 main 方法。调用 t1.start() 后,JVM 创建一个新线程,新线程执行 run() 方法,打印 “hello”。
通过观察,我们会发现,结果并不是我们想象中的按照顺序输出,而是穿插的输出。任由计算机CPU的调度执行。
2.1.2 实现runnable接口
在这段代码中,我们使用createThread02 实现了 Runnable 接口。Runnable 是一个函数式接口,定义了一个抽象方法 run()。实现 Runnable 接口的类需要提供 run() 方法的具体实现。
public class createThread02 implements Runnable{@Overridepublic void run() {System.out.println("hello");}public static void main(String[] args) {createThread02 t1 = new createThread02();new Thread(t1).start();}
}
此代码通过主线程运行 main 方法,创建 createThread02 实例 t1。创建一个新的 Thread 对象,并将 t1 作为参数传递。调用 start() 后,JVM 创建一个新线程,新线程执行 t1 的 run() 方法,打印 “hello”。主线程和新建的线程并发运行。
2.1.2.1 思考:为什么推荐使用runnable接口?
2.1.2.1.1 更高的灵活性(避免单继承限制)
由于Java 是单继承语言,一个类只能继承一个父类。如果继承了 Thread 类,这个类就无法再继承其他类,限制了类的扩展能力。故实现 Runnable 接口不影响类的继承结构。一个类可以实现多个接口(包括 Runnable),同时还能继承其他类。这使得 Runnable 方式更适合需要继承其他类的复杂场景。
例如:
class MyClass extends SomeBaseClass implements Runnable {@Overridepublic void run() {System.out.println("Running in a thread");}
}
这里 MyClass 可以继承 SomeBaseClass 的功能,同时通过实现 Runnable 具备线程任务的能力。
2.1.2.1.2 任务与线程分离(更好的设计理念)
继承 Thread 类将线程(执行机制)和任务(run() 方法的逻辑)绑定在一起,代码结构不够清晰。实现 Runnable 接口将任务逻辑与线程执行分离,Runnable 对象仅定义任务,线程由 Thread 类负责。这种分离符合面向对象设计中的 单一职责原则,提高了代码的可读性和可维护性。
示例:
Runnable task = () -> System.out.println("Task running");
new Thread(task).start(); // 线程 1 执行任务
new Thread(task).start(); // 线程 2 复用同一个任务
一个 Runnable 对象可以被多个线程复用,逻辑更清晰。
2.1.3 实现callable接口
Callable 是 Java 5 引入的并发接口,位于 java.util.concurrent 包中。与 Runnable 不同,Callable 的 call() 方法,其特点是:
-
有返回值:可以返回任务执行的结果(泛型类型)。
-
可抛出异常:允许抛出受检异常(Exception 及其子类)。
2.1.4 线程并发问题:买票案例
我们设置了三个人员来买票,即创建了三个进程。
public class buy implements Runnable{private int ticket = 10;@Overridepublic void run() {//买票逻辑while(true) {if(ticket > 0) {System.out.println(Thread.currentThread().getName()+"买到了票"+ticket--);//先取值后减try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}else {break;}}}public static void main(String[] args) {buy buy = new buy();new Thread(buy,"me").start();new Thread(buy,"you").start();new Thread(buy,"bad man").start();}
}
测试结果:
但是结果通过发现,一个票会被两个人同时买走。多个数据操作同一个资源,数据紊乱。这种问题,后期我们通过线程同步解决。
2.1.5 龟兔赛跑
public class rabbitAndTurtle implements Runnable{private String winner;@Overridepublic void run() {for (int i = 0; i <= 1000; i++) {if(Thread.currentThread().getName().equals("Rabbit")&&i%10==0) {//让兔子休眠try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}boolean flag = gameover(i);if(flag) {break;}System.out.println(Thread.currentThread().getName()+" steps is"+i);}}public boolean gameover(int step) {if (winner != null) {//已经产生了冠军return true;} else {//还未找到冠军if (step == 1000) {winner = Thread.currentThread().getName();System.out.println("the winner is "+winner);return true;}return false;}}public static void main(String[] args) {rabbitAndTurtle r = new rabbitAndTurtle();new Thread(r,"rabbit").start();new Thread(r,"turtle").start();}
}
我们通过手动将兔子进程休眠,让每次乌龟进程跑赢。
2.2多线程详解
线程的底层实现就是静态代理模式。
2.2.1 静态代理模式
前提条件
1.真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
以婚庆公司为例子
public class staticProxy implements marry{//静态代理模式//人生四大喜事:// 久旱逢甘露// 他乡遇故知// 洞房花烛夜// 金榜题名时@Overridepublic void Happywedding() {}public static void main(String[] args) {weddingCompany weddingCompany = new weddingCompany(new you());weddingCompany.Happywedding();}}interface marry{void Happywedding();
}class you implements marry{//个人@Overridepublic void Happywedding() {System.out.println("我要结婚啦,猴嗨森");}
}
class weddingCompany implements marry{//婚庆公司private marry target;//目标结婚对象@Overridepublic void Happywedding() {before();target.Happywedding();//调用对象的特定方法(已被重写)after();}weddingCompany(marry target) {//把目标对象传进去this.target = target;}public void before() {System.out.println("筹备婚礼");}public void after() {System.out.println("收尾款");}
}
婚庆公司实现了you的方法,无须you创造对象实现方法。代理对象可以做很多真实对象做不了的事情,真实对象做自己的事情。
“代理对象可以做很多真实对象做不了的事情”:
- 代理对象(Proxy)是一个中间层,负责在调用真实对象(Real Subject)之前或之后执行额外的逻辑。这些逻辑可能是真实对象不具备或不适合直接实现的,例如日志记录、权限检查、事务管理、延迟加载、线程管理等。
- 代理对象通过扩展功能,增强了真实对象的行为,而不改变真实对象的核心逻辑。
“真实对象做自己的事情”:
真实对象(Real Subject)专注于实现其核心业务逻辑,不需要关心与业务无关的附加功能(如日志、同步、缓存等)。
代理模式通过将非核心逻辑分离到代理对象中,保持真实对象的简单性和单一职责。
我们由婚庆公司这个例子可以得出。我们每次创建出来的线程,之所以要实现Runable接口,目的就是为了和Thread类实现统一的接口,使Thread能够代理我们新创建的类,从而调用我们重写的Run方法。避免我们自己调用。
2.3 Lamba表达式
Lambda表达式(闭包):java8的新特性,lambda运行将函数作为一个方法的参数,也就是函数作为参数传递到方法中。使用lambda表达式可以让代码更加简洁。
Lambda表达式的使用场景:用以简化接口实现。
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
例如:
public interface Runnable{public abstract void run();
2.3.1Lambda表达式的推导:
最初我们如果想要写一个类实现一个接口,我们就需要这样写。
public class lambdaTest {public static void main(String[] args) {like ilike = new ilike();ilike.like(520);}
}class ilike implements like{@Overridepublic void like(int i) {System.out.println("i like you "+i);}
}interface like {void like(int i);
}
通过分析简化,我们可以变换成这样,将一个外部类,加上static关键字。实现一个静态内部类。
public class lambdaTest {static class ilike implements like{//转变为静态内部类@Overridepublic void like(int i) {System.out.println("i like you "+i);}}public static void main(String[] args) {like ilike = new ilike();ilike.like(520);}
}interface like {void like(int i);
}
进一步简化,我们可以转变为局部内部类。
public class lambdaTest {public static void main(String[] args) {class ilike implements like{//实现局部内部类@Overridepublic void like(int i) {System.out.println("i like you "+i);}}like ilike = new ilike();ilike.like(520);}
}interface like {void like(int i);
}
再进一步简化,我们可以转变为匿名内部类。
public class lambdaTest {public static void main(String[] args) {// 创建匿名内部类实例并赋值给变量Like like = new Like() {@Overridepublic void lambda(int i) {System.out.println("i like " + i);}};// 通过实例调用 lambda 方法,传入参数like.lambda(5);}
}// 接口定义
interface Like {void lambda(int i);
}
在jdk1.8出现后,工程师创造了一种更加完善,简化的书写规范——lambda表达式。
public class lambdaTest {public static void main(String[] args) {// 利用lambda表达式实现方法Like like = (i)->{System.out.println("i like "+i);} ;like.lambda(5);}
}// 接口定义
interface Like {void lambda(int i);
}
由于在接口的方法中,已经定义了每⼀个参数的类型是什么。而且在使用lambda表达式实现接口的时候,必须要保证参数的数量和类 型需要和接口中的方法保持⼀致。因此,此时lambda表达式中的参数的类型可以省略不写。以上就是Lambda表达式相关内容。
3.线程状态
线程的状态分为一下几种:
其具体的运行转化过程如下:
接下来,笔者将为你详细讲述线程每个状态的操作。
3.1 停止线程
不推荐使用JDK提供的 stop()、destroy0方法。【已废弃】
推荐线程自己停止下来。
建议使用一个标志位进行终止变量当flag=false,则终止线程运行。
测试案例:
public class ThreadStop implements Runnable {private boolean flag = true;@Overridepublic void run() {int i = 0;while(flag) {System.out.println("running "+ i++);}}public void shutdown() {this.flag = false;}public static void main(String[] args) {ThreadStop threadStop = new ThreadStop();new Thread(threadStop).start();for (int i = 0; i < 1000; i++) {if (i == 900) {threadStop.shutdown();System.out.println("stop!");}System.out.println(i);}}}
在这里,我们使用了一个外部方法shutdown将线程停止了下来。
3.2 线程休眠
关键词:sleep
在java中参数1000,即为1000ms = 1s 。
我们通过一个例子,实现打印当前系统时间的例子。代码中,我们调用了** **方法获取当前时间,通过休眠1s实现每秒打印。打印完后,将新的时间赋值,完成时间更新操作。
import java.text.SimpleDateFormat;
import java.util.Date;public class threadSleeptest {//打印当前系统时间public static void main(String[] args) {Date date = new Date(System.currentTimeMillis());while(true) {try {Thread.sleep(1000);System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));//SimpleDateFormat 是一个用于格式化和解析日期的类,属于 java.text 包date = new Date(System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}}}
}
测试结果:
根据结果分析,我们可以得出系统按照每分每秒每时实现当前时间打印。
3.3 线程礼让
关键词:yield
-
礼让线程,让当前正在执行的线程暂停,但不阻塞
-
将线程从运行状态转为就绪状态
-
让cpu重新调度,礼让不一定成功!看CPU心情
我们创建了两个线程,验证是否能实现线程礼让功能。
public class threadYield implements Runnable {@Overridepublic void run() {System.out.println("hello "+Thread.currentThread().getName());Thread.yield();//线程礼让System.out.println("bye "+Thread.currentThread().getName());}public static void main(String[] args) {threadYield thread = new threadYield();new Thread(thread,"a").start();new Thread(thread,"b").start();}}
输出结果:
hello a
hello b
bye b
bye a
我们可以得出,cpu礼让了线程a,输出了hello b语句。
3.4 线程强制执行
关键词:Join
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。可以想像成插队
样例:
public class forceTest extends Thread {@Overridepublic void run() {System.out.println(100+" vip");}public static void main(String[] args) {forceTest test = new forceTest();test.start();for (int i = 0; i < 10; i++) {if(i==5) {try {test.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(i);}}
}
输出结果:
0
1
2
3
4
100 vip
5
6
7
8
9
通过join方法,强制执行run方法。
3.5 观察测试线程的状态
线程状态。线程可以处于以下状态之一:
- NEW 尚未启动的线程处于此状态。
- RUNNABLE 在Java虚拟机中执行的线程处于此状态。
- BLOCKED 被阻塞等待监视器锁定的线程处于此状态。
- WAITING 正在等待另一个线程执行特定动作的线程处于此状态。
- TIED WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
- TERMINATED已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
3.6 线程的优先级
在Java中,线程优先级是指调度器根据线程的优先级决定哪个线程应该优先获得CPU的时间片。在多线程编程中,线程优先级是一个影响线程调度的重要因素。Java提供了线程优先级机制,允许开发者为不同的线程设置优先级,以便操作系统调度器在分配CPU时间时能够优先处理高优先级的线程。
在Java中,每个线程都有一个优先级,它是一个整数值,范围从1到10。Java定义了三个常量用于表示线程优先级的标准值:
-
Thread.MIN_PRIORITY: 1,表示最低优先级。
-
Thread.NORM_PRIORITY: 5,表示默认的普通优先级。
-
Thread.MAX_PRIORITY: 10,表示最高优先级。
-
默认情况下,Java中的线程继承创建它的父线程的优先级。例如,主线程的优先级为5,因此任何由主线程创建的新线程默认也具有优先级5。
3.6.1线程优先级的设置和获取
Java提供了两种方法来设置和获取线程的优先级:
- setPriority(int priority): 用于设置线程的优先级。
- getPriority(): 用于获取线程的优先级。
public class ThreadPriorityExample {public static void main(String[] args) {Thread thread1 = new Thread(() -> {System.out.println("Thread 1 is running with priority: " + Thread.currentThread().getPriority());});Thread thread2 = new Thread(() -> {System.out.println("Thread 2 is running with priority: " + Thread.currentThread().getPriority());});thread1.setPriority(Thread.MIN_PRIORITY);thread2.setPriority(Thread.MAX_PRIORITY);thread1.start();thread2.start();}
}
在这个例子中,我们创建了两个线程,并分别为它们设置了最低优先级和最高优先级。运行结果可能会因操作系统的调度策略而异,但通常情况下,高优先级的线程更有可能先执行。
3.7 守护线程
关键词:setdaemon
在Java中,守护线程(Daemon Thread)是一种特殊的线程,在后台运行并提供服务支持。当JVM中仅剩下守护线程时,JVM会自动退出,因为此时没有用户线程需要继续执行1。
默认情况下,主线程和其他通过Thread
类创建的线程都是非守护线程。要将一个线程设置为守护线程,可以在调用start()之前使用
setDaemon(true)`方法来实现。需要注意的是,一旦线程启动后就不能更改其守护状态2。
线程分为用户线程和守护线程
虛拟机心须确保用户线程执行完毕
虚拟机不用等待守护线程执行完毕
如:后台记录操作日志,监控内存,垃圾回收等待.
以下是设置守护线程的一个简单例子:
public class DaemonThreadExample {public static void main(String[] args) throws InterruptedException {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("Daemon thread is running...");try {Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("Daemon thread interrupted.");}}});// 将线程设为守护线程daemonThread.setDaemon(true);// 启动守护线程daemonThread.start();// 主线程休眠一段时间后结束Thread.sleep(3000);System.out.println("Main thread finished.");}
}
在这个例子中,守护线程会在后台持续打印消息直到主线程完成。由于只有守护线程存活,JVM随后终止整个程序。
4.线程同步
线程同步发生的情况:多个线程访问同一个对象时,并且某些线程还想修改这个对象的时候,这个时候就用到了线程同步。多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
形成条件:锁+队列
关键词:synchronized
4.1 三大不安全案例
现在笔者将提供三个例子,为读者讲解这些典型例子中,哪里有问题,为后期使用线程同步来解决埋下伏笔。
4.1.1 不安全的买票:
在当前例子中:
public class buy implements Runnable{private int ticket = 10;@Overridepublic void run() {//买票逻辑while(true) {if(ticket > 0) {System.out.println(Thread.currentThread().getName()+"买到了票"+ticket--);//先取值后减try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}else {break;}}}public static void main(String[] args) {buy buy = new buy();new Thread(buy,"me").start();new Thread(buy,"you").start();new Thread(buy,"bad man").start();}
}
输出结果:
you买到了票9
bad man买到了票8
me买到了票10
bad man买到了票7
me买到了票7
you买到了票6
bad man买到了票5
me买到了票4
you买到了票4
you买到了票3
me买到了票2
bad man买到了票3
bad man买到了票1
me买到了票0
you买到了票1
三个对象买票,但会出现多个人买到一张票,线程不安全。多个人会操作同一个资源。
4.1.2 线程不安全的例子:
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;public class unsafeExample{public static void main(String[] args) {List s = new LinkedList();unsafeExample un = new unsafeExample();for (int i = 0; i < 10000; i++) {new Thread(()->{s.add(Thread.currentThread().getName());}).start();}System.out.println(s.size());}
}
9999
通过观察结果,我们会发现,会有线程泄漏出去,无法保证线程安全。
4.2 同步方法
通过4.1的三个例子,我们发现了使用线程存在安全问题。这一环节我们就可以时候同步的方法保证一个对象(线程)操作一个资源。维护线程操作的安全性。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键宇,它包括两种用法:
synchronized
方法 和synchronized
块.
4.2.1 synchronized方法
同步方法:public synchronized void method(int args) g•synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁。
缺陷:若将一个大的方法申明为synchronized 将会影响效率
因此对于上面的买票例子,我们通过锁掉buy()
方法,实现一个对象操作一个资源。保证了线程的安全性。
public class buy implements Runnable{private int ticket = 100;private boolean flag = true;@Overridepublic void run() {//买票逻辑while (flag) {try {buy();} catch (InterruptedException e) {e.printStackTrace();}}}public synchronized void buy() throws InterruptedException {//判断票是否买完if(ticket > 0) {System.out.println(Thread.currentThread().getName()+"买到了票"+ticket--);//先取值后减Thread.sleep(100);}else {flag = false;return;}}public static void main(String[] args) {buy buy = new buy();new Thread(buy,"you").start();new Thread(buy,"me").start();new Thread(buy,"bad man").start();}
}
通过使用同步方法,结果如我们想的一样。
4.2.2 synchronized块
使用方法:synchronized(){}
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;public class unsafeExample{public static void main(String[] args){List s = new LinkedList();unsafeExample un = new unsafeExample();for (int i = 0; i < 222000; i++) {new Thread(()->{synchronized (s) {s.add(Thread.currentThread().getName());//拟合排队的逻辑}}).start();}System.out.println(s.size());}
}
以上便是笔者对于Java线程的有限见解,笔者编文不易,欢迎评论区大佬指点补错!希望这个讲解能帮助你深入理解代码的实现过程!如果有疑问,欢迎评论区交流!