比较
- 线程安全主要就是原子性和可见性俩个方面, Java中的同步机制就是围绕这俩个方面来确保线程的安全.
- 通过使用volatile关键字,是强制的从公共内存中读取变量的值, 是线程同步的轻量级实现, 所以volatile性能肯定比synchronized要好,并且 volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
- 随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
- volatile可以依托操作系统的内存屏障防止这个数据的指令重排序(因为操作系统指令级别也会有指令优化,所以直接使用操作系统的内存屏障实现)。在有数据依赖关系上的指令是禁止重排序的,而synchronize保证同步代码块,所以即使出现指令重排序但是不会有异常出现。
volatile的非原子特性
- 服务类
public class ServiceAdd {
volatile public int count = 0;
public void add() {
this.count++;
}
}
- 运行类
public class Run {
public static void main(String[] args) throws InterruptedException {
ServiceAdd serviceAdd = new ServiceAdd();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread() {
@Override
public void run() {
for (int j = 0; j < 100; j++) {
serviceAdd.add();
}
}
};
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
for (Thread t : threads
) {
t.join();
}
System.out.println(serviceAdd.count);
}
}
- 运行结果
关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样
的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
1)从内存中取出i的值;
2)计算i的值;
3)将i的值写到内存中。
假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法其实就是使用synchronized 关键字,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。
用图来演示一下使用关键字volatile时出现非线程安全的原因。变量在内存中工作的过
- 在多线程环境中,use和asign是多次出现的,但这一操作并不是原子性,也就是在read和 load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。
- 对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和 load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
使用原子类来进行++操作
原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全( thread-safe)。
- 服务类
public class ServiceAtomic {
public AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet();
}
}
- 运行类
public class Run2 {
public static void main(String[] args) throws InterruptedException {
ServiceAtomic atomic = new ServiceAtomic();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread() {
@Override
public void run() {
for (int j = 0; j < 100; j++) {
atomic.add();
}
}
};
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
for (Thread t : threads
) {
t.join();
}
System.out.println(atomic.count);
}
}
- 运行结果
原子类也不一定是线程安全的
- 因为原子类只是单纯保证了我运行其的某个方法是原子的, 但是在我们自己写的代码中, 并没有保证原子性, 所以即使我们使用了原子类但是在我们自己写的代码面前再加上一点逻辑的话, 就还会有线程安全问题
- 服务类
public class ServiceAdd2 {
public AtomicInteger count = new AtomicInteger(0);
public void addOne() {
System.out.println("执行加一: " + count.addAndGet(1));
}
public void addHundred() {
System.out.println("执行加一百: " + count.addAndGet(100));
}
}
- 运行类
public class Run3 {
public static void main(String[] args) throws InterruptedException {
ServiceAdd2 serviceAdd2 = new ServiceAdd2();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread() {
@Override
public void run() {
serviceAdd2.addOne();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
serviceAdd2.addHundred();
}
};
}
for (Thread t : threads
) {
t.start();
}
for (Thread t : threads
) {
t.join();
}
}
}
- 运行结果
想要改变上面的问题, 只需要给服务类加上synchronized修饰那俩个方法即可
关键字synchronized代码块有volatile同步变量的功能
- 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。