多线程

概述

进程是程序执行的最小单位,线程是进程的一部分,即一个进程对应多个线程,一个进程也可以启动多个线程。对于Java程序来说,当在DOS命令窗口输入:java HelloWorld 回车之后,会先启动JVM进程,JVM再启动一个主线程调用main()方法,同时再启动一个GC线程回收垃圾。因此整个操作是多线程。使用多线程之后,main方法结束了,JVM也不一定结束。详见下图:多线程 - 图1
JVM内存模型中,堆和方法区是多线程共享的,栈是各线程相互独立的。为什么栈是私有的?因为一个栈对应一个线程,线程之间执行各自任务,互不干扰,多线程并发是为了提高系统效率。
单核CPU能做到真正的多线程并发吗?不能。但是给人的感觉是能,就像电影院胶卷播放电影。
分析以下代码,总共指向了几个线程?(除了GC)

  1. package thread;
  2. public class ThreadTest01 {
  3. public static void main(String[] args) {
  4. System.out.println("main begin...");
  5. m1();
  6. System.out.println("main finish...");
  7. }
  8. private static void m1() {
  9. System.out.println("m1 begin...");
  10. m2();
  11. System.out.println("m1 finish...");
  12. }
  13. private static void m2() {
  14. System.out.println("m2 begin...");
  15. m3();
  16. System.out.println("m2 finish...");
  17. }
  18. private static void m3() {
  19. System.out.println("m3...");
  20. }
  21. }
  22. 输出:
  23. main begin...
  24. m1 begin...
  25. m2 begin...
  26. m3...
  27. m2 finish...
  28. m1 finish...
  29. main finish...
  30. //事实上只有一个,即main线程。以上方法都是在main线程中完成的。

线程实现的几种方式★★

方式一:继承Thread类,并重写run方法

  1. package com.simon;
  2. /**
  3. * 实现多线程的方式一:继承Thread类,并重写run方法
  4. */
  5. public class ThreadAcheive01 {
  6. public static void main(String[] args) {
  7. //这是一个main方法,这里的代码属于主线程,在主栈中执行。
  8. //新建一个分支线程的对象
  9. MyThread mt = new MyThread();
  10. //启动线程
  11. /**
  12. * start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,
  13. * 这段代码任务完成之后,瞬间就结束了,这段代码的任务只是为了开辟一个新的栈空间,
  14. * 只要新的栈空间开出来,start()方法就结束了,线程就启动成功了。
  15. * 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
  16. * run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的。
  17. * 如果将mt.run()代替mt.start(),系统是不会启动线程的,也不会分配新的分支栈,
  18. * 不能并发,本质上是单线程.方法体中代码执行顺序,总是自上而下的。
  19. * 是先启动分支栈,然后主线程再执行循环语句
  20. */
  21. mt.start();
  22. for (int i = 0; i < 10; i++) {
  23. System.out.println("主线程--->" + i);
  24. }
  25. }
  26. }
  27. class MyThread extends Thread{
  28. @Override
  29. public void run() {
  30. //编写程序,这段程序运行在分支线程中(分支栈)
  31. for (int i = 0; i < 10; i++) {
  32. System.out.println("分支线程--->" + i);
  33. }
  34. }
  35. }
  36. //输出如下:
  37. // 主线程--->0
  38. //分支线程--->0
  39. //主线程--->1
  40. //分支线程--->1
  41. //主线程--->2
  42. //分支线程--->2
  43. //主线程--->3
  44. //分支线程--->3
  45. //分支线程--->4
  46. //主线程--->4
  47. //分支线程--->5
  48. //主线程--->5
  49. //分支线程--->6
  50. //分支线程--->7
  51. //分支线程--->8
  52. //分支线程--->9
  53. //主线程--->6
  54. //主线程--->7
  55. //主线程--->8
  56. //主线程--->9

一幅图来区别run和start方法

多线程 - 图2多线程 - 图3

方式二:编写一个类实现Runnable接口

  1. package thread;
  2. /**
  3. * 实现编程的第二种方式,编写一个类实现java.lang.Runnable接口
  4. */
  5. public class ThreadAcheive02 {
  6. public static void main(String[] args) {
  7. //创建一个可运行对象
  8. MyRunnable mr = new MyRunnable();
  9. //将可运行对象封装成一个线程对象
  10. //注意,有一个Thread类的构造方法就是这样,参数是一个可运行对象
  11. Thread t = new Thread(mr);
  12. //启动线程
  13. t.start();
  14. for (int i = 0; i < 10; i++) {
  15. System.out.println("主线程--->" + i);
  16. }
  17. }
  18. }
  19. //这并不是一个线程类,而是一个可运行类,它不是一个线程
  20. class MyRunnable implements Runnable{
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 10; i++) {
  24. System.out.println("分支线程--->" + i);
  25. }
  26. }
  27. }
  28. //输出如下:
  29. // 主线程--->0
  30. //分支线程--->0
  31. //主线程--->1
  32. //分支线程--->1
  33. //分支线程--->2
  34. //主线程--->2
  35. //分支线程--->3
  36. //主线程--->3
  37. //分支线程--->4
  38. //主线程--->4
  39. //分支线程--->5
  40. //主线程--->5
  41. //分支线程--->6
  42. //主线程--->6
  43. //分支线程--->7
  44. //主线程--->7
  45. //分支线程--->8
  46. //主线程--->8
  47. //分支线程--->9
  48. //主线程--->9

