什么是线程不安全:
就是在编写多线程代码的时候,如果当前代码中因为多线程随机的调度顺序,导致程序出现bug,就称为线程不安全,反之就安全。
一个常见的线程不安全的案例:
class Increase {
public int t = 0;
public void inc() {
t++;
}
}
public class demo9 {
public static Increase increase = new Increase();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increase.inc();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increase.inc();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(increase.t);
}
}
可以看到,这里的结果并不是我们所希望的10000,而是一个介于5000和10000之间的数。
这里我们需要知道t++的原理,这个操作分为三个步骤,首先是从内存中将数据加载到cpu上,然后在cpu上进行加1操作,然后再放回内存中,这里的三个步骤不具有原子性,且进程会抢占执行,就会导致进程不安全。
那这里如何使进程安全??
一个典型的方法就是加锁
通过加锁操作,就可以把上诉的”无序“变为有序,把上诉的”随机“ 变为确定。
java中给进程加锁的方式有很多,最常用的就是synchronized 这个关键字。
这个关键字的意思是同步,也就是互斥。
synchronized public void inc() {
t++;
}
这里只需要在inc这个函数上加上synchronized这个关键字,程序在进入这个函数的时候就会自动加上锁。
此时结果就正确了。
原因:如果当前锁是未被占用的情况,有一个线程t1尝试进行lock操作,t1就可以立即获取到锁,并且继续执行后面的逻辑。
又有一个线程尝试进行lock操作,t2线程就只能等待(block状态),一直等待,直到t1线程释放,这个时候t2才能执行后续的操作
引入多线程,就是为了实现并发编程(提高速度),但是当加了锁之后,数据结果是对了,但是这里的并发性实际上就降低了,速度也就下降了。
线程不安全的原因:
1.线程的抢占式执行(根本原因) 2.两个进程在修改同一个变量 3.线程对变量的修改不是原子的(加锁的本质就是把这些不是原子的操作给打包到一起了) 4.内存可见性例如有一个变量,一个线程快速读取这个变量的值,另一个线程会在一定时间之后修改这个变量
import java.util.Scanner;
public class demo10 {
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 = scanner.nextInt();
System.out.println("main线程结束");
}
}
可以看到,当在main线程中修改了isQuit,但是t线程并没有随之退出
这显然是bug
这里的是由于java中的编译优化,在多线程中,编译器的判定优化可能出现误差,在此处,isQuit的值是可能发生改变的,但是编译器在进行优化的时候,没法对于多线程的代码做出过多的预判,只是看到了t线程内部没有地方在修改isQuit,并且这个t线程里,反复要执行太多次的load操作了,所以在t中,编译器的直观感受就是反复进行了太多次的load操作了,太慢了,同时这里的load的结果好想一直是不变的(编译器不能把多个线程联系到一起分析),因此编译器做出了一个大胆的优化操作,就是直接省略掉这里的load操作,只保留第一次,后续的操作都不再读取内存,而是直接从寄存器中度,这就是内存可见性(改了,但没看到)
解决方案:
(1)还是synchronized 就会禁止编译器在synchronized内部产生上述的优化
(2)还可以使用另一个关键字,volatile,只要用这个关键字修饰对应的变量,就会禁止这里的优化,每次都会去内存中读取数据
此时就能正确结束了,所以volatile的作用就是保证了内存的可见性,禁止了编译器的相关优化
5.指令重排序这个也可能引起线程的不安全,这也是和编译器优化相关的
指令重排序就是保证代码原有逻辑不变,调整了程序指令的执行顺序,从而提高了效率
如果是单线程环境下,这里的判定是比较准的,但是如果是多线程下,这里的判定就不准了
也可以使用synchronized来解决问题。
synchronized的基本使用起到的效果:
- 互斥(最核心的要点)保证内存可见性禁止指令重排序
具体的使用方法:
直接加到一个普通方法上,进入方法就相当于加锁,出了方法就相当于解锁
加到一个代码块上,需要手动指定一个”锁对象“
synchronized (this){
t++;
}
例如这段代码,就是在手动指定锁对象为this,当然也可以指定其他对象为锁对象,在java中,任何一个继承自Object的类的对象,都可以作为锁对象,这是因为synchornized加锁操作,本质上是在操作Object对象头中的一个标志位。
3.加到一个static方法上,此时就相当于指定当前的类对象为锁对象。
要注意,两个进程对同一个对象加锁才会产生竞争,两个进程对不同对象加锁,不会产生竞争
import java.util.Scanner;
public class demo11 {
private static Object object1 = new Object();
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Thread t1 = new Thread(() -> {
while (true) {
synchronized (object1) {
System.out.print("请输入一个整数:");
int a = scanner.nextInt();
System.out.println("a:" + a);
}
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
synchronized (object1) {
System.out.println("hello t2");
}
});
t2.start();
}
}
这里的t1先拿到锁,然后t2就会进入阻塞状态。
可重入锁,这是synchronized 的一个特性,如果synchronized不是可重入的,就很容易出现”死锁“的情况如果连续针对同一个对象,下面是this,就是Increase对象加锁两次,并且如果锁不是可重入的话,就会出现死锁。
class Increase {
public int t = 0;
synchronized public void inc() {
synchronized (this){
t++;
}
}
}
就是说,一个进程,可以对一个锁进行多次加锁操作。
这里是java里面设置了可重入锁,但是在其他地方就不一定是可重入锁的,那么就会出现死锁。
除了上面说的一个进程一把锁会产生死锁的情况,还有其他情况会产生死锁。比如两个进程两把锁,互相竞争对方的锁。或者M个进程M把锁(哲学家就餐问题)
线程安全的类:
vectorHashTableConcurrentHashMapStringBuffer
这几个类是线程安全,核心方法上都带有synchronized关键字,可以在多线程环境下使用
String是不可变对象,所以String也是安全的.
补充:JMM => Java Memory Model Java存储模型(内存模型)
多线程程序,最麻烦的就是在调度过程中充满了随机性,这个是在系统层面上解决不了的
但是可以通过wait和notify机制对进程之间的调度做出一定的控制。
假设有两个线程t1和t2,希望先执行t1,t1执行了一些工作之后,再执行t2,就可以先让t2wait,然后t1执行了一些工作,就调用notify唤醒t2。
要注意,再wait这个方法里会做三件事:
- 先针对o解锁进行等待当通知到来之后,就会被唤醒,同时尝试重新获取到锁,然后再继续执行
public class demo12 {
private static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("唤醒前");
o.wait();
System.out.println("唤醒后");
}
}
所以如果按照上面的来写对没加锁的再进行解锁就会出现报错。
所以需要修改代码:
public class demo12 {
private static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("唤醒前");
synchronized (o) {
o.wait();
}
System.out.println("唤醒后");
}
}
此时就能正常阻塞了
notify也是Object类的方法,哪个对象调用的wait,就需要哪个对象调用notify来唤醒
notify同样也要搭配synchronized来用
如果有多个线程都在等待,就只调用一个notify
public class demo12 {
private static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (o) {
System.out.println("wait 开始 ");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 结束");
}
}
});
t1.start();
Thread.sleep(3000);
synchronized (o) {
o.notify();
}
System.out.println("Main唤醒t1");
}
}
这里Main线程就会唤醒t1线程。
public class demo12 {
private static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
synchronized (o) {
System.out.println("t1 wait 开始 ");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 wait 结束");
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
synchronized (o) {
System.out.println("t2 wait 开始 ");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 wait 结束");
}
}
});
t2.start();
Thread.sleep(3000);
synchronized (o) {
o.notify();
}
System.out.println("Main唤醒t");
}
}
当有多个wait的时候,一个notify就只能随机唤醒一个线程。



