1.Atomic*(原子性)
什么是原子性?
不可分割的操作。一个操作要么成功 ,要么失败 , 这个操作就是原子性的操作。
属于原子性操作的:
赋值操作
int i=5;//原子性的操作
int b=i;
数学运算
非原子性的操作的
i++
i=i+1
i+1 进行加
i=i+1 进行复制操作
i-- 非原子性的操作
对于原子操作类,Java的concurrent并发包中主要为我们提供了这么几个常用的:
AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference。
对于原子操作类,最大的特点是在多线程并发操作同一个资源的情况下,原子性来实现并发情况下资源的安全、完整、一致性。这样开销小、速度快,对于原子操作类是采用原子操作指令实现的,从而可以保证操作的原子性。
实例代码:
import java.util.concurrent.atomic.AtomicInteger;
/**
多线程共同操作一个变量,atomic保证原子性操作,实现资源完整安全一致性
*/
public class TestAtomic {
/**
* 定义一个变量
* AtomicInteger
* int long boolean
* AtomicInteger-----int/integer
* int numb=0;
* 创建对象的时候 参数 相当于初始化numb变量
*/
public static AtomicInteger numb = new AtomicInteger(0);//保证原子性
//public static int numb = 0; 这种方式不能保证原子性
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// incrementAndGet numb++ 原子性的
numb.incrementAndGet();
}
}
}).start();
}
Thread.sleep(2000);
System.out.println(numb);
}
}
结果
100000
2.Volatile(可见性)
轻量级的synchronized,高并发下保证变量的可见性和有序性,不保证原子性
什么是可见性?
就是在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的,其二 volatile 禁止了指令重排,所以在处理同步问题上它大显作用,而且它的开销比synchronized小、使用成本更低。
虽然 volatile 变量具有可见性和禁止指令重排序,但是并不能说 volatile 变量能确保并发安全,也就是不保证原子性。
代码示例:
/**
* 描述: volatile只保证可见性和有序性(指令不重排), 无法保证原子性。
* 作用:
* 用于多线程之间的状态的交换
*/
public class TestVolatile {
//int num=0; volatile 不保证原子性操作的
public static volatile int numb = 0;
public static volatile boolean b = false;
public static void main(String[] args) throws Exception {
// 循环启动100个线程。按照正常逻辑,每个线程的累加和是1000, 最终的结果应该是10W ,但是结果不是。总比10W小。
// 原因是?
for (int i = 0; i < 100; i++) {
// 启动线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//b=true;
numb=numb+1;//这里是非原子性操作
}
}
}).start();
}
Thread.sleep(1000);
System.out.println(numb);
}
}
结果:
// 结果总是小于100000(不保证原子性)
89612
3.ThreadLocal(本地线程变量)
提供线程内的局部变量。每个线程都自己管理自己的局部变量,互不影响。
应用场景:
应用在一些对象比较难初始化的变量上(初始化的时候耗时过长,初始化的时候做的事情过多),需要多个线程使用的为了避每个线进行操作的时候都初始化这个对象 ,可以使用本地线程副本 ,相当于直接初始化一次 进行复制的工作
示例代码:
/**
* 本地线程变量:
* 描述: ThreadLocal让每个线程都拥有一个变量的副本。各自操作
*
* threadlocal:
* 本地线程副本 ---- 备份 在副本中 所有的备份和原始文件都是同等地位的 没有任何主次之分的
* int i;
* i=1;
* 应用场景:
* 本地的变量 创建出来多个副本 每个线程只取其中一个副本操作
* 应用在一些对象比较难初始化的变量上(初始化的时候耗时过长 初始化的时候做的事情过多) 需要多个线程使用的
* 为了避每个线进行操作的时候都初始化这个对象 可以使用本地线程副本 相当于直接初始化一次 进行复制的工作
* tomcat 3-8s
*/
public class TestThreadLocal {
//在上面的例子中首先利用匿名类覆盖ThreadLocal的initialValue()方法指定初始值
//泛型----需要定义的变量的类型
// int seqNum=0
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
@Override
//对变量进行初始化的方法 类似于 i=5;
protected Integer initialValue() {
//这个返回值就是变量的初始值 返回值----初始化的值
return 0;
}
};
public static void main(String[] args) {
//创建三个线程共享seqNum各自产生序列号
TestThreadLocal sn = new TestThreadLocal();
new TestThread(sn).start();
new TestThread(sn).start();
new TestThread(sn).start();
}
private static class TestThread extends Thread {
private TestThreadLocal sn;
public TestThread(TestThreadLocal sn) {
this.sn = sn;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("thread -" + Thread.currentThread().getName() + " -> " + sn.getNextNum());
}
}
}
//通过getNextNum获取下一个序列值
public int getNextNum() {
//设置变量的值 seqNum++
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
}
结果:
thread -Thread-1 -> 1
thread -Thread-1 -> 2
thread -Thread-1 -> 3
thread -Thread-2 -> 1
thread -Thread-2 -> 2
thread -Thread-2 -> 3
thread -Thread-0 -> 1
thread -Thread-0 -> 2
thread -Thread-0 -> 3
4.synchronized
synchronized叫同步锁,是Lock的一个简化版本。基于阻塞策略。
synchronized关键字是Java利用锁的机制自动实现的,一般有同步方法和同步代码块两种使用方式。
总结
关于Volatile关键字具有可见性,但不具有操作的原子性,而synchronized比volatile对资源的消耗稍微大点,但可以保证变量操作的原子性,保证变量的一致性,最佳实践则是二者结合一起使用。
-
对于synchronized的出现,是解决多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。同步机制是提供一份变量,让所有线程都可以访问。
-
对于Atomic的出现,是通过原子操作指令+Lock-Free完成,从而实现非阻塞式的并发问题。
-
对于Volatile,为多线程资源共享问题解决了部分需求,在非依赖自身的操作的情况下,对变量的改变将对任何线程可见。
-
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。