线程安全性
1.线程安全性
当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。----《并发编程实战》
多线程并发访问时,得不到正确的结果,就是线程不安全的。
线程不安全举例:
public class UnsafeThread {
public static int count = 0;
public static void sum() {
count++;
}
public static void main(String[] args) throws InterruptedException {
//循环启动十个线程,每个线程执行100次++操作.
for(int j=0;j<10;j++) {
Thread thread = new Thread() {
@Override
public void run() {
for(int i=0;i<1000;i++) {
UnsafeThread.sum();
}
}
};
thread.start();
}
Thread.sleep(1000);
System.out.println(count);
}
}
例子中,产生线程不安全问题的原因: num++ 不是原子性操作,被拆分成好几个步骤,在多线程并发执行的情况下,因为cpu调度,多线程快递切换,有可能两个同一时刻都读取了同一个num值,之后对它进行+1操作,导致线程安全性。
2.Synchronized关键字
原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
内置锁:每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
互斥锁:内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
修饰普通方法:锁住对象的实例。
修饰静态方法:锁住整个类。
修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容。
public class SyncDemo {
/**
* synchronized修饰方法
* 锁住的是该对象的实例,而不会影响其他实例
* @throws InterruptedException
*/
public synchronized void method() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
}
/**
* synchronized修饰静态方法
* 锁住这个类的所有实例
* @throws InterruptedException
*/
public synchronized static void method1() throws InterruptedException {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
}
private Object lock = new Object();
/**
* synchronized修饰代码块
* 锁住一个对象,此方法内,锁住lock对象
* @throws InterruptedException
*
*/
public void method2() throws InterruptedException {
synchronized (lock) {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
}
}
public static void main(String[] args) {
SyncDemo demo1 = new SyncDemo();
// SyncDemo demo2 = new SyncDemo();
Thread thread = new Thread() {
@SuppressWarnings("static-access")
@Override
public void run() {
try {
// demo1.method1();
demo1.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
Thread thread2 = new Thread() {
@SuppressWarnings("static-access")
@Override
public void run() {
try {
// demo2.method1();
demo1.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread2.start();
}
}
3.Volatile关键字
能且仅能修饰变量。
保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性。
禁止指令重排序。
A、B两个线程同时读取volatile关键字修饰的对象,A读取之后,修改了变量的值,修改后的值,对B线程来说,是可见
使用场景
1:作为线程开关 。
2:单例,修饰对象实例,禁止指令重排序。
4.单例与线程安全
饿汉式:本身线程安全,在类加载的时候,就已经进行实例化,无论之后用不用到。如果该类比较占内存,之后又没用到,就白白浪费了资源。
public class HungerSingleton {
private static HungerSingleton hungerSingleton = new HungerSingleton();
public static HungerSingleton getInstance() {
return hungerSingleton;
}
//构造方法一定私有化
private HungerSingleton() {
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
System.out.println(HungerSingleton.getInstance());
}
}.start();
}
}
}
懒汉式 :最简单的写法是非线程安全的,在需要的时候再实例化。
public class LazySingleton {
//volatile防止JVM指令重排序
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
/**
* 非线性安全的,加上synchronized
* 若加到方法上,太消耗性能
* @return
*/
public static LazySingleton getInstance() {
if(lazySingleton == null) {
synchronized (LazySingleton.class) {
if(lazySingleton == null) {
lazySingleton = new LazySingleton();
}
//若直接初始化对象,多个线程同时进入if()代码块,等待释放锁之后,重复初始化
// lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
System.out.println(LazySingleton.getInstance());
}
}.start();
}
}
}
5.避免线程安全问题
线程安全性问题成因
1.多线程环境
2.多个线程操作同一共享资源
3.对该共享资源进行了非原子性操作
如何避免,打破成因中三点任意一点 :
1.多线程环境--将多线程改单线程(必要的代码,加锁访问)
2.多个线程操作同一共享资源--不共享资源(ThreadLocal、不共享、操作无状态化、不可变)
3.对该共享资源进行了非原子性操作-- 将非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)