目录
1.观察线程的所有状态
2、线程转换简图
3.线程安全
3.1不安全状态
3.2安全状态(加锁synchronized)
4.线程不安全原因
4.1线程是抢占式执行的,线程间的调度充满随机性(根本原因)
4.3多个线程对同一个变量进行修改 *** 作
4.3针对变量的 *** 作不是原子行的
4.内存不可见性
5.指令重排序
1.观察线程的所有状态 线程的状态是一个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
1. NEW: 安排了工作 , 还未开始行动
把Thread 类对象创建好了,但是还没有调用start,如下
public static void main(String[] args) {
Thread t = new Thread(() ->{
});
System.out.println(t.getName());
}
通过 this.getName() 方法获取到指定线程的状态,通过 t 这个对象调用 getStare, 就是获取到了 t 的状态
2. TERMINATED: 工作完成了.
*** 作系统中的线程执行完毕,销毁了,但是 Thread 对象还在,通过t.getState() 获取到的状态
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
3、RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
线程处于就绪状态,就是在就绪队列中,随时可以被调度到 CPU 上,如果代码中没有进行 sleep,也没有进行其他的可能导致阻塞的 *** 作,代码可能处于 Runnable 状态
4、 TIMED_WAITING: 这几个都表示排队等着其他事情
代码中,调用了sleep 就会进入到 TIMED_WAITING,即在当前的线程一定时间内,处于阻塞状态
5、BLOCKED: 这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞。
6、WAITING: 这几个都表示排队等着其他事情
当前线程在等待唤醒,导致了阻塞。
2、线程转换简图 3.线程安全在 *** 作系统中,调度线程的时候是随机的(抢占式执行),所以会导致程序执行出现一些 bug。
如果因为这样的调度随机性引入了bug ,就认为代码是线程不安全的。
如果因为这样的调度随机性没有带来 bug ,就认为代码是线程安全的。
例子:使用两个线程,对同一个整型变量进行自增,每个线程自增 5w
3.1不安全状态因为两个线程是并发执行,即在下面的例子是两个抢占式执行的,有两个都在同时相加,因为使用的调用的是同一个 increse()方法进行 count ++;所以最终结果不准确
//两个线程对同一个变量进行自增,
//可能两个线程同时自增,而不是一个线程增完再到另一个,所以不准
//线程不安全
class Counter {
public int count;
public void increase() {
count++;
}
}
public class Test14 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t2.start();
//必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完
System.out.println(counter.count);//main线程
}
}
3.2安全状态(加锁synchronized)
通过对一个线程进行加锁(其他线程处于阻塞),即先执行完第一个线程再到下一个线程(阻塞解除),按顺序执行就不会出现错乱情况。
虽然加锁之后,并发执行程度降低了,但是数据更靠谱了
class Counter {
public int count;
/*public void increase() {
count++;
}*/
//加锁 synchronized 即可解决线程安全
synchronized public void increase() {
count++;
}
}
public class Test14 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t2.start();
//必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
// 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
t1.join();
t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完
System.out.println(counter.count);//main线程
}
}
给方法加上 synchronized 关键字,当一个线程进程此方法就会自动加锁,成功后,其他线程进入尝试加锁时就会触发阻塞等待(处于BLOCKED状态),阻塞会一直持续到占用锁的线程释放为止
通过加锁,得出的结果是正确的。
4.线程不安全原因 4.1线程是抢占式执行的,线程间的调度充满随机性(根本原因) 4.3多个线程对同一个变量进行修改 *** 作 4.3针对变量的 *** 作不是原子行的原子性:
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示 *** 作是互相排斥的。 一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们看到的 n++ ,其实是由三步 *** 作组成的: 1. 从内存把数据读到 CPU 2. 进行数据更新 3. 把数据写回到 CPU 不保证原子性会给多线程带来什么问题 如果一个线程正在对一个变量 *** 作,中途其他线程插入进来了,如果这个 *** 作被打断了,结果就可能是错误的。 4.内存不可见性 可见性指 , 一个线程对共享变量值的修改,能够及时地被其他线程看到。 Java 内存模型 (JMM) : Java 虚拟机规范中定义了 Java 内存模型 .线程之间的共享变量存在主内存 (Main Memory). 每一个线程都有自己的 " 工作内存 " (Working Memory) . 当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 . 当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .
因此 t1 就不在从内存读取数据了,而是直接从寄存器里面读取。因此一旦 t1 执行了该 *** 作,t2进行了修改, t1 就感知不到了
内存不可见性代码示例
public class Test15 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
}
System.out.println("循环结束,t 线程退出");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个isQuit 的值:");
isQuit = scanner.nextInt();
System.out.println("main线程执行完毕");
}
}
t 线程一直在读取,感知不到内存的修改,即内存不可见性
(1)可以使用synchronized 关键字,既能保证指令的原子性同时也保证内存可见性
(2)使用 volatile 关键字,volatile和原子性无关,但能保证内存可见性
即内存可见性
5.指令重排序指令重排序会影响到线程安全问题,也是编译器优化中的一种 *** 作
编译器对于指令重排序的前提是 " 保持逻辑不发生变化 ". 这一点在单线程环境下比较容易判断 , 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高 , 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价 .例如看着菜单去买东西,可以不按菜单的顺序,而是按超市摆放的先后顺序
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)