目录前言一、线程的生命周期二、线程的安全问题1.什么是线程的安全问题2.问题举例三、解决线程的安全问题1.同步代码块前言在上一篇博客中已经掌握了如何创建和启动一个 Java 线程。但是当成百上千个线程同时在系统中如果不了解它们的生命周期不掌握它们抢夺资源的规矩那么将会造成巨大的灾难。一、线程的生命周期很多教程喜欢用操作系统的 5 种状态来解释 Java 线程这其实容易造成混淆。在 Java 的世界里具体来说是java.lang.Thread.State枚举中官方明确定义了线程的6 种状态。这 6 种状态的流转构成了线程的一生NEW (新建状态)你刚new出来一个Thread对象但还没有调用start()方法。此时它还只是内存里的一个普通 Java 对象操作系统还没为它分配底层线程资源。RUNNABLE (可运行状态)调用了start()方法后进入此状态。注意Java 中的RUNNABLE包含了操作系统的“就绪Ready”和“运行中Running”两种状态。处于这个状态的线程可能正在 CPU 上狂奔也可能正在排队等待操作系统的调度分配时间片。BLOCKED (阻塞状态)这是因为“锁”而停滞的状态。当线程试图进入一个被synchronized关键字保护的代码块但这个锁正被其他线程霸占着它就会进入BLOCKED状态在门外排队。WAITING (无限期等待状态)线程主动罢工除非被别人唤醒否则永远等下去。通常是因为调用了Object.wait()不带超时时间、Thread.join()或者LockSupport.park()。它在等另一个线程执行特定的操作比如调用notify()。TIMED_WAITING (限期等待状态)和WAITING类似但它有个结束时间。时间一到就算没人叫它它也会自己醒来。比如调用了Thread.sleep(1000)它就会在这个状态待上 1000 毫秒。TERMINATED (终止/死亡状态)线程的run()方法正常执行完毕或者因为发生未捕获的异常而意外终止。死亡的线程绝对不能再次调用start()。二、线程的安全问题1.什么是线程的安全问题当多个线程同时访问同一个共享资源例如同一个对象的成员变量、同一个数据库记录并且至少有一个线程在对该资源进行修改操作时如果程序的最终执行结果偏离了我们的预期这就是线程安全问题。引发问题的核心原因有三个可见性问题一个线程修改了共享变量另一个线程没能立刻看到最新值CPU 缓存导致。原子性问题一个看似简单的操作如count在底层其实分成了“读取-计算-赋值”三步如果在这三步中间被其他线程插队数据就乱了。有序性问题编译器或 CPU 为了优化性能擅自打乱了代码的执行顺序指令重排。2.问题举例需求某电影院再卖票共有100张票而它有3个窗口卖票设计一个程序模拟该电影院卖票。首先定义一个卖票线程public class MyThread extends Thread{ static int ticket 0; Override public void run() { while(true){ if(ticket 100){ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(getName() 正在卖第 ticket 张票); ticket ; }else { break; } } } }可以看到尽管我们将ticket设置为静态变量程序还是会卖同一张票这是因为一个线程修改了共享变量另一个线程没能立刻看到最新值。三、解决线程的安全问题1.同步代码块就像给门票上了一把锁规定同一时刻只能有一个线程进去执行卖票操作其他线程必须在门外等待。格式synchronized(锁){操作共享数据的代码}特点1锁默认打开有一个线程进去了锁自动关闭特点2里面的代码全部执行完毕线程出来锁自动打开public class MyThread extends Thread{ static int ticket 1; //锁对象一定要是唯一的 static Object obj new Object(); Override public void run() { while(true){ synchronized (obj){ if(ticket 100){ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(getName() 正在卖第 ticket 张票); ticket ; }else { break; } } } } }细节1synchronized锁必须在while(true)内部因为如果在外面的话如果窗口1抢夺到cpu资源此时上锁其他进程无法抢夺cpu窗口1会在这个代码块内部一直执行一直卖票直到break才会解锁其他窗口才能进行抢夺cpu资源。细节2锁必须是唯一的标志着锁住了一个共享的操作对象数据库等。