一. 线程状态
线程的状态是一个枚举类型
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指令
- 把内存中的count的值加载到CPU寄存器中
- 把寄存器的值+1
- 把寄存器的值写回到内存的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 的工作过程
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
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