多线程概述
多线程是Java的特点之一, 掌握多线程编程技术, 可以充分利用CPU的资源,更容易解决实际中的问题,多线程技术广泛应用于和网络有关的程序设计中,因此掌握多线程技术,对于学习网络是至关重要的。
什么是进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。在一个操作系统中,每个独立执行的程序都可称为一个进程,也就是 “正在运行的程序”。
什么是线程
一个程序至少有一个进程,一个进程可以包含多个线程。(相当于航母和舰载机),同一进程中的所有线程共享该进程的资源。
总结
- 多线程是指一个应用程序中有多个线程并发执行。
- 并发是指通过CPU的调度算法,使用户感觉像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行。即使多核多CPU环境还是会使用并发,以提高处理效率。(切换执行)
- 多线程技术并不能直接提高程序的运行效率,而是通过提高CPU的使用率的方式来达到提高效率的目的。
Demo 多线程初体验:同一个Java程序执行多个无限循环
1.package com.java.demo;
2.
3.public class Demo {
4. public static void main(String[] args) {
5. // 线程初体验 : 同一个Java程序执行多个无限循环
6. HelloThread hello = new HelloThread();
7. WorldThread world = new WorldThread();
8.
9. // 执行两个线程类
10. hello.start();
11. world.start();
12.
13. while (true) {
14. System.out.println("main -> run ...");
15. }
16. }
17.}
18.
19.// 定义了一个 `Hello` 线程类
20.class HelloThread extends Thread {
21. @Override
22. public void run() {
23. while (true) {
24. System.out.println("hello");
25. }
26. }
27.}
28.
29.// 定义一个 `World` 线程类
30.class WorldThread extends Thread {
31. @Override
32. public void run() {
33. while (true) {
34. System.out.println("world");
35. }
36. }
37.}
线程的创建
Java提供了两种多线程实现的方式.
- 继承java.lang包下的Thread类, 重写Thread类的run方法.在run()方法中实现运行在线程上的代码.
- 实现java.lang.Runnable接口, 同样是在run()方法中实现运行在线程上的代码.
方式一:继承Thread
并发地运行多个执行线程第一种方法是:将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。
start方法和run方法的区别?
run:封装线程任务, 不调用系统资源,开辟新线程。
start:先调用系统资源,启动线程,再执行run方法。
多个线程之间的独立性
- 如果java程序中有多条线程, 如果一条线程发生了异常, 不会影响其它线程执行。
- 如果主线程发生了异常,程序不会终止, 程序会继续执行其它的子线程, 直到所有子线程的代码都执行完毕后才会结束程序。
继承Thread方式优缺点
- 优点是: 可以在子类中增加新的成员变量, 使线程具有某种属性,也可以在子类中增加新的方法,使线程具有某种功能。
- 缺点是: 由于Java不支持类的多继承,Thread 类的子类不能在扩展其他的类。
方式二:实现Runnable接口
Thread类中的run方法就是实现自Runnable接口。run方法是用来封装线程任务的。Runnable接口中,只有一个run方法,因此,这个接口就是专门用来封装线程任务的接口。因此,实现该接口的类,称为线程任务类。
自定义类,实现Runnable接口,把任务对象传给Thread对象。调用Thread对象的start方法,执行Thread的run。
为什么最后执行的是任务类中的run呢?
在我们创建Thread 类对象时,我们已经将task 任务类的对象作为参数传递给了线程类对象,在其内容就会将task 赋值给内部属性target 进行存储。
当我们调用Thread 对象的start 方法启动线程时,肯定会执行Thread 类的run 方法。
在Thread 的run 方法中, 会先判断target 是否为null。 这个target就是我们创建Thread 对象时传入的任务类对象,所以target 不为null,因此就会执行target 的run 方法,也就是任务类的run 方法。
使用接口完成多线程的好处
- 避免了Java单继承带来的局限性
- 适合多个相同程序代码的线程去处理同一个资源的情况, 更灵活的实现数据的共享。
线程同步
说明 : 多线程的并发执行可以提高程序的效率, 但是,当多个线程去访问同一个资源时,也会引发一些安全问题。我们必须注意这样一个问题。当两个或多个线程同时访问同一个变量, 并且一些线程需要修改这个变量,程序应对这样的问题作出处理, 否则可能发生混乱。
总结多线程的安全问题发生的原因:
- 首先必须有多线程。
- 多个线程在操作共享的数据,并且对共享数据有修改。
- 本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的。
多线程安全问题解决
我们了解到线程的安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程的安全问题,必须保证下面用于处理共享资源的代码在任何时刻只能被一个线程访问。
同步代码块
为了实现这种限制, Java中提供了同步机制, 当多个线程使用同一个共享资源时, 可以将处理共享资源的代码放置在一个代码块中, 使用Synchronized关键字来修饰。 被称作同步代码块。
1.synchronized(锁对象) {
2. // 操作共享资源代码块
3.}
1.package com.java.multithread;
2.
3.public class Demo {
4. public static void main(String[] args) {
5. // 1. 创建一个 `任务类` 的对象
6. TicketWindow tw = new TicketWindow();
7.
8. // 2. 创建线程, 并将任务类对象作为参数传入
9. Thread t1 = new Thread(tw, "窗口一");
10. Thread t2 = new Thread(tw, "窗口二");
11. Thread t3 = new Thread(tw, "窗口三");
12.
13. // 3. 启动线程
14. t1.start();
15. t2.start();
16. t3.start();
17. }
18.}
19.
20.class TicketWindow implements Runnable {
21. // 属性
22. private int tickets = 100;
23. // 定义任意的一个 `锁对象`, 用于同步代码块
24. Object lock = new Object();
25.
26. //行为
27. @Override
28. public void run() {
29. // 实现循环模拟不断售票的过程
30. while (true) {
31. synchronized (lock) {
32. // 睡 1 毫秒
33. try {
34. Thread.sleep(1);
35. } catch (InterruptedException e) {
36. e.printStackTrace();
37. }
38. // 判断
39. if (tickets > 0) {
40. System.out.println(Thread.currentThread().getName() + "正在发售第 " + tickets-- +"张票.");
41. } else {
42. break; // 完成后结束任务代码
43. }
44. }
45. }
46. }
47.}
同步方法
同步方法 : 在方法前面同样可以使Synchronized关键字来修饰,被修饰的方法称为同步方法,它能实现和同步代码块同样的功能。
修饰符 synchronized 返回值类型方法名 (参数列表){}
1.package cn.java.demo;
2.
3.// 同步方法
4.
5.public class Demo {
6. public static void main(String[] args) {
7. // 创建一个 `任务类` 对象
8. TicketWindow task = new TicketWindow();
9.
10. // 创建3个线程对象, 将任务类对象作为参数传入
11. Thread t1 = new Thread(task, "窗口一");
12. Thread t2 = new Thread(task, "窗口二");
13. Thread t3 = new Thread(task, "窗口三");
14.
15. // 启动线程
16. t1.start();
17. t2.start();
18. t3.start();
19. }
20.}
21.
22.// 售票案例 : 使用接口方式实现
23.// 定义一个 `售票窗口` 类 (任务类)
24.class TicketWindow implements Runnable {
25. // 属性
26. private int tickets = 100;
27.
28. // 行为
29. @Override
30. public void run() {
31. // 使用循环模拟不断售票过程
32. while (true) {
33. // 调用售票的方法
34. this.sellTicket();
35.
36. if (tickets <= 0) {
37. break;
38. }
39. }
40. }
41.
42. // 如果一个方法中所有的代码都需要同步, 就可以直接使用 `同步方法` 实现
43. // 问题一 : 同步方法有没有锁? 有
44. // 问题二 : 同步方法的锁是谁? this 当前调用这个方法的对象.
45. public synchronized void sellTicket() {
46. // 判断是否还有余票
47. if (tickets > 0) {
48. try {
49. Thread.sleep(10);
50. } catch (InterruptedException e) {
51. e.printStackTrace();
52. }
53. System.out.println(Thread.currentThread().getName() + "正在出售第 " + tickets-- + " 张票.");
54. }
55. }
56.}
思考 : 大家可能会有这样的疑问 : 同步代码块的锁是自己定义的任意类型的对象, 那么同步方法是否也存在锁? 如果有, 它的锁是什么呢?
答案是肯定的, 同步方法也有锁, 它的锁就是当前调用该方法的对象, 也就是this指向的对象.
这样做的好处是, 同步方法被所有线程所共享, 方法所在的对象相对于所有线程来说是唯一的, 从而保证了锁的唯一性. 当一个线程执行该方法时, 其它的线程就不能进入到该方法中, 直到这个线程执行完该方法为止, 从而达到了线程同步的效果.
Lock
JDK5提供的Lock接口比JDK5之前的同步更好使用。Lock接口代替JDK5之前的同步代码块. 更加灵活.
注意 : Lock接口的实现类对象不能和 Object类中的 wait(), notify(), notifyAll(), 共同使用, 因为Object类中的 wait(), notify(), notifyAll(), 方法必须要和 同步锁 synchronized 共同使用, 否则报异常!
使用Lock演示卖票案例Demo
1.package com.java.demo;
2.
3.import java.util.concurrent.locks.Lock;
4.import java.util.concurrent.locks.ReentrantLock;
5.
6.// 演示 : Lock 接口 & ReentrantLock实现类
7.
8.public class Demo {
9. public static void main(String[] args) {
10. // 创建一个 `任务类` 对象
11. TicketWindow task = new TicketWindow();
12.
13. // 创建3个线程对象, 将任务类对象作为参数传入
14. Thread t1 = new Thread(task, "窗口一");
15. Thread t2 = new Thread(task, "窗口二");
16. Thread t3 = new Thread(task, "窗口三");
17.
18. // 启动线程
19. t1.start();
20. t2.start();
21. t3.start();
22. }
23.}
24.
25.// 售票案例 : 使用接口方式实现
26.// 定义一个 `售票窗口` 类 (任务类)
27.class TicketWindow implements Runnable {
28. // 属性
29. private int tickets = 100;
30. // 定义一个锁属性
31. Lock lock = new ReentrantLock();
32.
33. // 行为
34. @Override
35. public void run() {
36. // 使用循环模拟不断售票过程
37. while (true) {
38. // 调用售票的方法
39. this.sellTicket();
40.
41. if (tickets <= 0) {
42. break;
43. }
44. }
45. }
46.
47. public void sellTicket() {
48. // 获取锁
49. lock.lock();
50. // 判断是否还有余票
51. if (tickets > 0) {
52. try {
53. Thread.sleep(10);
54. } catch (InterruptedException e) {
55. e.printStackTrace();
56. }
57. System.out.println(Thread.currentThread().getName() + "正在出售第 " + tickets-- + " 张票.");
58. }
59. // 释放锁
60. lock.unlock();
61. }
62.}
线程的运行状态图(生命周期)
面试题
1./*
2. * 1. 多线程有几种实现方案, 分别是哪几种 ?
3. *
4. * 第一种 : 继承 Thread 类
5. * a. 自定义类, 继承Thread
6. * b. 重写 run() 方法
7. * c. 创建线程类对象
8. * d. 启动线程
9. *
10. * 第二种 : 实现 Runnable 接口
11. * a. 自定义类, 实现Runnable 接口
12. * b. 实现 run() 方法
13. * c. 创建线程任务类对象
14. * d. 创建线程对象, 将任务类对象作为参数传入.
15. * e. 启动线程
16. */
17.
18./*
19. * 2. 同步有几种方式, 分别是什么 ?
20. *
21. * 第一种 : 同步代码块. 锁是任意对象, 但是必须保证唯一性.
22. *
23. * 第二种 : 同步方法. 锁是 this 当前对象.
24. *
25. * 其实 : JDK5.0 之后提供了新的一个同步机制 :
26. * Lock接口
27. * 获取锁 : lock.lock();
28. * 释放锁 : lock.unlock();
29. */
30.
31./*
32. * 3. run() 和 start() 的区别 ?
33. *
34. * run : 仅仅是封装线程任务执行代码, 不调用系统资源开辟新线程.
35. * start : 先调用系统资源,开辟新线程, 在新线程中执行 run()方法的任务代码.
36. */
37.
38./*
39. * 4. 线程的生命周期.
40. *
41. * Block阻塞状态
42. *
43. * New新建状态 Runnable就绪状态 Running运行状态 Terminated死亡状态
44. */