进程:正在运行中的程序(直译)。

线程:进程中一个负责程序执行的控制单元(执行路径)。

多线程: 一个进程中可以多执行路径,称之为多线程。

多线程的优缺点:

多线程的好处:解决了多部分同时运行的问题。

多线程的弊端:线程太多会导致效率的降低。

1. Java中实现多线程的手段:

1)、继承Thread类。

  1. // 定义一个类,继承Thread,重写run方法
  2. class ThreadSub extends Thread {
  3. private String name; // 为了区分线程
  4. public ThreadSub(String name) {
  5. this.name = name;
  6. }
  7. @Override
  8. public void run() {
  9. for (int i = 0; i < 3; i++) {
  10. System.out.println(name + "执行" + i);
  11. }
  12. }
  13. }
  14. // main函数
  15. public static void main(String[] args) {
  16. Demo1Sub sub1 = new Demo1Sub("多线程01号");
  17. Demo1Sub sub2 = new Demo1Sub("多线程02号");
  18. sub1.start();
  19. sub2.start();
  20. }
  21. 打印结果:
  22. 多线程01号执行0
  23. 多线程02号执行0
  24. 多线程02号执行1
  25. 多线程02号执行2
  26. 多线程01号执行1
  27. 多线程01号执行2

2)、实现Runnable接口。

实现Runnable接口的好处:

1,将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
2,避免了java单继承的局限性。

  1. // Runnable接口详情, 只有一个抽象方法
  2. @FunctionalInterface
  3. public interface Runnable {
  4. public abstract void run();
  5. }
  6. // 定义一个类,实现Runnable接口,实现未实现的run方法
  7. class ThreadSub2 implements Runnable {
  8. private String name; // 为了区分线程
  9. public ThreadSub2(String name) {
  10. this.name = name;
  11. }
  12. @Override
  13. public void run() {
  14. for (int i = 0; i < 3; i++) {
  15. System.out.println(name + "执行" + i);
  16. }
  17. }
  18. }
  19. // main函数
  20. public static void main(String[] args) {
  21. Demo1Sub2 sub1 = new Demo1Sub2("多线程01号");
  22. Demo1Sub2 sub2 = new Demo1Sub2("多线程02号");
  23. Thread t1 = new Thread(sub1);
  24. Thread t2 = new Thread(sub2);
  25. t1.start();
  26. t2.start();
  27. }
  28. 打印结果:
  29. 多线程01号执行0
  30. 多线程02号执行0
  31. 多线程02号执行1
  32. 多线程02号执行2
  33. 多线程01号执行1
  34. 多线程01号执行2

3)、匿名函数(其实就两种 这还是实现Runnable的一种)

  1. // 定义一个类
  2. class ThreadSub {
  3. private String name; // 为了区分线程
  4. public ThreadSub(String name) {
  5. this.name = name;
  6. }
  7. // 该类中的方法
  8. public void show() {
  9. for (int i = 0; i < 3; i++) {
  10. System.out.println(name + "执行" + i);
  11. }
  12. }
  13. }
  14. // 主函数入口
  15. public static void main(String[] args) {
  16. Demo1Sub sub1 = new Demo1Sub("多线程01号");
  17. Demo1Sub sub2 = new Demo1Sub("多线程02号");
  18. // 使用匿名函数直接调用 start 方法
  19. new Thread(new Runnable() {
  20. @Override
  21. public void run() {
  22. sub1.show();
  23. }
  24. }).start();
  25. Thread t2 = new Thread(new Runnable() {
  26. @Override
  27. public void run() {
  28. sub2.show();
  29. }
  30. });
  31. t2.start()
  32. }
  33. 打印结果:
  34. 多线程01号执行0
  35. 多线程02号执行0
  36. 多线程02号执行1
  37. 多线程02号执行2
  38. 多线程01号执行1
  39. 多线程01号执行2

2. Thread类中的 start() 方法和 run() 方法的区别:

