01 多线程的基本概念
线程指进程中的一个执行场景,即执行流程。进程与线程的区别:
- 每个进程是一个应用程序,都有独立的内存空间
- 同一个进程中的线程共享其进程中的内存和资源(共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的栈内存)
1.1 什么是进程
一个进程就是一个应用程序。在操作系统中每启动一个应用程序就会相应的启动一个进程。如:QQ、网易云等。
1.2 系统引入多进程的作用
提高CPU的使用率。
重点:进程与进程之前的内存相互独立。
1.3 什么是线程
线程是进程的一个执行场景,一个进程可以启动多个线程。
1.4 进程引入多线程的作用
提高进程的使用率。
重点:线程和线程之间栈内存独立,堆内存和方法区内存共享,一个线程一个栈。
1.5 描述Java程序的执行原理
Java命令执行会启动JVM,JVM的启动表示启动一个应用程序即启动一个进程。该进程会自动启动一个“主线程”,然后主线程负责调用某个类的main
方法。main
方法的执行是在主线程中执行的,然后通过main
方法代码的执行可以启动其他的“分支线程”。所以main
方法结束程序不一定结束,因为其他的支线分支有可能还在执行。
02 线程的创建和启动
Java虚拟机的主线程入口是main
方法,用户可以自己创建线程,创建方式有两种:
- 继承
Thread
类 - 实现
Runnable
接口(推荐使用)
2.1 继承Thread类
Thread
类中创建线程最重要的两个方法为public void run()
和public void start()
。采用Thread
类创建线程,用户只需要继承Thread
,重写Thread
的run()
方法,父类Thread
中的run()
方法没有抛出异常,那么子类也不能抛出异常,最后采用start()
启动线程即可。
public class ThreadTest {
public static void main(String[] args) {
Processor p=new Processor();
p.start();
method1();
}
private static void method1(){
System.out.println("------method1()-------");
}
}
class Processor extends Thread{
// 重写Thread中的run方法
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
输出结果为:
------method1()-------
0
1
2
3
4
以上输出并没有顺序执行,从效率上看,采用多线程的示例要快一些。method1
并没有等待前面的操作完成才执行,称为“异步编程模型”
2.2 实现Runnable接口
Thread
对象本身实现了Runnable
接口,但一般建议直接使用Runnable
接口来写多线程,因为接口比类更好用。
public class ThreadTest {
public static void main(String[] args) {
Runnable r1=new Processor();
// 不能直接调用run
Thread t1=new Thread(r1);
// 启动线程
t1.start();
method1();
}
private static void method1(){
System.out.println("----method1()------");
}
}
class Processor implements Runnable{
// 实现Runnable中的run方法
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
}
}
03 线程的生命周期
线程的生命周期存在五个状态:新建、就绪、运行、阻塞、死亡
- 新建:采用new语句创建完成
- 就绪:执行start后
- 运行:占用CPU时间
- 阻塞:执行了wait语句、执行了sleep语句和等待某个对象锁,等待输入的场合
- 终止:退出
run()
方法
04 线程的调度与控制
通常我们的计算机只有一个 CPU,CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。在单 CPU 的机器上线程不是并行运行的,只有在多个 CPU 上线程才可以并行运行。Java 虚拟机要负责线程的调度,取得 CPU的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型,Java 使用抢占式调度模型。
- 分时调度模型:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片 - 抢占式调度模型:
优先级高的线程获取 CPU 的时间片相对多一些,如果线程的优先级相同,那么会随机选择一个
4.1 线程优先级
线程优先级主要分三种:MAX_PRIORITY( 最高级 );MIN_PRIORITY(最低级); NOM_PRIORITY(标准)默认
public class ThreadTest {
public static void main(String[] args) {
Runnable r1=new Processor();
Thread t1=new Thread(r1,"t1");
// 设置线程的优先级,线程启动后不能再设置优先级
t1.setPriority(Thread.MAX_PRIORITY);
// 启动线程
t1.start();
Thread t2=new Thread(r1,"t2");
// 设置最低优先级
t2.setPriority(Thread.MIN_PRIORITY);
t2.start();
System.out.println(Thread.currentThread().getName());
}
}
class Processor implements Runnable{
// 实现Runnable中的run方法
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
}
}
}
优先级高的线程(t1)会抢占到更多的CPU时间片,优先执行完成
4.2 Thread.sleep
sleep 设置休眠的时间,单位毫秒,当一个线程遇到 sleep 的时候,就会睡眠,进入到阻塞状态,放弃 CPU,腾出 cpu 时间片,给其他线程用,所以在开发中通常我们会这样做,使其他的线程能够取得 CPU 时间片,当睡眠时间到达了,线程会进入可运行状态,得到 CPU 时间片继续执行,如果线程在睡眠状态被中断了,将会抛出 IterruptedException
。
class Processor implements Runnable{
// 实现Runnable中的run方法
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
if (i%10==0){
try{
// 睡眠100毫秒,将时间片交给其他线程使用
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
4.3 Thread.yield
它与 sleep()类似,只是不能由用户指定暂停多长时间,并且 yield()方法只能让同优先级的线程有执行的机会
class Processor implements Runnable{
// 实现Runnable中的run方法
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
if (i%10==0) {
System.out.println("------------");
// 采用yield可以将CPU的使用权让给同一个优先级的线程
Thread.yield();
}
}
}
}
4.4 t.join()
当前线程可以调用另一个线程的 join 方法,调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行。
4.5 interrupt(中断)
如果我们的线程正在睡眠,可以采用 interrupt 进行中断
4.6 如何正确停止一个线程
通常定义一个标记来判断标记的状态停止线程的执行
public class ThreadTest {
public static void main(String[] args) {
Processor r1=new Processor();
Thread t1=new Thread(r1,"t1");
t1.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止线程
r1.setFlag(true);
}
}
class Processor implements Runnable{
// 线程停止标记,true为停止
private boolean flag;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
// 为true停止线程执行
if(flag){
break;
}
}
}
public void setFlag(boolean flag){
this.flag=flag;
}
}
05 线程的同步(加锁)
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
5.1 为什么需要同步
- 共享一个对象启动两个线程,s为每个线程的局部变量
public class ThreadTest {
public static void main(String[] args) {
Processor r1=new Processor();
Thread t1=new Thread(r1,"t1");
t1.start();
Thread t2=new Thread(r1,"t2");
t2.start();
}
}
class Processor implements Runnable{
@Override
public void run() {
// 定义局部变量s,作为累加变量
int s=0;
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(Thread.currentThread().getName()+","+s);
}
}
输出:
t1,45
t2,45
以上t1和t2并发执行,s为每个线程的局部变量,位于各自的栈帧中,因为栈帧中的数据是互不干扰的,所以计算结果都为45
- 共享一个对象启动两个线程,s为每个线程的成员变量
class Processor implements Runnable{
// 定义成员变量s,作为累加变量
int s=0;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(Thread.currentThread().getName()+","+s);
}
}
输出:
t1,45
t2,90
因为共享同一对象的成员变量,两个线程同时对其进行操作,所以出现了问题,此时称Processor为线程不安全的。想得到正确结果,必须采用线程同步,加锁,该变量不能共享使用。
线程安全问题:
- 多线程并发
- 有共享数据
- 共享数据有修改行为
5.2 使用线程同步
线程同步指某一时刻允许一个线程来访问共享资源,其实是对对象加锁,如果对象中的方法都是同步方法,那么某一时刻只能执行一个方法。
采用线程同步解决上面的问题:
class Processor implements Runnable{
// 定义局部变量s,作为累加变量
int s=0;
public void run(){
// 使用同步块
synchronized (this){
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(Thread.currentThread().getName()+","+s);
s=0;
}
}
}
synchronized
是对对象加锁,优先考虑synchronized
同步块,因为同步的代码越多,其他线程等待的时间就会越长影响效率。
synchronized
的三种写法
- 同步代码块
synchronized(线程共享对象){
同步代码块;
} - 在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。 - 在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
5.3 为每一个线程创建一个对象来解决线程安全问题
public class ThreadTest {
public static void main(String[] args) {
Processor r1=new Processor();
Thread t1=new Thread(r1,"t1");
t1.start();
// 再次创建Processor对象
Processor r2=new Processor();
Thread t2=new Thread(r2,"t2");
t2.start();
}
}
class Processor implements Runnable{
// 定义局部变量s,作为累加变量
int s=0;
public void run(){
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(Thread.currentThread().getName()+","+s);
}
}
每个线程操作都是自己的对象,没有操作共享的资源
如何解决线程安全问题?
- 尽量使用局部变量代替“实例变量和静态变量”
- 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存不共享
- 不能使用局部变量及对象不能创建多个时,考虑
synchronized
06 守护线程
从线程分类上可以分为:用户线程(以上讲的都是用户线程),另一个是守护线程。所有的用户线程结束生命周期后,守护线程才会结束生命周期,只要有一个用户线程存在,那么守护线程就不会结束。例如 java 中著名的垃圾回收器就是一个守护线程,只有应用程序中所有的线程结束,它才会结束。
6.1 用户线程
public class ThreadTest {
public static void main(String[] args) {
Processor r1=new Processor();
Thread t1=new Thread(r1,"t1");
t1.start();
for (int i = 0; i <5; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
}
System.out.println("主线程结束!!!");
}
}
class Processor implements Runnable{
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
}
}
}
main,0
main,1
main,2
t1,0
main,3
main,4
主线程结束!!!
t1,1
t1,2
t1,3
t1,4
主线程执行结束后,用户线程仍然在执行
6.2 守护线程(服务线程)
public class ThreadTest {
public static void main(String[] args) {
Processor r1=new Processor();
Thread t1=new Thread(r1,"t1");
// 将当前线程修改为守护线程
t1.setDaemon(true);
t1.start();
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
}
System.out.println("主线程结束!!!");
}
}
class Processor implements Runnable{
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+","+i);
}
}
}
输出:
main,0
main,1
t1,0
main,2
t1,1
main,3
t1,2
main,4
t1,3
主线程结束!!!
t1,4
当主线程结束后,守护线程并没有把所有数据输出完就结束了。
**结论:**当用户线程全部结束,守护线程会自动结束
07 Timer定时器
08 Window定时器(任务)
09 重点掌握
- 进程与线程的概念
2. 线程的两种实现方式(Thread
,Runnable
) - 了解线程的优先级
sleep
的含义- 如果正确的结束一个线程
- 线程同步的含义(同步共享资源,局部变量不存在共享的问题)
- 守护线程的概念
- 了解
Timer
- 了解
winodw
提供的计划