文章目录
在阅读这篇文章之前,你需要了解线程创建过程中经由的几个状态,如果对于这些概念有一些模糊,没有关系,你一样可以看懂并且会使用这些有趣的方法!如果你需要对它们有足够的认识和理解,请戳下面的链接;
在了解线程安全之前,需要了解线程中一些与状态相关的方法,进而更完善的理解线程执行的整个过程中所经历的几种状态,深入理解线程不安全造成的原因;
线程中与状态相关的方法
1.休眠
该方法主要作用是使当前线程主动休眠millis
毫秒
方法 | 描述 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
public class TestRunnable {
public static void main(String[] args){
System.out.println("程序开始");
MyRunnable runnable = new MyRunnable();
//分别创建两个线程,并传入runnable参数
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
//运行线程
thread1.start();
thread2.start();
System.out.println("程序结束");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
//打印当前线程的名字和值
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.放弃
该方法主要作用是当前线程主动放弃时间片,回到就绪状态,竞争下一次的时间片;(通俗地就是当前线程好不容易等到系统随机分配到了时间片,在即将拿到使用的时候,它突然放弃了…蛮可惜的QAQ)
方法 | 描述 |
---|---|
static void yield() | 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。 |
在下列实例中,Task1做了放弃时间片的操作,具体形式是:每当i
为5的倍数的时候,若当前状态恰好拿到时间片,则放弃使用时间片,放弃之后,因为是随机的,很可能被分配给其他线程,基于这个原因,该实例能看懂微小的变化,但是因为中途Task1做了放弃时间片的操作,最终Task1最后执行完毕的肯能行最大。
public class TestYield {
public static void main(String[] args){
Thread thread1 = new Thread(new Task1());
Thread thread2 = new Thread(new Task2());
thread1.start();
thread2.start();
}
}
class Task1 implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println("Task1:"+i);
if (i % 5 == 0){
Thread.yield();//主动放弃时间片
}
}
}
}
class Task2 implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println("=====Task2:"+i);
}
}
}
3.结合
将其他线程加入到当前线程,一旦加入,必须等待加入线程执行完毕之后,该线程才可以继续执行;
方法 | 描述 |
---|---|
void join() | 等待这个线程终止。 |
void join(long millis) | 等待这个线程终止最多millis 毫秒。 |
void join(long millis, int nanos) | 等待最多 millis 毫秒加上 nanos 纳秒这个线程终止。 |
public class TestYield {
public static void main(String[] args){
Thread thread1 = new Thread(new Task());
Thread thread2 = new Thread(new Task());
//thread1.start();
thread2.start();
for (int i = 0; i <= 50 ; i++) {
System.out.println("main :" +i);
if(i == 30){
try {
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" : "+i);
}
}
}
- 打印结果:
总结线程中的状态
线程安全
举一个简单的案例:现有两个线程A和B,同时需要将所含字符串传入s
数组的首位,两个线程竞争OS分发的时间片,拿到时间片的可以执行将字符串存入数组的首位
的操作;
第一次竞争:现在A拿到时间片(绿色的方格),假设当A在拿到时间片,准备将HELLO
赋给s[1]
,但还未执行完操作,时间片到期,导致未完成赋值;
第二次竞争:再次竞争时间片,假设线程B那拿到了时间片,而B在时间片到期之前完成了赋值,即将WORLD
赋给s[1]
,此时s[1]
不为空,线程B执行完毕;
最后:只剩下线程A还未执行完毕,此时A拿到时间片在原来停止的地方继续执行,显然线程A准备将HELLO
赋给s[1]
,这是后就会导致原本已存入WORLD
的s[1]
,被HELLO
覆盖掉;
为什么会这样?
这种情况出现的原因主要是因为这里访问了同一个线程共享的对象;因此就会出现这种类似“争抢”的情况发生;这就是所谓的线程不安全
线程不安全
- 当线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致;
临界资源: 共享资源(同一对象),一次允许一个线程使用,才可以保证其正确性。
原子操作:: 不可分割的多步操作,被视为一个整体,其顺序和步骤不能打乱或缺省(比如此处,A在执行过程中因时间片到期,却被B“插了队”,这就破坏了原子操作)
线程不安全案例——银行欠我400万!
现妻子和丈夫共用一个账户,且余额2000万元,此时两人同时一时间输入统一账户密码进行取款,取款金额为1200万元整;
代码设计:
public class TestSynchronized {
public static void main(String[] args){
//开户存入2000元
Account account = new Account("65455656","123456",2000);
Husband husband = new Husband(account);
Wife wife = new Wife(account);
Thread thread1 = new Thread(husband);
Thread thread2 = new Thread(wife);
thread1.start();
thread2.start();
}
}
//丈夫
class Husband implements Runnable{
Account account;
public Husband (Account account) {
this.account = account;
}
@Override
public void run() {
System.out.println("Husband 开始取款");
this.account.withDraw("65455656","123456",1200);
}
}
//妻子
class Wife implements Runnable{
Account account;
public Wife(Account account) {
this.account = account;
}
@Override
public void run() {
System.out.println("Wife 开始取款");
this.account.withDraw("65455656","123456",1200);
}
}
//银行账户
class Account{
String cardNo;
String password;
double balance;
public Account(String cardNo, String password, double balance) {
this.cardNo = cardNo;
this.password = password;
this.balance = balance;
}
//取款
public void withDraw(String no,String pwd,double money){
System.out.println("正在读取账户信息,请稍后......");
if(this.cardNo.equals(no) && this.password.equals(pwd)){
System.out.println("验证成功!");
if (money < balance){
balance -= money;
System.out.println("取款成功,当前余额为:"+balance);
}else {
System.out.println("卡内余额不足!");
}
}else {
System.out.println("卡号或密码错误!");
}
}
}
执行结果:
原因叙述:
由于丈夫妻子两个人取钱的动作是两个线程同时访问临界资源,导致一个线程执行过程中被其他线程“插入”,因此出现了银行余额-400的奇怪现象;
说明两个线程同时验证成功,即在丈夫取钱的线程没有执行完毕而时间片到期的时候,妻子取钱的线程拿到时间片并验证成功进入了取钱状态;因为丈夫取钱的动作还未执行完毕,此时两者都满足if (money < balance)
的条件,因此导致银行亏损400万元;哈哈哈,如果现实中这样,银行亏大了!
修复线程不安全——加锁
每个对象都有一个互斥标记锁,用来分配给线程的。只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块。线程退出同步代码块时会释放相应的互斥锁标记。
synchronized (临界资源对象){ //对临界资源加锁
//代码(原子操作)
}
在本例中进行线程安全的修改:
class Husband implements Runnable{
Account account;
public Husband (Account account) {
this.account = account;
}
@Override
public void run() {
synchronized (account){
System.out.println("Husband 开始取款");
this.account.withDraw("65455656","123456",1200);
}
}
}
class Wife implements Runnable{
Account account;
public Wife(Account account) {
this.account = account;
}
@Override
public void run() {
synchronized (account){
System.out.println("Wife 开始取款");
this.account.withDraw("65455656","123456",1200);
}
}
}
分析代码:
- 此时,只有拿到从
acount
对象拿到锁标记的线程才能执行取款操作(原子操作),而在执行过程中,如果说先拿到时间片和锁标记的线程A
的时间片到期了(限期等待),而线程B
拿到OS
分配的时间片,但是此时锁标记在线程A
身上,因此线程B
就无法进行取款操作(阻塞状态),只能等待线程A
取款完毕,释放锁标记才可以执行取款操作;
相似地,我们也可以不分别在Husband 和Wife类下加锁,而直接将锁标记加在Account
类里面,即定义一个原子操作模块;
打印结果:
银行再也不会欠我钱了…
总结
线程执行过程中的所有状态
线程状态。 线程可以处于以下状态之一:
- NEW (初始状态)
尚未启动的线程处于此状态。 - RUNNABLE (运行状态)
在Java虚拟机中执行的线程处于此状态。 - BLOCKED (阻塞状态)
被阻塞等待监视器锁定的线程处于此状态。 - WAITING (无限期等待)
正在等待另一个线程执行特定动作的线程处于此状态。 - TIMED_WAITING (有限期等待)
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 - TERMINATED (终止状态)
已退出的线程处于此状态。