一.对java多线程的认识
- 如果在一个
进程
中同时运行了多个线程,且每个线程间相互独立
,用来完成不同的工作,则称之为多线程
- 每个线程在
栈区
中都有自己的执行空间,自己的方法区、自己的变量
。 - 线程与线程之间存在争夺关系,
相互争夺cpu资源竞相执行相关对象的代码段。
- 多个线程
同时执行
可能会造成线程安全
问题(线程之间同时拥有一个变量,且发生了修改),为了避免这个问题,需要同步锁
来使一个线程执行时,其他线程会等待这个线程执行完毕才执行
1.进程和线程的区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 进程是作为资源分配 的单位,一个程序就是一个进程 | 线程是cpu调度和执行 的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进 程间的切换会有较大的开销。 | 线程可以看成时轻量级的进程 ,同一类线程共享代码 和数据空间 ,每个线程有独立的运行栈 和程序计数器(PC),CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 |
所处环境 | 在操作系统 中能同时运行多个任务(程序) | 在同一程序(进程) 中有多个顺序流同时执行 |
内存分配 | 系统在运行的时候会为每个进程分配独立的内存区域,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段 | 除了CPU之外,不会为线程分配内存 ,线程是共享进程中的数据的,使用相同的地址空间(线程所使用的资源是它所属的进程的资源) |
包含关系 | 没有线程的进程是可以被看作单线程的,如果一个进 程内拥有多个线程,则执行过程不是一条线的,而是 多条线(线程)共同完成的。 | 线程是进程的一部分,所以线程有的时候被称 为是轻权进程或者轻量级进程。 |
通信方式 | 同一进程下的线程共享全局变量、静态变量等数据,线程之间的通信方便 ,如何处理好同步与互斥 是编写多线程程序的难点 | 进程之间的通信需要以通信的方式(IPC)进行 |
健壮性 | 一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间 | 多线程程序只要有一个线程死掉,整个进程也死掉 |
进程互斥与同步的区别 |
2.多线程的使用场景
- 常见的
浏览器
、Web服务
(现在web是中间件帮你完成了线程的控制),web处理请求
,各种专用服务器
(如游戏服务器) - servlet多线程
- FTP下载,多线程操作文件
- 数据库用到的多线程
- 分布式计算
- Tomcat:tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
- 后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
- 自动作业处理:比如定期备份日志、定期备份数据库
- 异步处理:如发微博、记录日志
- 页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
- 数据库的数据分析(待分析的数据太多),数据迁移
- 多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
- 桌面应用开发,一个费时的计算开个线程,前台加个进度条显示
- JavaSwing编程(
Java桌面应用程序开发
)
3.使用是不是很多线程就能提高效率呢?
- 不一定的。因为程序中上下文的切换开销也很重要,
如果创建了太多的线程,CPU花费在上下文的切换的时间将多于执行程序的时间!
这会降低程序执行效率的。所以有效利用多线程的关键是理解程序是并发执行
而不是串行执行
的。
4.JVM默认启动的线程
- 主线程 (Main方法)
- GC
- 异常
5.线程是如何创建起来的
-
进程仅仅是一个
容器
,包含了线程运行中所需要的数据结构等信息。 -
一个进程创建时,操作系统会创建一个线程,这就是
主线程
,而其他的子线程
,却要主线程的代码来创建,也就是由程序员来创建。 -
当一个
程序启动
时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行
,该线程通常叫做程序的 主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少都有一个主线程, 主线程的重要性体现在两方面:1.是产生其他子线程的线程;2.通常它必须最后完成执行比如执行各种关闭动作
6.什么是串行、并发、并行
- 串行:一个线程执行到底,相当于单线程。
- 并发:多个线程交替执行,抢占cpu的时间片,但是速度很快,在外人看来就像是多个线程同时执行。
- 并行:多个线程在不同的cpu中同时执行。
7.并发与并行的区别
- 并发严格的说不是同时执行多个线程,只是
线程交替执行且速度很快,相当于同时执行
。 - 而并行是同时执行多个线程,也就是多个cpu核心同时执行多个线程。
8. 三高
- 高可用:数据不能出错
- 高性能: 体验要好,不能等太久
- 高并发: 同时操作,有竞争的乐趣
9.单线程和多线程
- 单线程:程序中只存在一个线程,实际上主方法就是一个主线程。
- 多线程:多线程是指在同一程序中有多个顺序流在执行。 简单的说就是在一个程序中有多个任务运行。
10.线程分类
对于线程的分类,我们可以简单划分为:
-
主线程 (每个进程只有一个主线程)
- 主线程: main方法
-
子线程: 子线程中可以简单划分为:
-
非守护线程,即用户线程
- 非守护线程/用户线程: 通常异步处理一些业务或逻辑
-
守护线程
- 守护线程: 主要是指在进程中,为主线程提供一种通用服务的线程。
比如 gc线程,因为主线程一旦结束或者销毁,守护线程没有了守护对象,也将同步进行结束或销毁。
- 守护线程: 主要是指在进程中,为主线程提供一种通用服务的线程。
-
Java 线程分为守护线程(DaemonThread) 和 用户线程(UserThread)两类
- 通常情况下,我们使用Thread 创建的线程在
默认情况下都属于用户线程
, 当在启动线程之前, 执行thread.setDaemon(true)
时, 线程会变成守护线程
。 - 其实在本质上,用户线程和守护线程并没有太大区别,
唯一的区别就是会影响虚拟机的退出(程序的终止)。
- 当jvm中
只剩下守护线程时,虚拟机会退出
,及程序终止;而当jvm中至少拥有一个用户线程时,jvm都不会退出。
由此可以得到:
守护线程是依赖于用户线程
,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。- 用户线程是独立存在的,不会因为其他用户线程退出而退出。
- 默认情况下启动的线程是用户线程,在线程执行前通过
setDaemon(true)
将线程设置成守护线程,- 在守护线程中启动的
子线程
也是守护线程
- 守护线程不建议进行
写操作
, 因为守护进程随时可能结束
。
- 在守护线程中启动的
守护线程适用场景
- 针对于守护线程的特点,笔者认为,java 守护线程通常可用于
开发一些为其它用户线程服务的功能
。比如说心跳检测
,事件监听
等。Java 中最有名的守护进程当属GC(垃圾回收)
测试程序退出
- 注释thread.setDaemon(true)时, 线程为用户线程, 程序进行无线循环, 程序不终止。
public class TestThread { public static void main(String[] args) { AnsyTask ansyTask = new AnsyTask(); Thread thread = new Thread(ansyTask); // 设置线程为守护线程 // thread.setDaemon(true); // 启动线程 thread.start(); System.out.println("main thread done"); } } class AnsyTask implements Runnable{ @Override public void run() { while (true){ System.out.println(LocalDateTime.now() + "-hello,thread"); } } }
2. 设置thread.setDaemon(true)
后, 线程变成守护线程
(守护线程), 程序直接终止, 仅输出一行信息"main thread done"。 因为程序执行完system 语句之后, main 程序作为唯一的一个用户线程执行结束了, jvm 中只剩下一个守护进程,所以jvm 便退出了。
测试守护线程中创建新的线程
- 测试会发现, 默认情况下,
守护线程创建的子线程依然是守护线程
,用户创建的守护线程依然是用户线程。也可以在创建子线程时通过setDaemon()方法修改.public class TestThread { public static void main(String[] args) throws InterruptedException { AnsyTask ansyTask = new AnsyTask(); Thread thread = new Thread(ansyTask); // 设置线程为守护线程 thread.setDaemon(true); // 启动线程 thread.start(); // 给守护线程点儿执行时间 Thread.sleep(1000l); } } class AnsyTask implements Runnable{ @Override public void run() { Thread thread = new Thread("subThread"); System.out.println(thread.getName() + " is daemon:" + thread.isDaemon()); } }
二.JAVA实现多线程
1.实现多线程的3种方式:
继承java.lang.Thread类
,并且重写它的run方法
(线程体),将线程的执行主体放在其中;实现java.lang.Runnable接口
,重写它的run方法
(线程体),并将线程的执行主体放在其中;实现java.util.concurrent.Callable<T>接口
,重写它的call方法
(线程体),并通过Futrue获取 call 方法的返回值
2.线程的创建
- 在java中负责线程的这个功能是
Java.lang.Thread
这个类 - 每个线程都是通过某个特定Thread对象的run 方法,来完成其操作的,
run() 称为”线程体”
, run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。 - 通过调用Thread类的
start()
方法来启动一个线程 - 一般来说,我们在对线程进行创建的时候,一般是
继承Thread类
或实现Runnable 接口
。其实还有一种方式是实现Callable与Future接口
, 类似于Runnable接口,但是就功能上来说更为强大一些,也就是被执行之后,可以拿到返回值。
推荐单线程的时候使用继承 Thread 类方式创建,多线线程的时候使用Runnable、Callable 接口的方式来创建创建线程
-
继承
Thread
类创建的线程,编写最为简单,可以直接使用Thread类中的方法
,比如休眠直接就可以使用sleep
方法,而不必在前面加个Thread;获取当前线程Id,只需调用getId
就行,而不必使用Thread.currentThread().getId() 这么一长串的代码。但是使用Thread 类创建的线程,也有其局限性。比如资源不能共享,无法放入线程池
中等等。 -
使用
Runnable、Callable
接口的方式创建的线程,可以增强代码的复用性,并且可以避免
单继承的局限性,可以
和线程池完美结合。但是也有不好的,就是
写起来不太方便,使用其中的方法不够简介```。
3.线程的启动
- 新建的线程不会自动开始运行,必须通过start( )方法启动
- 执行线程必须调用start()加入调度器中, 但是不一定立即执行,由系统安排调度器安排cpu时间片调用线程体()
- 不能直接调用run()来启动线程,这样run()将作为一个普通方法立即执行,执行完毕前其他线程无法并发执行
- Java程序启动时,会立刻创建主线程,main就是在这个线程上运行。当不再产生新线程时,程序是单线程的
Thread类与静态代理模式
- 真实角色
- 代理角色 代理角色要持有真实角色的引用
- 二者实现相同接口
- Thread类就是使用静态代理模式来实现线程的 , Thread类就是一个写好的静态代理(代理角色)
- 创建接口
- 创建代理角色并实现接口,代理类中设置真实角色的引用,并通过构造方法传入,然后通过真实角色的引用调用真实角色的方法,然后在方法前后对方法进行处理。
- 创建真实角色并实现接口
/**
* 代理角色
*/
public class ProxyRole implements MyInterface {
private RealRole realRole;
public ProxyRole(RealRole realRole){
this.realRole = realRole;
}
public void before(){
System.out.println("向对方问好");
}
public void after(){
System.out.println("再见");
}
@Override
public void talk() {
before();
realRole.talk();
after();
}
}
/**
* 共同接口
*/
interface MyInterface{
void talk();//谈话
}
/**
* 真实角色
*/
class RealRole implements MyInterface {
@Override
public void talk() {
System.out.println("正在谈话中。。。。");
}
}
class TestMain {
public static void main(String[] args) {
//创建真实角色
RealRole realRole = new RealRole();
//创建代理角色 + 传入真实角色的引用
ProxyRole proxyRole = new ProxyRole(realRole);
//由代理角色代理真实角色调用真实方法
proxyRole.talk();
}
}
4.继承Thread
描述:
创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类
,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例
。
public class RunMain {
public static void main(String[] args) {
//调用run 方法相当于普通方法的调用
Rabbit rabbit =new Rabbit();
Tortoise tortoise = new Tortoise();
//调用start()方法会将线程加入
rabbit.start();
tortoise.start();
//默认启动的线程
//1.主线程 2.GC 3.异常
for (int i = 1;i<1000 ;i++ ) {
System.out.println("主线程走了"+i+"步");
}
}
}
/**
* 兔子
* 1.继承Thread类 + 重写 run()(线程体)
* 2.创建子类对象 + 调用start()
*/
class Rabbit extends Thread {
/**
* run方法称为线程体
*/
@Override
public void run() {
for (int i = 1; i<= 1000 ; i++) {
System.out.println("兔子跑了"+i+"步");
}
}
}
class Tortoise extends Thread{
@Override
public void run() {
//线程体
for (int i = 1;i<1000 ;i++) {
System.out.println("乌龟跑了"+i+"步 ");
}
}
}
5.实现Runable接口 + 重写run()
推荐使用Runnable接口创建线程
- 避免单继承的局限性
- 便于共享资源
实现方式
- 实现Runable接口 + 重写run() ——真实角色类
- 启动多线程,使用静态代理
- 创建真实角色(实现Runnable接口,实现run()方法-线程体)
- 创建代理角色(Thread类实例) + 真实角色引用(将真实角色实例传入Thead)
- 调用start(),启动线程
/**
* 继承Runnable接口实现Run()方法
* 并发:(当一份资源多个使用的时候,可能会产生线程安全问题)
*/
class Web12306 implements Runnable{
private int num = 50;
@Override
public void run() {
while (true){
if(num<=0){
break;//跳出循环
}
System.out.println(Thread.currentThread().getName()+"抢到了票"+(--num));
}
}
}
class TestMain{
public static void main(String[] args) {
// 真实角色(一份资源)
Web12306 web = new Web12306 ();
// 代理角色(多个代理使用)
Thread thread1 = new Thread(web,"张三");
Thread thread2 = new Thread(web,"李四");
Thread thread3 = new Thread(web,"王五");
//启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
实例2
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;
RunnableDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while.
Thread.sleep(50);9
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo( "Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo( "Thread-2");
R2.start();
}
}
//Thread也可以进行类似的拓展重写,一模一样
3.Thread和Runnable创建方式的比较
- 继承Thread类方式的多线程(
Thread也是实现Runable接口,队Runnable接口进行了一些封装
)- 优势:可以直接使用Thread类中的方法,编写简单
- 劣势:如果已经有了父类,就不能用这种方法 或者 如果继承了Thread就不能其他类了
- 实现Runnable接口方式的多线程
-
优势:避免类单继承的局限,一个类可以继承多个接口
(接口是可以多实现的)
,实现Runnable接口方式要通用一些。 -
劣势:编程方式稍微复杂,不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法。
-
4.java.util.concurrent.callable(高级并发编程 JUC-并发领域)
-
与Runnable相比, Callable功能更强大些
- 方法不同
- 可以有返回值,支持泛型的返回值
- 可以抛出异常
- 需要借助FutureTask,比如获取返回结果
-
Future接口
- 可以对具体
Runnable、Callable
任务的执行结果
进行取消
、查询是否完成
、获取结果
等。 FutrueTask
是Futrue接口的唯一的实现类
FutureTask
同时实现
了Runnable, Future
接口。它既可以作为Runnable被线程执
行,又可以作为 Future得到Callable的返回值
- 可以对具体
-
通过 Callable 和 Future 创建线程
-
创建
Callable 接口的实现类
,并实现 call(
) 方法,该 call() 方法将作为线程执行体,并且有返回值。 -
创建
Callable 实现类的实例
,使用FutureTask
类来包装 Callable 对象
,该FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
使用
FutureTask
对象作为Thread 对象的 target
创建并启动新线程。 -
调用
FutureTask
对象的get()````方法来
获得子线程执行结束后的返回值```。
/** * 使用Callable创建线程 */ public class TestCallable { public static void main(String[] args) throws InterruptedException, ExecutionException { // 从线程池中获取线程 ExecutorService executorService = Executors.newFixedThreadPool(2); Race tortoise = new Race("乌龟", 1000L); Race rabbit = new Race("兔子", 5000L); // 执行并获取值保存到Future中 Future<Integer> tortoiseResult = executorService.submit(tortoise); Future<Integer> rabbitResult = executorService.submit(rabbit); //休眠10秒 //Thread.sleep(10000);//2秒 //设置flag为false停止线程 tortoise.setFlag(false); rabbit.setFlag(false); //获取线程返回结果 int tortoiseNum = tortoiseResult.get(); int rabbitNum = rabbitResult.get(); System.out.println("乌龟跑了-->"+tortoiseNum+"步"); System.out.println("兔子跑了-->"+rabbitNum+"步"); //停止服务 executorService.shutdownNow(); } } /** * 使用lombok来在编译期间自动生成get/set方法 */ @Data class Race implements Callable<Integer> { private String name;//名称 private Long time;//延时时间 private Boolean flag = true; private Integer step = 0;//步 public Race(String name) { this.name = name; } public Race(String name, Long time) { this.name = name; this.time = time; } @Override public Integer call() throws Exception { while (flag) { Thread.sleep(time); step++; System.out.println(this.getName()+(step)); } return step; } }
-
5.Lambda方式创建线程
/**
* Lambda 简化(使用一次)线程方式
*
* Lambda 避免内部类创建过多,属于函数式编程概念
*/
public class LambdaThread {
// 1.静态内部类
static class Test implements Runnable{
@Override
public void run() {
System.out.println("听歌");
}
}
public static void main(String[] args) {
new Thread(new Test()).start();
// 2.局部内部类
class Test2 implements Runnable{
@Override
public void run() {
System.out.println("看电视");
}
}
new Thread(new Test2()).start();
// 3.匿名内部类(连名字都没有,必须结束接口或者父类)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("打篮球");
}
}).start();
// 4.JDK8 Lambda表示简化线程
new Thread(()-> {
System.out.println("吃饭");
}).start();
}
}
三.线程调度
1.理解线程的优先权(priority)
- 当线程的优先级没有指定时,默认普通优先级(
Thread.NORM_PRIORITY 默认为普通优先级 5
)。 - 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
- 优先级高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。优先级不能保证线程执行的顺序,由操作系统线程调度算法决定
- 线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程
Thread类的常量 | 描述 |
---|---|
static int MAX_PRIORITY | 线程可以具有的最高优先级,取值为10 |
static int MIN_PRIORITY | 线程可以具有的最低优先级,取值为1 |
static int NORM_PRIORITY | 分配给线程的默认优先级,取值为5 |
注意: 设置优先级并不能保证线程一定先执行。我们可以通过一下代码来验证。
public class PriorityTest {
public static void main(String[] args) {
Priority t1 = new Priority("张三");
Priority t2 = new Priority("李四");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
class Priority extends Thread {
public Priority(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " 线程运行开始!");
for (int i = 1; i <= 5; i++) {
System.out.println("子线程" + this.getName() + "运行 : " + i);
try {
sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 线程运行结束!");
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/20abe39a43d62d71b8835eac9855163c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/82d84f328d435f13b950f5f6eab955f1.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a0f0f92781f78e76ebd2726abd53e7f6.png)
2.如何改变线程的状态以及停止线程
2.1.停止线程的两种方法
-
自然停止:线程体自然执行完毕
-
外部干涉:通过线程体标识
- 线程类中定义线程体能够使用的标识
- 线程体使用该标识
- 提供对外的方法改变该标识
- 外部根据条件调用该方法即可
具体见代码:
/**
* 线程的状态:
* 如何干涉线程的状态以及停止方法:
*/
public class ThreadStop {
public static void main(String[] args) {
Study study = new Study();
new Thread(study, "张三").start();
// 外部干涉
for (int i = 0; i < 100; i++) {
if (50 == i) {//外部干涉
study.stop();
}
System.out.println("main-->" + i);
}
}
}
class Study implements Runnable {
// 1.线程体中定义线程体使用的标识
private boolean flag = true;
@Override
public void run() {
//2.线程体中使用标识
while (flag) {
System.out.println(Thread.currentThread().getName() + "-->study thread......");
}
}
// 3.对外提供方法改变标识
public void stop() {
this.flag = false;
}
}
可见,当i=50时,stop方法并不是立刻被执行的,这与CPU的调度有关
- 但还是可以看出通过外部干涉的方法使线程终止
- Thread类也提供了stop方法,但是不建议使用,API中也明确提出该方法不安全
2.1.线程调度方法
2.2.1.内置方法
- join ()
- 阻塞指定线程等到另一个线程完成以后再继续执行(
简单说,在哪个线程的线程体中调用join,哪个线程就会进入等待状态,并且释放锁
)
- 阻塞指定线程等到另一个线程完成以后再继续执行(
- sleep () 使线程停止运行一段时间,将处于阻塞状态
- 如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
- sleep()模拟网络延时,放大了发生问题的可能性
- yield ()
- 让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态
- 如果调用了yield方法之后,没有其他等待执行的线程,这个时候当前线程就会马上恢复执行!
- setDaemon()
- 可以将指定的线程设置成后台线程
- 创建后台线程的线程结束时,后台线程也随之消亡
- 只能在线程启动之前把它设为后台线程
- interrupt()
- 并没有直接中断线程,而是需要被中断线程自己处理
- stop()
- 结束线程,不推荐
2.2.2.yield()与join()区别
-
yield()会让当前运行线程回到可运行状态,让具有相同优先级的其他线程之间能适当的轮转执行。但是,
实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
public class YieldTest { public static void main(String[] args) { Test1 t1 = new Test1("张三"); Test1 t2 = new Test1("李四"); new Thread(t1).start(); new Thread(t2).start(); } } class Test1 implements Runnable { private String name; public Test1(String name) { this.name=name; } @Override public void run() { System.out.println(this.name + " 线程运行开始!"); for (int i = 1; i <= 5; i++) { System.out.println(""+this.name + "-----" + i); // 当为3的时候,让出资源 if (i == 3) { Thread.yield(); } } System.out.println(this.name + " 线程运行结束!"); } }
上述中的例子我们可以看到,启动两个线程之后
- 执行yield()的线程有可能在进入到可执行状态后马上又被执行
- main方法也是个线程。如果直接执行的话main方法执行完毕了,可子线程还没执行完毕,这里我们就
让子线程使用join方法使main方法等待子线程终止
在这里顺便说下,yield和sleep的区别。
- yield: yield只是
使当前线程重新回到可执行状态
,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行
- sleep:sleep使
当前线程进入停滞状态
,所以执行sleep()的线程在指定的时间内肯定不会被执行
代码2
public class JoinTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+ "主线程开始运行!");
Join t1=new Join("A");
Join t2=new Join("B");
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
}
}
class Join extends Thread{
public Join(String name) {
super(name);
}
public void run() {
System.out.println(this.getName() + " 线程运行开始!");
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+this.getName() + "运行 : " + i);
try {
sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 线程运行结束!");
}
注释掉所有join()<左>
效果 与 使用join()<右>
如下
- 结论:如果t1、t2调用join,主线程会进入等待状态,必须等待t1、t2线程终止才能继续执行(如效果图右)
3.线程的常用方法
方法 | 描述 |
---|---|
sleep() | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);不会释放对象锁 |
join() | 父线程进入等待状态,等待t线程终止,继续执行 |
yield() | 暂停当前线程,允许具有相同优先级的其他线程获得运行机会, 但执行yield()的线程有可能在进入到可执行状态后马上又被执行 |
wait() | 强迫一个线程等待。它是Object的方法,也常常和sleep作为比较。需要注意的是wait会释放对象锁,让其它的线程可以访问;使用wait必须要进行异常捕获,并且要对当前锁实例所调用,即必须采用synchronized中的对象。 |
isAlive() | 判断线程是否还“活”着,即线程是未终止 |
activeCount() | 程序中活跃的线程数 |
enumerate() | 枚举程序中的线程 |
static Thread currentThread() | 得到当前线程 |
setDaemon() | 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) |
getName() | 得到线程的名字 |
setName(String name) | 设置线程的名字 |
int getPriority() | 获取线程的优先级 |
int setPriority() | 设置一个线程的优先级 |
void() start() | 调用run( )方法启动线程,开启一个线程,但不保证立即运行,交给cpu调度运行 |
void run() | 调用线程的线程体 |
interrupt() | 一个线程是否为守护线程 |
notify() | 通知一个线程继续运行。它也是Object的一个方法,经常和wait方法一起使用 |
status() | 获取线程的状态 NEW RUNNABLE |
4.线程的生命周期
新建/新生状态(New)
- 使用new关键字创建一个线程的时候,线程还没有开始执行,该线程对象就处于新生状态。
- 处于新生状态的线程有自己的内存空间,通过调用start()进入就绪状态 (而不是运行状态)
就绪状态(Runnable)
- 当线程调用了start方法之后,线程就进入就绪状态;处于就绪状态的线程不一定立即运行run方法,只有获取到cpu时间才可以执行run方法
- 处于就绪状态线程具备了运行条件,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU ,当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称之为“cpu调度”。
- 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
产生就绪状态的四种情况- 调用start()
- 阻塞解除,重新进入就绪状态
- yield(), 高峰量前,中断线程,避免一个线程占用资源过多
- jvm本身将cpu从本地线程切换到其他线程,此线程进入就绪状态,根据自己的算法进行切换
运行状态(running)
- 当线程获取到了cpu运行时间之后,就进入到运行状态了,调用run方法;
- 在运行状态的线程执行自己的run方法中代码,直到等待某资源而阻塞或完成任务而死亡。
- 如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
- 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态(blocked)
-
正在运行的线程还没有结束,暂时让出cpu,这时其他就绪线程就有机会获得cpu时间;
-
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
-
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
以下原因会导致线程进入阻塞状态- 线程调用sleep方法进入睡眠状态;
- 线程在调用一个在i/o上被阻塞的操作
- 线程试图去获得一个锁,但是这个锁被其他线程持有;
-
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态(Dead)
- 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有三个。
- 一个是正常运行的线程完成了它的全部工作;
- 另一个是线程被强制性地终止,如通过执行stop/destory方法来终止一个线程[不推荐使用】,
- 是线程抛出未捕获的异常
注意: 可以用isAlive方法来判断线程是否还活着,只要是线程处于运行或者阻塞状态,就返回true;如果线程状态是New且不是可运行的状态或者线程死亡了,则返回false;
四.线程重要概念
在多线程编程时,你需要了解以下几个概念:
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制:挂起、停止和恢复
1.线程同步
描述
- synchronized (obj){ }中的obj称为同步监视器
- 同步代码块中同步监视器可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,也就是该对象本身
Lock锁
- JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活
- java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
- 注意:如果同步代码有异常,要将unlock()写入finally语句块
Lock和synchronized的区别
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序
Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
线程同步的好处
- 解决了线程安全问题
线程同步的缺点
- 性能下降
- 会带来死锁
/**
* 线程不安全,以及synchronized使用不当实例
*/
public class ThreadSynchronizedTest {
public static void main(String[] args) {
Web12306 web = new Web12306();
//新建状态
Thread thread1 = new Thread(web, "路人甲");
Thread thread2 = new Thread(web, "黄牛乙");
Thread thread3 = new Thread(web, "攻城狮");
//就绪状态(不保证立即运行,由cpu调度)
//运行实际:运行代码前两条路,之后可能还是一条路
thread1.start();
thread2.start();
thread3.start();
}
}
class Web12306 implements Runnable {
private boolean flag = true;
private int num = 10;
@Override
public void run() {
while (flag) {
testSync1();
}
}
/**
*线程不安全
*/
public void testSync6() {
if (num <= 0) {
flag = false;//跳出循环
return;
}
//a b c
synchronized (this) {
try {
Thread.sleep(500);//模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
}
}
/**
* 线程不安全 锁定资源不正确
*/
public void testSync5() {
//a b c
synchronized ((Integer) num) {
if (num <= 0) {
flag = false;
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
}
}
/**
* 锁定范围不正确 线程不安全
*/
public void testSync4() {
// c 1
synchronized (this) {
//b
if (num <= 0) {
flag = false;
return;
}
}
// b
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
}//a -->1
/**
* 线程安全 锁定正确
*/
public void testSync3() {
synchronized (this) {
if (num <= 0) {
flag = false;
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
}
}
public synchronized void testSync2() {
if(num <= 0){
flag = false;
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+ (num--));
}
/**
* 线程不安全
*/
public void testSync1() {
if(num <= 0){
flag = false;
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了" + (num--));
}
}
下图分别对应 testSync6——testSync1 方法调用结果
![](https://i-blog.csdnimg.cn/blog_migrate/76c8fe11ca809ab839bd4f6a64b18d70.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7d00567659e938729e62fe0a453156ba.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b4013a509582ab1536762b2781ff1137.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7d95b3ac84c44b88da9fb5b79cc48249.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7d95b3ac84c44b88da9fb5b79cc48249.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7cd380fbc1d465987a55904d9c7bcbd2.png)
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器未锁,锁定并访问
2.线程死锁
- 过多的同步容易造成死锁
- 当两个线程相互等待对方释放“锁”时就会发生死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 多线程编程时应该注意避免死锁的发生
public class ThreadDeadLock {
public static void main(String[] args) {
Object goods = new Object();
Object money = new Object();
// 生产者
Producers producers = new Producers(goods, money);
// 消费者
Consumers consumers = new Consumers(goods, money);
// 生产者线程
Thread producersThread = new Thread(producers, "生产者");
// 消费者线程
Thread consumersThread = new Thread(consumers, "消费者");
producersThread.start();
consumersThread .start();
}
}
//生产者
class Producers implements Runnable {
Object goods;
Object money;
public Producers(Object goods, Object money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
while (true) {
toGoods();
}
}
public void toGoods() {
synchronized (goods) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money) {
}
}
System.out.println(Thread.currentThread().getName() + "一手给货");
}
}
//消费者
class Consumers implements Runnable {
Object goods;
Object money;
public Consumers(Object goods, Object money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
while (true) {
toMoney();
}
}
public void toMoney() {
synchronized (money) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (goods) {
}
}
System.out.println(Thread.currentThread().getName() + "一手给钱");
}
}
//结果导致程序阻塞
生产者与消费者模式解决线程死锁
当多个线程共享一份资源
的时候,会发生死锁
的现象,我们一般是通过生产者与消费者模式
进行解决。也有称为信号灯法
。
- wait() :等待,释放锁
- sleep 休眠,不释放锁
- notify()/notifyAll(): 唤醒
- synchronized
public class ThreadDeadProducersaAndConsumersMode {
public static void main(String[] args) {
Moive moive = new Moive();
Player player = new Player(moive);
Watcher watcher = new Watcher(moive);
Thread playerThread = new Thread(player, "生产者");
Thread watcherThread = new Thread(watcher, "消费者");
playerThread.start();
watcherThread.start();
}
}
/**
* 共享电影资源类
*/
class Moive {
private String pic;
// 信号灯
// flag->T 生产者生产,消费者等待, 生产完成后通知消费
// flag->F 消费者消费 生产者等待, 消费者完成后通知生产
private boolean flag = true;
/**
* 播放
*/
public synchronized void play(String pic) {
if (!flag) {// 生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始生产
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产了:" + pic);
// 生产完毕
this.pic = pic;
// 通知消费
this.notify();
// 生产者停下
this.flag = false;
}
public synchronized void watch() {
if (flag) {// 消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始消费
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费了:" + pic);
// 消费完毕
// 通知生产
this.notifyAll();
// 消费者停止
this.flag = true;
}
}
/**
* 生产者
*/
class Player implements Runnable {
private Moive moive;
public Player(Moive moive) {
this.moive = moive;
}
@Override
public void run() {
for (int i = 0; i <= 20; i++) {
if (0 == i % 2) {
moive.play("左青龙");
} else {
moive.play("右白虎");
}
}
}
}
/**
* 消费者
*/
class Watcher implements Runnable {
private Moive moive;
public Watcher(Moive moive) {
this.moive = moive;
}
@Override
public void run() {
for (int i = 0; i <= 20; i++) {
moive.watch();
}
}
}
一下省略, 一共会执行20轮,
五.线程过期方法suspend/resume/stop
1.如何使用
这三个方法已经是jdk是过期的方法,为什么仍然要单独拿出来说呢?
- 主要目的是理解jdk多线程API设计的初衷,理解并且更好使用线程API。那么就来说说这三个方法吧:
stop方法用于终止一个线程的执行,resume方法用于恢复线程的执行,suspend方法用于暂停线程的执行。
- 要注意的resume方法需要和suspend方法配对使用,因为被暂停的线程需要执行恢复方法才能继续执行。
方法 | 说明 |
---|---|
public void suspend() | 该方法使线程处于挂起状态,可以使用resume()方法恢复 |
public void stop() | 该方法使线程完全停止 |
public void resume() | 该方法恢复使用suspend()方法挂起的线程 |
public void wait() | 导致当前线程等到另一个线程调用notify() |
public void notify() | 唤醒在此对象监视器上等待的单个线程 |
请注意,最新版本的Java已经不再使用suspend(),resume()和stop() 方法,因此您需要使用可用的替代方法。 |
class RunnableDemo implements Runnable {
public Thread thread;
private String threadName;
boolean suspended = false;
RunnableDemo(String name) {
threadName = name;
System.out.println("创建 " + threadName);
}
public void run() {
System.out.println("运行 " + threadName);
try {
for (int i = 10; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while.
Thread.sleep(300);
synchronized (this) {
while (suspended) {
wait();
}
}
}
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted.");
}
System.out.println(threadName + " exiting.");
}
public void start() {
System.out.println(threadName + "处于就绪状态");
if (thread == null) {
thread = new Thread(this, threadName);
thread.start();
}
}
void suspend() {
suspended = true;
}
synchronized void resume() {
suspended = false;
notify();
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo r1 = new RunnableDemo("线程1");
r1.start();
RunnableDemo r2 = new RunnableDemo("线程2");
r2.start();
try {
Thread.sleep(1000);
r1.suspend();
System.out.println("线程1:suspend ");
Thread.sleep(1000);
r1.resume();
System.out.println("线程1:resume");
r2.suspend();
System.out.println("线程2:suspend");
Thread.sleep(1000);
r2.resume();
System.out.println("线程2:resume");
} catch (InterruptedException e) {
System.out.println("主线程 Interrupted");
}
try {
System.out.println("Waiting for threads to finish.");
r1.thread.join();
r2.thread.join();
} catch (InterruptedException e) {
System.out.println("主线程 Interrupted");
}
System.out.println("主线程 exiting.");
}
}
2.使用stop()的风险
- 当调用stop()方法时会发生两件事:
- 即刻停止run()方法中剩余的全部工作,包括在
catch或finally语句
中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭
。 - 会
立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题
。
- 即刻停止run()方法中剩余的全部工作,包括在
public class Main {
public static void main(String[] args) throws Exception {
TestObject testObject = new TestObject();
Thread thread = new Thread() {
public void run() {
try {
testObject.print("1", "2");
} catch (Exception e) {
e.printStackTrace();
}
}
};
thread.start();
Thread.sleep(1000);
thread.stop();
System.out.println("first : " + testObject.getFirst() + " " + "second : " + testObject.getSecond());
}
}
class TestObject {
private String first = "ja";
private String second = "va";
public synchronized void print(String first, String second) throws Exception {
this.first = first;
Thread.sleep(10000);
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
}
//first : 1 second : va
- stop()的不安全主要是针对于第二点:
释放该线程所持有的所有的锁
。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误
3.使用suspend()和resume()的风险
suspend()和resume()必须要成对出现,否则非常容易发生死锁,这两个操作就好比播放器的暂停和恢复。
- 不推荐使用
suspend()
去挂起线程的原因,是因为suspend()
在导致线程暂停的同时,并不会去释放任何锁资源
。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。- 如果一个线程在resume目标线程之前尝试持有这个重要的系统资源锁再去resume目标线程,这两条线程就相互死锁了
public class Test {
public static void main(String[] args) throws Exception {
TestObject testObject = new TestObject();
Thread t1 = new Thread(() -> testObject.print());
t1.setName("A");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
System.out.println("B已启动,但进入不到print方法中");
testObject.print();
});
t2.setName("B");
t2.start();
}
}
class TestObject {
public synchronized void print() {
if (Thread.currentThread().getName().equals("A")) {
System.out.println("A 线程 独占该资源了");
Thread.currentThread().suspend();
}
}
}
//A 线程 独占该资源了
//已启动,但进入不到print方法中
- 但是,如果resume()操作出现在suspend()之前执行,那么线程将一直处于挂起状态,同时一直占用锁,这就产生了死锁。此时,通过 jps 和 jstack 命令,来观察线程状态,对于被挂起的线程,它的线程状态居然还是Runnable。
六.线程通信
如果你知道进程间通信,那么就很容易理解线程间通信。 当您开发两个或多个线程交换一些信息的应用程序时,线程间通信很重要。
有三个简单的方法和一个小技巧,使线程通信成为可能。 所有三种方法都列在下面
方法 | 描述 |
---|---|
public void wait() | 使当前线程等到另一个线程调用notify()方法。 |
public void notify() | 唤醒在此对象监视器上等待的单个线程。 |
public void notifyAll() | 唤醒所有在同一个对象上调用wait()的线程。 |
这些方法已被实现为Object 中的最终(final) 方法,因此它们在所有类中都可用。 所有这三种方法只能从同步 上下文中调用。 |
这个例子显示了两个线程如何使用wait()和notify()
方法进行通信。 您可以使用相同的概念来创建一个复杂的系统。
class Chat {
boolean flag = false;
public synchronized void Question(String msg) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = true;
notify();
}
public synchronized void Answer(String msg) {
if (!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(msg);
flag = false;
notify();
}
}
class Thread1 implements Runnable {
Chat chat;
String[] str = {"Hi", "How are you ?", "I am also doing fine!"};
public Thread1(Chat chat) {
this.chat = chat;
new Thread(this, "Question").start();
}
public void run() {
for (int i = 0; i < str.length; i++) {
chat.Question(str[i]);
}
}
}
class Thread2 implements Runnable {
Chat chat;
String[] str = {"Hi", "I am good, what about you?", "Great!"};
public Thread2(Chat chat) {
this.chat = chat;
new Thread(this, "Answer").start();
}
public void run() {
for (int i = 0; i < str.length; i++) {
chat.Answer(str[i]);
}
}
}
public class ThreadCommunication{
public static void main(String[] args) {
Chat m = new Chat();
new Thread1(m);
new Thread2(m);
}
}