线程的生命周期

JDK中用Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态。

新建

  1. 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处理新建状态。

就绪

  1. 处于新建状态的线程被 start()后,将进入线程队列等待CPU的时间片,此时它已经具备了运行条件,知识还没分配到CPU的资源。

运行

  1. 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。

阻塞

  1. 在某种特殊的情况下,被人为的挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。

死亡

  1. 线程完成了它全部的工作或线程被提前强制性的中止或出现异常导致结束。

17.线程的生命周期与安全问题 - 图1

线程的同步

问题的提出(模拟火车站售票程序,开启三个售票窗口)

  1. class Windows1 implements Runnable{
  2. private int ticket=100;
  3. @Override
  4. public void run() {
  5. while (true){
  6. if (ticket>0) {
  7. try {
  8. //模拟网络延时等情况
  9. Thread.sleep(100);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread().getName()+": 卖票,票号为:"+ticket);
  14. ticket--;
  15. }else{
  16. break;
  17. }
  18. }
  19. }
  20. }
  21. public class WindowsTest1 {
  22. public static void main(String[] args) {
  23. Windows1 windows1=new Windows1();
  24. Thread thread = new Thread(windows1);
  25. Thread thread2 = new Thread(windows1);
  26. Thread thread3 = new Thread(windows1);
  27. thread.setName("窗口1");
  28. thread2.setName("窗口2");
  29. thread3.setName("窗口3");
  30. thread.setPriority(Thread.MIN_PRIORITY);
  31. thread3.setPriority(Thread.MAX_PRIORITY);
  32. thread.start();
  33. thread2.start();
  34. thread3.start();
  35. }
  36. }

执行结果

17.线程的生命周期与安全问题 - 图2

问题:卖票过程中,出现了重票(上图未出现)、错票(出现0,-1的票号),表示出现了线程安全问题

问题出现的原因:当某个线程操作车票的过程当中,尚未完成操作时,其他线程参与进来,也来操作车票。(当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来。导致共享数据的错误)

17.线程的生命周期与安全问题 - 图3

17.线程的生命周期与安全问题 - 图4

解决办法:对多线程操作共享数据的情况,只能让一个线程都执行完所有对该数据的操作,同时在执行过程中,其他线程不可以参与操作。

  • 当一个线程a在操作ticket的时候,其他的线程不能参与进来。直到线程a操作完ticket时,其他的线程才可以开始操作ticket。这种情况下,即使线程a出现了阻塞,也不能被改变。
  • 在java当中通过同步机制,来解决线程的安全问题。

方式一:同步代码块(synchronized)

  1. synchronized(同步监视器){
  2. //需要被同步的代码(也就是操作共享数据的代码)
  3. }
  • 同步监视器,俗称,锁。任何一个类的对象都可以充当锁。(不能包多了代码块,也不能包少了代码块)
  • 要求:多个线程必须共用同一把锁(共用同一个对象)