run()方法: 在本线程内调用该Runnable对象的run()方法,可以重复多次调用;

start()方法: 启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程

直接调用run()方法,相当于没有使用多线程技术,需要在run()方法体执行完毕后才会执行下面的代码,还是只有一个主线程的执行路径,按照顺序执行代码。

调用start()方法后,会来启动一个线程,这时线程处于就绪状态,真正实现了多线程。

3. 多线程的状态 (重要)

image.png

4. 卖票(使用Runnable接口,将票封装在任务中,只有一个票对象)

  1. package se.thread;
  2. /*
  3. * 需求: 卖票
  4. *
  5. * 一共有100张票,使用四个线程同时卖票。
  6. * */
  7. public class SaleTicket {
  8. public static void main(String[] args) {
  9. // 使用继承Thread 类
  10. // 既然创建多个线程会有多个车票对象,是否可以创建一个线程,开启四次?
  11. // 答案是否定,会在main函数中抛出异常,非法的线程状态异常IllegalThreadStateException
  12. // 一个线程只能被开启一次
  13. /*
  14. Ticket t1 = new Ticket();
  15. Ticket t2 = new Ticket();
  16. Ticket t3 = new Ticket();
  17. Ticket t4 = new Ticket();
  18. t1.start();
  19. t2.start();
  20. t3.start();
  21. t4.start();
  22. */
  23. // 使用实现Runnable 接口
  24. Ticket t = new Ticket(); // 创建一个线程任务。
  25. Thread t1 = new Thread(t);
  26. Thread t2 = new Thread(t);
  27. Thread t3 = new Thread(t);
  28. Thread t4 = new Thread(t);
  29. t1.start();
  30. t2.start();
  31. t3.start();
  32. t4.start();
  33. }
  34. }
  35. // 继承Thread后创建了四个线程,导致每个线程对象都有一个自己的num,就会导致有400张票。
  36. // 实现Runnable创建一个线程任务对象,该对象中有num属性,而其他线程将此对象作为参数,都会使用此对象的num属性
  37. class Ticket /*extends Thread*/ implements Runnable {
  38. // 继承时使用static可以解决此问题,但是很多情况我们是不想数据共享的。
  39. // private static int num = 100;
  40. private int num = 100;
  41. /*@Override
  42. public void run() {
  43. while (true) {
  44. if(num > 0) {
  45. // 注意!!!!!这里存在安全隐患
  46. // 即A线程通过了num>0的判断后,CPU不再执行A,执行B也通过了num>0的判断
  47. System.out.println(Thread.currentThread().getName()
  48. + "......sale....." + (--this.num));
  49. }
  50. }
  51. }*/
  52. @Override
  53. public void run() {
  54. while (true) {
  55. // 同步 来解决安全隐患
  56. synchronized(Ticket.class){
  57. if(num > 0) {
  58. // sleep 用来演示临时阻塞状态下的安全隐患
  59. // Thread.sleep(time),存在InterruptedException异常
  60. // 由于实现的Runable接口,并且该接口没有声明过InterruptedException异常,
  61. // 所以run方法中的异常只能catch
  62. try { Thread.sleep(20); } catch (InterruptedException e) { }
  63. System.out.println(Thread.currentThread().getName()
  64. + "......sale....." + (--this.num));
  65. }
  66. }
  67. }
  68. }
  69. }

5. 线程安全问题产生的原因:

(1)多个线程在操作共享的数据。

(2)操作共享数据的线程代码有多条。(例如售票的,run方法中需要判断 num是否大于零再进行售票,就会有安全隐患)

当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。

6. 同步代码块可以解决以上问题

解决思路;

就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算的。

必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

7. 同步代码块的好处和弊端:

同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。

同步的前提:同步中必须有多个线程并使用同一个锁。

8. 同步函数使用的锁是 this

9. 静态的同步函数使用的锁是 该函数所属字节码文件对象, 可以使用getClass()来获取, 也可以使用 当前类名.class

