当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
先了解一下java内存模型
上图描述了一个多线程执行场景。 线程 A 和线程 B 分别对主内存的变量
进行读写操作。其中主内存中的变量
为共享变量
,也就是说此变量只此一份,多个线程间共享。但是线程不能直接读写主内存的共享变量
,每个线程都有自己的工作内存,线程需要读写主内存的共享变量
时需要先将该变量拷贝一份副本到自己的工作内存,然后在自己的工作内存中对该变量进行所有操作,线程工作内存对变量副本完成操作之后需要将结果同步至主内存。
线程对变量赋值的流程
那么问题来了,线程工作内存怎么知道什么时候又是怎样将数据同步到主内存呢? 这里就轮到 JMM 出场了。 JMM 规定了何时以及如何做线程工作内存与主内存之间的数据同步。
对 JMM 有了初步的了解,简单总结一下原子性、可见性、有序性
原子性:
对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。
可见性:
多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
有序性:
程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。
解决办法:
使用多线程之间同步synchronized或使用锁(lock)。将可能会发生数据冲突问题,只能让当前一个线程进行执行。代码执行完成后释放锁,锁释放后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
方式①:同步代码块
就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(obj){
//可能会发生线程冲突问题
}
对象如同锁,持有锁的线程可以在同步中执行 ;没持有锁的线程即使获取CPU的执行权,也进不去
同步的前提:
1,必须要有两个或者两个以上的线程
2,必须是多个线程使用同一个锁
必须保证同步中只能有一个线程在运行
好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。
方式②:同步函数
在方法上修饰synchronized 称为同步函数。同步函数使用this锁;
public synchronized void sale() {
// 前提 多线程进行使用、多个线程只能拿到一把锁。
// 保证只能让一个线程 在执行 缺点效率降低
// synchronized (oj) {
if (count > 0) {
try {
Thread.sleep(50);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - count + 1) + "票");
count--;
}
// }
}
方式③:静态同步函数
方法上加上static关键字,使用synchronized 关键字修饰 或者使用类.class文件。
静态的同步函数使用的锁是该函数所属字节码文件对象;
synchronized (ThreadTrain.class) {
System.out.println(Thread.currentThread().getName() + ",出售 第" + (100 - trainCount + 1) + "张票.");
trainCount--;
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
总结:
synchronized 修饰方法使用锁是当前this锁。
synchronized 修饰静态方法使用锁是当前类的字节码文件。