17.线程的生命周期与安全问题 - 图5

  1. /**
  2. * 使用同步代码块的方式解决实现Runnable接口的线程安全问题
  3. */
  4. class Windows1 implements Runnable{
  5. private int ticket=100;
  6. Object object=new Object();
  7. @Override
  8. public void run() {
  9. while (true){
  10. //使用反射创建的Class对象也是唯一,或者使用this当前对象也是唯一
  11. //synchronized(Windows1.class){
  12. //synchronized(this){
  13. synchronized(object) {
  14. if (ticket > 0) {
  15. try {
  16. Thread.sleep(100);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
  21. ticket--;
  22. } else {
  23. break;
  24. }
  25. }
  26. }
  27. }
  28. }
  29. public class WindowsTest1 {
  30. public static void main(String[] args) {
  31. Windows1 windows1=new Windows1();
  32. Thread thread = new Thread(windows1);
  33. Thread thread2 = new Thread(windows1);
  34. Thread thread3 = new Thread(windows1);
  35. thread.setName("窗口1");
  36. thread2.setName("窗口2");
  37. thread3.setName("窗口3");
  38. thread.setPriority(Thread.MIN_PRIORITY);
  39. thread3.setPriority(Thread.MAX_PRIORITY);
  40. thread.start();
  41. thread2.start();
  42. thread3.start();
  43. }
  44. }
  1. /**
  2. * 使用同步代码块的方式解决继承Thread类的线程安全问题
  3. */
  4. class Windows extends Thread{
  5. private static int ticket=100;
  6. @Override
  7. public void run() {
  8. while (true) {
  9. synchronized (Windows.class) {
  10. if (ticket > 0) {
  11. try {
  12. Thread.sleep(100);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println(getName() + ": 卖票,票号为:" + ticket);
  17. ticket--;
  18. } else {
  19. break;
  20. }
  21. }
  22. }
  23. }
  24. }
  25. public class WindowsTest {
  26. public static void main(String[] args) {
  27. Windows w1=new Windows();
  28. Windows w2=new Windows();
  29. Windows w3=new Windows();
  30. w1.setName("窗口1");
  31. w2.setName("窗口2");
  32. w3.setName("窗口3");
  33. w1.start();
  34. w2.start();
  35. w3.start();
  36. }
  37. }

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。

注意

1、同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。

2、非静态的同步方法,同步监视器是:this;静态的同步方法,同步监视器是:当前类本身。

  1. /**
  2. *
  3. * 使用同步方法解决实现Runnable接口的线程安全问题
  4. */
  5. class Windows2 implements Runnable{
  6. private int ticket=100;
  7. Object object=new Object();
  8. @Override
  9. public void run() {
  10. while (true){
  11. //使用反射创建的Class对象也是唯一
  12. //synchronized(Windows1.class){
  13. show();
  14. }
  15. }
  16. private synchronized void show(){ //同步监视器:this
  17. if (ticket > 0) {
  18. try {
  19. Thread.sleep(100);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
  24. ticket--;
  25. }
  26. }
  27. }
  28. public class WindowsTest2 {
  29. public static void main(String[] args) {
  30. Windows2 windows2=new Windows2();
  31. Thread thread = new Thread(windows2);
  32. Thread thread2 = new Thread(windows2);
  33. Thread thread3 = new Thread(windows2);
  34. thread.setName("窗口1");
  35. thread2.setName("窗口2");
  36. thread3.setName("窗口3");
  37. thread.setPriority(Thread.MIN_PRIORITY);
  38. thread3.setPriority(Thread.MAX_PRIORITY);
  39. thread.start();
  40. thread2.start();
  41. thread3.start();
  42. }
  43. }
  1. /**
  2. *
  3. * 使用同步方法解决继承Thread类的线程安全问题
  4. */
  5. class Windows3 extends Thread{
  6. private static int ticket=100;
  7. @Override
  8. public void run() {
  9. while (true) {
  10. show();
  11. }
  12. }
  13. private static synchronized void show(){ //同步监视器:Windows3.class
  14. //private synchronized void show(){ //此种解决方式是错误的
  15. if (ticket > 0) {
  16. try {
  17. Thread.sleep(100);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
  22. ticket--;
  23. }
  24. }
  25. }
  26. public class WindowsTest3 {
  27. public static void main(String[] args) {
  28. Windows3 w1=new Windows3();
  29. Windows3 w2=new Windows3();
  30. Windows3 w3=new Windows3();
  31. w1.setName("窗口1");
  32. w2.setName("窗口2");
  33. w3.setName("窗口3");
  34. w1.start();
  35. w2.start();
  36. w3.start();
  37. }
  38. }

修改单例模式中的懒汉式线程安全问题

  1. /**
  2. *
  3. * 修改单例模式中的线程安全问题
  4. */
  5. public class Bank {
  6. private Bank(){};
  7. private static Bank instance=null;
  8. public static Bank getInstance(){
  9. //方式一:效率较差
  10. // synchronized(Bank.class){
  11. // if(instance == null){
  12. // instance=new Bank();
  13. // }
  14. // return instance;
  15. // }
  16. if (instance == null) {
  17. //方式二:效率较高
  18. synchronized(Bank.class){
  19. if(instance == null){
  20. instance=new Bank();
  21. }
  22. }
  23. }
  24. return instance;
  25. }
  26. }

释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到了break、return终止了该代码块、该方法的继续执行
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  • 当前线程在同步代码快、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的操作

  • 线程执行同步代码块或同步方法的时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法,将该线程挂起,该线程不会释放锁(同步监视器)
  • 应该避免使用suspend()和resume()来控制线程

线程的死锁问题

  • 1、死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 2、说明:出现死锁后,不会出现异常、不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行
  • 3、我们使用同步时,要避免出现死锁
  1. /**
  2. * 演示线程的死锁问题
  3. *
  4. * 1、死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  5. * 2、说明:出现死锁后,不会出现异常、不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行
  6. * 3、我们使用同步时,要避免出现死锁
  7. *
  8. */
  9. public class DeadLock1 {
  10. public static void main(String[] args) {
  11. StringBuffer s1=new StringBuffer();
  12. StringBuffer s2=new StringBuffer();
  13. new Thread(){
  14. @Override
  15. public void run() {
  16. //先锁s1
  17. synchronized(s1){
  18. s1.append("a");
  19. s2.append("1");
  20. //添加阻塞,增大出现死锁的概率
  21. try {
  22. Thread.sleep(100);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. //再锁s2
  27. synchronized (s2){
  28. s1.append("2");
  29. s2.append("b");
  30. System.out.println(s1);
  31. System.out.println(s2);
  32. }
  33. }
  34. }
  35. }.start();
  36. new Thread(new Runnable() {
  37. @Override
  38. public void run() {
  39. //先锁s2
  40. synchronized (s2){
  41. s1.append("c");
  42. s2.append("3");
  43. try {
  44. Thread.sleep(100);
  45. } catch (InterruptedException e) {
  46. e.printStackTrace();
  47. }
  48. //再锁s1
  49. synchronized (s1){
  50. s1.append("d");
  51. s2.append("4");
  52. System.out.println(s1);
  53. System.out.println(s2);
  54. }
  55. }
  56. }
  57. }).start();
  58. }
  59. }
  1. class A {
  2. public synchronized void foo(B b) {
  3. System.out.println("当前线程名: " + Thread.currentThread().getName()
  4. + " 进入了A实例的foo方法"); // ①
  5. try {
  6. Thread.sleep(200);
  7. } catch (InterruptedException ex) {
  8. ex.printStackTrace();
  9. }
  10. System.out.println("当前线程名: " + Thread.currentThread().getName()
  11. + " 企图调用B实例的last方法"); // ③
  12. b.last();
  13. }
  14. public synchronized void last() {
  15. System.out.println("进入了A类的last方法内部");
  16. }
  17. }
  18. class B {
  19. public synchronized void bar(A a) {
  20. System.out.println("当前线程名: " + Thread.currentThread().getName()
  21. + " 进入了B实例的bar方法"); // ②
  22. try {
  23. Thread.sleep(200);
  24. } catch (InterruptedException ex) {
  25. ex.printStackTrace();
  26. }
  27. System.out.println("当前线程名: " + Thread.currentThread().getName()
  28. + " 企图调用A实例的last方法"); // ④
  29. a.last();
  30. }
  31. public synchronized void last() {
  32. System.out.println("进入了B类的last方法内部");
  33. }
  34. }
  35. public class DeadLock implements Runnable {
  36. A a = new A();
  37. B b = new B();
  38. public void init() {
  39. Thread.currentThread().setName("主线程");
  40. // 调用a对象的foo方法
  41. a.foo(b);
  42. System.out.println("进入了主线程之后");
  43. }
  44. public void run() {
  45. Thread.currentThread().setName("副线程");
  46. // 调用b对象的bar方法
  47. b.bar(a);
  48. System.out.println("进入了副线程之后");
  49. }
  50. public static void main(String[] args) {
  51. DeadLock dl = new DeadLock();
  52. new Thread(dl).start();
  53. dl.init();
  54. }
  55. }

解决办法

  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步

解决线程安全问题的方式三:Lock锁 —- JDK5.0新增

  • 从JDK5.0开始,Java提供了更强大的线程同步机制————通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.until.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对LOck对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。
  1. class A{
  2. private final ReentrantLock lock = new ReenTrantLock();
  3. public void m(){
  4. lock.lock();
  5. try{
  6. //保证线程安全的代码;
  7. }
  8. finally{
  9. lock.unlock();
  10. }
  11. }
  12. }
  13. 注意:如果同步代码有异常,要将unlock()写入finally语句块
  1. /**
  2. *
  3. * 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增
  4. */
  5. class Window implements Runnable{
  6. public static int ticket=100;
  7. //创建锁对象
  8. private ReentrantLock lock=new ReentrantLock();
  9. @Override
  10. public void run() {
  11. while (true){
  12. try{
  13. //2、调用锁定的方法:lock()
  14. lock.lock();
  15. if(ticket>0) {
  16. try {
  17. Thread.sleep(100);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName() + "卖票:票号" + ticket);
  22. ticket--;
  23. }
  24. else{
  25. break;
  26. }
  27. }finally {
  28. //3、调用解锁方法:unlock()
  29. lock.unlock();
  30. }
  31. }
  32. }
  33. }
  34. public class LockTest {
  35. public static void main(String[] args) {
  36. Window window=new Window();
  37. Thread threa1 = new Thread(window);
  38. Thread thread2 = new Thread(window);
  39. Thread thread3 = new Thread(window);
  40. threa1.setName("窗口1");
  41. thread2.setName("窗口2");
  42. thread3.setName("窗口3");
  43. threa1.start();
  44. thread2.start();
  45. thread3.start();
  46. }
  47. }

Synchronized 与 Lock 的不同之处

  • Lock是显示锁(手动开启和关闭锁,别忘记关闭锁),Synchronized是隐式锁,出了作用域自动释放。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

优先使用顺序

Lock->同步代码块->同步方法

课后习题

  1. /**
  2. * 银行有一个账户
  3. * 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额
  4. * @author tongfangping
  5. * @create 2020-11-07 14:43
  6. */
  7. class Account{
  8. private double balance;
  9. public Account(double balance) {
  10. this.balance = balance;
  11. }
  12. //存钱
  13. public synchronized void deposit(double amt){
  14. if(amt > 0){
  15. balance+=amt;
  16. try {
  17. Thread.sleep(1000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName()+"存了"+amt+",余额为"+balance);
  22. }
  23. }
  24. }
  25. class Customer extends Thread{
  26. private Account acc;
  27. public Customer(Account acc) {
  28. this.acc = acc;
  29. }
  30. @Override
  31. public void run() {
  32. for(int i=0;i<3;i++){
  33. acc.deposit(1000);
  34. }
  35. }
  36. }
  37. public class AccountTest {
  38. public static void main(String[] args) {
  39. Account account=new Account(0);
  40. Customer customer1=new Customer(account);
  41. Customer customer2=new Customer(account);
  42. customer1.setName("张三");
  43. customer2.setName("李四");
  44. customer1.start();
  45. customer2.start();
  46. }
  47. }