文章目录
为了保证多线程的三个特性(原子性、可见性、有序性),Java引入了很多线程控制机制,下面介绍其中常用的几种:
**ThreadLocal:**线程本地变量
**原子类:**保证变量进行原子操作
**Lock类:**保证线程的有序性
**Volatile关键字:**保证线程变量可见性
ThreadLocal
作用
ThreadLocal提供线程局部变量,为使用相同变量的每一个线程维护一个该变量的副本。当某些数据是以线程为作用域,并且不同的线程具有不同的数据副本时,就可以考虑采用ThreadLocal来完成任务,例如:数据库连接Connection,每个请求处理线程都需要,当他们互不影响,就可以用ThreadLocal实现
Thread
常用方法
**initialValue:**副本创建方法
**get:**获取副本方法
**set:**设置副本方法
代码演示:
public class TheadLocalDemo {
//1.创建银行对像:存款,取款,余额
static class Bank {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
public Integer get() {
return threadLocal.get();
}
//存钱
public void set(Integer money) {
threadLocal.set(threadLocal.get()+money);
}
//2.创建转账对象,从银行中取钱、转账、保存到账户
static class Transfer implements Runnable{
private Bank bank;
public Transfer(Bank bank){
this.bank = bank;
}
@Override
public void run() {
for(int i=0; i<10; i++){
bank.set(10);
System.out.println(Thread.currentThread().getName()+"账户余额:"+bank.get());
}
}
}
}
//2.创建转账对象:从银行中取钱,转账,保存到账户
//3.在main方法中使用两个对象模拟转账
public static void main(String[] args) {
Bank bank = new Bank();
Bank.Transfer transfer = new Bank.Transfer(bank);
Thread t1 = new Thread(transfer,"一号客户");
Thread t2 = new Thread(transfer,"二号客户");
t1.start();
t2.start();
}
}
原子类
Java中的java.util.concurrent.atomic包下面提供了很多可以进行原子操作的类,这些原子类可以帮助我们解决基本类型操作的非原子性所导致的在多线程并发的情况下产生的各种问题,可以分为以下四类:
1.原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
2.原子更新数组类型:AtomicIntegerArray、AtomicLongArray
3.原子更新引用类型:AtomicReference、AtomicStampedReference
4.原子更新属性类型:AtomicInterFieldUpdater、AtomicLongFieldUpdater
非原子性操作问题演示
非原子性操作有时会引发线程安全问题,以下以i++为例,演示非原子性操作问题。首先我们需要知道i++并不是一个原子性的操作,而是由三个步骤所组成的:
①:temp1 = i;
②:temp2 = temp1 + 1;
③:i = temp2;
在单线程情况下,进行i++操作是不会有问题的,但是一旦到了多线程的环境下,就有可能会出错
代码演示:
public class ThreadAtomicDemo {
static private int n;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
n = 0;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
n++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
n++;
}
}
});
t1.start();
t2.start();
//为了防止主线程中的打印方法在子线程还没有将for循环走完之前就开始执行,
//我们需要让主线程等待子线程
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("n的最终值为:" + n);
j++;
}
}
}
解决方案
我们可以利用AtomicInteger类来保证++操作的原子性
i++,对应getAndIncrement;
++i,对应IncrementAndGet;
i–,对应getAndDecrement
–i,对应decrementAndGet
利用这些操作,我们可以将以上代码修改如下:
public class ThreadAtomicDemo {
//static private int n;
//将int类型的变量改为AtomicInteger类型的变量
static AtomicInteger n;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
//n = 0;
//创建原子整数初始值为0
n = new AtomicInteger(0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
//n++;
n.getAndIncrement(); //用于替换n++
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
//n++;
n.getAndIncrement();//用于替换n++
}
}
});
t1.start();
t2.start();
//为了防止主线程中的打印方法在子线程还没有将for循环走完之前就开始执行,
//我们需要让主线程等待子线程
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("n的最终值为:" + n);
j++;
}
}
}
原子类CAS原理分析
CAS操作中的ABA问题
ABA问题主要的意思就是,一个线程将数据A变为了B,然后又将B变为了A,但是另一个线程在读取数据时,并没有发现这种变化,读取到的数据还是A。为了解决这个问题,我们可以引入AtomicStampedReference:
AtomicStampedReference(初始值,时间戳):构造函数设置初始值和初始时间戳
getStamp:获取时间戳
getReference:获取预期值
compareAndSet(预期值,更新值,预期时间戳,更新时间戳):实现CAS时间戳和预期值的对比
代码演示:
public class ThreadAtomicDemo {
//static private int n;
//将int类型的变量改为AtomicInteger类型的变量
//static AtomicInteger n;
//避免发生ABA问题,引入AtomicStampedReference
static AtomicStampedReference<Integer> n;
public static void main(String[] args) {
int j = 0;
while (j < 100) {
//n = 0;
//创建原子整数初始值为0
//n = new AtomicInteger(0);
n = new AtomicStampedReference<>(0,0);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
//n++;
//n.getAndIncrement(); //用于替换n++
int stamp;
Integer reference;
do{
stamp = n.getStamp();//获取预期时间戳
reference = n.getReference();//获取预期值
}while(!n.compareAndSet(reference,reference+1,stamp,stamp+1));
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
//n++;
//n.getAndIncrement();//用于替换n++
int stamp;
Integer reference;
do{
stamp = n.getStamp();//获取预期时间戳
reference = n.getReference();//获取预期值
}while(!n.compareAndSet(reference,reference+1,stamp,stamp+1));
}
}
});
t1.start();
t2.start();
//为了防止主线程中的打印方法在子线程还没有将for循环走完之前就开始执行,
//我们需要让主线程等待子线程
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("n的最终值为:" + n.getReference());
j++;
}
}
}
Lock
Lock接口关系图
Lock与ReadWriteLock是两大锁的根接口,
Lock接口支持重入、公平等锁规则,其实现类有ReentrantLock、ReadLock、WriteLock
ReadWriteLock接口定义了读取者共享而写入者独占的锁,其实现类有:ReentrantReadWriteLock
重入锁
不可重入锁指的是,一个线程请求它已经拥有的锁时会阻塞;
可重入锁指的是,线程可以进入它已经拥有的锁的同步代码块,而拥有的锁的次数会加1,相应的在解锁的时候也需要解锁相同的次数,也就是说,如果加锁5次,那么也就需要解锁5次
代码演示:
public class ReLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
lock.lock();
System.out.println("加锁次数:" + (i + 1));
}
System.out.println("=====================================");
for (int i = 0; i < 10; i++) {
try {
System.out.println("解锁次数:" + (i + 1));
}finally {
lock.unlock();
}
}
}
}
读写锁
读写锁,读与读不互斥,读与写互斥,写与写互斥,也就是多个线程可以同时读数据;读数据的时候不能写;写数据的时候不能读;不能同时写数据
代码演示:
public class ReadWriteLockDemo {
private Map<String,String> map = new HashMap<>(); //操作的map对象
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); //读操作锁
private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); //写操作锁
//读操作加锁
public String get(String key){
readLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"读操作已加锁,开始读操作......");
TimeUnit.SECONDS.sleep(3);
return map.get(key);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("读操作发生异常!");
return null;
} finally {
System.out.println(Thread.currentThread().getName()+"读操作已解锁,读操作结束!");
readLock.unlock();
}
}
//写操作加锁
public void put(String key,String value){
writeLock.lock();
try{
System.out.println(Thread.currentThread().getName()+"写操作已加锁,");
TimeUnit.SECONDS.sleep(3);
map.put(key,value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("写操作已解锁!");
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
readWriteLockDemo.put("k1","v1");
new Thread("一号读线程") {
@Override
public void run() {
System.out.println(readWriteLockDemo.get("k1"));
}
}.start();
new Thread("二号读线程"){
@Override
public void run() {
System.out.println(readWriteLockDemo.get("k1"));
}
}.start();
new Thread("三号读线程"){
@Override
public void run() {
System.out.println(readWriteLockDemo.get("k1"));
}
}.start();
}
}
Volatile关键字
作用
使用volatile关键字修饰一个共享变量(类的成员变量、类的静态成员变量)以后,这个共享变量就具有了以下特性:
①:保证了不同的线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对于其他线程来说是立即可见的(注意:不保证原子性);
②:禁止进行指令重排序(注意:保证变量所在行的有序性);
当程序执行到volatile变量的读操作或写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
应用场景
使用volatile关键字必须满足以下条件:
①:该变量的写操作不依赖于当前值
②:该变量没有包含在具有其他变量的不变式中
状态量标记:
//不依赖于当前值
volatile boolean flag = false;
flag = true;
while(flag){
do();
}
双重校验:
class SingleMode{
private volatile static SingelMode instance = null;
//设置为private后,外部无法通过构造函数创建该对象
private SingleMode(){
}
public static SingleMode getInstance(){
if(instance == null){
synchronized(SingleMode.class){
if(instance == null){
instance = new SingleMode();
}
}
}
return instance;
}
}