Bootstrap

多线程——保证线程安全


不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵!

多线程——保证线程安全

含义

线程安全:指在多线程对一个共享资源同时进行操作的时候,所得到的结果每次都是一样的。

如何保证线程安全

方法:要保证线程安全,就必须保证线程同步。保证线程的可见性有序性原子性

  • 线程同步

线程同步的含义和字面意义相反。同步其实是线程“排队”的意思。就是让线程按照一定的顺序执行,每一时刻,只有一个线程,对共享资源进行操作。

  • 可见性

一个线程对共享资源的操作,所有的线程都可以看见。

以具体实例来说明。就好比一个线程修改了一个数据,其他线程立马知道该数据被修改了。即就是在线程和主存之间有一个缓存,线程修改数据,是在缓存中修改,还需要在主存修改,而可见性就是立刻在主存中修改,防止其他线程读取时,发生数据错误。

  • 有序性

就是代码的执行顺序是有序的,执行的顺序不会发生改变。

  • 原子性

顾名思义,原子是一个不可分的整体。就是一个代码块,要么全部执行,要么全部不执行,只要其执行了,就无法被任何事务打断。

具体方法

volatile关键字

作用:保证线程可见性和禁止指令重排序。

保证可见性

实现原理:当一个变量被volatile修饰,一旦其发生改变,那么根据缓存一致性协议,其他线程的缓存都会失效,需要重新从内存中获取数据,就可以保证数据的准确性。就好比这个数据修改了,其他线程缓存失效,就知道了它被修改了,就要重新获取,即可见的含义。

不加volatile关键字

package com.hnu;

public class Main2 {
	// static 静态变量 全局
	private static boolean flag = false;

	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			//未停止,不知道已修改
			public void run() {
				while(true) {
					if(flag) {
						break;
					}
				}
				System.out.println("变量变化");
			};
		}).start();
		
		//主线程修改flag值
		Thread.sleep(1000);
		flag = true;
	}
}

线程先启动,启动后修改变量,未加关键字修饰,子线程不知道其变量值已经发生变化,即不可见,所以死循环无法停止。

加volatile关键字

package com.hnu;

public class Main2 {
	// static 静态变量 全局
	private static volatile boolean flag = false;

	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {
			//停止,知道已修改
			public void run() {
				while(true) {
					if(flag) {
						break;
					}
				}
				System.out.println("变量变化");
			};
		}).start();
		
		//主线程修改flag值
		Thread.sleep(1000);
		flag = true;
	}
}

在这里插入图片描述

加入关键字,首先先死循环,然后修改其值,知道其修改完成,然后调用,停止死循环,输出文字。可以证明其保证了可见性。

禁止重排序

实现原理:通过jvm给指令的前后加上内存屏障,屏障两边的指令不可以重排序,保证有序。

例子:单例模式

禁止重排序的经典案例就是单例模式的创建过程中的双重检测锁。

package com.hnu;

public class Main2 {
	//自己对象,禁止重排序
	private volatile static Main2 main = null;
	//构造方法
	private Main2() {
		
	}
	//创建自己
	public static Main2 getInstance() {
		if(main == null) {
			//类锁,双重检测锁
			synchronized(Main2.class) {
				if(main == null) {
					main = new Main2();
				}
			}
		}
		return main;
	}
	
	public static void main(String[] args) {
		Main2 m1 = getInstance();
		Main2 m2 = getInstance();
		Main2 m3 = getInstance();
		System.out.println(m1 == m2);
		System.out.println(m1 == m3);
		System.out.println(m2 == m3);
	}
}

在这里插入图片描述

可知三个对象,都是同一个对象,内存地址相同。

synchronized关键字

作用:利用线程互斥来实现线程同步,即通过同一时刻只有一个线程可以访问(互斥),来实现线程的原子性,全部执行完,才能换线程执行,线程顺序执行(同步)。

synchronized最主要的就是保持原子性,保持原子性,最主要的就是线程同步,同步最基本的方法就是加锁,加锁最直接的就是加synchronized关键字

效率:synchronized在早期是一把重量级锁,但是随着java发展,如今的效率已经很高。例如i++不是原子操作,它分为三步:1.获取i的值 2.修改i的值 3.将修改的值赋予i 。如果在其外面加入synchronized关键字,保证了每次只有一个线程可以修改i,那么就保证了i++在并发环境下的安全性。

保证原子性

上面的双重检测锁也使用了synchronized关键字,加同一个锁的线程同步互斥,里面的代码块在同一时刻,只有一个线程可以访问,所以保证了唯一实例。

防止死锁

原因

两个线程相互请求对方持有的资源,都不释放自己持有的资源,相互阻塞,导致死锁。

后果

至少有两个线程相互阻塞等待对方的资源。

检查死锁

使用jdk工具jconsole查看线程的状态。

解决方法
  1. 资源一次性分配
  2. 当线程满足条件时释放自己已占有的资源
  3. 资源有序分配
;