方式二相对于方式一其实更好,在实现Runnable接口的基础上还可以继承,一句话:面向接口编程。所以,既然面向接口编程,那么可以采取匿名内部类的方式去实现:

方式二的改进:匿名内部类

  1. package thread;
  2. /**
  3. * 方式二采用匿名内部类
  4. */
  5. public class ThreadAcheiveTest2_Improve {
  6. public static void main(String[] args) {
  7. //接口是不能new对象的,但是这里,Runnable不再是接口,而是一个匿名内部类,所以可以new
  8. Thread t = new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. for (int i = 0; i < 10; i++) {
  12. System.out.println("分支线程-->" + (i + 1));
  13. }
  14. }
  15. });
  16. t.start();
  17. for (int i = 0; i < 10; i++) {
  18. System.out.println("主线程-->" + (i + 1));
  19. }
  20. }
  21. }
  22. //输出如下:
  23. // 主线程-->1
  24. //主线程-->2
  25. //主线程-->3
  26. //分支线程-->1
  27. //主线程-->4
  28. //主线程-->5
  29. //主线程-->6
  30. //分支线程-->2
  31. //分支线程-->3
  32. //主线程-->7
  33. //分支线程-->4
  34. //主线程-->8
  35. //主线程-->9
  36. //分支线程-->5
  37. //主线程-->10
  38. //分支线程-->6
  39. //分支线程-->7
  40. //分支线程-->8
  41. //分支线程-->9
  42. //分支线程-->10

