一、什么是线程安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
二、线程不安全的原因
1、线程是抢占式执行
线程之间的调度完全是由内核负责,用户代码感知不到,也无法控制。
2、自增操作不是原子的
每次++都能拆成三个步骤
(1)把内存中的数据加载到 cpu 上
(2)cpu 进行++操作
(3)将++后的值重新写入内存中
当 CPU 在执行三个步骤中的任何一个步骤的时候都有可能被调度器调走,让其他线程来执行。
注:什么是原子性:
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还 没有出来;B 也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
不保证原子性会给多线程带来什么问题:如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
3、多个线程尝试修同一个变量
这也是由于多个线程修改同一个变量不是原子的。
4、可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线 程之间不能及时看到改变,这个就是可见性问题。这样就会造成线程不安全。
5、指令重排序
什么是指令重排序?
例如家人让你去买菜,让你买黄瓜,西红柿和肉;正常的顺序是先去买黄瓜,然后西红柿,最后买肉,但是由于超市入口处就是肉,之后是西红柿,出口处是黄瓜,代码就会重排序,会先买肉,然后西红柿,最后买肉,最后的结果都是买这三样食材。单线程这样的优化没有问题,但是多线程就会出现线程不安全。
三、如何解决线程不安全
1、加锁(synchronized 关键字-监视器锁monitor lock) 实现原子性
synchronized的底层是使用操作系统的mutex lock实现的。
锁的特点:互斥性,同一时刻只能有一个线程获得锁。其他线程如果也尝试获取锁,就会发生阻塞等待。一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁。
当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内 存中读取共享变量。
1.1、synchronized 几种常见的方法:
1)加到普通方法之前:表示锁 this;
public class SynchronizedDemo {
public synchronized void method() {
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method(); // 进入方法会锁 demo 指向对象中的锁;出方法会释放 demo 指向的对象中的锁
}
}
2)加到静态方法之前:表示锁当前类的类对象;
public class SynchronizedDemo {
public synchronized static void method() {
}
public static void main(String[] args) {
method(); // 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
// SynchronizedDemo.class 指向的对象中的锁
}
}
3)加到某个代码块之前:显示给某个对象加锁;
public class SynchronizedDemo {
public void method() {
// 进入代码块会锁 this 指向对象中的锁;出代码块会释放 this 指向的对象中的锁
synchronized (this) {
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();
}
}
2、volatile 关键字
修饰的共享变量,可以保证可见性,部分保证顺序性;
加了 volatile 之后,对于内存的读取操作肯定是从内存中读取的,不加 volatile ,对于内存读取操作可能就是在 CPU 上直接读取的。
3、对象等待集
3.1、wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”) 。
3.2、notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
3.3、wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法, 或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
1)wait() 方法
其实wait()方法就是使线程停止运行。
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程 置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中...");
object.wait();
System.out.println("等待已过...");
}
System.out.println("main方法结束...");
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了 另外一个方法唤醒的方法notify()。
2)notify() 方法
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对 其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个 呈wait状态的线程。
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public static void main(String[] args) {
Object o = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (o) {
while (true) {
System.out.println("等待前");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待后");
}
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run() {
Scanner in = new Scanner(System.in);
System.out.println("输入任何一个数开始notify");
int num = in.nextInt();
synchronized (o) {
System.out.println("notify开始");
o.notify();
System.out.println("notify结束");
}
}
};
t2.start();
}
3) notifyAll()方法
以上notify方法只是唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用 notifyAll方法可以一次唤醒所有的等待线程。