java多线程入门(二)

线程的上下文切换

概念:

  • CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

  • 在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。

  • 所以任务从保存到再加载的过程就是一次上下文切换

  • 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。

  • 时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

多线程运行速度:

 

  • 当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。

  • 这是因为线程有创建和上下文切换的开销。

减少上下文切换的方法:

减少上下文切换的方法有无锁并发编程CAS算法使用最少线程使用协程

  • 无锁并发编程。 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

  • CAS算法。 Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

  • 使用最少线程。 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

  • 协程: 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

线程安全(同步)

在cpu进行上下文切换时,由于指令抢占分配导致的数据问题。

条件:1.多线程 2同时间 3.同执行.

下面我们用一个银行转账来模拟线程安全问题:

案例:

public class Demo01 {
    /**
     * 模拟账户
     */
    private int[] account = new int[100];
    {
        //初始化账户
        Arrays.fill(account, 10000);
    }
    /**
     * 模拟转账方法
     */
    //void钱加synchronized关键字可以自动在执行方法的时候上锁和释放锁
    public  void transfer(int from,int to ,int money){
        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());
            System.out.println(2);
    }
    /**
     * 计算银行总额的方法
     */
    public synchronized int getTotal(){
        int sum = 0;
        for (int value : account) {
            sum += value;
        }
        return sum;
    }
    //启动类
    public static void main(String[] args) {
        Demo01 demo01 = new Demo01();
        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);
                    demo01.transfer(from,to,money);
            }).start();
        }
    }
}

接下来我们看看输出结果:

从78转出649
从47转入649
银行账户总额999153
​
从51转出1309
从23转入1309
银行账户总额999153
​
从48转出847
从67转入847
银行账户总额1000000
​
从88转出1930
从67转入1930
银行账户总额1000000

可以看到,银行的总额并不是每次都是显示的1000000,这对一个银行系统来说,是一个非常大的问题。

而出现这种问题的原因就是多线程安全问题。

解析:

由于线程是互相抢占的,所以在线程里面的指令集在执行完某一个指令后,会出现切换到另外一个指令集中去执行某一个指令的情况,而在这里就体现在A线程执行到了取钱,就被B线程抢占执行了输出总额,这时并没有执行A线程的存钱,所以总额显示会变少。(A线程中的存钱指令处在队列中,只要进程一直运行,最终还是会被执行)

解决:

我们的目的是要将取钱,存钱,显示变为一个整体,执行的过程中不被抢占。于是java程序设计了一把“锁”,来保证某一段指令能连续执行不被抢占。下面介绍常用的三种线程同步方法:

  • synchronized同步

    synchronized有两种使用方法:

    • 同步方法

      //void前加synchronized关键字可以自动在执行方法的时候上锁和释放锁
      public synchronized void transfer(int from,int to ,int money)
    • 同步代码块

      //定义全局变量,作为统一标识
           final Object obj = new Object();
      //内部代码块锁
      synchronized (obj){
          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());
          System.out.println(1);
      }
  • Lock(java.concurrent)接口

常用方法:

lock() :上锁

unlock() :释放锁

常见实现类:

ReentrantLock 重入锁

WriteLock 写锁

ReadLock 读锁

ReadWriteLock 读写锁

使用:

//定义同步锁
Lock lock = new ReentrantLock();
//同步锁的使用方法
lock.lock();
try {
    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());
    System.out.println(2);
}finally {
//重点,执行完之后,必须解锁,否则整个程序会锁死
lock.unlock();

由于上锁开锁会消耗资源,不锁又会导致线程安全问题,下一章节继续讲解锁的选择和折中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值