第十九章 多线程

1. 多线程基本概述

1.1 什么是进程?什么是线程?

  1. - 进程是一个应用程序(1个进程是一个软件),线程是一个进程中的执行场景/执行单元;一个进程可以启动多个线程。
  2. - 对于java程序来说,当在DOS命令窗口中输入:java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾;最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
  3. - 进程可以看做是现实生活当中的公司,线程可以看做是公司当中的某个员工。
  4. - java中之所以有多线程机制,目的就是为了提高程序的处理效率。

1.2 线程、进程在内存中的表现

image.png

线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈。

  1. - 使用了多线程机制之后,main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。
  2. - 对于多核的CPU电脑来说,真正的多线程并发是没问题的;对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情同时在做;电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,人类的眼睛产生了错觉,感觉是动画的。

2. 实现线程的方式

2.1 第一种方式

  1. - 编写一个类,直接继承java.lang.Thread,重写run方法。
  1. public class ThreadTest02 {
  2. public static void main(String[] args) {
  3. // 这里是main方法,这里的代码属于主线程,在主栈中运行。
  4. // 新建一个分支线程对象
  5. MyThread t = new MyThread();
  6. // 启动线程
  7. //t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
  8. // start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
  9. // 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
  10. // 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
  11. // run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
  12. t.start();
  13. // 这里的代码还是运行在主线程中。
  14. for(int i = 0; i < 1000; i++){
  15. System.out.println("主线程--->" + i);
  16. }
  17. }
  18. }
  19. class MyThread extends Thread {
  20. @Override
  21. public void run() {
  22. // 编写程序,这段程序运行在分支线程中(分支栈)。
  23. for(int i = 0; i < 1000; i++){
  24. System.out.println("分支线程--->" + i);
  25. }
  26. }
  27. }

2.1.1 线程的start方法

image.png

2.2 第二种方式

  1. - 编写一个类,实现java.lang.Runnable接口,实现run方法。
  1. public class ThreadTest03 {
  2. public static void main(String[] args) {
  3. // 创建一个可运行的对象
  4. //MyRunnable r = new MyRunnable();
  5. // 将可运行的对象封装成一个线程对象
  6. //Thread t = new Thread(r);
  7. Thread t = new Thread(new MyRunnable()); // 合并代码
  8. // 启动线程
  9. t.start();
  10. for(int i = 0; i < 100; i++){
  11. System.out.println("主线程--->" + i);
  12. }
  13. }
  14. }
  15. // 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
  16. class MyRunnable implements Runnable {
  17. @Override
  18. public void run() {
  19. for(int i = 0; i < 100; i++){
  20. System.out.println("分支线程--->" + i);
  21. }
  22. }
  23. }

2.2.1 匿名内部类方式

  1. public class ThreadTest04 {
  2. public static void main(String[] args) {
  3. // 创建线程对象,采用匿名内部类方式。
  4. // 这是通过一个没有名字的类,new出来的对象。
  5. Thread t = new Thread(new Runnable(){
  6. @Override
  7. public void run() {
  8. for(int i = 0; i < 100; i++){
  9. System.out.println("t线程---> " + i);
  10. }
  11. }
  12. });
  13. // 启动线程
  14. t.start();
  15. for(int i = 0; i < 100; i++){
  16. System.out.println("main线程---> " + i);
  17. }
  18. }
  19. }

注意: 第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。

3. 线程对象的生命周期

image.png

运行状态进入阻塞状态,还有join方法

4. 获取当前线程对象

1、怎么获取当前线程对象?
Thread t = Thread.currentThread(); 返回值t就是当前线程。

2、获取线程对象的名字
String name = 线程对象.getName();

3、修改线程对象的名字
线程对象.setName(“线程名字”);

4、当线程没有设置名字的时候,默认的名字有什么规律?(了解一下)
主线程默认:main
Thread-0
Thread-1
Thread-2
…..

  1. public class ThreadTest05 {
  2. public void doSome(){
  3. // 这样就不行了
  4. //this.getName();
  5. //super.getName();
  6. // 但是这样可以
  7. String name = Thread.currentThread().getName();
  8. System.out.println("------->" + name);
  9. }
  10. public static void main(String[] args) {
  11. ThreadTest05 tt = new ThreadTest05();
  12. tt.doSome();
  13. //currentThread就是当前线程对象
  14. // 这个代码出现在main方法当中,所以当前线程就是主线程。
  15. Thread currentThread = Thread.currentThread();
  16. System.out.println(currentThread.getName()); //main
  17. // 创建线程对象
  18. MyThread2 t = new MyThread2();
  19. // 设置线程的名字
  20. t.setName("t1");
  21. // 获取线程的名字
  22. String tName = t.getName();
  23. System.out.println(tName); //Thread-0
  24. MyThread2 t2 = new MyThread2();
  25. t2.setName("t2");
  26. System.out.println(t2.getName()); //Thread-1\
  27. t2.start();
  28. // 启动线程
  29. t.start();
  30. }
  31. }
  32. class MyThread2 extends Thread {
  33. public void run(){
  34. for(int i = 0; i < 100; i++){
  35. // currentThread就是当前线程对象。当前线程是谁呢?
  36. // 当t1线程执行run方法,那么这个当前线程就是t1
  37. // 当t2线程执行run方法,那么这个当前线程就是t2
  38. Thread currentThread = Thread.currentThread();
  39. System.out.println(currentThread.getName() + "-->" + i);
  40. //System.out.println(super.getName() + "-->" + i);
  41. //System.out.println(this.getName() + "-->" + i);
  42. }
  43. }
  44. }

