线程的安全性
什么是线程的安全性?
当多个线程访问某个类,不管运行时环境采用何种调度方式或者如何交替执行,并且在主调代码中不需要任何的同步或者协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
什么是线程不安全?
多线程访问某个类时,得不到正确的结果,就称为线程不安全
原子性操作
什么是原子性?简单来说就是要么成功,要么失败,不存在第三种情况,这样,就可以在一定程度上避免线程同步出现的问题,这样就可以减少线程安全问题。
具体怎么操作呢?
synchronized关键字能够帮助我们实现线程的原子性操作
private static int num = 0;
//并发10
private static CountDownLatch countDownLatch = new CountDownLatch(10);
//让num自增,加上关键字synchronized
public static synchronized void inCreate() {
num++;
}
//让线程循环10次,每次一百下,调用上面的inCreate()方法,最终结果应该是1000,但是如果不加上synchronized关键字不进行原子性操作的话,最终的结果会引发安全性问题,结果总不是1000
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
countDownLatch.countDown();
}).start(); ;
}
while(true) {
if(countDownLatch.getCount() == 0) {
System.out.println(num);
break;
}
}
}
那我们又可以抛出一个问题,这个synchronized关键字到底是干啥的,他为什么能实现原子性呢?
了解到这些就要先知道什么是内置锁和互斥锁
内置锁:每个java对象都可以用作一个实现同步的锁,这些锁称为内置锁,线程进入同步代码块或方法的时候就会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁:内置锁就是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到B释放这个锁,如果不释放,那么A线程就将继续等待下去。
synchronized不能修饰类,但是可以修饰一个方法,当synchronized修饰一个方法时,这个方法就已经使用了一个内置锁。
当修饰一个普通方法时,synchronized关键字只会锁住对象的实例,以下代码输出的结果是。
Thread-0
Thread-1
只锁住对象的实例,不会影响多个线程同时执行一个对象中的方法,那么现在只有这个实例被内置锁锁住了。
public synchronized void out() {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread04 thread=new Thread04();
Thread04 thread2=new Thread04();
new Thread(()->{
thread.out();
}).start();
new Thread(()->{
thread2.out();
}).start();
}
当synchronized修饰静态方法就会锁住整个类,当将上面普通方法修改为静态方法输出的结果如下
Thread-0
Thread-1
结果中有一处明显的停顿,这是当Thread-0执行后释放资源才让Thread-1开始执行,就是当synchronized修饰静态方法时,就只能有一个线程占用资源,其他的线程只能等待,那么现在整个类就被一个内置锁锁住了
当修饰一个代码块时,就锁住了传入的对象的实例,和锁住普通方法效果相同
volatile关键字
它是干什么的?
能且仅能修饰变量
保证该变量的可见性,但仅保证可见性,不保证原子性
禁止指令重排序
A、B两个线程同时读取volatile关键字修饰的对象,A读取之后修改了变量的值,修改之后,对B线程来说,是可见的
在哪里用的?
可以用作线程开关
修饰对象实例,将实例转化为一个单例,进制指令重排序
那什么是单例呢?
学过Spring的都知道,当一个对象加载进Spring时就将这个对象默认修改为单例,那单例是啥?单例就是每次使用这个对象时,永远使用的是一个实例。
单例有什么种类吗?
单例分为懒汉和饿汉模式
懒汉模式:在需要的时候再实例化,当实例化比较耗时而且有多个线程访问时,就会成为多例状态,所以懒汉模式并不安全
饿汉模式:在类加载的时候就已经实例化,无论之后能不能用到,所以如果用不到就会比较占资源,但是线程较安全
那么怎么将懒汉模式变成线程安全或者相对安全呢,用最有效的一个方法,就是上面的volatile关键字,用它来修饰当前对象,禁止指令重排序,让所有的线程使用对当前对象时用的都是一个实例,以达到单例的效果。
线程安全性问题的成因:
1.多线程环境
2.多个线程操作同一共享资源
3.对该共享资源进行非原子性操作
如何避免:
打破成因中任意一点
1.多线程环境–将多线程改成单线程
2.多个线程操作同一共享资源–不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3.对该共享资源进行非原子性操作–将非原子性操作改为原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的响应并发工具类)