线程的上下文切换
一个cpu的内核同一时间只能执行一个线程中的一个指令
线程并发
CPU内核会在多个线程间来回切换运行,切换速度非常快,达到同时运行的效果
线程的(同步)安全问题
cpu在多个线程之间来回切换,可能导致某些重要的命令不能完整的执行,出现数据的遗失等问题,在多个线程同一时间,同时运行,执行修改一段指令或者同一变量就会出现以下问题
public class BankDemo {
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());
}
public int getTotal() {
int sun = 0;
for (int i = 0; i < accounts.length; i++) {
sun += accounts[i];
}
return sun;
}
public static void main(String[] args) {
BankDemo bankDemo = 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);
bankDemo.transfer(from, to, money);
}).start();
}
}
}
会出现银行总金额发生变化的问题,这是因为当线程发起转账之后,别的线程在此时抢占了cpu,也进行了转账,然后之前的线程未执行入账,这样就造成了数据丢失解决方法如下
线程安全问题的解决方法
解决方法:给程序上锁,让当前线程完整执行一段指令,执行完释放锁,其它线程再执行
几种不同上锁方法:
-
同步方法
-
同步代码块
-
同步锁
同步方法
给方法添加synchronized关键字
作用是给整个方法上锁
过程:
当前线程调用方法后,方法上锁,其它线程无法执行,调用结束后,释放锁。
public class BankDemo {
private int[] accounts = new int[100];
{
for (int i = 0; i < accounts.length; i++) {
accounts[i] = 10000;
}
}
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());
}
public int getTotal() {
int sun = 0;
for (int i = 0; i < accounts.length; i++) {
sun += accounts[i];
}
return sun;
}
public static void main(String[] args) {
BankDemo bankDemo = 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);
bankDemo.transfer(from, to, money);
}).start();
}
}
}
锁对象的方法是,非静态方法使用 this关键字,静态则 当前类.class
同步代码块
粒度比同步方法小,粒度越小越灵活,性能更高 。锁对象,可以对当前线程进行控制,如: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会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放,如果锁没有其它线程持有,当前线程就持有锁,执行代码
同步锁
在java.concurrent并发包中的
Lock接口
基本方法:
-
lock() 上锁
-
unlock() 释放锁
常见实现类
-
ReentrantLock 重入锁
-
WriteLock 写锁
-
ReadLock 读锁
-
ReadWriteLock 读写锁
使用方法:
-
定义同步锁对象(成员变量)
-
上锁
-
释放锁
Lock lock = new ReentrantLock();
//方法内部上锁
lock.lock();
try{
代码...
}finally{
//释放锁
lock.unlock();
}
三种锁对比:
-
粒度
同步代码块=同步锁 < 同步方法
-
编程简便
同步方法 > 同步代码块 > 同步锁
-
性能
同步锁 > 同步代码块 > 同步方法
-
功能性/灵活性
同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法
悲观锁和乐观锁
悲观锁
认为线程的安全问题非常容易出现,会对代码上锁
前面所讲的锁机制都属于悲观锁
悲观锁的锁定和释放需要消耗比较多的资源,降低程序的性能
乐观锁
认为线程的安全问题不是非常常见的,不会对代码上锁
有两种实现方式:
-
版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。
-
CAS (Compare And Swap)比较和交换算法
-
通过内存的偏移量获得数据的值
-
计算出一个预计的值
-
将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
-
悲观锁和乐观锁对比
-
悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
-
乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景