10. 单例中的懒汉式存在多线程安全隐患

11. 死锁

容易出现死锁的一种形式: 同步嵌套(着重看打印结果以及解释)

  1. package com.lius.javase.demo.thread;
  2. /**
  3. * 死锁:
  4. * 容易出现死锁的一种形式: 同步嵌套
  5. */
  6. public class DeathLock implements Runnable{
  7. public boolean flag;
  8. DeathLock(boolean flag) {
  9. this.flag = flag;
  10. }
  11. @Override
  12. public void run() {
  13. if(flag) {
  14. synchronized (MyLock.LOCK_A) {
  15. System.out.println(Thread.currentThread().getName() + "...if...LOCK_A");
  16. synchronized (MyLock.LOCK_B) {
  17. System.out.println(Thread.currentThread().getName() + "...if...LOCK_B");
  18. }
  19. }
  20. } else {
  21. synchronized (MyLock.LOCK_B) {
  22. System.out.println(Thread.currentThread().getName() + "...else...LOCK_B");
  23. synchronized (MyLock.LOCK_A) {
  24. System.out.println(Thread.currentThread().getName() + "...else...LOCK_A");
  25. }
  26. }
  27. }
  28. }
  29. }
  30. class MyLock {
  31. public static final Object LOCK_A = new Object();
  32. public static final Object LOCK_B = new Object();
  33. }
  34. class Test {
  35. public static void main(String[] args) {
  36. DeathLock target1 = new DeathLock(true);
  37. DeathLock target2 = new DeathLock(false);
  38. Thread t1 = new Thread(target1);
  39. Thread t2 = new Thread(target2);
  40. t1.start();
  41. t2.start();
  42. }
  43. }
  44. 死锁的打印结果:
  45. Thread-1...else...LOCK_B
  46. Thread-0...if...LOCK_A
  47. 对打印结果进行解释:
  48. 线程1的标志位(flag)为false, 走了else分支, 拿着B锁进入了else里的第一个同步代码块,
  49. 在想要拿A锁进入else的下一个代码块的时候, 线程1失去了CPU的执行权, 进入到阻塞状态, 此时线程2抢到执行权;
  50. 线程2的标志位(flag)是true, 走了if分支, 拿着A锁进入了if里的第一个同步代码块,
  51. 在想要拿B锁进入if的下一哥代码块的时候, B锁却还在线程1手里, 所以拿不到, 就发生了死锁现象.

12. 等待唤醒机制

代码示例(同一个资源, 一个线程负责存值, 另一个线程负责取值)

  1. package com.lius.javase.demo.thread;
  2. /**
  3. * 同一个资源, 一个赋值, 一个取值
  4. *
  5. * 使用等待唤醒机制:
  6. * 设计到的方法:
  7. * wait(): 让线程处于冻结状态, 被wait()的线程会被存放到线程池中.
  8. * notify(): 唤醒线程池中的一个线程(随机的).
  9. * notifyAll(): 唤醒线程池中的所有线程.
  10. *
  11. * 这些方法都必须定义在同步中,
  12. * 因为这些方法都是改变线程状态的方法, 必须要明确操作的到底是哪个锁上的线程.
  13. */
  14. public class ResourceDemo {
  15. public static void main(String[] args) {
  16. Resource r = new Resource();
  17. Input in = new Input(r);
  18. Output out = new Output(r);
  19. Thread t1 = new Thread(in);
  20. Thread t2 = new Thread(out);
  21. t1.start();
  22. t2.start();
  23. }
  24. }
  25. // 资源
  26. class Resource {
  27. private String name;
  28. private String sex;
  29. private boolean isExists = false; // 是否已有值
  30. public synchronized void set(String name, String sex) {
  31. if(isExists) {
  32. try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
  33. } /* 注意!!! 这里不能使用 else 代码块 */
  34. this.name = name;
  35. this.sex = sex;
  36. isExists = true;
  37. this.notify();
  38. }
  39. public synchronized void print() {
  40. if(!isExists) {
  41. try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
  42. } else {
  43. System.out.println(this.name + "..." + this.sex);
  44. isExists = false;
  45. this.notify();
  46. }
  47. }
  48. }
  49. // 赋值
  50. class Input implements Runnable{
  51. private final Resource r;
  52. Input(Resource r) {
  53. this.r = r;
  54. }
  55. boolean flag = false; // 控制赋值切换的
  56. @Override
  57. public void run() {
  58. for(;;) {
  59. if (flag) {
  60. r.set("mike", "man");
  61. } else {
  62. r.set("丽丽", "女女女女女女女女女");
  63. }
  64. flag = !flag;
  65. }
  66. }
  67. }
  68. // 取值
  69. class Output implements Runnable {
  70. private final Resource r;
  71. Output(Resource r) {
  72. this.r = r;
  73. }
  74. @Override
  75. public void run() {
  76. for(;;) {
  77. r.print();
  78. }
  79. }
  80. }

