1.多线程特性
多线程编程要保证满足三个特性:原子性,可见性,有序性
1.1.原子性
原子性,即一个操作或者多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行
1.2可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值,显然,对于多线程来说,可见性问题是不存在的
1.3有序性
有序性即程序执行的顺序按照代码的先后顺序执行
2.多线程控制类
为了保证多线程的三个特性,Java引入了很多线程控制机制,下面介绍其中常用的几种:
- ThreadLocal
- 原子类
- Lock类
- Volatile关键字
2.1ThreadLocal
2.1.1作用
ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,就是用ThreadLocal实现
2.1.2示例
两个线程分别转账
public class Bank {
ThreadLocal<Integer> t = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 0;
}
};
public Integer get(){
return t.get();
}
public void set(){
t.set(t.get()+10);
}
public static void main(String[] args){
Bank bank = new Bank();
Transfer transfer = new Transfer(bank);
Thread t1 = new Thread(transfer);
Thread t2 = new Thread(transfer);
t1.start();
t2.start();
}
}
class Transfer implements Runnable{
Bank bank;
public Transfer(Bank bank){
this.bank = bank;
}
public void run() {
for (int i=0;i<10;i++){
bank.set();
System.out.println(Thread.currentThread()+""+bank.get());
}
}
}
2.1.3分析
- 在ThreadLocal类中定义了一个ThreadLocalMap,
- 每一个Thread都有一个ThreadLocalMap类型的变量threadLocals
- threadLocals内部有一个Entry,Entry的key是ThreadLocal对象实例,value就是共享变量副本
- ThreadLocal的get方法就是根据ThreadLocal对象实例获取共享变量副本
5.ThreadLocal的set方法就是根据ThreadLocal对象实例保存共享变量副本
2.2原子类
Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:
- 原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
- 原子更新数组:AtomicIntegerArray、AtomicLongArray
- 原子更新引用:AtomicReference、AtomicStampedReference等
- 原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。
2.2.1非原子性操作问题演示
非原子性的操作会引发什么问题呢?下面以i++为例演示非原子性操作问题。
i++并不是原子操作,而是由三个操作构成:
tp1 = i;
tp2 = tp1+1;
i = tp2;
所以单线程i的值不会有问题,但多线程下就会出错,多线程示例代码如下:
public class AtomicClass {
static int n = 0;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = 0;
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n);
j++;
}
}
}
执行结果如下:发现n的最终值可能不是2000
2.2.2原子类解决非原子性操作问题
public class AtomicClass {
static AtomicInteger n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicInteger(0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n.getAndIncrement();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n);
j++;
}
}
}
执行结果如下:n的值永远是2000
2.2.3原子类CAS原理分析
2.2.4CAS的ABA问题及解决
2.2.4.1ABA问题分析
当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过,在某些场景下这样是存在错误风险的,如下图:
2.2.4.2ABA问题解决
public class AtomicClass {
static AtomicStampedReference<Integer> n;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = new AtomicStampedReference<Integer>(0,0);
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();
} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
int stamp;
Integer reference;
do{
stamp = n.getStamp();
reference = n.getReference();
} while(!n.compareAndSet(reference, reference+1, stamp, stamp+1));
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n.getReference());
j++;
}
}
}
执行结果都是2000