- 多线程概述
- 进程:正在进行中的程序。
- 线程:进程中的独立控制单元。线程控制着进程的执行。
- 一个进程中至少有一个线程。
- jvm启动时有一个java.exe进程,该进程至少有一个主线程负责程序的执行,这个线程运行的代码存在于main函数中,该线程称之为主线程。
- jvm启动时,除了主线程,还有负责垃圾回收的线程。
- 多线程程序:有不止一个线程(执行路径)的程序就是多线程程序,如迅雷。
- 多线程存在的意义:
- 使程序中不同部分的代码产生“同时运行”的效果。
- 线程的创建方式
- 继承Thread类
- 创建线程的步骤:
- 定义类继承Thread。
- 覆写Thread类中的run方法。
- 目的:将自定义代码存储在run()方法中,让线程运行。
- 调用线程的start方法
- 该方法有两个作用:启动线程,调用run方法。
- 如果直接调用run方法,相当于仅仅是对象调用普通方法。线程虽然创建了,但是并没有运行。
- Thread类
- Thread类用于描述线程。
- Thread类中的run()方法用于存储线程要运行的代码。
- start()方法用于开启线程,并执行该线程的run()方法。
- 练习
- 需求:创建两个线程,和主线程交替运行。
- 代码:
打印结果:package cn.itcast.heima; public class Test { public static void main(String[] args) { SubThread st1 = new SubThread(); SubThread st2 = new SubThread(); st1.start(); st2.start(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "run..." + i); } } } class SubThread extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "run..." + i); } } }
- 创建线程的步骤:
- 实现Runnable接口
- 创建线程的步骤:
- 定义类实现Runnable接口。
- 覆盖Runnable接口中的run()方法。
- 创建类Thread的线程对象。
- 将Runnable子类对象作为实际参数传递给Thread类的构造函数。
- 通过Thread对象的start()方法开启线程并调用Runnable接口子类的run()方法。
- 定义类实现Runnable接口。
- 优势:
- 避免了单继承的局限性。
- 可以创建多个线程共享Runnable子类对象的数据,避免了共享数据定义为静态。
- 例子:
- 需求:简单的卖票程序。实现多个窗口同时卖票。
- 如果按照继承Thread类的方式实现多线程,代码如下:
package cn.itcast.heima; public class Test { public static void main(String[] args) { //开放四个窗口售票 Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); Ticket t4 = new Ticket(); t1.start(); t2.start(); t3.start(); t4.start(); } } class Ticket extends Thread{ private static int ticket = 20;//总共20张票 @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--); } } }
打印结果:
这种方式需要将票数定义为静态成员。
缺陷:ticket成员变量生命周期过长,且Ticket类不能继承其它类。 - 可以将其改为实现Runnable接口的方式实现多线程,代码如下:
打印结果:package cn.itcast.heima; public class Test { public static void main(String[] args) { Ticket t = new Ticket(); //开放四个窗口售票 Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } } class Ticket implements Runnable{ private int ticket = 20;//总共20张票 @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + "...sale:" + ticket--); } } }
这种方式避免了将ticket成员变量定义为static,并且避免了单继承的局限性。 - 实际开发中,建议使用实现Runnable接口的方式实现多线程。
- 创建线程的步骤:
- 两种方式区别:
- 继承Thread类:线程代码存放在Thread子类的run()方法中。
- 实现Runnable接口:线程代码存放在Runnable接口子类的run()方法中。
- 继承Thread类:线程代码存放在Thread子类的run()方法中。
- 多线程的特性:随机性
- 对于多线程程序,每一次的运行结果都不相同。
- 因为多个线程都需要获取CPU的执行权,CPU执行到哪个线程,哪个线程就运行。
- 在某一时刻,只能有一个线程在运行,CPU在做着快速的切换,以达到看上去是同时运行的效果。
- 线程的五种状态
- 线程的五种状态:被创建,运行,冻结,临时(阻塞),消亡。关系如下图:
- 线程的五种状态:被创建,运行,冻结,临时(阻塞),消亡。关系如下图:
- 获取线程对象以及名称
- 线程有默认的名称:Thread-编号(从0开始)。
- currentThread():获取当前线程对象(静态)。
- getName():获取线程名称。
- 设置线程名称:setName()或者构造函数。
- 线程有默认的名称:Thread-编号(从0开始)。
- 多线程的安全问题:同步
- 问题原因:
- 多个线程在操作同一个共享数据时,一个线程对多条语句只执行了一部分,另一个线程就参与进来,导致共享数据出现问题。
- 多个线程在操作同一个共享数据时,一个线程对多条语句只执行了一部分,另一个线程就参与进来,导致共享数据出现问题。
- 解决办法:
- 一个线程在执行过程中,其它线程不可以参与执行。
- 一个线程在执行过程中,其它线程不可以参与执行。
- Java对于多线程的安全问题提供了专业的解决方案:同步代码块。
- 同步代码块格式:
- 判断需要被同步的代码:
- 操作共享数据的代码就是需要被同步的代码。
- 操作共享数据的代码就是需要被同步的代码。
- 同步代码块的原理:
- 对象如同锁。持有锁的线程才可以进入同步代码块执行语句。不持有锁的线程即使获得了CPU的执行权,也不能进入同步代码块执行语句。
- 火车上的卫生间的例子。
- 对象如同锁。持有锁的线程才可以进入同步代码块执行语句。不持有锁的线程即使获得了CPU的执行权,也不能进入同步代码块执行语句。
- 同步的前提:
- 同步需要两个或者两个以上的线程。
- 多个线程使用的是同一个锁。
- 同步需要两个或者两个以上的线程。
- 同步的好处:
- 解决了多线程的安全问题。
- 同步的弊端:
- 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形 中会降低程序的运行效率。
- 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形 中会降低程序的运行效率。
- 同步函数
- 格式:
在函数上加上synchronized修饰符即可。
- 同步函数使用的锁是this。要保证同步代码块和同步函数用的是同一个锁,同步代码块的参数必须是this。
- 格式:
- 静态同步函数
- 静态同步函数的锁是该方法所在类的字节码文件对象:类名.class。
- 静态同步函数的锁是该方法所在类的字节码文件对象:类名.class。
- 死锁
- 死锁的出现:同步中嵌套同步。
- 实际开发中,要避免出现死锁问题。
- 问题原因:
- 线程间通信
- 概念
- 线程间通信就是多个线程在操作同一个资源,但是操作的动作不同。
- 等待唤醒机制
- 多线程操作共享数据时要考虑使用等待唤醒机制。
- 等待线程存放在线程池中,notify()通常唤醒线程池中的最先等待的线程,notifyAll()唤醒线程池中的所有线程。
- wait(),notify(),notifyAll()都要用在同步中,因为要对持有监视器(锁)的线程操作。
- wait(),notify(),notifyAll()都是Object类中的方法,因为这些方法在操作线程时,都必须要标识他们所操作线程持有的锁。
一个被等待的线程,只能被持有同一个锁的线程唤醒,也就是说,等待和唤醒必须是同一个锁。
为锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
- wait和sleep比较:
- wait():释放cpu执行权,释放锁。
- sleep():释放cpu执行权,不释放锁。到达休眠时间后线程将继续执行,直到完成。若在休眠期另一线程中断该线程,则该线程退出。
- wait():释放cpu执行权,释放锁。
- 多线程操作共享数据时要考虑使用等待唤醒机制。
- 生产者消费者例子
- 需求:生产者和消费者分别创建两个线程,保证生产者进程和消费者进程能够交替执行。
- 代码:
打印结果:package cn.itcast.heima; public class Test { public static void main(String[] args) { Resource r = new Resource();//商品 Producer pro = new Producer(r);//生产者 Consumer con = new Consumer(r);//消费者 Thread t1 = new Thread(pro);//生产线程 Thread t2 = new Thread(pro);//生产线程 Thread t3 = new Thread(con);//消费线程 Thread t4 = new Thread(con);//消费线程 t1.start(); t2.start(); t3.start(); t4.start(); } } //商品类 class Resource{ private String name;//商品名称 private int count = 1;//商品编号 private boolean flag = false;//判断商品是否还在 //生产商品 public synchronized void set(String name){ //如果商品还在,生产线程等待 while (flag) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + "---" + count++; System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); flag = true;//商品生产出来了 notifyAll();//唤醒所有等待线程 } //消费商品 public synchronized void out(){ //如果商品没了,消费线程等待 while (!flag) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "...消费者......" + this.name); flag = false;//商品被拿走了 notifyAll();//唤醒所有等待线程 } } //生产者类 class Producer implements Runnable{ private Resource res; public Producer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.set("商品"); } } } //消费者类 class Consumer implements Runnable{ private Resource res; public Consumer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.out(); } } }
- 需要定义while判断标记flag:
- 原因:让被唤醒的线程再一次判断标记,否则会导致重复生产或者重复消费。
- 并且为了避免线程全部冻结,需要在解锁之前唤醒所有线程:
- 原因:需要唤醒对方线程。因为只用notify()容易出现只唤醒本方线程的情况,导致程序中所有线程都挂起。
- 原因:需要唤醒对方线程。因为只用notify()容易出现只唤醒本方线程的情况,导致程序中所有线程都挂起。
- JDK5.0升级版
- 定义了Lock接口,代替了同步函数和同步代码块。
- 定义了Condition接口,代替了Object中的wait(),notify(),notifyAll()方法。Condition对象可以通过Lock接口子类的newCondition()方法获取。
- 一个Lock可以对应多个Condition对象,该方式可以对指定的某个Condition对象对应的线程进行等待和唤醒处理,而不需要唤醒所有线程。
- 生产者消费者例子的JDK5.0升级版代码:
打印结果:package cn.itcast.heima; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test { public static void main(String[] args) { Resource r = new Resource();//商品 Producer pro = new Producer(r);//生产商品 Consumer con = new Consumer(r);//消费商品 Thread t1 = new Thread(pro);//生产线程 Thread t2 = new Thread(pro);//生产线程 Thread t3 = new Thread(con);//消费线程 Thread t4 = new Thread(con);//消费线程 t1.start(); t2.start(); t3.start(); t4.start(); } } //商品类 class Resource{ private String name;//商品名称 private int count = 1;//商品编号 private boolean flag = false;//判断商品是否还在 private Lock lock = new ReentrantLock();//定义锁对象 private Condition condition_pro = lock.newCondition();//定义生产线程的Condition对象 private Condition condition_con = lock.newCondition();//定义消费线程的Condition对象 //生产商品 public void set(String name){ try{ lock.lock();//上锁 //如果商品还在,生产线程等待 while (flag) { try { condition_pro.await();//生产线程等待 } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + "---" + count++; System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name); flag = true;//商品生产出来了 condition_con.signal();//唤醒消费线程 }finally{ lock.unlock();//解锁 } } //消费商品 public void out(){ try { lock.lock();//上锁 //如果商品没了,消费线程等待 while (!flag) { try { condition_con.await();//消费线程等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + "...消费者......" + this.name); flag = false;//商品被拿走了 condition_pro.signal();//唤醒生产线程 } finally { lock.unlock();//解锁 } } } //生产者类 class Producer implements Runnable{ private Resource res; public Producer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.set("商品"); } } } //消费者类 class Consumer implements Runnable{ private Resource res; public Consumer(Resource res) { this.res = res; } @Override public void run() { while (true) { res.out(); } } }
- 定义了Lock接口,代替了同步函数和同步代码块。
- 线程生命控制
- 停止线程
- stop()方法已经过时。
- 如何停止线程:
- 只有一种方法,run()方法结束。
- 开启多线程运行,运行代码通常都是循环结构。只要控制住循环,就可以结束run()方法,从而结束线程。
- 只有一种方法,run()方法结束。
- Thread类提供的interrupt()方法,可以中断线程的冻结状态。
当没有指定的方式让冻结的线程恢复到运行状态时,就需要对冻结状态进行清除。强制线程恢复到运行状态,并在异常处理中通过改变标记让线程结束。
- stop()方法已经过时。
- 守护线程
- 守护线程相当于后台线程,当前台线程全部结束后,后台线程自动结束,JVM退出。
- 如果一个线程的运行依赖于另一个线程的运行,当第一个线程结束时,希望第二个线程也随之结束,则第二个线程可以标记为守护线程。
- 设置守护线程的方法:
- 在线程开启前调用setDaemon(true)方法。
- 在线程开启前调用setDaemon(true)方法。
- 守护线程相当于后台线程,当前台线程全部结束后,后台线程自动结束,JVM退出。
- 联合线程
- 当A线程执行到了B线程的join()方法时,A就会等待,直到B线程都执行完,A线程才会继续执行。
- join()方法可以用来临时加入线程执行。
- 当A线程执行到了B线程的join()方法时,A就会等待,直到B线程都执行完,A线程才会继续执行。
- 优先级和yield方法
- 优先级
- 线程被哪个线程开启,就属于哪个线程组。
- 线程默认优先级为5。优先级取值范围1-10。
- Thread类的toString()方法返回线程名称、优先级和线程组。
- 优先级代表抢占cpu的频率。
- setPriority方法:改变线程优先级。
- yield方法
- yield方法:暂停当前正在执行的线程对象,并执行其他线程。
- 可以实现当前线程执行完后让出CPU执行权,达到不同线程交替执行的效果。
- yield方法:暂停当前正在执行的线程对象,并执行其他线程。
- 优先级