为什么操作线程的方法wait, notify, notifyAll 定义在了Object类中?

因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。

13. 等待唤醒中 多生产多消费问题 ☆☆☆☆

单生产单消费, 因为一共有两个线程, 所以唤醒时必定会唤醒对方的线程, 不会出现多生产多消费的问题

多生产多消费有什么问题?

1. 使用 if 来判断标记, 生产或者消费方唤醒的仍是本方的线程的话, 本方线程不会再去判断标记, 导致生产或者消费执行多次.

解决方式: 将 if 判断 改为 while 循环, 醒来之后仍能判断标记, 避免了多次生产或者消费

2. 将if改为while之后会出现的问题: 如果对方线程全部wait(), 只剩本的一个线程在运行, 执行完一次后唤醒的仍然是本方线程, 会导致所有线程全部被wait();

解决方式: 在唤醒时, 至少能够保证唤醒对方一个线程, 才不会出现该结果, 但无法指定唤醒, 所以只能将 notify() 改为 notifyAll();

  1. package com.lius.javase.demo.thread;
  2. /**
  3. * 多生产者 -- 多消费者
  4. *
  5. * 多生产多消费出现的问题:
  6. * 1. 使用 if 判断标记, 被 notify 后不会再判断标记了, 直接执行后面的代码, 需要重新判断标记, 将 if 改为 while
  7. * 2. 将 if 改为 while 后, 造成所有线程全部被 wait(), 即死锁的另外一种情况, 如何解决?
  8. * 因为至少需要唤醒一个对方的线程, 但是没有指定唤醒, 所以干脆全唤醒, 使用 notifyAll()
  9. */
  10. public class ProducerConsumerDemo {
  11. public static void main(String[] args) {
  12. Resource r = new Resource();
  13. Producer producer = new Producer(r);
  14. Consumer consumer = new Consumer(r);
  15. Thread t0 = new Thread(producer);
  16. Thread t1 = new Thread(producer);
  17. Thread t2 = new Thread(consumer);
  18. Thread t3 = new Thread(consumer);
  19. t0.start();
  20. t1.start();
  21. t2.start();
  22. t3.start();
  23. }
  24. }
  25. class Resource {
  26. private String name;
  27. private int index = 1;
  28. private boolean flag = false;
  29. public synchronized void set(String name) {
  30. while (this.flag) { // 这里将 if 改为 while 是为了醒来之后再去判断标记, 以免本方线程多次执行
  31. try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
  32. }
  33. this.name = name + index;
  34. index++;
  35. System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
  36. this.flag = true;
  37. // notify() 改为 notifyAll()
  38. // 因为使用了 while 会导致所有线程全部被 wait(),
  39. // 所以必须保证唤醒时可以至少唤醒对面一个, 但无法指定唤醒那个线程, 干脆全部唤醒
  40. this.notifyAll();
  41. }
  42. public synchronized void out() {
  43. while (!this.flag) { // 这里改为 while 与上同理
  44. try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}
  45. }
  46. System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
  47. this.flag = false;
  48. this.notifyAll(); // notify() 改为 notifyAll() 与上同理
  49. }
  50. }
  51. class Producer implements Runnable {
  52. private final Resource r;
  53. Producer(Resource r) {
  54. this.r = r;
  55. }
  56. @Override
  57. public void run() {
  58. for(;;) {
  59. r.set("烤鸭");
  60. }
  61. }
  62. }
  63. class Consumer implements Runnable {
  64. private final Resource r;
  65. Consumer(Resource r) {
  66. this.r = r;
  67. }
  68. @Override
  69. public void run() {
  70. for(;;) {
  71. r.out();
  72. }
  73. }
  74. }

