线程安全: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
观察下边代码:
package UnsafeThread;
public class UnsafeThread {
private static int COUNT;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0;j < 10000;j++){
COUNT++;
}
}
});
t.start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(COUNT);
}
}
按经验来说,答案应该是20,0000。我们运行代码看看:
Connected to the target VM, address: '127.0.0.1:55920', transport: 'socket'
194290
Disconnected from the target VM, address: '127.0.0.1:55920', transport: 'socket'
结果不到20,0000。这就是线程不安全。
线程安全的三大特性
1.原子性:
把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制,A进入房间之后,还没有出来。B 也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
解决这个问题呢,只要给房间加一把锁,A 进去就把门锁上,其他人就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们常用的 n++
,其实是由三步操作组成的
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
比如 Object object = new Object();
也分为三条指令:
- 分配内存
- 为变量赋值
- 写回到内存
不保证原子性会带来的问题:
一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
2.可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。比如,两个线程同时执行,一个先获取到n = 1,对其进行++操作,还没有返回的时候。另一个线程也获取到n ,准备进行++操作,由于上个线程还没有把数据写回cpu,所以此时获取到n =1。当两个线程都结束的时候,n结果为2,发生了错误
3.代码顺序性
一段代码是这样的:
- 去A店吃饭
- 去B店吃饭
- 去C店吃饭
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,这种叫做指令重排序
代码重排序会给多线程带来什么问题
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能你去A吃饭,吃完之后,B店人满了进不去或者直接关门了,如果指令重排序了,代码就会是错误的。
线程内看自己代码运行都是有序的(保证代码执行的依赖关系),看其他线程代码都是无序的
解决线程不安全问题
1.synchronized关键字(满足线程安全三大特性)
实现原理: 线程进入到
synchronized
代码行时,需要获取对象锁:
1.获取成功,往下执行代码
2.获取失败,阻塞在synchronized代码行,jvm将竞争失败的线程全部放在同步队列。
当线程退出
synchronized
修饰的代码块或者synchronized
方法时,
1.退回对象锁
2.通知JVM及系统,唤醒同步队列的其他线程,开始竞争这把锁
语法使用:
1.作用于静态方法(static )
//相当于对SafeThread.class,当前类对象加锁
package SafeThread;
public class synchronizedTest implements Runnable{
//共享资源
static int i =0;
/**
* synchronized 修饰静态方法
*/
public static synchronized void increase(){
i++;
}
@Override
public void run(){
for (int j =0 ; j<10000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new synchronizedTest());
Thread t2 = new Thread(new synchronizedTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
Connected to the target VM, address: '127.0.0.1:51292', transport: 'socket'
20000
Disconnected from the target VM, address: '127.0.0.1:51292', transport: 'socket'
分析:由例子可知,两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。
2.实例方法(无static )
package SafeThread;
public class synchronizedTest implements Runnable {
//共享资源
static int i = 0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
synchronizedTest test = new synchronizedTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
Connected to the target VM, address: '127.0.0.1:51292', transport: 'socket'
20000
Disconnected from the target VM, address: '127.0.0.1:51292', transport: 'socket'
分析:当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法
3.代码块
package SafeThread;
public class SafeThread {
private static int COUNT;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
//this指new的Runnable()
synchronized (this){
//synchronized (SafeThread.class){ //SafeThread.class指当前类对象
//synchronized (getClass()){ //指new的Runnable()实例对象
COUNT++;
}
}
}
});
t.start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(COUNT);
}
}
其中
作用于静态方法
public static synchronized void add() {
}
和
public static void add() {
synchronized (SafeThread.class){
}
}
的作用是一样的,锁的是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
作用于实例方法
//new SafeThread().add() 相当于对SafeThread()对象加锁
public synchronized void add() { //谁调用它,对谁加锁
}
和
public void add()
synchronized (this){
}
作用是一样的,锁的是当前实例对象 ,进入同步代码前要获得当前实例的锁
如果不在同步代码块内部,则没有同步互斥作用。
使用
synchronized
加锁操作的关注点:
1.对哪个对象加锁 —— 一个对象只有一把锁
2.只有同一个对象,才会有同步互斥的作用(我正在使用这个方法,你就不能使用,满足了线程安全的三大特性)
3.对于synchronized
内代码,同一个时间只有一个线程在运行
4.运行的线程数越多,性能下降越快(因为线程越多,归还对象锁的时候,越多的线程就会不停的在唤醒、阻塞间互相切换)
5.synchronized
内的代码执行时间越短,性能下降越快
2.Volatile关键字(满足可见性,顺序性)
1.被volatile修饰的变量保证对所有线程可见。
既然volatile变量对所有线程是立即可见的,在各个线程中不存在一致性问题。那么,我们是否能得出:volatile变量在并发运算下是线程安全的呢?不多说,直接上代码
package Volatile;
public class Volatile {
private volatile static int COUNT;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0;j < 10000;j++){
COUNT++;
}
}
});
t.start();
}
while (Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(COUNT);
}
}
结果:
Connected to the target VM, address: '127.0.0.1:54850', transport: 'socket'
193702
Disconnected from the target VM, address: '127.0.0.1:54850', transport: 'socket'
答案还是不对。
原因在于被volatile
修饰的变量,不满足三大特性中的原子性, 所以线程还是不安全的
volatile
赋值时候不能依赖变量(常量赋值可以保证线程安全)
使用场景:
可以结合线程加锁的一些手段,提高效率
只是变量的读取,常量的赋值,可以不加锁,而是使用volatile
,可以提高效率
案例:
// 饿汉模式
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
// 懒汉模式-单线程版
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 懒汉模式-多线程版-性能低
class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2.禁止指令重排序优化 -(单例模式-双重校验锁)
指令重排序是什么?简单点说就是jvm会把代码中没有依赖赋值的地方打乱执行顺序,由于一些规则限定,我们在单线程内观察不到打乱的现象(线程内表现为串行的语义),但是在并发程序中,从别的线程看另一个线程,操作是无序的。
双重校验锁描述:
1.volatile 修饰变量
2.私有构造方法
3.双重校验锁的写法保证线程安全
// 懒汉模式-多线程版-双重校验锁-性能高
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
//由于该步骤只是读取变量,具有原子性,用volatile修饰,可以保证可见性,提高效率
if (instance == null) {
synchronized (Singleton.class) {
// 如果少了内部校验,会产生多次new。保证单例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如果不禁止重排序,会发生什么问题?
上文说过 instance = new Singleton()
可分解为:
1.分配对象的内存空间
2.初始化对象
3.设置instance指向刚分配的内存地址
操作2依赖1,但是操作3不依赖2,此时若是线程内出现指令重排序,则有可能出现1,3,2的顺序。
当出现这种顺序,也就是程序执行了1和3之后,变量已经赋值成功,但不是instance
对象,instance
仍然为空。
此时其他线程若也在运行,将会在第一个if判断错误,对象有可能没有还正确初始化instance
,就直接返回了变量的值,而不是我们需要的对象,此时程序就会出错。
volatile
通过禁止指令重排序保证了有序性,避免了这种错误出现