方式三:实现Callable接口

  1. package thread;
  2. import java.util.concurrent.Callable;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.FutureTask;
  5. //java.util.concurrent就是我们常说的JUC,属于Java的并发包,老jdk没有这个包,是jdk的新特性
  6. /**
  7. * 实现线程的第三种方式:实现Callable接口(jdk8新特性)
  8. * 这种方式实现的线程可以获取线程的返回值。
  9. * 场景:系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,
  10. * 这种情况下最好实现Callable接口
  11. */
  12. public class ThreadAcheiveTest03 {
  13. public static void main(String[] args) {
  14. //创建一个“未来任务类”对象。
  15. // Callable是一个接口,可用匿名内部类,call方法相当于run方法,只不过有返回值
  16. FutureTask task = new FutureTask(new Callable() {
  17. @Override
  18. public Object call() throws Exception {
  19. //线程执行一个任务,执行之后可能会有一个结果,以下模拟执行
  20. System.out.println("call method begin..");
  21. Thread.sleep(1000*5);
  22. System.out.println("call method ends..");
  23. int a=10,b=20;
  24. return a+b; //自动装箱(基本数据类型变为引用数据类型)
  25. }
  26. });
  27. //创建线程对象
  28. Thread t = new Thread(task);
  29. //启动线程
  30. t.start();
  31. //在main方法即主线程中,如何获得t线程的返回结果?
  32. try {
  33. //这行代码会让主线程受阻么?
  34. //main方法想要执行必须等待get方法的结束,而get方法是为了拿到另一个线程的执行结果
  35. //这一行代码肯定是在call方法执行完并且有返回值以后才执行,所以main线程要受阻
  36. Object obj = task.get();
  37. System.out.println("线程执行结果:" + obj);
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. } catch (ExecutionException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println("hello,world");
  44. }
  45. }
  46. //内容输出:
  47. //call method begin..
  48. //call method ends..
  49. //线程执行结果:30
  50. //hello,world
  51. //优缺点:
  52. //优点:可获得线程的执行结果
  53. //缺点:执行效率低--在获取t线程执行结果时受阻。其实优缺点都讲的返回结果的问题。

线程的生命周期

线程的生命周期包括:新建、就绪、运行、阻塞和死亡五个时期。其中,在代码中能体现的是,就绪调用start方法,运行时调用run方法,阻塞是放弃CPU执行权(这个不算),死亡是run方法执行结束。
image.png

关于线程的几个方法

  1. package thread;
  2. /**
  3. * 三个内容:
  4. * 1、怎么获取当前线程的对象 static Thread currentThread();
  5. * 2、获取线程对象的名字 String name = 线程对象.getName();
  6. * 3、修改线程对象的名字 线程对象.setName("线程名字");
  7. * 4、当线程没有设置名字的时候,默认名字的规律是:Thread-0、Thread-1、Thread-2...
  8. */
  9. public class ThreadTest02 {
  10. public static void main(String[] args) {
  11. System.out.println(Thread.currentThread().getName()); //main
  12. MyThread2 mt2 = new MyThread2();
  13. //若没有设置线程名字,则默认为:Thread-0
  14. mt2.setName("设置当前线程名字为:分支线程A");
  15. String mt2Name = mt2.getName();
  16. System.out.println(mt2Name);
  17. mt2.start();
  18. for (int i = 0; i < 10; i++) {
  19. System.out.println("主线程-->" + (i + 1));
  20. }
  21. }
  22. }
  23. class MyThread2 extends Thread{
  24. @Override
  25. public void run() {
  26. for (int i = 0; i < 10; i++) {
  27. System.out.println("分支线程--->" + (i + 1));
  28. }
  29. }
  30. }
  31. //输出内容如下:
  32. //设置当前线程名字为:分支线程A
  33. //主线程-->1
  34. //主线程-->2
  35. //主线程-->3
  36. //主线程-->4
  37. //主线程-->5
  38. //主线程-->6
  39. //分支线程--->1
  40. //主线程-->7
  41. //分支线程--->2
  42. //主线程-->8
  43. //分支线程--->3
  44. //主线程-->9
  45. //分支线程--->4
  46. //主线程-->10
  47. //分支线程--->5
  48. //分支线程--->6
  49. //分支线程--->7
  50. //分支线程--->8
  51. //分支线程--->9
  52. //分支线程--->10
  1. package thread;
  2. /**
  3. * 关于线程的sleep方法:
  4. * static void sleep(long mills)
  5. * 1、静态方法
  6. * 2、参数是毫秒
  7. * 3、作用:让当前线程进入休眠即阻塞状态,放弃CPU执行权给其他线程使用.
  8. * 4、每隔多久执行一次某些代码
  9. */
  10. public class ThreadTest03 {
  11. //让当前线程休眠
  12. public static void main(String[] args) {
  13. try {
  14. Thread.sleep(1000*3);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. System.out.println("hello,Thread sleep method...");
  19. }
  20. }
  1. package thread;
  2. /**
  3. * 关于Thread.sleep()方法的面试题
  4. */
  5. public class ThreadTest04 {
  6. public static void main(String[] args) throws InterruptedException {
  7. Thread t = new MyThread4();
  8. t.setName("t");
  9. t.start();
  10. //调用sleep方法
  11. //问题:这行代码会让线程t进入休眠状态吗?
  12. //并不会,因为在执行的时候还是会转成:Thread.sleep(1000*5);
  13. //这行代码的作用是,让当前线程进入休眠,也就是说main线程进入休眠
  14. //这种感觉和下面这个Test类的情形有点像
  15. t.sleep(1000*3);
  16. //3秒之后才会输出
  17. System.out.println("hello,world");
  18. }
  19. }
  20. class MyThread4 extends Thread{
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 10; i++) {
  24. System.out.println(Thread.currentThread().getName() + "-->" + (i + 1));
  25. }
  26. }
  27. }
  28. //输出:
  29. //t-->1
  30. //t-->2
  31. //t-->3
  32. //t-->4
  33. //t-->5
  34. //t-->6
  35. //t-->7
  36. //t-->8
  37. //t-->9
  38. //t-->10
  39. //hello,world
  1. package demo2;
  2. public class Test {
  3. public static void main(String[] args) {
  4. Test.doSome(); //do some
  5. Test test = new Test();
  6. test.doSome(); //do some
  7. test = null;
  8. test.doSome(); //do some,并没有出现空指针异常
  9. }
  10. public static void doSome(){
  11. System.out.println("do some");
  12. }
  13. }
  1. package thread;
  2. /**
  3. * sleep睡眠太久了,如果希望半道上醒来,该怎么办?
  4. * 也就是说,如何叫醒正在睡眠的线程?
  5. */
  6. public class ThreadTest05 {
  7. public static void main(String[] args) {
  8. Thread t = new Thread(new MyRunnable2());
  9. t.setName("t");
  10. t.start();
  11. //希望5秒之后醒来
  12. try {
  13. Thread.sleep(5000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. //一盆冷水泼过来
  18. //本质上是让MyRunnable2的run方法的try语句块出现异常,异常机制
  19. t.interrupt();
  20. }
  21. }
  22. class MyRunnable2 implements Runnable{
  23. @Override
  24. public void run() {
  25. System.out.println(Thread.currentThread().getName() + "--->begin");
  26. try {
  27. //这里sleep为什么只能try catch...?因为run方法在父类中没有抛出任何异常,子类异常不能更多
  28. Thread.sleep(1000*60*60*24*365);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. System.out.println(Thread.currentThread().getName() + "--->end");
  33. }
  34. }
  35. //输出内容如下:
  36. //t--->begin
  37. //java.lang.InterruptedException: sleep interrupted
  38. // at java.lang.Thread.sleep(Native Method)
  39. // at thread.MyRunnable2.run(ThreadTest05.java:31)
  40. // at java.lang.Thread.run(Thread.java:748)
  41. //t--->end
  1. package thread;
  2. /**
  3. * 在Java中如何强行终止一个线程的执行?
  4. * 缺点是什么??容易损坏数据
  5. */
  6. public class ThreadTest06 {
  7. public static void main(String[] args) {
  8. Thread t = new Thread(new MyRunnable3());
  9. t.setName("t");
  10. t.start();
  11. try {
  12. Thread.sleep(1000*5);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. t.stop();
  17. }
  18. }
  19. class MyRunnable3 implements Runnable{
  20. @Override
  21. public void run() {
  22. for (int i = 0; i < 10; i++) {
  23. System.out.println(Thread.currentThread().getName() + "-->"+ (i+1));
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }
  31. }
  32. //输出如下:
  33. //t-->1
  34. //t-->2
  35. //t-->3
  36. //t-->4
  37. //t-->5
  1. package com.simon;
  2. /**
  3. * 如何合理终止一个线程的执行? 改标志位!
  4. */
  5. public class ThreadTest07 {
  6. public static void main(String[] args) {
  7. MyRunnable4 mr = new MyRunnable4();
  8. Thread t = new Thread(mr);
  9. t.setName("t");
  10. t.start();
  11. try {
  12. Thread.sleep(1000*3);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. mr.run =false;
  17. }
  18. }
  19. class MyRunnable4 implements Runnable{
  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+1));
  26. try {
  27. Thread.sleep(1000);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. }else {
  32. //在结束之前有什么善后工作要做的,都在return之前做完,就能保证数据不丢失
  33. return;
  34. }
  35. }
  36. }
  37. }
  38. //输出结果如下:
  39. //t-->1
  40. //t-->2
  41. //t-->3
  1. package mythread;
  2. public class ThreadTest02 {
  3. public static void main(String[] args) {
  4. Thread t = new Thread(new MyRunnable());
  5. t.start();
  6. for (int i = 0; i < 10000; i++) {
  7. System.out.println(Thread.currentThread().getName() + ":" + i);
  8. }
  9. }
  10. }
  11. class MyRunnable implements Runnable{
  12. @Override
  13. public void run() {
  14. for (int i = 0; i < 10000; i++) {
  15. if (i%1000==0){
  16. //每隔1000,即当999-->1000,3999-->4000时,两者都不会连续而是硬生生让给了main线程
  17. Thread.yield();
  18. }
  19. System.out.println(Thread.currentThread().getName() + ":" + i);
  20. }
  21. }
  22. }

image.pngimage.png这就是线程礼让,让一点,程度轻微。

  1. package mythread;
  2. /**
  3. * 线程合并:这里,t.join()是指t线程加入进来之后,
  4. * main线程等待t线程结束后再执行,让整个线程,程度较深。
  5. */
  6. public class ThreadTest03 {
  7. public static void main(String[] args) {
  8. System.out.println("main begin...");
  9. Thread t = new Thread(new MyRunnable2());
  10. t.start();
  11. try {
  12. t.join();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println("main over...");
  17. }
  18. }
  19. class MyRunnable2 implements Runnable{
  20. @Override
  21. public void run() {
  22. for (int i = 0; i < 5; i++) {
  23. System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
  24. }
  25. }
  26. }
  27. //内容输出为:
  28. //main begin...
  29. //Thread-0:1
  30. //Thread-0:2
  31. //Thread-0:3
  32. //Thread-0:4
  33. //Thread-0:5
  34. //main over...

线程安全

假定有以下情况,两个人都去ATM上输入相同账号和密码交易,同时进行取款的动作,并且取完。由于网络存在延迟,总有一个人先取完钱,如果银行存储的数据未能及时变更,那么第二个人应该也能继续取钱,这就意味着,由于数据不同步导致的数据安全会给日常生活带来极大的麻烦。所以,数据安全是数据交易的生命,如果谈不上安全,那么就没必要进行数据交易了。由此可知,什么情况下数据不安全?①多线程并发 ②有共享的数据 ③数据有修改行为 解决办法是让线程排队执行,宁可牺牲效率,也要保证数据同步。
这里,存在同步和异步的概念,同步就是排队、相互等待,异步就是并发,谁也不用等谁。
具体解决线程并发的问题:synchronized,参数很关键:此参数为多线程共享对象 。每个Java对象都有1把锁,这个锁就是标记。注意:这个共享对象一定要选好,一定是你需要排队执行的这些线程所共享的对象。如何体现在线程周期上? 运行状态—->锁池
多线程 - 图7

一个账户类的并发安全事件★★

  1. package mythread;
  2. public class Account {
  3. private String no;
  4. private double balance;
  5. Object obj = new Object();
  6. public String getNo() {
  7. return no;
  8. }
  9. public void setNo(String no) {
  10. this.no = no;
  11. }
  12. public double getBalance() {
  13. return balance;
  14. }
  15. public void setBalance(double balance) {
  16. this.balance = balance;
  17. }
  18. public Account(String no, double balance) {
  19. this.no = no;
  20. this.balance = balance;
  21. }
  22. public Account() {
  23. }
  24. //取款方法
  25. public void withdraw(double money){
  26. //synchronized()中的小括号的数据必须是共享的,才能达到多线程排队的效果
  27. //()写什么?哪些对象需要排队,那么小括号就写哪些对象的共享数据。
  28. // synchronized (this){//不一定是this,只要是共享的就行,但this最佳
  29. Object obj2 = new Object();
  30. // synchronized (obj){//可以,因为是实例变量,共享
  31. // synchronized (obj2){//不可以,因为是局部变量起不到排队即同步的效果
  32. synchronized ("1"){//可以,因为常量池只有一份,共享
  33. //取款之前:
  34. double before = this.getBalance();
  35. //取款之后:
  36. double after = balance-money;
  37. try {
  38. //模拟网络延迟,此时一定会出问题
  39. Thread.sleep(1000);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. //更新余额
  44. this.setBalance(after);
  45. }
  46. }
  47. }
  1. package mythread;
  2. /**
  3. * 模拟2个线程对同一个账户做取款操作
  4. */
  5. public class AccountThread extends Thread{
  6. //保证2个线程共享同一个账户对象
  7. private Account act;
  8. public AccountThread(Account act) {
  9. this.act = act;
  10. }
  11. @Override
  12. public void run() {
  13. //run方法的执行表示取款操作,这里假设取款5000
  14. double money = 5000;
  15. //多线程并发执行
  16. act.withdraw(money);
  17. System.out.println(act.getNo() +"取款成功,余额为:"+ act.getBalance());
  18. }
  19. }
  1. package mythread;
  2. public class AccountThreadTest {
  3. public static void main(String[] args) {
  4. Account act = new Account("act-001", 10000.0);
  5. Thread at1 = new AccountThread(act);
  6. Thread at2 = new AccountThread(act);
  7. at1.start();
  8. at2.start();
  9. }
  10. }

image.png

安全事件的解决方式

如何解决以上的问题呢?用同步代码块synchronized去修饰容易出现并发的那些代码块
修改withdraw()方法的代码:

  1. //取款方法
  2. public void withdraw(double money){
  3. //synchronized()中的小括号的数据必须是共享的,才能达到多线程排队的效果
  4. //()写什么?哪些对象需要排队,那么小括号就写哪些对象的共享数据。
  5. synchronized (this){//不一定是this,只要是共享的就行
  6. //取款之前:
  7. double before = this.getBalance();
  8. //取款之后:
  9. double after = balance-money;
  10. //模拟网络延迟,此时一定会出问题
  11. try {
  12. Thread.sleep(1000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. //更新余额
  17. this.setBalance(after);
  18. }
  19. }
  20. //输出:
  21. //act-001取款成功,余额为:5000.0
  22. //act-001取款成功,余额为:0.0

多线程 - 图9
however,synchronized也可出现在实例方法上:

  1. //取款方法
  2. /*
  3. synchronized出现在实例方法上,一定是锁的this,所以这种方式不灵活
  4. 缺点之二是锁的范围是整个代码块,导致整个程序的执行效率降低,这种方式不常用
  5. */
  6. public synchronized void withdraw(double money){
  7. //取款之前:
  8. double before = this.getBalance();
  9. //取款之后:
  10. double after = balance-money;
  11. //模拟网络延迟,此时一定会出问题
  12. try {
  13. Thread.sleep(1000);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. //更新余额
  18. this.setBalance(after);
  19. }

哪些变量有线程安全的问题?

原则:越私有,线程越安全。所以,局部变量线程安全,实例变量在堆中,静态变量在方法区中,堆和方法区都是共享的,所以实例变量和静态变量是不安全的。使用局部变量的话,建议使用StringBuilder,因为StringBuffer效率比较低。问题:为啥是变量?锁的不应该是方法或者代码块吗?
Synchronized的三种用法:
synchronized的三种写法: ①同步代码块 ②实例方法上用synchronized,一个对象一把对象锁 ③静态方法上用synchronized,就算对象创建100个,类锁永远只有1把

面试题

—四种变形:

  1. package thread.exam;
  2. /**
  3. * 面试题:doOther的执行是否需要doSome方法的结束?
  4. */
  5. public class Exam1 {
  6. public static void main(String[] args) {
  7. MyClass mc = new MyClass();
  8. Thread t1 = new MyThread(mc);
  9. Thread t2 = new MyThread(mc);
  10. t1.setName("t1");
  11. t2.setName("t2");
  12. t1.start();
  13. try {
  14. //睡眠的作用是为了保证t1先执行
  15. Thread.sleep(1000);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. t2.start();
  20. }
  21. }
  22. class MyThread extends Thread{
  23. private MyClass mc;
  24. MyThread(MyClass mc){
  25. this.mc = mc;
  26. }
  27. @Override
  28. public void run() {
  29. if (Thread.currentThread().getName().equals("t1")){
  30. mc.doSome();
  31. }
  32. if (Thread.currentThread().getName().equals("t2")){
  33. mc.doOther();
  34. }
  35. }
  36. }
  37. class MyClass{
  38. public synchronized void doSome(){
  39. System.out.println("doSome begin..");
  40. try {
  41. Thread.sleep(1000*5);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. System.out.println("doSome over..");
  46. }
  47. public void doOther(){
  48. System.out.println("doOther begin...");
  49. System.out.println("doOther over...");
  50. }
  51. }
  52. //执行结果:
  53. //doSome begin..
  54. //doOther begin...
  55. //doOther over...
  56. //doSome over..
  1. package thread.exam2;
  2. /**
  3. * 面试题:doOther的执行是否需要doSome方法的结束?
  4. */
  5. public class Exam2 {
  6. public static void main(String[] args) {
  7. MyClass mc = new MyClass();
  8. Thread t1 = new MyThread(mc);
  9. Thread t2 = new MyThread(mc);
  10. t1.setName("t1");
  11. t2.setName("t2");
  12. t1.start();
  13. try {
  14. //睡眠的作用是为了保证t1先执行
  15. Thread.sleep(1000);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. t2.start();
  20. }
  21. }
  22. class MyThread extends Thread{
  23. private MyClass mc;
  24. MyThread(MyClass mc){
  25. this.mc = mc;
  26. }
  27. @Override
  28. public void run() {
  29. if (Thread.currentThread().getName().equals("t1")){
  30. mc.doSome();
  31. }
  32. if (Thread.currentThread().getName().equals("t2")){
  33. mc.doOther();
  34. }
  35. }
  36. }
  37. class MyClass{
  38. public synchronized void doSome(){
  39. System.out.println("doSome begin..");
  40. try {
  41. Thread.sleep(1000*5);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. System.out.println("doSome over..");
  46. }
  47. public synchronized void doOther(){
  48. System.out.println("doOther begin...");
  49. System.out.println("doOther over...");
  50. }
  51. }
  52. //执行结果:
  53. //doSome begin..
  54. //doSome over..
  55. //doOther begin...
  56. //doOther over...
  1. package thread.exam3;
  2. /**
  3. * 面试题:doOther的执行是否需要doSome方法的结束?
  4. */
  5. public class Exam3 {
  6. public static void main(String[] args) {
  7. MyClass mc1 = new MyClass();
  8. MyClass mc2 = new MyClass();
  9. Thread t1 = new MyThread(mc1);
  10. Thread t2 = new MyThread(mc2);
  11. t1.setName("t1");
  12. t2.setName("t2");
  13. t1.start();
  14. try {
  15. //睡眠的作用是为了保证t1先执行
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. t2.start();
  21. }
  22. }
  23. class MyThread extends Thread{
  24. private MyClass mc;
  25. MyThread(MyClass mc){
  26. this.mc = mc;
  27. }
  28. @Override
  29. public void run() {
  30. if (Thread.currentThread().getName().equals("t1")){
  31. mc.doSome();
  32. }
  33. if (Thread.currentThread().getName().equals("t2")){
  34. mc.doOther();
  35. }
  36. }
  37. }
  38. class MyClass{
  39. public synchronized void doSome(){
  40. System.out.println("doSome begin..");
  41. try {
  42. Thread.sleep(1000*5);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. System.out.println("doSome over..");
  47. }
  48. public synchronized void doOther(){
  49. System.out.println("doOther begin...");
  50. System.out.println("doOther over...");
  51. }
  52. }
  53. //执行结果:
  54. //doSome begin..
  55. //doOther begin...
  56. //doOther over...
  57. //doSome over..
  58. //对象锁有2把锁,各自执行各自的
  1. package thread.exam4;
  2. /**
  3. * 面试题:doOther的执行是否需要doSome方法的结束?
  4. */
  5. public class Exam4 {
  6. public static void main(String[] args) {
  7. MyClass mc1 = new MyClass();
  8. MyClass mc2 = new MyClass();
  9. Thread t1 = new MyThread(mc1);
  10. Thread t2 = new MyThread(mc2);
  11. t1.setName("t1");
  12. t2.setName("t2");
  13. t1.start();
  14. try {
  15. //睡眠的作用是为了保证t1先执行
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. t2.start();
  21. }
  22. }
  23. class MyThread extends Thread{
  24. private MyClass mc;
  25. MyThread(MyClass mc){
  26. this.mc = mc;
  27. }
  28. @Override
  29. public void run() {
  30. if (Thread.currentThread().getName().equals("t1")){
  31. mc.doSome();
  32. }
  33. if (Thread.currentThread().getName().equals("t2")){
  34. mc.doOther();
  35. }
  36. }
  37. }
  38. class MyClass{
  39. public synchronized static void doSome(){
  40. System.out.println("doSome begin..");
  41. try {
  42. Thread.sleep(1000*5);
  43. } catch (InterruptedException e) {
  44. e.printStackTrace();
  45. }
  46. System.out.println("doSome over..");
  47. }
  48. public synchronized static void doOther(){
  49. System.out.println("doOther begin...");
  50. System.out.println("doOther over...");
  51. }
  52. }
  53. //执行结果:
  54. //doSome begin..
  55. //doSome over..
  56. //doOther begin...
  57. //doOther over...
  58. //类锁只有1把,需要等待

手写一个死锁★★

  1. package thread.deadlock;
  2. /**
  3. * 死锁代码要求会写
  4. * 一般面试官会要求手写,只有会写的,才会在开发中注意这事,因为死锁很难调试
  5. */
  6. public class DeadLock {
  7. public static void main(String[] args) {
  8. Object o1 = new Object();
  9. Object o2 = new Object();
  10. //t1和t2两个线程共享o1,o2
  11. Thread t1 = new MyThread1(o1, o2);
  12. Thread t2 = new MyThread2(o1,o2);
  13. t1.start();
  14. t2.start();
  15. }
  16. }
  17. class MyThread1 extends Thread{
  18. Object o1;
  19. Object o2;
  20. public MyThread1(Object o1, Object o2) {
  21. this.o1 = o1;
  22. this.o2 = o2;
  23. }
  24. @Override
  25. public void run() {
  26. synchronized (o1){
  27. try {
  28. //确保程序一定死锁
  29. Thread.sleep(1000);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. synchronized (o2){}
  34. }
  35. }
  36. }
  37. class MyThread2 extends Thread{
  38. Object o1;
  39. Object o2;
  40. public MyThread2(Object o1, Object o2) {
  41. this.o1 = o1;
  42. this.o2 = o2;
  43. }
  44. @Override
  45. public void run() {
  46. synchronized (o2){
  47. try {
  48. Thread.sleep(1000);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. synchronized (o1){}
  53. }
  54. }
  55. }

日常开发如何解决线程安全的问题?

尽量不要首选synchronized,因为synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低会导致用户体验差,在不得已的情况下再选择线程同步机制。
第一种方案,尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全的问题了)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,线程同步机制。

守护线程(后台线程)

Java语言中线程分为2大类:用户线程和守护线程。守护线程又叫后台线程,一个典型的守护线程就是垃圾回收线程。一般一个守护线程是一个死循环,所有的用户线程一旦结束,守护线程就自动结束。(注:主线程main方法是一个用户线程)守护线程的用处:比如每天0:00的时候需要数据自动备份,这个需要使用到定时器,并且我们可以将定时器设置为守护线程,一直在那看着,每到0:00就备份一次,所有的用户线程如果结束了,守护线程就自动退出,没有必要再进行数据备份了。

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

定时器★★

定时器是每隔特定时间,执行特定的程序。
在Java中其实可以采用多种方式实现:可以使用sleep方法,但比较low;也可以使用java.util.Timer,可以直接用,但现在很多高级框架都是支持定时任务的,所以这种方式使用不频繁。目前实际开发中,使用较多的是Spring框架提供的SpringTask,这个框架只要进行简单的配置,就能完成定时器的任务。

  1. package thread;
  2. import java.text.ParseException;
  3. import java.text.SimpleDateFormat;
  4. import java.util.Date;
  5. import java.util.Timer;
  6. import java.util.TimerTask;
  7. /**
  8. * 使用定时器指定定时任务
  9. */
  10. public class TimerTest {
  11. public static void main(String[] args) throws ParseException {
  12. //创建定时器对象
  13. Timer timer = new Timer();
  14. //创建定时器对象并指定为守护线程
  15. // Timer timer = new Timer(true);
  16. // timer.schedule(定时任务,第一次执行时间,间隔多久执行一次)
  17. //注意:这里的第一次执行时间是到点才执行,不可以是过去的时间,否则从现在的时间算起
  18. SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSS");
  19. Date firstTime = sdf.parse("2021/10/13 11:35:32 893");
  20. //第一个参数其实可以使用匿名内部类
  21. timer.schedule(new LogTimerTask(),firstTime,1000*3);
  22. }
  23. }
  24. //单独编写一个定时器任务类
  25. //假设这是一个记录日志的定时任务
  26. class LogTimerTask extends TimerTask{
  27. @Override
  28. public void run() {
  29. //编写你需要执行的任务就行了
  30. SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss SSS");
  31. String strTime = sdf.format(new Date());
  32. System.out.println(strTime + ":成功完成了一次数据备份!");
  33. }
  34. }
  35. //输出内容如下:
  36. //2021/10/13 11:35:38 007:成功完成了一次数据备份!
  37. //2021/10/13 11:35:41 006:成功完成了一次数据备份!
  38. //2021/10/13 11:35:44 006:成功完成了一次数据备份!
  39. //2021/10/13 11:35:47 007:成功完成了一次数据备份!

关于Object类的wait和notify方法

几点说明

第一,wait和notify方法不是线程对象的方法,是Java中任何一个对象都有的方法,因为这2个方法是Object类中 自带的
第二、wait方法的作用:
Object o = new Object();
o.wait();
表示让o对象上活动的线程进入等待状态,无限期等待,直到被唤醒。
第三、notify方法的作用:
Object o = new Object();
o.notify();
表示唤醒正在o对象上等待的线程。
还有一个notifyAll()方法,这个方法是唤醒o对象上处于等待的所有线程
wait方法和notify方法是建立在synchronized线程同步的基础之上的
第四、wait方法和notify方法关于是否释放锁的区别?
o.wait()会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁;
o.notify()只会通知而不会释放之前占有的o对象的锁

生产者消费者案例★★

  1. package thread;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. //模拟这样一个需求:
  5. //仓库我们采用list集合,且这个集合有1个元素就表示满了,0个表示空了
  6. //必须做到这样的效果:生产一个,消费一个
  7. public class ThreadTest09 {
  8. public static void main(String[] args) {
  9. //创建1个仓库对象,共享的
  10. ArrayList list = new ArrayList();
  11. //创建2个线程对象
  12. //生产者线程
  13. Thread t1 = new Thread(new Producer(list));
  14. //消费者线程
  15. Thread t2 = new Thread(new Consumer(list));
  16. t1.setName("t1");
  17. t2.setName("t2");
  18. t1.start();
  19. t2.start();
  20. }
  21. }
  22. class Producer implements Runnable{
  23. private List list;
  24. public Producer(List list) {
  25. this.list = list;
  26. }
  27. @Override
  28. public void run() {
  29. //一直生产(用死循环来模拟一直生产)
  30. while (true){
  31. synchronized (list){
  32. if (list.size()>0){
  33. //当前线程进入等待状态,且释放Producer之前占有的list集合的锁
  34. try {
  35. list.wait();
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. //程序能执行到这里说明仓库是空的,可以生产
  41. Object obj = new Object();
  42. list.add(obj);
  43. System.out.println(Thread.currentThread().getName() + "--->" + obj);
  44. //唤醒消费者去消费
  45. // list.notify();
  46. list.notifyAll();
  47. }
  48. }
  49. }
  50. }
  51. class Consumer implements Runnable{
  52. private List list;
  53. public Consumer(List list) {
  54. this.list = list;
  55. }
  56. @Override
  57. public void run() {
  58. //一直消费
  59. while (true){
  60. synchronized (list){
  61. if (list.size()==0){
  62. //仓库已经空了
  63. try {
  64. //消费者线程等待,释放掉list集合的锁
  65. list.wait();
  66. } catch (InterruptedException e) {
  67. e.printStackTrace();
  68. }
  69. }
  70. //程序能进行到此处,说明仓库中有数据进行消费
  71. Object obj = list.remove(0);
  72. System.out.println(Thread.currentThread().getName() + "-->" + obj);
  73. //唤醒生产者继续生产
  74. // list.notify();
  75. list.notifyAll();
  76. }
  77. }
  78. }
  79. }
  80. //内容输出:生产一个,消费一个
  81. //t1--->java.lang.Object@4b527522
  82. //t2-->java.lang.Object@4b527522
  83. //t1--->java.lang.Object@4a92f21c
  84. //t2-->java.lang.Object@4a92f21c
  85. //t1--->java.lang.Object@76238b4f
  86. //t2-->java.lang.Object@76238b4f
  87. ....................

小练习:使用生产者和消费者模式实现交替输出

假设只有2个线程,输出以下结果:
t1—>1
t2—>2
t1—>3
t2—>4
t1—>5
t2—>6
要求:必须交替,并且t1线程输出奇数,t2线程输出偶数。
(提示:2个线程共享一个数字,每个线程执行时都要对这个数字进行加一操作)