多线程的特性
多线程编程要保证满足三个特性:原子性、可见性、有序性
- 原子性:类似事务,一个操作或者多个操作,要么全部执行并且不会被任何因素打断,要么全部不执行。
- 可见性:当前一个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值,在单线程就没有这个概念
- 有序性:程序执行的顺序按照我们代码指定的顺序来执行
多线程的控制类
为了保证多线程的三个特性,java引入了很多线程控制机制
- ThreadLocal:线程本地变量
- 原子类:保证变量原子操作
- Lock类:保证线程有序性
- Volatile关键字:保证线程变量可见性
1.ThreadLocal
1.作用
- ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本
- 当某些数据是以线程作为作用域并且不同线程具有不同的数据副本的时候,可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现
2.常用方法
- initialValue:副本创建方法
- get:获取副本方法
- set:设置副本方法
3.应用实例
模拟一个银行转账
public class ThreadLocalDemo {
//创建一个银行,钱,取款,存款
static class Bank{
//使用ThreadLocal创建该变量,数据类型以泛型传入
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
//使用initialValue初始化该值,每个线程得到该变量副本时,都是这个初始值
@Override
protected Integer initialValue() {
return 0;
}
};
//为该变量副本赋值
public void set(Integer money){
threadLocal.set(threadLocal.get()+money);
}
//获取该变量副本
public Integer get(){
return threadLocal.get();
}
}
//创建线程,执行转账
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());
}
}
}
public static void main(String[] args) {
Bank bank = new Bank();
Thread thread1 = new Thread(new Transfer(bank),"用户A");
Thread thread2 = new Thread(new Transfer(bank),"用户B");
thread1.start();
thread2.start();
}
}
4.实现原理
先从TreadLocal本身讲起,先看到get方法(set方法类似)
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//拿到当前线程中的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//取出值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
//返回结果
return result;
}
}
//如果当前线程中的ThreadLocalMap为空,那么对线程的ThreadLocalMap做一些初始化处理
return setInitialValue();
}
//初始化处理
private T setInitialValue() {
//返回ThreadLocal对象创建时的默认值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//该方法一般在创建时被重写
protected T initialValue() {
return null;
}
由get方法可知,在线程中,是已经有了一个ThreadLocalMap对象的,那ThreadLocalMap对象是什么呢
如图,可知ThreadLocalMap内部实际上是有一个Entry对象来存放数据
ThreadLocal的get和set都是获取当前线程,然后对其内部的ThreadLocalMap进行数据操作,也就是说在线程中的是已经初始化了一个ThreadLocalMap,去到源码看看
可以看到在Thread中有很多的构造器,拿我们上述例子的构造器来说
//线程类中就组合了一个ThreadLocalMap对象
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//构造器
Thread(Runnable target, AccessControlContext acc) {
init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
//注意到这里有一个inheritThreadLocals的布尔值
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
…………
//获取当前线程(在例子中为主线程)
Thread parent = currentThread();
…………
//如果当前线程中存在ThreadLocalMap对象,则将其元素也赋值到新建线程的ThreadLocalMap对象中
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
…………
}
//ThreadLocal类
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
//可以看到,就是将当前线程的ThreadLocalMap给到新建的线程
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
小结:也就是说,重写的initialValue方法,是当调用get和set时,为线程中的ThreadLocalMap进行一个初始化的定制,在第一个调用get和set时,基本上都会为线程调用一个initialValue
2.原子类
1.非原子类问题演示
i++是一个非线程安全的操作,演示一个多线程对同一个变量++的例子
public class ThreadAtomicDemo {
static private int n;
public static void main(String[] args) throws InterruptedException {
int j=0;
while (j<100){
n=0;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i =0;i<3000;i++){
n++;
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i =0;i<3000;i++){
n++;
}
}
});
thread1.start();
thread2.start();
//加入主线程,主线程会等待该线程完成后继续进行
thread1.join();
thread2.join();
System.out.println("n最终结果:"+n);
j++;
}
}
}
结果如下:
可以看到,结果不是稳定的6000,而是会出现少加多加的情况,因为i++实际在cpu中是分了三步,这是非原子类操作,多线程的情况下就容易出现问题
tp1 = i;
tp2 = i+1;
i = tp2;
2.原子类解决非原子类问题
Integer有对应的原子类AtomicInteger:赋值改为 n=new AtomicInteger(0) ;n++改为n.getAndIncrement() 用get()取出值
3.原子类介绍
java的java.util.concurrent.atomic包里提供了很多可以进行原子操作的类
- 更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
- 更新数组类型:AtomicIntegerArray、AtomicLongArray
- 更新引用类型:AtomicReference、AtomicStampedReference等
- 更新属性类型:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
3.Lock
1.Lock锁的类图介绍
2.可重入锁
- 可重入锁就是线程可以进入它已经拥有的锁的同步代码块
- 不可重入锁就是线程请求它已经拥有的锁时会阻塞
用代码演示:
public class Reentrant {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
for (int i =0 ;i<10;i++){
lock.lock();
System.out.println("加锁次数:"+(i+1));
}
for (int i =0 ;i<10;i++){
try {
System.out.println("解锁次数:"+(i+1));
}finally {
lock.lock();
}
}
}
}
结果发现,重入锁可以被已拥有该锁的线程重复获取
3.读写锁
多个线程可以同时读,但是读的时候不能写;多个线程不可以同时写,写的时候也不能读
package com.JIAT.deadLock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWrite {
private Map<String,String> map = new HashMap<String, String>();
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.sleep(3000);
return map.get(key);
}catch (InterruptedException e){
e.printStackTrace();
return null;
}finally {
System.out.println("读操作已经解锁");
readLock.unlock();
}
}
public void set(String key , String value){
writeLock.lock();
try {
System.out.println("写操作已经加锁,开始写操作");
Thread.sleep(3000);
map.put(key,value);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
System.out.println("写操作完成,已经解锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
final ReadWrite readWrite = new ReadWrite();
//主线程写操作
readWrite.set("key","value");
//线程一读操作
new Thread(){
@Override
public void run() {
System.out.println(readWrite.get("key"));
}
}.start();
//线程二写操作
new Thread(){
@Override
public void run() {
readWrite.set("key1","value1");
}
}.start();
//线程三读操作
new Thread(){
@Override
public void run() {
System.out.println(readWrite.get("key"));
}
}.start();
}
}
运行结果我们可以看到,当线程一读的时候,线程二是不能进行写的,线程一读完毕,线程二开始写,而此时线程三也不能读,只有等线程二写完了才可以读
4.Volatile关键字
1.作用
一个共享变量被Volatile修饰之后,那么就具备了两层语义
-
保证了不同线程对这个变量操作的可见性,当一个线程对其进行操作,对于其他线程来说是立即可见的(不保证原子性)
-
禁止进行指令重排序(保证变量所在行的有序性)
-
什么是指令重排序?
例如
int i = 0 ; i=1;i=2;i=3; //java会直接运行i=3忽略了前两句,因为编译器认为这是无效操作,会自动进行优化重排序我们的代码指令 Volatile int i = 0 ; i=1;i=2;i=3; //java会按照我们的指令逐个进行,不再重排序指令
-
2.应用场景
基于Volatile作用,使用Volatile时需要满足以下两点:
- 对该变量的写操作不依赖当前值
- 该变量没有包含在具有其他变量的表达式中
例如单例模式的双重校验
class Singleton{
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//看看是否已经创建
if(instance==null){
//只让单线程进行创建
synchronized(Singleton.class){
if(instance==null){
//此时如果没有volatile关键字立刻刷新,那其他线程有可能来不及看到最新的instance就已经通过第一层校验,就会再创建一个instance
instance = new Singleton();
}
}
}
return instance
}
}