线程的上下文切换
概念:
-
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();
由于上锁开锁会消耗资源,不锁又会导致线程安全问题,下一章节继续讲解锁的选择和折中