学习目标
-
线程的上下文切换
-
线程的安全(同步)问题
-
线程安全问题的解决方法
-
ThreadLocal的介绍
线程的上下文切换
前提:一个CPU的内核一个时间只能运行一个线程中的一个指令
线程并发:CPU内核会在多个线程间来回切换运行,切换速度非常快,达到同时运行的效果
问题1:
线程切换回来后,如何从上次执行的指令后执行?
程序计数器(每个线程都有,用于记录上次执行的行数)
问题2:
线程执行会随时切换,如何保证重要的指令能完全完成?
线程安全问题
如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这 样会造成大量线程都处于等待状态
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
线程的安全(同步)问题
CPU在多个线程间切换,可能导致某些重要的指令不能完整执行,出现数据的问题。
出现线程安全问题的三个条件:
-
多个线程
-
同一个时间
-
执行同一段指令或修改同一个变量
案例的实现
/**
* 银行转账的案例
*/
public class BankDemo {
//模拟100个银行账户
private int[] accounts = new int[100];
{
//初始化账户
for (int i = 0; i < accounts.length; i++) {
accounts[i] = 10000;
}
}
/**
* 模拟转账
*/
public void transfer(int from,int to,int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
/**
* 计算总余额
* @return
*/
public int getTotal(){
int sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
return sum;
}
public static void main(String[] args) {
BankDemo bank = new BankDemo();
Random random = new Random();
//模拟多次转账过程
for (int i = 0; i < 50; i++) {
new Thread(() -> {
int from = random.nextInt(100);
int to = random.nextInt(100);
int money = random.nextInt(2000);
bank.transfer(from,to,money);
}).start();
}
}
}
线程安全问题的解决方法
解决多线程的并发安全问题,java无非就是加锁,具体就是两个方法
(1) Synchronized(java自带的关键字)
(2) lock 可重入锁 (可重入锁这个包java.util.concurrent.locks 底下有两个接口,分别对应两个类实现了这个两个接口:
(a)lock接口, 实现的类为:ReentrantLock类 可重入锁;
(b)readwritelock接口,实现类为:ReentrantReadWriteLock 读写锁)
也就是说有三种:
(1)synchronized 是互斥锁;
(2)ReentrantLock 顾名思义 :可重入锁
(3)ReentrantReadWriteLock :读写锁
同步方法
给方法添加synchronized关键字
作用是给整个方法上锁
过程:
当前线程调用方法后,方法上锁,其它线程无法执行,调用结束后,释放锁。
/**
* 模拟转账
*/
public synchronized void transfer(int from,int to,int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
锁对象:
-
非静态方法 --> this
-
静态方法 ---> 当前类.class
同步代码块
粒度比同步方法小,粒度越小越灵活,性能更高
给一段代码上锁
synchronized(锁对象){ 代码 }
锁对象,可以对当前线程进行控制,如:wait等待、notify通知;
任何对象都可以作为锁,对象不能是局部变量
//同步代码块
synchronized (lock) {
accounts[from] -= money;
System.out.printf("从%d转出%d%n", from, money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
synchronized的基本的原理:
一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控
线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放
如果锁没有其它线程持有,当前线程就持有锁,执行代码
底层汇编实现:
monitorenter .... monitorexit
同步锁
在java.concurrent并发包中的
Lock接口
基本方法:
-
lock() 上锁
-
unlock() 释放锁
常见实现类
-
ReentrantLock 重入锁
-
WriteLock 写锁
-
ReadLock 读锁
-
ReadWriteLock 读写锁
使用方法:
-
定义同步锁对象(成员变量)
-
上锁
-
释放锁
//成员变量 Lock lock = new ReentrantLock(); //方法内部上锁 lock.lock(); try{ 代码... }finally{ //释放锁 lock.unlock(); }
三种锁对比:
-
粒度
同步代码块/同步锁 < 同步方法
-
编程简便
同步方法 > 同步代码块 > 同步锁
-
性能
同步锁 > 同步代码块 > 同步方法
-
功能性/灵活性
同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法
悲观锁和乐观锁
何谓悲观锁与乐观锁
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
悲观锁
认为线程的安全问题非常容易出现,会对代码上锁
前面所讲的锁机制都属于悲观锁
悲观锁的锁定和释放需要消耗比较多的资源,降低程序的性能
乐观锁
认为线程的安全问题不是非常常见的,不会对代码上锁
有两种实现方式:
-
版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
-
CAS (Compare And Swap)比较和交换算法
-
通过内存的偏移量获得数据的值
-
计算出一个预计的值
-
将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
-
悲观锁和乐观锁对比
-
悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
-
乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
案例分析
public class AtomicDemo {
static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Thread(() ->{
count++;
}).start();
}
System.out.println(count);
}
}
问题:多线程同时执行++操作,最后结果少了
分析:
count++ 分解为三个指令:
-
从内存中读取count的值
-
计算count+1的值
-
将计算结果赋值给count
这三个指令不是原子性的,A线程读取count值10,加1后得到11,准备赋值给count;B线程进入读取count也是10,加1得到11,赋值给count为11;切换会A线程,赋值count为11。
解决方案:
-
悲观锁,使用同步方法、同步块、同步锁
-
乐观锁
使用原子整数
原子类
AtomicInteger类
AtomicInteger介绍
AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。
AtomicInteger使用场景
AtomicInteger提供原子操作来进行Integer的使用,因此十分适合高并发情况下的使用。
案例演示
public class AtomicDemo {
static int count = 0;
static AtomicInteger integer = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(() ->{
count++;
//递增
integer.incrementAndGet();
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count:"+count);
System.out.println("atomic:"+integer.get());
}
}
ThreadLocal
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
public class ThreadLocalDemo {
static int count = 0;
//线程局部变量
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
//设置初始值
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
count++;
local.set(local.get() + 1);
System.out.println(Thread.currentThread().getName() + "--->" + local.get());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
System.out.println(local.get());
}
}
ThreadLocal底层的实现
维持线程封闭性的一种规范方法是使用ThreadLocal。它提供了set和get等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get方法总是返回由当前执行线程在调用set时设置的最新值。那么,我们就看看关于这两个方法的JDK源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
用文字描述就是:get方法里通过Thread.currentThread()获取当前执行的线程,该方法内部包含一个ThreadLocalMap对象,针对每个thread保留一个entry,该entry的值就是当前线程保留的一个变量副本。
应用场景:ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。例如,由于JDBC的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,就不是线程安全的。通过将JDBC的连接对象保存到ThreadLocal中,每个线程都会拥有属于自己的连接对象副本。
最后案列实现
编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)
分析问题原因,解决问题
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}).start();
}
}
static class Singleton{
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}