文章目录
- 线程安全的概念
- 数据交互形式
- 🍹线程不安全的原因
- 线程的抢占式执行过程
- 多个线程 修改同一个变量
- 原子性
- 内存可见性
- 指令重排序
- synchronized — 监视器锁
- volatile 关键字
- 通信 —对象的等待集 wait set
- wait()方法
- notify()方法
- notifyAll()方法
线程安全的概念
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现意外情况。
数据交互形式
首先,需要知道Java程序和内存之间是如何进行交互的
- 先把内存的数据读取到CPU的寄存器中
- 针对寄存器的内容,通过操作指令进行相应的操作,把操作的结果放在寄存器中
- 把寄存器中的数据写回到内存中
🍹线程不安全的原因
线程的抢占式执行过程
这是造成线程安全问题的根本原因。但这是随机的,是由操作内核决定的。
多个线程 修改同一个变量
关键在于修改和同一个变量。
举个例子,我和我弟盯上了同一个鸡腿🍗,我们俩都只是看看,那ok,不会出现什么问题,但是如果我弟想吃,那我肯定不干啊,它吃了我看什么(卑微)?这就会造成矛盾;但是如果我和我弟手里各有一个🍗,那没事了,想吃就吃,不会出现什么问题。
原子性
这里指把操作看成一个整体,要么都做,要么都不做,是不可拆分的
举例:对于数据0,每次+1
实际上,最后得到的数字应该是2
上图中,第一种情况,是与预期结果相符的。线程2的读取操作是在线程1结束之后进行的。
但是,却会出现结果为 1 的情况,如上图(还有其他情况,这里只是列举)
这是因为线程1先对数据进行了操作,但是还没有写回到内存中,那线程2对数据进行读取的时候,读到的是数字1(是线程1进行操作前的结果),那么就导致最后写回到内存中的数据出现错误。
所以,需要保证原子性。
内存可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
指令重排序
在保证整体逻辑不变的情况下,编译器会对代码进行优化,来提高效率(比如说从1加到100,按照代码的逻辑会一次次从内存中读取数据进行+1操作,进行100次。但是实际上,从内存中读取数据的消耗会比从寄存器中读取的消耗大,那么此时在每次操作完成之后,没有把数据写回给内存,而是从寄存器中读取直接进行操作,直至加到100再写回给内存,以此来提高效率,这是编译器的优化)
如果是单线程,是没有问题的,优化是正确的,但是如果是多线程,就可能会出错。
举个例子:
按照代码逻辑是这样的:
- 去隔壁家取U盘
- 去教室写作业
- 去门卫处取快递
那么如果是单线程,那没有问题,先执行哪个都不影响;
但是如果是多线程,就会有问题:可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的
synchronized — 监视器锁
synchronized 锁的作用就是保证了操作的原子性,同时禁止指令重排序和保证内存可见性。
简单说一下synchronized 的工作原理:
当第一个线程执行时,会对该资源上锁,只有当UNLOCK执行完毕之后,其他线程才能对该资源进行操作。在上锁期间,所有想操作该资源的线程都要等待。那么此时保证了操作的原子性,同时禁止指令重排序和保证内存可见性。
用法一:修饰一个方法
public class test9 {
static class Counter {
public int count = 0 ;
synchronized public void increase() {
count++;
}
public static void main(String[] args)throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run () {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
Thread t2 = new Thread(){
@Override
public void run () {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
}
在这个例子里,如果不用synchronized ,那么运行结果是小于等于100000的。
此时,用synchronized 来修饰 increase()方法,得到的结果就是预期的结果100000。
用法二:修饰一个代码块
//上面的代码进行部分修改
public void increase() {
synchronized (this){
count++;
}
}
明确锁的对象
public class test10 {
//synchronized 修饰代码块
static class Counter {
public int count = 0 ;
public void increase() {
count++;
}
public static void main(String[] args)throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run () {
for (int i = 0; i < 50000; i++) {
synchronized (counter){
counter.increase();
}
}
}
};
Thread t2 = new Thread(){
@Override
public void run () {
for (int i = 0; i < 50000; i++) {
synchronized (counter){
counter.increase();
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
}
这里要注意:如果是普通方法,相对于加锁的对象是this;如果是静态方法,相当于加锁的对象是类对象
那么如果嵌套执行 synchronized 的加锁操作怎么办呢?是和前面说的一样进行阻塞i等待吗?
不是的!!在Java中,synchronized 实现了 可重入锁
synchronized 内部记录了当前锁是哪个线程持有的。如果当前加锁的线程和持有锁的线程是同一个的话,此时并不是真的进行加锁操作,而是一个计数器加1
如果该线程进行解锁操作,也不是立刻就解锁,而是计数器逐渐减,直到计数器减到了0,才是真的进行解锁操作。(仅限Java)
volatile 关键字
volatile 的作用是辅助线程安全
volatile 和 synchronized 的区别是:volatile 不保证操作的原子性
主要用于读写同一个变量的时候
import java.util.Scanner;
public class test11 {
//volatile
static class Counter {
volatile public int flag = 0 ;
public static void main(String[] args)throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run () {
while (counter.flag == 0) {
//什么都不做
}
System.out.println("线程1结束");
}1
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run() {
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个整数:");
counter.flag = scan.nextInt();
}
};
t2.start();
}
}
}
通信 —对象的等待集 wait set
- wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入就绪状态)
- notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程
- wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
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()
notify()方法
notify方法就是使停止的线程继续运行。
- 方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知 notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈 wait 状态的线程
- 在 notify()方法后,当前线程不会马上释放该对象锁,要等到执行 notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
package bit.java.thread;
class MyThread implements Runnable {
private boolean flag;
private Object obj;
public MyThread(boolean flag, Object obj) {
super();
this.flag = flag;
this.obj = obj;
}
public void waitMethod() {
synchronized (obj) {
try {
while (true) {
System.out.println("wait()方法开始 ");
obj.wait();
System.out.println("wait()方法结束 ");
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void notifyMethod() {
synchronized (obj) {
try {
System.out.println("notify()方法开始 ");
obj.notify();
System.out.println("notify()方法结束 ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (flag) {
this.waitMethod();
} else {
this.notifyMethod();
}
}
}
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
MyThread waitThread = new MyThread(true, object);
MyThread notifyThread = new MyThread(false, object);
Thread thread1 = new Thread(waitThread, "wait线程");
Thread thread2 = new Thread(notifyThread, "notify线程");
thread1.start();
Thread.sleep(1000);
thread2.start();
System.out.println("main方法结束");
}
}
从结果上来看第一个线程执行的是一个 waitMethod方法,该方法里面有个死循环并且使用了 wait方法进入等待状态将释放锁,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是 notifyMethod方法,该方法里面执行了一个唤醒线程的操作,并且一直将 notify的同步代码块执行完毕之后才会释放锁然后继续执行 wait结束打印语句
notifyAll()方法
如果有多个线程都在等待中就可以使用 notifyAll方法可以一次唤醒所有的等待线程