3. JDK1.5 之后将同步和锁封装成了对象, 来解决多生产和多消费的问题。

  1. package com.lius.javase.demo.thread;
  2. import java.util.concurrent.locks.Condition;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. /**
  6. * 多生产多消费, 使用Lock接口替换同步代码块或者同步函数 (JDK1.5之后才有的)
  7. */
  8. public class ProConLockDemo {
  9. public static void main(String[] args) {
  10. Resource r = new Resource();
  11. Pro producer = new Pro(r);
  12. Cons consumer = new Cons(r);
  13. Thread t0 = new Thread(producer);
  14. Thread t1 = new Thread(producer);
  15. Thread t2 = new Thread(consumer);
  16. Thread t3 = new Thread(consumer);
  17. t0.start();
  18. t1.start();
  19. t2.start();
  20. t3.start();
  21. }
  22. }
  23. class Resource {
  24. private String name;
  25. private int index = 1;
  26. private boolean flag = false;
  27. Lock lock = new ReentrantLock();
  28. // 通过一个锁上挂多组监视器, 来解决 notifyAll() 的效率问题
  29. Condition con_condition = lock.newCondition(); // 生产者监视器
  30. Condition pro_condition = lock.newCondition(); // 消费者监视器
  31. public void set(String name) {
  32. lock.lock();
  33. try {
  34. while (this.flag) {
  35. try {
  36. con_condition.await();} catch (InterruptedException e) {e.printStackTrace();}
  37. }
  38. this.name = name + index;
  39. index++;
  40. System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
  41. this.flag = true;
  42. pro_condition.signal();
  43. } finally {
  44. lock.unlock();
  45. }
  46. }
  47. public void out() {
  48. lock.lock();
  49. try {
  50. while (!this.flag) {
  51. try {
  52. pro_condition.await();} catch (InterruptedException e) {e.printStackTrace();}
  53. }
  54. System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);
  55. this.flag = false;
  56. con_condition.signal();
  57. } finally {
  58. lock.unlock();
  59. }
  60. }
  61. }
  62. class Pro implements Runnable {
  63. private final Resource r;
  64. Pro(Resource r) {
  65. this.r = r;
  66. }
  67. @Override
  68. public void run() {
  69. for(;;) {
  70. r.set("烤鸭");
  71. }
  72. }
  73. }
  74. class Cons implements Runnable {
  75. private final Resource r;
  76. Cons(Resource r) {
  77. this.r = r;
  78. }
  79. @Override
  80. public void run() {
  81. for(;;) {
  82. r.out();
  83. }
  84. }
  85. }

14. wait 和 sleep 的区别:

1)wait可以指定时间, 也可以不指定时间(该时间为等待的最长时间);而sleep必须指定时间。

2)在同步中,对CPU的执行权和锁的处理不同:

wait:释放执行权,释放锁

sleep:释放执行权,不释放锁

15. 停止线程的方法

1,stop方法(过时,存在安全隐患)

2,run方法结束。

如何控制线程任务结束呢?通常使用标记来结束。

如果线程处于冻结状态,无法读取标记该怎么办?interrupt —>