1、什么是线程并发?
并发:一个cpu内核同一时间只能执行一个线程,在多个线程之间来回切换,由于cpu切换速度非常快,达到同时运行的效果,就是并发(实际并不是同一时间)。
如果实现来回切换?
程序计数器:每个线程创建时,都会为其分配独立的内存(栈,程序计数器),程序计数器就是指向当前线程执行的字节码的位置,从而达到线程之间来回切换。
如果保证当前线程重要的指令能够执行,不会被切换呢?
列如:模拟银行转账,随机十个账号之间转账,每次转账输出总账。总账按道理不会改变。
import java.util.Arrays;
import java.util.Random;
public class BankDemo {
//客户数组
int[] customers = new int[100];
//初始化账户
{
Arrays.fill(customers,10000);
}
//转账方法
public void transfer(int a,int b,int money){
if(customers[a]<money){
new RuntimeException("余额不足");
}
customers[a]-=money;
System.out.println(a+"转给"+b+":"+money);
customers[b]+=money;
System.out.println(b+"收到"+a+":"+money+"总账:"+total());
}
//统计总账方法
public int total(){
int sum=0;
for (int i = 0; i < customers.length; i++) {
sum+=customers[i];
}
return sum;
}
public static void main(String[] args) {
Random random = new Random();
BankDemo bankDemo = new BankDemo();
for (int i = 0; i <10 ; i++) {
new Thread(()->{
int money = random.nextInt(10000);
int a = random.nextInt(100);
int b = random.nextInt(100);
bankDemo.transfer(a,b,money);
}).start();
}
}
}
运行之后会发现,中间的总账会出现偏差。
出现的原因是什么呢?
这就是因为cpu一个线程里的指令还未执行完成(一个账号扣钱,另一个账户还没有加钱),被别的线程抢占,导致出现数据的偏差。这就出现了线程安全问题。
2、线程安全
出现线程安全问题的前提
同一时间,多个线程,执行同一段指令或同一个变量。
如果解决线程安全问题?
给程序上锁,让一个线程执行完所有指令后,释放锁,再执行其它线程。
3、上锁的几种方式
锁又分为乐观锁和悲观锁。
悲观锁:认为线程都不安全,所有的程序都上锁
1,同步方法
在方法上加 synchronized 关键字
//转账方法(同步方法)
public synchronized void transfer(int a,int b,int money){
if(customers[a]<money){
new RuntimeException("余额不足");
}
customers[a]-=money;
System.out.println(a+"转给"+b+":"+money);
customers[b]+=money;
System.out.println(b+"收到"+a+":"+money+"总账:"+total());
}
2,同步代码块
给要执行的一段代码上锁
synchronized(this){ //(同步代码块)
customers[a]-=money;
System.out.println(a+"转给"+b+":"+money);
customers[b]+=money;
System.out.println(b+"收到"+a+":"+money+"总账:"+total());
}
this代表锁对象
非静态方法用this,静态方法用 类名.class
3,同步锁
在java.concurrent并发包中的Lock接口
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankDemo3 {
//客户数组
int[] customers = new int[100];
Lock lock = new ReentrantLock();
//初始化账户
{
Arrays.fill(customers,10000);
}
//转账方法
public void transfer(int a,int b,int money){
if(customers[a]<money){
new RuntimeException("余额不足");
}
lock.lock();
try{
customers[a]-=money;
System.out.println(a+"转给"+b+":"+money);
customers[b]+=money;
System.out.println(b+"收到"+a+":"+money+"总账:"+total());
}finally {
lock.unlock();
}
}
//统计总账方法
public int total(){
int sum=0;
for (int i = 0; i < customers.length; i++) {
sum+=customers[i];
}
return sum;
}
public static void main(String[] args) {
Random random = new Random();
BankDemo3 bankDemo = new BankDemo3();
for (int i = 0; i <20 ; i++) {
new Thread(()->{
int money = random.nextInt(10000);
int a = random.nextInt(100);
int b = random.nextInt(100);
bankDemo.transfer(a,b,money);
}).start();
}
}
}
基本方法:
lock 上锁
unlock 解锁
解锁如果没有执行的话,那么一个线程结束后,没有解锁,其它线程就无法执行,程序就结束不了。所以写在finall里。
常见实现类
-
ReentrantLock 重入锁(如上所示)
-
WriteLock 写锁
-
ReadLock 读锁
-
ReadWriteLock 读写锁
上面三种方法的对比:
粒度大小(线程锁的范围)
同步方法>同步代码块>同步锁
执行效率
同步方法<同步代码块<同步锁
编写复杂度
同步方法<同步代码块/同步锁
乐观锁:认为线程安全问题不常见,不会对代码上锁
实现方式:
1,版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
2,CAS (Compare And Swap)比较和交换算法
通过内存的偏移量获得数据的值计算出一个预计的值将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
乐观锁和悲观锁比较
乐观锁:更轻量级,消耗的资源小,效率更高,适合多读少写;
悲观锁:更重量级,消耗的资源更多,效率慢,适合少读多写;
4,实现案例:for循环操作count++
原子类:
AtomicInteger类
原子整数,底层使用了CAS算法实现整数递增和递减操作,让count++成为一个整体执行。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
static int current = 0;
static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(()->{
atomic.incrementAndGet();
System.out.println(atomic);
}).start();
}
}
}
-
incrementAndGet 原子递增
-
decrementAndGet 原子递减
CAS会出现ABA问题
根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。
如果预期值与实际值不一致,会一直等待,对cpu消耗大。
ABA问题案例:饭店做活动,给会员卡里不足19元的客户充值20元,大于19元的不充值。假如有一百个客户,创建十个线程去执行,最后结果可能会发现充值的数值大于一百个客户。
问题原因:A线程执行充值后,客户又消费到19元一下,线程B又会充值。
解决方案:AtomicStampedReference
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampReferenceDemo {
static AtomicStampedReference<Integer> atomic = new AtomicStampedReference<>(19, 0);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
int stamp = atomic.getStamp();
System.out.println("标记值stamp:" + stamp);
//充值线程
new Thread(() -> {
while (true) {
Integer count = atomic.getReference();
if (count < 20) {
// 四个参数代表的意思:期望值,写入的新值,期望标记,新标记值
if (atomic.compareAndSet(count, count + 20, stamp, stamp + 1)) {
System.out.println("满足条件充值了20元,余额为:" + atomic.getReference());
break;
}
} else {
System.out.println("不满足充值条件");
break;
}
}
}).start();
}
//消费线程
new Thread(() -> {
for (int i = 0; i < 10; i++) {
while (true) {
int stamp = atomic.getStamp();
System.out.println("标记值为:" + stamp);
Integer money = atomic.getReference();
if (money > 10) {
if (atomic.compareAndSet(money, money - 10, stamp, stamp + 1)) {
System.out.println("成功消费了10元,余额为:" + atomic.getReference());
break;
}
} else {
System.out.println("余额不足");
break;
}
}
}
}).start();
}
}
ThreadLocal类
线程局部变量,会为每个线程分配一个变量副本,线程中的变量不互相影响。
public class ThreadLocalDemo {
//线程局部变量
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
new Thread(()->{
local.set(local.get()+1);
System.out.println(Thread.currentThread().getName()+":"+local.get());
}).start();
}
}
}
AtomicInteger类与ThreadLocal类比较:
AtomicInteger:让指令成为一个整体,例如多个人踢足球,每个人踢两脚(两个指令一起执行)。
ThreadLocal:给每个线程分配一个线程副本,每个人发一个足球(每个足球不互相影响)
5,案例:
编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)
分析问题原因,解决问题
问题:会出现不同的对象,而单例模式只会有一个实例。
原因:当一个线程去调用方法创建对象时,被另一个线程抢占,导致创建不同的对象。
解决方案:给程序上锁(同步锁)
单例模式
public class SunDemo {
private static SunDemo sunDemo=null;
private SunDemo() {
}
public static SunDemo getSun(){
if(sunDemo==null){
sunDemo=new SunDemo();
}
return sunDemo;
}
}
创建对象
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SunThread {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i <100 ; i++) {
new Thread(()->{
lock.lock();
try{
SunDemo sun = SunDemo.getSun();
System.out.println("获得了对象,"+sun);
}finally {
lock.unlock();
}
}).start();
}
}
}
结果:
总结
线程安全问题出现的前提,解决的方案。乐观锁:同步方法,同步代码块,同步锁。
悲观锁:CAS,原子类。CAS出现ABA问题,以及解决方式:AtomicStampedReference。
目录
AtomicInteger类与ThreadLocal类比较:
编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)