多线程--线程状态和线程安全

一. 线程状态

线程的状态是一个枚举类型

public static void main(String[] args) {
     for (Thread.State state : Thread.State.values()) {
         System.out.println(state);
     }
}

通过上述代码可以得到

NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED

新建(NEW): 创建线程并未启动
可运行(RUNNABLE): 正在Java虚拟机中运行, 资源调度完成进入运行状态.
阻塞(BLOCKED): 当前线程等待锁
无限期等待(WAITING): 等待其他线程唤醒, Object.wait() 方法使线程进入限期等待或者无限期等待.
限期等待(TIMED_WAITING): 调用Thread.sleep()方法会使线程进入限期等待, 无须等待其他线程唤醒, 在一定时间之后会被系统自动唤醒
死亡(TERMINATED): 操作系统中线程以及执行完毕, 销毁了, 但是Thread对象还在

二. 线程安全

线程安全问题

当多个线程共享一个数据时出现的情况

public class ThreadTest {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

运行结果在50000到100000之间

两个线程同时对共享数据count递增操作, 正常情况下结果应该是100000, 但是上述结果并没有达到这种效果. 发生这种情况是因为 count++ 不是原子操作

原子操作: 要么全部执行, 要么全都不执行

在CPU中,对于count递增的操作实际上是三个CPU指令

  1. 把内存中的count的值加载到CPU寄存器中
  2. 把寄存器的值+1
  3. 把寄存器的值写回到内存的count中

因为线程是抢占式执行, 所以两个线程执行这三个指令的时候是随机的

在这里插入图片描述以上的随机性还存在很多, 上面的一个例子就表明当t1线程写回数据之前, t2加载到内存的count值还是为0, 所以就造成t1和t2线程同时对count++操作

内存可见性

来看一段代码

public class ThreadTest {
    public static int count = 0;
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Thread t1 = new Thread(() -> {
            while(count == 0) {

            }
        });
        t1.start();
        System.out.println("请输入一个数:>");
        count = sc.nextInt();
        System.out.println("结束!");
}

执行结果

请输入一个数:>
1
结束!

当执行上述代码时, 当输入一个不为0的数时, t1线程会停止, 事实上t1线程并没有停止.

t1线程优先去缓存中取count,发现缓存中有count,于是就取到了count=0,while循环条件成立。

当我们输入一个数时,缓存和主内存中的count都变为了1。但是, t1线程并不知道count已被主线程所修改,因为主线程在修改count值的时候并没有通知到各个已拥有count的缓存。所以, t1线程任然执行while循环

每个线程都有自己独立的工作内存, 当使用到主内存的变量时, 只是使用主内存变量的一份拷贝

线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

可以通过下面的方法来变更线程中共享数据.

1.把工作内存1中更新过的共享变量刷新到主内存中
2.把内存中最新的共享变量的值更新到工作内存2中

指令重排序

重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提供程序的性能而做的优化

线程问题解决方法

不可变(Immutable)的对象一定是线程安全的, 多线程环境下,应当尽量使对象成为不可变,来满足线程安全。

final 关键字修饰的基本数据类型
String
枚举类型
Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

synchronized - 监视器锁monitor lock

Java关键字synchronized 用于创建同步代码,

进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

当加锁就相当于上厕所, 给门加上锁, 当自己在xx时, 不允许其他人一起. 以至于其他线程处于阻塞状态.

当前线程释放锁时, 其他线程会被操作系统唤醒来抢锁, 并不是按照排队顺序依次获得锁.

synchronized 的工作过程

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

synchronized 的使用

修饰普通方法

public class Demo {
	public synchronized void methond() {
	
	}
}

修饰静态方法

public class Demo {
	public synchronized static void methond() {
	}
}

修饰代码块

// 锁当前对象
public class Demo {
	public void methond() {
		synchronized (this) {
		}
	}
}
// 锁类对象
public class Demo {
	public void method() {
		synchronized (Demo.class) {
		}
	}
}

通过加锁操作可以使count++ 成为原子操作

volatile

volatile可以保证内存可见性, 但不保证原子性

volatile变量在每次被线程访问时, 会从主内存中重新获取修改后的值, 当变量发生变化时, 会强制将变量重新刷新到主内存中.

参考资料

https://segmentfault.com/a/1190000009828216

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT自习小空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值