JAVA——线程(线程和进程,线程安全)

线程的处理

CPU在某一时刻只能处理一个线程中的一个指令,CPU内核会在多个线程间来回切换运行,切换速度非常快,就像同时运行的一样,但是这意味着会出现线程安全问题。

CPU在多个线程间切换,可能导致某些重要的指令不能完整执行,此时如果CPU切换到的另一个线程对前一个线程使用的数据操作,就会造成数据出错,那么如何在程序中保证线程安全?

在Java中我们通过同步机制,来解决线程安全问题
同步的方式—操作代码块的同时,只能一个线程参与,其他线程等待,相当于一个单线程的过程。效率较多线程低

方法一:同步代码块
synchronize(同步监视器){
需要同步的代码
}

  1. 一定是需要被同步的代码
  2. 多个线程共同操作的变量
  3. 同步监视器,俗称 锁,任何一个对象都可以充当锁 (对象不能是局部变量)
    注意:同一个线程要用同一把锁
    在实现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. 悲观锁,使用同步方法、同步块、同步锁(没错,他们很悲观…)

  2. 乐观锁
    什么是乐观锁?

    有两种实现方式:

  • 版本号机制
    利用版本号记录数据更新的次数,一旦更新版本号加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;
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值