JAVA多线程(二)


前言

前面我们学习了什么是线程,今天我们学习一下线程的安全问题


提示:以下是本篇文章正文内容,下面案例可供参考

一、线程的上下文切换

线程的上下文切换有一个前提条件:一个CPU的内核一个时间只能运行一个线程中的一个指令。

我们都知道CPU内核会在多个线程中来回切换来达到同时运行的效果,所以多线程创建到

目录

前言

一、线程的上下文切换

1.线程切换回来后,如何在上次执行的指令后执行?

2.线程执行随时都会切换,如何保证重要的指令能全部完成?

二、线程安全(同步)问题

CPU在多个线程中来回切换,可能导致某些重要的指令不能完整执行

三、线程安全问题的解决方法

总结


切换到另一个线程的过程叫做上下文切换。

既然是在多个线程中来回切换的就会出现几个问题:

1.线程切换回来后,如何在上次执行的指令后执行?

这是通过程序计数器来实现的,java虚拟机中有一个程序计数器,每个线程都有他私有的程序计数器,生命周期与线程的生命周期保持一致。

2.线程执行随时都会切换,如何保证重要的指令能全部完成?

这就是我下面要讲到的线程安全问题了

二、线程安全(同步)问题

CPU在多个线程中来回切换,可能导致某些重要的指令不能完整执行

出现线程安全的问题需要满足一些个条件,即多个线程在同一时间执行同一段指令或修改同一个变量。

举个例子:银行向100个账户转账,我们来看看银行的总账

import java.util.Random;

/**
 * 银行转账的案例
 */
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多线程(二)

三、线程安全问题的解决方法

要解决上面的问题就是给程序上锁,让当前线程完整执行一段指令,执行完后释放锁,再让其他线程运行

给程序上锁有几种方式

(一)同步方法

同步方法就是在方法上添加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());
    }

我们可以看到上锁后每次银行的总账都没有出现问题了。

JAVA多线程(二)

(二)同步代码块

使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块。多个同步代码块如果使用相同的锁对象,那么他们就是同步的。如果是非静态方法就用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());
        }

JAVA多线程(二)

(三)同步锁

同步锁的使用就是定义一个同步锁对象(成员变量)然后在方法内部上锁,最后释放锁

//成员变量
Lock lock = new ReentrantLock();

//方法内部上锁
lock.lock();
try{
	代码...
}finally{
	//释放锁
	lock.unlock();
}

同样拿上面的案例来试试

    Lock lock = new ReentrantLock();
    private void transfer(int from,int to,int money) {
        lock.lock();
        try {
            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());
        }finally {
            lock.unlock();
        }
    }

JAVA多线程(二)

总结

三种锁的对比:

粒度:同步代码块/同步锁 < 同步方法

编程简便:同步方法 > 同步代码块 > 同步锁

性能:同步锁 > 同步代码块 > 同步方法

功能性/灵活性:同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法

 加锁牢记两点:1.锁的对象2.锁的范围

上一篇:网络地址转换 NAT配置


下一篇:Solidity基础语法结构 - 个人笔记(2)