两万字带你了解 Java 多线程(详细大总结)
目录
多线程概念
什么是进程?进程是一个应用程序(1 个进程是一个软件)。
什么是线程?线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。
对于 java 程序来说,当在 DOS 命令窗口中输入:java HelloWorld 回车之后。会先启动 JVM,而 JVM 就是一个进程。 JVM 再启动一个主线程调用 main 方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的 java 程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行 main 方法的主线程。
进程可以看做是现实生活当中的公司,线程可以看做是公司当中的某个员工。注意:进程 A 和进程 B 的内存独立不共享。
线程 A 和线程 B 呢?在 java 语言中:线程 A 和线程 B,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
假设启动 10 个线程,会有 10 个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
什么是真正的多线程并发?t1 线程执行 t1 的。 t2 线程执行 t 的。t1 不会影响 t2,t2 也不会影响 t1。这叫做真正的多线程并发。
举例火车站,可以看做是一个进程。火车站中的每一个售票窗口可以看做是一个线程。我在窗口 1 购票,你可以在窗口 2 购票,你不需要等我,我也不需要等你。所以多线程并发可以提高效率。java 中之所以有多线程机制,目的就是为了提高程序的处理效率。
实现线程的第一种方式
实现线程的第一种方式:编写一个类,直接继承 java.lang.Thread,重写 run 方法。怎么创建线程对象?new 就行了。怎么启动线程呢?调用线程对象的 start()方法。注意:亘古不变的道理:方法体当中的代码永远都是自上而下的顺序依次逐行执行的。
代码示例
复制代码
输出结果
实现线程的第二种方式
第二种方式以接口的方式:编写一个类,实现 Runnable 接口,里面实现 run 方法。new 一个 Thread 对象,里面传递的参数是 Runnable 对象,编写的那个类实现了 Runnable,所以直接 new 这个类的对象传进去。
代码示例
复制代码
输出结果
采用匿名内部类方式
复制代码
实现线程的第三种方式
实现线程的第三种方式:实现 Callable 接口。(JDK8 新特性。)这种方式实现的线程可以获取线程的返回值。之前讲解的那两种方式是无法获取线程返回值的,因为 run 方法返回 void 思考:系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?使用第三种方式:实现 callable 接口方式。
实现线程的第三种方式:实现 Callable 接口这种方式的优点:可以获取到线程的执行结果。这种方式的缺点:效率比较低,在获取 t 线程执行结果的时候,当前线程受阻塞,效率较低
复制代码
线程的生命周期
关于线程对象的生命周期
新建状态
就绪状态
运行状态
阻塞状态
这个要记住,面试可能会问。
线程对象的方法
代码演示
复制代码
执行结果
Sleep 方法
举例
复制代码
Sleep 面试题
复制代码
终止线程的睡眠
sleep 睡眠太久了,如果希望半道上醒来,你应该怎么办呢?也就是说怎么叫醒一个正在睡眠的线程??注意:这个不是终断线程的执行,是终断线程的睡眠。
复制代码
终止一个线程的执行
复制代码
线程调度(了解)
常见的线程调度模型有哪些?抢占式调度模型:那个线程的优先级比较高,抢到的 CPU 时间片的概率就高一些/多一些。 java 采用的就是抢占式调度模型。
均分式调度模型:平均分配 CPU 时间片。每个线程占有的 CPU 时间片时间长度一样。平均分配,一切平等。有一些编程语言,线程调度模型采用的是这种方式。
java 中提供了哪些方法是和线程调度有关系的呢
线程优先级
复制代码
线程让位
复制代码
线程合并
复制代码
线程安全(重点)
关于多线程并发环境下,数据的安全问题。为什么这个是重点?以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点)
什么时候数据在多线程并发的环境下会存在安全问题呢?三个条件:条件 1:多线程并发。条件 2:有共享数据。条件 3:共享数据有修改的行为。满足以上 3 个条件之后,就会存在线程安全问题。
怎么解决线程安全问题呢?当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?线程排队执行。(不能并发)。用排队执行解决线程安全问题。这种机制被称为:线程同步机制。专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
怎么解决线程安全问题?使用“线程同步机制"。线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
说到线程同步这块,涉及到这两个专业术语:异步编程模型:线程 t1 和线程 t2,各自执行各自的,t1 不管 t2,t2 不管 t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)异步就是并发。同步编程模型:线程 t1 和线程 t2,在线程 t 执行的时候,必须等待 t2 线程执行结束,或者说在 t2 线程执行的时候,必须等待 t1 线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。同步就是排队。
Java 中有三大变量?【重要的内容。】实例变量:在堆中。静态变量:在方法区。局部变量:在栈中。以上三大变量中:局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享。实例变量在堆中,堆只有 1 个,静态变量在方法区中,方法区只有 1 个。堆和方法区都是多线程共享的,所以可能存在线程安全问题。局部变量+常量:不会有线程安全问题。成员变量:可能会有线程安全问题。
如果使用局部变量的话:建议使用:StringBuilder.因为局部变量不存在线程安全问题。选择 stringBuildere stringBuffer 效率比较低。ArrayList 是非线程安全的。 Vector 是线程安全的。HashMap Hashset 是非线程安全的。 Hashtable 是线程安全的。
复制代码
聊一聊,我们以后开发中应该怎么解决线程安全问题?是一上来就选择线程同步吗?synchronized 不是,synchronized 会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。第一种方案:尽量使用局部变量代替"实例变量和静态变量"。第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应 1 个对象,100 个线程对应 100 个对象,对象不共享,就没有数据安全问题了。)第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择 synchronized 了。线程同步机制。
同步代码块 synchronized
复制代码
在 java 语言中,任何一个对象都有"一把锁”,其实这把便就是标记。(只是把它叫做锁。100 个对象,100 把锁。1 个对象 1 把锁。以下代码的执行原理?1、假设 t1 和 t2 线程并发,开始执行代码的时候,肯定有一个先一个后。2、线设 t1 先执行了,遇到了 synchronized,这个时候自动找“后面共享对亲”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。3、假设 t1 已经占有这把锁,此时 t2 也遇到 synchronized 关键字,也会去占有后同共享对象的这把锁,结果这把锁被 t1 占有,t2 只能在同步代码块外面等待的结束,直到 t1 把同步代码快执行结束了,t1 会归还这把锁,此时 t2 终于等到这把锁,然后 t2 占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。这里需要注意的是,这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的
实例方法上和静态方法上使用 synchronized
在实例方法上可以使用 synchronized 吗?可以的。synchronized 出现在实例方法上,一定锁的是 this 没得挑。只能是 this。不能是其他的对象了。所以这种方式不灵活。另外还有一个缺点:synchronized 出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
synchronized 使用在实例方法上有什么优点?代码写的少了。节俭了。如果共享的对象就是 this,并且需要同步的代码块是整个方法体建议使用这种方式。
synchronized 出现在静态方法上是找类锁,因为静态方法是类锁,不管创建几个对象类锁只有一把。
死锁代码要会写,一般面试官要求你会写,只有会写的,才会在以后的开发中注意这个事,因为死锁很难调试
实例
复制代码
守护线程
守护线程 java 语言中线程分为两大类:一类是:用户线程一类是:守护线程(后台线程)其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。注意:主线程 main 方法是一个用户线程。
守护线程用在什么地方呢?每天 00:00 的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,每到 00:00 的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
实例
复制代码
定时器的作用:间隔特定的时间,执行特定的程序。每周要进行银行账户的总账操作,每天要进行数据的备份操作…。
在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在 java 中其实可以采用多种方式实现:可以使用 sleep 方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较 low)在 java 的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。在实际的开发中,目前使用较多的是 Spring 框架中提供的 SpringTask 框架,这个框架只要进行简单的配置,就可以完成定时器的任务。
实例
复制代码
评论