一、多线程
- 定义
- 进程
- 进程是程序执行的一条路径
- 一个系统级的应用执行的就是一个进程
- 线程
- 线程在进程的内部,一个进程中可以有多个线程
- 多个线程并发执行可以提高程序的效率,可以同时完成多项工作
- 多线程节约的是执行程序的等待时间,如果程序排列紧密,没有等待时间,多线程就不能真正的提高效率了
- 进程
- 多线程的应该
- 迅雷下载多个任务
- 服务器同时处理多个请求
- 多线程并发和并行的区别
- 并发是指两个任务都请求运行,而处理器只能接受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行
- 并行就是两个任务同时运行,就是甲任务运行的同时,乙任务也在进行(需要多核CPU)
- 如果一台电脑我先给甲发个信息,然后立刻给乙发消息,然后再跟甲聊,再跟乙聊,这就叫并发
- 比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一个电脑跟乙聊天,这就叫并行
- Java虚拟机运行流程
- java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类中的main方法
- JVM启动后至少会创建垃圾回收线程和主线程
- 思考
- 四核CPU和双核四线程的CPU哪个效率更高?(四核CPU)
二、Java中多线程的实现方式一
-
步骤
- 继承Thread方式
- 定义类继承Thread
- 重写run方法
- 把新线程要做的事写在run方法中
- 创建线程对象
- 调用start()方法开启新线程,内部会自动执行run()方法
-
演示
public class MyThread extends Thread{ @Override- public void run() { while(true){ System.out.println("我是子线程"); } } } public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); }
-
原理解析
- 创建多线程肯定要跟系统平台的底层打交道,我们程序猿根本就不知道如何去做,所以,我们仅仅是提供运行代码,至于如何创建多线程全靠java实现
- 继承Thread的形式,每个Thread的子类对象只能创建一个线程
三、Java中多线程的实现方式二
-
步骤
- 定义类实现Runnable接口
- 实现run方法
- 把新线程要做的事写在run方法中
- 创建自定义的Runnable的子类对象
- 创建Thread对象,传入Runnable
- 调用start()开启新线程,内部会自动调取Runnable中的run()方法
-
演示
public class MyRunnable implements Runnable{ @Override public void run() { while(true){ System.out.println("我是子线程"); } } } public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); Thread thread2 = new Thread(myRunnable); thread.start(); thread2.start(); }
-
原理
- Thread类中定义了一个Runnable类型的成员变量target用来接收Runnable的子类对象
- 当调用Thread对象的start()方法的时候,方法内首先会判断target是否为null,如果不为null,就调用Runnable子类对象的run方法
- 多个Thread对象可以共享一个Runnable子类对象
四、多线程两种方式的区别
- 查看源码的区别
- 继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run方法
- 实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空,不为空,编译时看的是Runnable的run(),运行时执行的是子类的run方法
- 继承Thread
- 好处是:可以直接使用Thread类中的方法,代码简单
- 弊端是:如果已经有了父类,就不能使用这种方法了
- 实现Runnable接口
- 好处是:即使自己定义的线程类有了父类也没有关系,因为有了父类也可以实现接口,而且接口是可以多实现的,多个线程可以非常方便的使用同一个对象的成员变量
- 弊端是:不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法,代码复杂
- 思考
- 当我们调用start()方法之后,是否就意味着线程立即运行呢?(否)
- CPU的执行权(时间片):如果当前线程被CPU执行的时候,我们称当前线程获取了CPU的执行权
- 调用start方法之后,意味着当前线程准备好被执行了
五、匿名内部类实现多线程的两种方式
-
继承Thread类
public static void main(String[] args) { new Thread(){ public void run(){ while(true){ System.out.println("我是子线程"); } } }.start(); }
-
实现Runnable接口
public static void main(String[] args) { //创建Thread对象,提供Runnable匿名内部类对象 new Thread(new Runnable(){ public void run(){ while(true){ System.out.println("我是子线程"); } } }).start(); }
六、多线程中获取名字和设置名字
-
获取名字
- 通过getName()方法获取线程对象的名字
- 此方法只适用于Thread的形式
- Runnable的形式必须通过获取线程对象来获取名字
-
演示
public class MyThread extends Thread{ @Override public void run() { System.out.println(this.getName()); } }
-
设置名字
-
通过构造方法传入String类型的名字
public static void main(String[] args) { new Thread("线程一"){ public void run(){ System.out.println(this.getName()); } }.start(); }
-
通过setName方法可以设置线程对象的名字
public static void main(String[] args) { Thread thread1 = new Thread(){ public void run(){ System.out.println(this.getName()); } }; thread1.setName("线程一"); thread1.start(); }
-
七、获取当前线程的对象名
-
定义
- 我们可以在多线程运行代码中获取代表当前执行线程的对象,并对其进行操作
-
演示
-
通过获取对象的形式在Runnable运行代码中查看当前线程的名称
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { //获取线程的名称 System.out.println(Thread.currentThread().getName()); } }).start(); }
-
通过获取线程对象的形式设置线程的名称
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { Thread.currentThread().setName("线程二");//设置线程的名称 System.out.println(Thread.currentThread().getName()); } }).start(); }
-
通过Thread的构造方法设置线程的名称
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } //通过构造方法设置线程的名称 },"线程二").start(); }
-
八、休眠线程(sleep)
-
定义
- 在线程的执行代码中调用Thread.sleep()方法,就可以将线程休眠若干毫秒
- 休眠结束的线程进入可运行状态(就绪状态)而不是直接运行
-
演示
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { long time1 = System.currentTimeMillis(); System.out.println(time1); try { Thread.sleep(10000);//设置线程等待10000毫秒 } catch (InterruptedException e) { e.printStackTrace(); } long time2 = System.currentTimeMillis(); System.out.println(time2); } //通过构造方法设置线程的名称 }).start(); }
-
测试题(龟兔赛跑)
- 需求: 乌龟和兔子赛跑总赛程100m, 兔子的速度是10m/s, 乌龟的速度是5m/s.乌龟和兔子都是每跑完10米输出一次结果, 当兔子跑到70米的时候休息2s ,编程模拟比赛过程
九、守护线程(setDaemon)
-
定义
- 围绕着其他非守护线程运行,该线程不会单独运行,当其他非守护线程都执行结束后,自动退出
- 调用Thread的setDaemon()方法设置一个线程为守护线程
-
应用场景
- 使用飞秋聊天时,我们打开了多个聊天窗口,当我们关闭主程序时,其他所有的聊天窗口都会随之关闭
-
演示
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程一运行中"); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("守护线程运行.........."); } } }); t2.setDaemon(true); t1.start(); t2.start(); }
十、加入线程(join)
-
定义
- 当前线程暂停,等待指定的线程执行结束后,当前线程再继续
-
常用方法
- join()优先执行指定线程
- join(毫秒) 优先执行指定线程若干毫秒
-
演示
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程一运行中"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程二运行中.........."); try { t1.join(); //让线程一先执行 t1.join(100);//让线程一先执行100毫秒 } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //只有调用了加入线程的线程才会停止运行,其他线程不受影响 Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程三运行中.........."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t1.start(); t2.start(); t3.start(); }
-
注意事项
- 只有调用了加入线程(也就是join方法)的线程才会停止运行,其他线程不受影响
十一、礼让线程(了解)(yield)
-
定义
- 仅仅让出当前线程的一次执行权
- 基本看不到效果
-
演示
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程一运行中"); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程二运行中.........."); //让出当前线程的执行权 Thread.yield(); } } }); t1.start(); t2.start(); }
十二、设置线程的优先级(setPriority)
-
定义
- 每个线程都有优先级,默认都是5,范围是1-10,1表示优先级最低
- 优先级高的线程在争夺CPU的执行权上有一定的优势,但是表示绝对的
-
演示
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程一运行中"); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("线程二运行中.........."); } } }); t1.setPriority(1); t2.setPriority(10); //线程二的优先级高 t1.start(); t2.start(); }
十三、多线程安全问题
-
定义
- 多个线程操作同一个数据时,因为线程执行的随机性,就有可能出现线程安全问题
- 使用同步可以解决这个问题,把操作数据的代码进行同步,同一时间只能有一个线程操作数据,这样就可以保证数据的安全性
-
线程安全问题演示
static int num =10; public static void main(String[] args) { Thread t1 = new Thread(){ public void run(){ while(true){ if (num>0) { System.out.println(getName()+":"+num--); }else{ break; } } } }; Thread t2 = new Thread(){ public void run(){ while(true){ if (num>0) { System.out.println(getName()+":"+num--); }else{ break; } } } }; t1.start(); t2.start(); }
-
解决安全问题
-
使用synchronized关键字锁定部分代码
static int num = 10; public static void main(String[] args) { Thread t1 = new Thread(){ public void run(){ while(true){ //加上锁 synchronized (Class.class) { if (num>0) { System.out.println(getName()+":"+num--); }else{ break; } } } } }; Thread t2 = new Thread(){ public void run(){ while(true){ //加上统一把锁 synchronized (Class.class) { if (num>0) { System.out.println(getName()+":"+num--); }else{ break; } } } } }; t1.start(); t2.start(); }
-
注意事项
- 使用同一把锁的代码才能实现同步
- 没有获取到锁的线程即使得到了CPU的执行权,也不能执行
- 尽量减少锁的范围,避免效率低下
- 锁可以加在任意类的代码中或方法上
-
测试题
- 火车站总共有100张票, 四个窗口同时卖票, 当票卖光了之后,提示"没票了...",编程模拟场景
-
十四、死锁
-
定义
- 使用同步的多个线程同时持有对方运行时所需要的资源
- 多线程同步时,多个同步代码块嵌套,很容易就会出现死锁
- 锁嵌套的越多,越容易造成死锁的情况
-
演示
- 线程一需要先获取左边的筷子然后获取右边的筷子才能吃饭
- 线程二需要先获取右边的筷子然后获取左边的筷子才能吃饭
- 当线程一持有左边的筷子,线程二持有右边的筷子时,相互等待对方释放锁,但又同时持有对方释放锁的条件,互不相让,就会造成无限等待
private static String s1 = "筷子左"; private static String s2 = "筷子右"; public static void main(String[] args) { new Thread() { public void run() { while(true) { synchronized(s1) { System.out.println(getName() + "...拿到" + s1 + "等待" + s2); synchronized(s2) { System.out.println(getName() + "...拿到" + s2 + "开吃"); } } } } }.start(); new Thread() { public void run() { while(true) { synchronized(s2) { System.out.println(getName() + "...拿到" + s2 + "等待" + s1); synchronized(s1) { System.out.println(getName() + "...拿到" + s1 + "开吃"); } } } } }.start(); }
总结
- 多线程
- 节省等待时间
- 平均运行时间
- 创建多线程的两种方式
- 继承Thread
- 实现Runnable
- 常用方法
- Thread.currentThread()
- Thread.sleep(毫秒)
- thread.setDeamon()
- thread.join()
- Thread.yield()
- thread.setPriority(1-10)
- 多线程的安全问题
- 多个线程同时争抢资源,一定会出现问题
- 同步锁 synchronized
- 死锁
- 使用同步的多个线程同时持有对方运行时所需要的资源