线程的处理
CPU在某一时刻只能处理一个线程中的一个指令,CPU内核会在多个线程间来回切换运行,切换速度非常快,就像同时运行的一样,但是这意味着会出现线程安全问题。
CPU在多个线程间切换,可能导致某些重要的指令不能完整执行,此时如果CPU切换到的另一个线程对前一个线程使用的数据操作,就会造成数据出错,那么如何在程序中保证线程安全?
在Java中我们通过同步机制,来解决线程安全问题
同步的方式—操作代码块的同时,只能一个线程参与,其他线程等待,相当于一个单线程的过程。效率较多线程低
方法一:同步代码块
synchronize(同步监视器){
需要同步的代码
}
- 一定是需要被同步的代码
- 多个线程共同操作的变量
- 同步监视器,俗称 锁,任何一个对象都可以充当锁 (对象不能是局部变量)
注意:同一个线程要用同一把锁
在实现runnable 接口创建多个线程的方式中,可以考虑this充当同步监视器,在继承Thread类 创建多个线程对象的方式中,可以考虑this.class充当同步监视器
方法二:同步方法
操作共享数据的代码完整声明在一个方法中,可以将此方法声明为synchronized 同步的
关于同步方法的总结
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2.非静态同步方法,同步监视器都是: this
静态同步方法,同步监视器是: 当前类本身
方法三:解决线程安全的方式:lock锁
在java.concurrent并发包中的Lock接口
- lock() 上锁
- unlock() 释放锁
常见实现类
- ReentrantLock 重入锁
- WriteLock 写锁
- ReadLock 读锁
- ReadWriteLock 读写锁
//成员变量
Lock lock = new ReentrantLock();
//方法内部上锁
lock.lock();
try{
//代码...
}finally{
//释放锁
lock.unlock();
}
三种方法解决线程安全问题的例子
public class BankDemo {
//模拟100个银行账户
private int[] account = new int[100];
Lock lock = new ReentrantLock();
{
//初始化银行账户
for (int i = 0; i < account.length; i++) {
account[i] = 10000;
}
}
//转账 同步方法 方式2
public synchornized void transfer(int from ,int to,int money){
//lock锁 方式3
//lock.lock();
try {
//static(){ 静态代码块方式1
if (account[from] < money){
throw new RuntimeException("余额不足");
}
account[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
account[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账"+getTotal());
// }
}finally {
// lock.unlock();
}
}
//计算银行总金额
public int getTotal(){
int total = 0;
for (int i = 0; i < account.length; i++) {
total += account[i];
}
return total;
}
public static void main(String[] args) {
BankDemo bankDemo = new BankDemo();
for (int i = 0; i < bankDemo.account.length; i++) {
new Thread(() -> {
int from = (int) (Math.random()*100);
int to = (int) (Math.random()*100);
bankDemo.transfer(from,to,200);
}).start();
}
}
}
synchronized的基本的原理:
一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控
线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放
如果锁没有其它线程持有,当前线程就持有锁,执行代码
乐观锁和悲观锁
- 悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
- 乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
举个例子
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);
}
}
输出结果为 99997,说明发生了线程冲突,在某一个时刻多个线程访问操作count变量
解决方案:
-
悲观锁,使用同步方法、同步块、同步锁(没错,他们很悲观…)
-
乐观锁
什么是乐观锁?有两种实现方式:
-
版本号机制
利用版本号记录数据更新的次数,一旦更新版本号加1,线程修改数据后会判断版本号是否是自己更新的次数,如果不是就不更新数据。 -
CAS (Compare And Swap)比较和交换算法
- 通过内存的偏移量获得数据的值
- 计算出一个预计的值
- 将提交的实际值和预计值进行比较,如果相同执行修改,如果不同就不修改
刚刚的例子,我们使用乐观锁CAS的方法优化
/**
* @author Kan
* @Time 2021-12-08-20:33
*/
public class lock1 {
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());
}
}
结果
完美解决,但是CAS还是会遇到一些问题
比如:
一个线程 a 将数值改成了 b,接着又改成了 a,此时 CAS 认为是没有变化,其实 是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次 version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。
除此之外
CAS 造成 CPU 利用率增加 , CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu 资源会一直被占用
思考:懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)
class Bank {
private static Bank instance = null;
public static Bank getInstance() {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
class UserDesignThread implements Runnable{
@Override
public void run() {
Bank.getInstance();
}
}
public class BankTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Bank instance = Bank.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
哈希码不同代表单例模式获取的对象不唯一,是不允许发生这样的情况,原因是多个线程抢占,导致对象还没创建,或还没赋值,就把null赋值给了新进入getInstance()方法的线程,于是乎,懒汉不知不觉凌乱了。解决方法,保证线程安全,加个锁
public static Bank getInstance() {
synchronized(Bank.class){
if (instance == null) {
instance = new Bank();
}
}
return instance;
}
//或者
public static Bank getInstance() {
lock.lock();
if (instance == null) {
instance = new Bank();
}
lock.unlock();
return instance;
}
//或者
public static synchronized Bank getInstance(){
if (instance != null){
instance = new Bank();
}
return instance;
}