什么是线程安全性?
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的–《并发编程实战》
什么是线程不安全?
多线程并发访问时,得不到正确的结果
案例:无论执行多少次 都无法得出预期正确的结果 正确结果(1000)
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次调用对num进行++操作
*/
public static void inCreate(){
num ++;
}
public static void main(String [] args) {
//启动10个线程 并发访问inCreate方法
for (int i =0; i<10;i++){
new Thread(()->{
//10个线程不明显 循环100次
for (int j =0 ;j<100; j++){
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countDownLatch
countDownLatch.countDown();
}).start();
}
while (true){
if (countDownLatch.getCount() == 0){
System.out.println(num);
break;
}
}
}
}
从字节码角度刨析线程不安全操作
控制台打开 就拿我上面的代码案例来操作 进入到相应目录
指定编码编译字节码:javac -encoding UTF-8 UnSafeThread.java
之后目录会生成class文件
反编译字节码:javap -c UnSafeThread.class
复制我图片中选取的部分
0: getstatic #2 // Field num:I
3: iconst_1
4: iadd
5: putstatic #2 // Field num:I
8: return
getstatic:指定类的静态域,并将其押入栈顶
iconst_1:将int型1押入栈顶
iadd:将栈顶两个int型相加,加完之后将结果押入栈顶
putstatic:为指定类静态域赋值 再进行返回
原因:有可能两个线程都去读取到静态域的值,在进行+1,导致两个线程执行同样的num++,实际上只加了1。
num++并不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快速的切换,有可能两个线程同一时刻都读取了同一个num值,之后对它进行+1的操作,导致最终结果不符合,线程安全性问题。
原子性操作
什么是原子性操作?
这个问题很常见,一句话概括就是要么全部执行,要么全部不执行
如何把非原子性操作变为原子性
还是针对上面的案例进行修改
有的伙伴想到了 我将num加上volatile关键字不就行了么。很遗憾,volatile只是使变量具有可见性,但并不保证原子性,即时使用关键字也不会保证原子性
在方法上加synchronized关键字
深入理解synchronized
synchronized,使得方法内的非原子性操作变为原子性,就像一把锁,要想进入该代码块的线程必须获得锁,拿了锁之后就把门锁住,直到运行完成退出释放锁,其它线程再进来
基本概念:内置锁、互斥锁
内置锁: 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁: 内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
synchronized无法修饰类
-
修饰普通方法:锁住对象的实例 如果new了两个实例 会各自锁住各自的 另外线程去访问是不会干预的
案例:结果是同时输出线程名称 锁住的是对象的实例
public class SynChonizedDemo { /** * 修饰方法 */ public synchronized void out() throws InterruptedException { System.out.println(Thread.currentThread().getName()); Thread.sleep(5000L); } public static void main(String [] args){ SynChonizedDemo synChonizedDemo = new SynChonizedDemo(); SynChonizedDemo synChonizedDemo1 =new SynChonizedDemo(); new Thread(()->{ try { synChonizedDemo.out(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(()->{ try { synChonizedDemo1.out(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }
-
修饰静态方法:锁住整个类
案例:会先输出线程0,再休眠输出线程1 锁住整个类 一直到锁释放 下一个线程再进来
public class SynChonizedDemo { /** * 修饰静态方法 */ public static synchronized void staticOut() throws InterruptedException { System.out.println(Thread.currentThread().getName()); Thread.sleep(5000L); } public static void main(String [] args){ SynChonizedDemo synChonizedDemo = new SynChonizedDemo(); SynChonizedDemo synChonizedDemo1 =new SynChonizedDemo(); new Thread(()->{ try { synChonizedDemo.staticOut(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(()->{ try { synChonizedDemo1.staticOut(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }
-
修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容
案例:同一实例下! 锁住对象
public class SynChonizedDemo {
private Object lock = new Object();
/**
* 修饰代码块
*/
public void myOut(){
//传入对象会锁住对象
synchronized (lock){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String [] args){
SynChonizedDemo synChonizedDemo = new SynChonizedDemo();
new Thread(()->{
synChonizedDemo.myOut();
}).start();
new Thread(()->{
synChonizedDemo.myOut();
}).start();
}
}
Volatile关键字及其使用场景
仅能用于修饰变量
保证该变量的可见性,可通知道其它线程,volatile关键字仅仅保证可见性,并不保证原子性
JVM会发现是共享变量,禁止指令重排序
A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见
使用场景 1:作为线程开关 2:单例,修饰对象实例,禁止指令重排序
单例与线程安全
说最常见的单例模式,毕竟这里不是重点
饿汉式–本身线程安全
典型的空间换时间。在类加载的时候就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就拜拜浪费资源了
案例
/**
* 饿汉式单例
*/
public class HungerSingleton {
private static HungerSingleton Instance = new HungerSingleton();
private HungerSingleton(){}
public static HungerSingleton getInstance(){
return Instance;
}
public static void main(String [] args){
for (int i = 0 ;i<10;i++){
new Thread(()->{
//每一个线程取到对象实例都是相同的
System.out.println(HungerSingleton.getInstance());
}).start();
}
}
}
懒汉式–最简单的写法是非线程安全的
典型的时间换空间
案例:输出的都不是相同实例
/**
* 懒汉式单例Demo
*/
public class LazySingleton {
//只有在需要才实例
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
//判断实例是否为空
if(lazySingleton==null){
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
public static void main(String [] args){
for (int i=0;i<10;i++){
new Thread(()->{
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
改进饿汉单例变为线程安全
问题:上面讲到加锁 synchronized 我们直接将锁加到方法上呢? 这样实例就一样了啊 但问题是这样太消性能了
如果我在判断的时候 再锁住类呢? 但这样又衍生出了新的问题 第一次假设10个线程进来 它们都是null 第一个线程运气好 拿到锁了 并且new了实例退出了 但你别忘了 后面还有9个线程等着拿锁呢 所以这样也不行
双重加锁
案例:说是双重加所 不如说双重判断
public class LazySingleton {
//只有在需要才实例
private static volatile LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
//判断实例是否为空
if(lazySingleton==null){
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LazySingleton.class){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
public static void main(String [] args){
for (int i=0;i<10;i++){
new Thread(()->{
System.out.println(LazySingleton.getInstance());
}).start();
}
}
}
但以上的方式并不是彻彻底底的线程安全
在JVM中有指令重排序 所以volatile关键字的作用就出现了 它能禁止指令重排序 这个没办法演示
如何避免线程安全问题
线程安全性问题成因
多线程环境
多个线程操作同一个共享资源
对该共享资源进行了非原子性操作
如何避免
打破成因中三点任意一点
1:改单线程(必要的代码,加锁访问)
2:不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3:将非原子性操作改原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)