5. 关于线程的sleep方法

  1. /*
  2. 关于线程的sleep方法:
  3. static void sleep(long millis)
  4. 1、静态方法:Thread.sleep(1000);
  5. 2、参数是毫秒
  6. 3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
  7. 这行代码出现在A线程中,A线程就会进入休眠。
  8. 这行代码出现在B线程中,B线程就会进入休眠。
  9. 4、Thread.sleep()方法,可以做到这种效果:
  10. 间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
  11. */
  12. public class ThreadTest06 {
  13. public static void main(String[] args) {
  14. // 让当前线程进入休眠,睡眠5秒
  15. // 当前线程是主线程!!!
  16. /*try {
  17. Thread.sleep(1000 * 5);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }*/
  21. // 5秒之后执行这里的代码
  22. //System.out.println("hello world!");
  23. for(int i = 0; i < 10; i++){
  24. System.out.println(Thread.currentThread().getName() + "--->" + i);
  25. // 睡眠1秒
  26. try {
  27. Thread.sleep(1000);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }

5.1 Thread.sleep()方法的一个面试题

  1. public class ThreadTest07 {
  2. public static void main(String[] args) {
  3. // 创建线程对象
  4. Thread t = new MyThread3();
  5. t.setName("t");
  6. t.start();
  7. // 调用sleep方法
  8. try {
  9. // 问题:这行代码会让线程t进入休眠状态吗?
  10. t.sleep(1000 * 5); // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);
  11. // 这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。
  12. // 这样代码出现在main方法中,main线程睡眠。
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. // 5秒之后这里才会执行。
  17. System.out.println("hello World!");
  18. }
  19. }
  20. class MyThread3 extends Thread {
  21. public void run(){
  22. for(int i = 0; i < 10000; i++){
  23. System.out.println(Thread.currentThread().getName() + "--->" + i);
  24. }
  25. }
  26. }

6. 终止线程的睡眠

  1. /*
  2. sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程??
  3. 注意:这个不是终断线程的执行,是终止线程的睡眠。
  4. */
  5. public class ThreadTest08 {
  6. public static void main(String[] args) {
  7. Thread t = new Thread(new MyRunnable2());
  8. t.setName("t");
  9. t.start();
  10. // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
  11. try {
  12. Thread.sleep(1000 * 5);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
  17. t.interrupt(); // 干扰,一盆冷水过去!
  18. }
  19. }
  20. class MyRunnable2 implements Runnable {
  21. // 重点:run()当中的异常不能throws,只能try catch
  22. // 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
  23. @Override
  24. public void run() {
  25. System.out.println(Thread.currentThread().getName() + "---> begin");
  26. try {
  27. // 睡眠1年
  28. Thread.sleep(1000 * 60 * 60 * 24 * 365);
  29. } catch (InterruptedException e) {
  30. // 打印异常信息
  31. //e.printStackTrace();
  32. }
  33. //1年之后才会执行这里
  34. System.out.println(Thread.currentThread().getName() + "---> end");
  35. // 调用doOther
  36. //doOther();
  37. }
  38. // 其它方法可以throws
  39. /*public void doOther() throws Exception{
  40. }*/
  41. }

6.1 强行终止一个线程的执行

  1. /*
  2. 在java中怎么强行终止一个线程的执行。
  3. 这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,
  4. 线程没有保存的数据将会丢失。不建议使用。
  5. */
  6. public class ThreadTest09 {
  7. public static void main(String[] args) {
  8. Thread t = new Thread(new MyRunnable3());
  9. t.setName("t");
  10. t.start();
  11. // 模拟5秒
  12. try {
  13. Thread.sleep(1000 * 5);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. // 5秒之后强行终止t线程
  18. t.stop(); // 已过时(不建议使用。)
  19. }
  20. }
  21. class MyRunnable3 implements Runnable {
  22. @Override
  23. public void run() {
  24. for(int i = 0; i < 10; i++){
  25. System.out.println(Thread.currentThread().getName() + "--->" + i);
  26. try {
  27. Thread.sleep(1000);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }

6.2 合理的终止一个线程的执行

  1. public class ThreadTest10 {
  2. public static void main(String[] args) {
  3. MyRunable4 r = new MyRunable4();
  4. Thread t = new Thread(r);
  5. t.setName("t");
  6. t.start();
  7. // 模拟5秒
  8. try {
  9. Thread.sleep(5000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. // 终止线程
  14. // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
  15. r.run = false;
  16. }
  17. }
  18. class MyRunable4 implements Runnable {
  19. // 打一个布尔标记
  20. boolean run = true;
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 10; i++){
  24. if(run){
  25. System.out.println(Thread.currentThread().getName() + "--->" + i);
  26. try {
  27. Thread.sleep(1000);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }else{
  32. // return就结束了,你在结束之前还有什么没保存的。
  33. // 在这里可以保存呀。
  34. //save....
  35. //终止当前线程
  36. return;
  37. }
  38. }
  39. }
  40. }

注意: if判断语句是在for循环里

7. 线程的调度

7.1 常见的线程调度模型

  1. - 抢占式调度模型:

那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型。

  1. - 均分式调度模型:

平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样,平均分配,一切平等,有一些编程语言,线程调度模型采用的是这种方式。

7.2 java中提供了哪些方法是和线程调度有关系的

实例方法:
void setPriority(int newPriority) 设置线程的优先级
int getPriority() 获取线程优先级
最低优先级1,默认优先级是5,最高优先级10,优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)

静态方法:
static void yield() 让位方法
暂停当前正在执行的线程对象,并执行其他线程,yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用;yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

注意: 在回到就绪之后,有可能还会再次抢到。


实例方法:
void join() 合并线程

  1. class MyThread1 extends Thread {
  2. public void doSome(){
  3. MyThread2 t = new MyThread2();
  4. t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
  5. }
  6. }
  7. class MyThread2 extends Thread{}

8. 线程的优先级

  1. public class ThreadTest11 {
  2. public static void main(String[] args) {
  3. // 设置主线程的优先级为1
  4. Thread.currentThread().setPriority(1);
  5. /*System.out.println("最高优先级" + Thread.MAX_PRIORITY);
  6. System.out.println("最低优先级" + Thread.MIN_PRIORITY);
  7. System.out.println("默认优先级" + Thread.NORM_PRIORITY);*/
  8. // 获取当前线程对象,获取当前线程的优先级
  9. Thread currentThread = Thread.currentThread();
  10. // main线程的默认优先级是:5
  11. //System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());
  12. Thread t = new Thread(new MyRunnable5());
  13. t.setPriority(10);
  14. t.setName("t");
  15. t.start();
  16. // 优先级较高的,只是抢到的CPU时间片相对多一些。
  17. // 大概率方向更偏向于优先级比较高的。
  18. for(int i = 0; i < 10000; i++){
  19. System.out.println(Thread.currentThread().getName() + "-->" + i);
  20. }
  21. }
  22. }
  23. class MyRunnable5 implements Runnable {
  24. @Override
  25. public void run() {
  26. // 获取线程优先级
  27. //System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());
  28. for(int i = 0; i < 10000; i++){
  29. System.out.println(Thread.currentThread().getName() + "-->" + i);
  30. }
  31. }
  32. }

8.1 yield方法

  1. /*
  2. 让位,当前线程暂停,回到就绪状态,让给其它线程。
  3. 静态方法:Thread.yield();
  4. */
  5. public class ThreadTest12 {
  6. public static void main(String[] args) {
  7. Thread t = new Thread(new MyRunnable6());
  8. t.setName("t");
  9. t.start();
  10. for(int i = 1; i <= 10000; i++) {
  11. System.out.println(Thread.currentThread().getName() + "--->" + i);
  12. }
  13. }
  14. }
  15. class MyRunnable6 implements Runnable {
  16. @Override
  17. public void run() {
  18. for(int i = 1; i <= 10000; i++) {
  19. //每100个让位一次。
  20. if(i % 100 == 0){
  21. Thread.yield(); // 当前线程暂停一下,让给主线程。
  22. }
  23. System.out.println(Thread.currentThread().getName() + "--->" + i);
  24. }
  25. }
  26. }

8.2 线程合并

  1. public class ThreadTest13 {
  2. public static void main(String[] args) {
  3. System.out.println("main begin");
  4. Thread t = new Thread(new MyRunnable7());
  5. t.setName("t");
  6. t.start();
  7. //合并线程
  8. try {
  9. t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println("main over");
  14. }
  15. }
  16. class MyRunnable7 implements Runnable {
  17. @Override
  18. public void run() {
  19. for(int i = 0; i < 10000; i++){
  20. System.out.println(Thread.currentThread().getName() + "--->" + i);
  21. }
  22. }
  23. }

9. 关于多线程并发环境下,数据的安全问题

  • 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
  • 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。

9.1 什么时候数据在多线程并发的环境下会存在安全问题

9.1.1 三个条件

条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为

9.1.2 怎么解决线程安全问题

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)用排队执行解决线程安全问题,这种机制被称为:线程同步机制,专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是多线程并发,效率较高(异步就是并发)

同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型;线程排队执行,效率较低(同步就是排队)

  1. /*
  2. 银行账户
  3. 使用线程同步机制,解决线程安全问题。
  4. */
  5. public class Account {
  6. // 账号
  7. private String actno;
  8. // 余额
  9. private double balance; //实例变量。
  10. //对象
  11. Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)
  12. public Account() {
  13. }
  14. public Account(String actno, double balance) {
  15. this.actno = actno;
  16. this.balance = balance;
  17. }
  18. public String getActno() {
  19. return actno;
  20. }
  21. public void setActno(String actno) {
  22. this.actno = actno;
  23. }
  24. public double getBalance() {
  25. return balance;
  26. }
  27. public void setBalance(double balance) {
  28. this.balance = balance;
  29. }
  30. //取款的方法
  31. public void withdraw(double money){
  32. //int i = 100;
  33. //i = 101;
  34. // 以下这几行代码必须是线程排队的,不能并发。
  35. // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
  36. /*
  37. 线程同步机制的语法是:
  38. synchronized(){
  39. // 线程同步代码块。
  40. }
  41. synchronized后面小括号中传的这个“数据”是相当关键的。
  42. 这个数据必须是多线程共享的数据。才能达到多线程排队。
  43. ()中写什么?
  44. 那要看你想让哪些线程同步。
  45. 假设t1、t2、t3、t4、t5,有5个线程,
  46. 你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
  47. 你一定要在()中写一个t1 t2 t3共享的对象。而这个
  48. 对象对于t4 t5来说不是共享的。
  49. 这里的共享对象是:账户对象。
  50. 账户对象是共享的,那么this就是账户对象吧!!!
  51. 不一定是this,这里只要是多线程共享的那个对象就行。
  52. 在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
  53. 100个对象,100把锁。1个对象1把锁。
  54. 以下代码的执行原理?
  55. 1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
  56. 2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
  57. 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
  58. 占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
  59. 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
  60. 共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
  61. 直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
  62. t2占有这把锁之后,进入同步代码块执行程序。
  63. 这样就达到了线程排队执行。
  64. 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
  65. 执行的这些线程对象所共享的。
  66. */
  67. //Object obj2 = new Object();
  68. //synchronized (this){
  69. //synchronized (obj) {
  70. //synchronized ("abc") { // "abc"在字符串常量池当中。
  71. //synchronized (null) { // 报错:空指针。
  72. //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
  73. double before = this.getBalance();
  74. double after = before - money;
  75. try {
  76. Thread.sleep(1000);
  77. } catch (InterruptedException e) {
  78. e.printStackTrace();
  79. }
  80. this.setBalance(after);
  81. //}
  82. }
  83. }
  84. public class AccountThread extends Thread {
  85. // 两个线程必须共享同一个账户对象。
  86. private Account act;
  87. // 通过构造方法传递过来账户对象
  88. public AccountThread(Account act) {
  89. this.act = act;
  90. }
  91. public void run(){
  92. // run方法的执行表示取款操作。
  93. // 假设取款5000
  94. double money = 5000;
  95. // 取款
  96. // 多线程并发执行这个方法。
  97. //synchronized (this) { //这里的this是AccountThread对象,这个对象不共享!
  98. synchronized (act) { // 这种方式也可以,只不过扩大了同步的范围,效率更低了。
  99. act.withdraw(money);
  100. }
  101. System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
  102. }
  103. }
  104. public class Test {
  105. public static void main(String[] args) {
  106. // 创建账户对象(只创建1个)
  107. Account act = new Account("act-001", 10000);
  108. // 创建两个线程
  109. Thread t1 = new AccountThread(act);
  110. Thread t2 = new AccountThread(act);
  111. // 设置name
  112. t1.setName("t1");
  113. t2.setName("t2");
  114. // 启动线程取款
  115. t1.start();
  116. t2.start();
  117. }
  118. }

9.1.3 那些变量存在线程安全问题

实例变量:在堆中
静态变量:在方法区
局部变量:在栈中

以上三大变量中:
局部变量永远都不会存在线程安全问题
局部变量不共享,(一个线程一个栈)局部变量在栈中,所以局部变量永远都不会共享

实例变量在堆中,堆只有1个;静态变量在方法区中,方法区只有1个,堆和方法区都是多线程共享的,所以可能存在线程安全问题

局部变量+常量(不可变):不会有线程安全问题;成员变量:可能会有线程安全问题

如果使用局部变量的话:建议使用:StringBuilder;因为局部变量不存在线程安全问题,选择StringBuilder,StringBuffer效率比较低; ArrayList是非线程安全的;Vector是线程安全的; HashMap HashSet是非线程安全的;Hashtable是线程安全的

9.1.4 扩大同步范围

  1. - synchronized出现在实例方法中
  1. public class Account {
  2. // 账号
  3. private String actno;
  4. // 余额
  5. private double balance;
  6. public Account() {
  7. }
  8. public Account(String actno, double balance) {
  9. this.actno = actno;
  10. this.balance = balance;
  11. }
  12. public String getActno() {
  13. return actno;
  14. }
  15. public void setActno(String actno) {
  16. this.actno = actno;
  17. }
  18. public double getBalance() {
  19. return balance;
  20. }
  21. public void setBalance(double balance) {
  22. this.balance = balance;
  23. }
  24. //取款的方法
  25. /*
  26. 在实例方法上可以使用synchronized吗?可以的。
  27. synchronized出现在实例方法上,一定锁的是this,只能是this。
  28. 不能是其他的对象了,所以这种方式不灵活。
  29. 另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,
  30. 可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
  31. synchronized使用在实例方法上有什么优点? 代码写的少了,简洁
  32. 如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
  33. */
  34. public synchronized void withdraw(double money){
  35. double before = this.getBalance(); // 10000
  36. double after = before - money;
  37. try {
  38. Thread.sleep(1000);
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. this.setBalance(after);
  43. }
  44. }
  45. public class AccountThread extends Thread {
  46. // 两个线程必须共享同一个账户对象。
  47. private Account act;
  48. // 通过构造方法传递过来账户对象
  49. public AccountThread(Account act) {
  50. this.act = act;
  51. }
  52. public void run(){
  53. // run方法的执行表示取款操作。
  54. // 假设取款5000
  55. double money = 5000;
  56. // 取款
  57. // 多线程并发执行这个方法。
  58. act.withdraw(money);
  59. System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
  60. }
  61. }
  62. public class Test {
  63. public static void main(String[] args) {
  64. // 创建账户对象(只创建1个)
  65. Account act = new Account("act-001", 10000);
  66. // 创建两个线程
  67. Thread t1 = new AccountThread(act);
  68. Thread t2 = new AccountThread(act);
  69. // 设置name
  70. t1.setName("t1");
  71. t2.setName("t2");
  72. // 启动线程取款
  73. t1.start();
  74. t2.start();
  75. }
  76. }

10. synchronized

10.1 synchronized的三种写法

第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块;
}

  1. 第二种:在实例方法上使用synchronized<br />表示共享对象一定是this,并且同步代码块是整个方法体。<br /> <br /> 第三种:在静态方法上使用synchronized<br />表示找类锁。类锁永远只有1把,就算创建了100个对象,那类锁也只有一把;对象锁:1个对象1把锁,100个对象100把锁,类锁:100个对象,也可能只是1把类锁。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/933254/1598080317874-84f2c63c-0a44-41db-9494-99f22d5067ce.png#crop=0&crop=0&crop=1&crop=1&height=347&id=eTOIn&margin=%5Bobject%20Object%5D&name=image.png&originHeight=550&originWidth=778&originalType=binary&ratio=1&rotation=0&showTitle=false&size=305448&status=done&style=stroke&title=&width=491)

10.2 面试题

10.2.1

  1. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
  2. //不需要,因为doOther()方法没有synchronized
  3. public class Exam01 {
  4. public static void main(String[] args) throws InterruptedException {
  5. MyClass mc = new MyClass();
  6. Thread t1 = new MyThread(mc);
  7. Thread t2 = new MyThread(mc);
  8. t1.setName("t1");
  9. t2.setName("t2");
  10. t1.start();
  11. Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
  12. t2.start();
  13. }
  14. }
  15. class MyThread extends Thread {
  16. private MyClass mc;
  17. public MyThread(MyClass mc){
  18. this.mc = mc;
  19. }
  20. public void run(){
  21. if(Thread.currentThread().getName().equals("t1")){
  22. mc.doSome();
  23. }
  24. if(Thread.currentThread().getName().equals("t2")){
  25. mc.doOther();
  26. }
  27. }
  28. }
  29. class MyClass {
  30. public synchronized void doSome(){
  31. System.out.println("doSome begin");
  32. try {
  33. Thread.sleep(1000 * 10);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. System.out.println("doSome over");
  38. }
  39. public void doOther(){
  40. System.out.println("doOther begin");
  41. System.out.println("doOther over");
  42. }
  43. }

10.2.2

  1. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
  2. //需要
  3. public class Exam01 {
  4. public static void main(String[] args) throws InterruptedException {
  5. MyClass mc = new MyClass();
  6. Thread t1 = new MyThread(mc);
  7. Thread t2 = new MyThread(mc);
  8. t1.setName("t1");
  9. t2.setName("t2");
  10. t1.start();
  11. Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
  12. t2.start();
  13. }
  14. }
  15. class MyThread extends Thread {
  16. private MyClass mc;
  17. public MyThread(MyClass mc){
  18. this.mc = mc;
  19. }
  20. public void run(){
  21. if(Thread.currentThread().getName().equals("t1")){
  22. mc.doSome();
  23. }
  24. if(Thread.currentThread().getName().equals("t2")){
  25. mc.doOther();
  26. }
  27. }
  28. }
  29. class MyClass {
  30. public synchronized void doSome(){
  31. System.out.println("doSome begin");
  32. try {
  33. Thread.sleep(1000 * 10);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. System.out.println("doSome over");
  38. }
  39. public synchronized void doOther(){
  40. System.out.println("doOther begin");
  41. System.out.println("doOther over");
  42. }
  43. }

10.2.3

  1. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
  2. //不需要,因为MyClass对象是两个,两把锁。
  3. public class Exam01 {
  4. public static void main(String[] args) throws InterruptedException {
  5. MyClass mc1 = new MyClass();
  6. MyClass mc2 = new MyClass();
  7. Thread t1 = new MyThread(mc1);
  8. Thread t2 = new MyThread(mc2);
  9. t1.setName("t1");
  10. t2.setName("t2");
  11. t1.start();
  12. Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
  13. t2.start();
  14. }
  15. }
  16. class MyThread extends Thread {
  17. private MyClass mc;
  18. public MyThread(MyClass mc){
  19. this.mc = mc;
  20. }
  21. public void run(){
  22. if(Thread.currentThread().getName().equals("t1")){
  23. mc.doSome();
  24. }
  25. if(Thread.currentThread().getName().equals("t2")){
  26. mc.doOther();
  27. }
  28. }
  29. }
  30. class MyClass {
  31. public synchronized void doSome(){
  32. System.out.println("doSome begin");
  33. try {
  34. Thread.sleep(1000 * 10);
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. System.out.println("doSome over");
  39. }
  40. public synchronized void doOther(){
  41. System.out.println("doOther begin");
  42. System.out.println("doOther over");
  43. }
  44. }

10.2.4

  1. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
  2. //需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。
  3. public class Exam01 {
  4. public static void main(String[] args) throws InterruptedException {
  5. MyClass mc1 = new MyClass();
  6. MyClass mc2 = new MyClass();
  7. Thread t1 = new MyThread(mc1);
  8. Thread t2 = new MyThread(mc2);
  9. t1.setName("t1");
  10. t2.setName("t2");
  11. t1.start();
  12. Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
  13. t2.start();
  14. }
  15. }
  16. class MyThread extends Thread {
  17. private MyClass mc;
  18. public MyThread(MyClass mc){
  19. this.mc = mc;
  20. }
  21. public void run(){
  22. if(Thread.currentThread().getName().equals("t1")){
  23. mc.doSome();
  24. }
  25. if(Thread.currentThread().getName().equals("t2")){
  26. mc.doOther();
  27. }
  28. }
  29. }
  30. class MyClass {
  31. // synchronized出现在静态方法上是找类锁。
  32. public synchronized static void doSome(){
  33. System.out.println("doSome begin");
  34. try {
  35. Thread.sleep(1000 * 10);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. System.out.println("doSome over");
  40. }
  41. public synchronized static void doOther(){
  42. System.out.println("doOther begin");
  43. System.out.println("doOther over");
  44. }
  45. }

10.3 死锁问题

image.png

10.3.1 死锁代码实现

  1. public class DeadLock {
  2. public static void main(String[] args) {
  3. Object o1 = new Object();
  4. Object o2 = new Object();
  5. // t1和t2两个线程共享o1,o2
  6. Thread t1 = new MyThread1(o1,o2);
  7. Thread t2 = new MyThread2(o1,o2);
  8. t1.start();
  9. t2.start();
  10. }
  11. }
  12. class MyThread1 extends Thread{
  13. Object o1;
  14. Object o2;
  15. public MyThread1(Object o1,Object o2){
  16. this.o1 = o1;
  17. this.o2 = o2;
  18. }
  19. public void run(){
  20. synchronized (o1){
  21. try {
  22. Thread.sleep(1000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. synchronized (o2){
  27. }
  28. }
  29. }
  30. }
  31. class MyThread2 extends Thread {
  32. Object o1;
  33. Object o2;
  34. public MyThread2(Object o1,Object o2){
  35. this.o1 = o1;
  36. this.o2 = o2;
  37. }
  38. public void run(){
  39. synchronized (o2){
  40. try {
  41. Thread.sleep(1000);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. synchronized (o1){
  46. }
  47. }
  48. }
  49. }

11. 总结:开发中应该怎么解决线程安全问题

是一上来就选择线程同步(synchronized)吗?
不是,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,用户体验差;在不得已的情况下再选择线程同步机制

第一种方案:
尽量使用局部变量代替“实例变量和静态变量”

第二种方案:
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)

第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择线程同步机制(synchronized)了

12. 守护线程

  • java语言中线程分为两大类:

一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)

  • 守护线程的特点:

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束

注意:主线程main方法是一个用户线程。

  • 守护线程用在什么地方呢?

每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程,一直在那里看着,每到00:00的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了

  1. public class ThreadTest14 {
  2. public static void main(String[] args) {
  3. Thread t = new BakDataThread();
  4. t.setName("备份数据的线程");
  5. // 启动线程之前,将线程设置为守护线程
  6. t.setDaemon(true);
  7. t.start();
  8. // 主线程:主线程是用户线程
  9. for(int i = 0; i < 10; i++){
  10. System.out.println(Thread.currentThread().getName() + "--->" + i);
  11. try {
  12. Thread.sleep(1000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
  19. class BakDataThread extends Thread {
  20. public void run(){
  21. int i = 0;
  22. // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
  23. while(true){
  24. System.out.println(Thread.currentThread().getName() + "--->" + (++i));
  25. try {
  26. Thread.sleep(1000);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }
  32. }

13. 定时器

  • 定时器的作用:

间隔特定的时间,执行特定的程序。例如:每周要进行银行账户的总账操作、每天要进行数据的备份操作、在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见

  • 那么在java中其实可以采用多种方式实现:

可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

  1. import java.text.SimpleDateFormat;
  2. import java.util.Date;
  3. import java.util.Timer;
  4. import java.util.TimerTask;
  5. /*
  6. 使用定时器指定定时任务。
  7. */
  8. public class TimerTest {
  9. public static void main(String[] args) throws Exception {
  10. // 创建定时器对象
  11. Timer timer = new Timer();
  12. //Timer timer = new Timer(true); //守护线程的方式
  13. // 指定定时任务
  14. //timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
  15. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  16. Date firstTime = sdf.parse("2020-03-14 09:34:30");
  17. //timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
  18. // 每年执行一次。
  19. //timer.schedule(new LogTimerTask() , firstTime, 1000 * 60 * 60 * 24 * 365);
  20. //匿名内部类方式
  21. timer.schedule(new TimerTask(){
  22. @Override
  23. public void run() {
  24. // code....
  25. }
  26. } , firstTime, 1000 * 10);
  27. }
  28. }
  29. // 编写一个定时任务类
  30. // 假设这是一个记录日志的定时任务
  31. class LogTimerTask extends TimerTask {
  32. @Override
  33. public void run() {
  34. // 编写你需要执行的任务就行了。
  35. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  36. String strTime = sdf.format(new Date());
  37. System.out.println(strTime + ":成功完成了一次数据备份!");
  38. }
  39. }

14. 实现线程的第三种方式

  • 实现Callable接口。(JDK8新特性。)这种方式实现的线程可以获取线程的返回值,之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。 ```java import java.util.concurrent.Callable; // JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。 import java.util.concurrent.FutureTask;

/ 实现线程的第三种方式: 实现Callable接口 这种方式的优点:可以获取到线程的执行结果。 这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。 / public class ThreadTest15 { public static void main(String[] args) throws Exception {

  1. // 第一步:创建一个“未来任务类”对象。
  2. // 参数非常重要,需要给一个Callable接口实现类对象。
  3. FutureTask task = new FutureTask(new Callable() {
  4. // call()方法就相当于run方法。只不过这个有返回值
  5. @Override
  6. public Object call() throws Exception {
  7. // 线程执行一个任务,执行之后可能会有一个执行结果
  8. // 模拟执行
  9. System.out.println("call method begin");
  10. Thread.sleep(1000 * 10);
  11. System.out.println("call method end!");
  12. int a = 100;
  13. int b = 200;
  14. return a + b; //自动装箱(300结果变成Integer)
  15. }
  16. });
  17. // 创建线程对象
  18. Thread t = new Thread(task);
  19. // 启动线程
  20. t.start();
  21. // 这里是main方法,这是在主线程中。
  22. // 在主线程中,怎么获取t线程的返回结果?
  23. // get()方法的执行会导致“当前线程阻塞”
  24. Object obj = task.get();
  25. System.out.println("线程执行结果:" + obj);
  26. // main方法这里的程序要想执行必须等待get()方法的结束
  27. // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
  28. // 另一个线程执行是需要时间的。
  29. System.out.println("hello world!");
  30. }

}

  1. <a name="3U1fk"></a>
  2. ## 15. Object类中的wait和notify方法
  3. - wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是 Object类中自带的;wait方法和notify方法不是通过线程对象调用
  4. - wait()方法作用?
  5. Object o = new Object();<br />o.wait();<br />表示: 让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止;o.wait()方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态
  6. - notify()方法作用?
  7. Object o = new Object();<br />o.notify();<br />表示:唤醒正在o对象上等待的线程。<br />
  8. - notifyAll()方法:
  9. 这个方法是唤醒o对象上处于等待的所有线程<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/933254/1598074011664-5bd45586-e886-4a0f-a087-f8244f6b25ee.png#crop=0&crop=0&crop=1&crop=1&height=256&id=fEKaD&margin=%5Bobject%20Object%5D&name=image.png&originHeight=420&originWidth=811&originalType=binary&ratio=1&rotation=0&showTitle=false&size=48701&status=done&style=stroke&title=&width=494)
  10. <a name="fF5WW"></a>
  11. ### 15.1 生产者和消费者模式
  12. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/933254/1598075014731-7eff407f-3c98-4762-a875-46b0badbba92.png#crop=0&crop=0&crop=1&crop=1&height=296&id=aV2ET&margin=%5Bobject%20Object%5D&name=image.png&originHeight=483&originWidth=972&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65311&status=done&style=stroke&title=&width=595)
  13. > notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放
  14. <a name="7spZX"></a>
  15. ### 15.2 生产者和消费者模式代码实现
  16. ```java
  17. import java.util.ArrayList;
  18. import java.util.List;
  19. /*
  20. 1、使用wait方法和notify方法实现“生产者和消费者模式”
  21. 2、什么是“生产者和消费者模式”?
  22. 生产线程负责生产,消费线程负责消费。
  23. 生产线程和消费线程要达到均衡。
  24. 这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
  25. 3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
  26. 4、wait方法和notify方法建立在线程同步的基础之上。
  27. 因为多线程要同时操作一个仓库。有线程安全问题。
  28. 5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,
  29. 并且释放掉t线程之前占有的o对象的锁。
  30. 6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,
  31. 不会释放o对象上之前占有的锁。
  32. 7、模拟这样一个需求:
  33. 仓库我们采用List集合。
  34. List集合中假设只能存储1个元素。
  35. 1个元素就表示仓库满了。
  36. 如果List集合中元素个数是0,就表示仓库空了。
  37. 保证List集合中永远都是最多存储1个元素。
  38. 必须做到这种效果:生产1个消费1个。
  39. */
  40. public class ThreadTest16 {
  41. public static void main(String[] args) {
  42. // 创建1个仓库对象,共享的。
  43. List list = new ArrayList();
  44. // 创建两个线程对象
  45. // 生产者线程
  46. Thread t1 = new Thread(new Producer(list));
  47. // 消费者线程
  48. Thread t2 = new Thread(new Consumer(list));
  49. t1.setName("生产者线程");
  50. t2.setName("消费者线程");
  51. t1.start();
  52. t2.start();
  53. }
  54. }
  55. // 生产线程
  56. class Producer implements Runnable {
  57. // 仓库
  58. private List list;
  59. public Producer(List list) {
  60. this.list = list;
  61. }
  62. @Override
  63. public void run() {
  64. // 一直生产(使用死循环来模拟一直生产)
  65. while(true){
  66. // 给仓库对象list加锁。
  67. synchronized (list){
  68. if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
  69. try {
  70. // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
  71. list.wait();
  72. } catch (InterruptedException e) {
  73. e.printStackTrace();
  74. }
  75. }
  76. // 程序能够执行到这里说明仓库是空的,可以生产
  77. Object obj = new Object();
  78. list.add(obj);
  79. System.out.println(Thread.currentThread().getName() + "--->" + obj);
  80. // 唤醒消费者进行消费
  81. list.notifyAll();
  82. }
  83. }
  84. }
  85. }
  86. // 消费线程
  87. class Consumer implements Runnable {
  88. // 仓库
  89. private List list;
  90. public Consumer(List list) {
  91. this.list = list;
  92. }
  93. @Override
  94. public void run() {
  95. // 一直消费
  96. while(true){
  97. synchronized (list) {
  98. if(list.size() == 0){
  99. try {
  100. // 仓库已经空了。
  101. // 消费者线程等待,释放掉list集合的锁
  102. list.wait();
  103. } catch (InterruptedException e) {
  104. e.printStackTrace();
  105. }
  106. }
  107. // 程序能够执行到此处说明仓库中有数据,进行消费。
  108. Object obj = list.remove(0);
  109. System.out.println(Thread.currentThread().getName() + "--->" + obj);
  110. // 唤醒生产者生产。
  111. list.notifyAll();
  112. }
  113. }
  114. }
  115. }