概念:CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行,任务的状态保存及再加载。
一个CPU的内核一个时间只能运行一个线程中的一个指令
当CPU内核在多个线程间来回切换运行,切换速度很快时,达到同时运行的效果(因为CPU是不停的在多个线程来回切换,切换的过程过于频繁,性能会降低)
每个线程都有一个程序计数器(记录要上次执行的行数)
一组寄存器(保存当前线程的工作变量)
堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。
二.线程的安全(同步)问题线程安全问题指,存在共享资源和多个线程共同操作共享数据(多个线程,同一时间,执行同一段指令或修改同一个变量),就是当多个线程同时操作同一个可共享的资源时,导致指令不能完整的执行,整体数据不一致。
package com.hopu.Thread;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
//模拟100个银行账户
private int[] accounts = new int[100];
Lock lock = new ReentrantLock();
{
//初始化
for (int i = 0; i < 100; i++) {
accounts[i] = 3000;
}
}
public void transfer(int start,int end,int temp){
if(accounts[start] < temp){
throw new RuntimeException("余额不足");
}
accounts[start] -= temp;
System.out.printf("%d转出%dn",start,temp);
accounts[end] += temp;
System.out.printf("%d转入%dn",end,temp);
System.out.println("银行总余额" + sum());
}
public int sum(){
int sum = 0;
for (int i = 0; i < accounts.length; i++) {
sum += accounts[i];
}
return sum;
}
public static void main(String[] args) {
Bank bank = new Bank();
Random random = new Random();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
int number1 = random.nextInt(100);
int number2 = random.nextInt(100);
int money = random.nextInt(1500);
bank.transfer(number1,number2,money);
}).start();
}
for (int i = 0; i < bank.accounts.length; i++) {
System.out.println(bank.accounts[i]);
}
}
}
三.线程的安全问题的解决方法
核心思想:将程序进行上锁,当前线程完整的执行全部指令后,在释放锁,其他线程才能执行
三种上锁方法 1.同步方法synchronized调用方法对线程进行上锁,其他线程无法执行,只有该线程结束后其他进程才能运行(在方法添加synchronized即可)
锁对象:非静态方法(this )静态方法 (当前类.class) 不能修饰构造器、成员变量等。
public synchronized void transfer(int start,int end,int temp)2.同步代码块
将代码上锁,(//代码)为上锁部分 obj为锁对象,任何对象都可以作为锁,局部变量不可以
synchronized(obj){
//代码
}
synchronized的基本的原理:
一旦代码被synchronized包含,JVM会启动监视器(monitor)对这段指令进行监控
线程执行该段代码时,monitor会判断锁对象是否有其它线程持有,如果其它线程持有,当前线程就无法执行,等待锁释放
如果锁没有其它线程持有,当前线程就持有锁,执行代码
3.同步锁定义同步锁对象
上锁lock.lock();
释放锁lock.unlock();//一定要释放锁
public void transfer(int start,int end,int temp){
lock.lock();
if(accounts[start] < temp){
throw new RuntimeException("余额不足");
}
try{
accounts[start] -= temp;
System.out.printf("%d转出%dn",start,temp);
accounts[end] += temp;
System.out.printf("%d转入%dn",end,temp);
System.out.println("银行总余额" + sum());
}finally {
lock.unlock();
}
}
三种锁对比:
-
粒度
同步代码块/同步锁 < 同步方法
-
编程简便
同步方法 > 同步代码块 > 同步锁
-
性能
同步锁 > 同步代码块 > 同步方法
-
功能性/灵活性
同步锁(有更多方法,可以加条件) > 同步代码块 (可以加条件) > 同步方法
悲观锁是认为别人每次拿数据都会进行修改
悲观锁因为拿一次就要上锁一次,锁定和释放就需要更多的资源,会降低程序的运行速度
乐观锁认为别人每次拿数据都不会修改(两种实现方式)
1.每次修改会记录数据的版本号(更新次数),更新后版号++,线程修改数据会对版本号和更新次数作比较,返回false就不更新数据
2.CAS (Compare And Swap)比较和交换算法
并发修改共享数据时,一个线程将共享内存修改后,另一线程尝试对共享内存的修改会失败。
每次更新时,通过要更新的值及原始值的比较,CAS会比较原始值和当前获取到的值。如果相等,那么将值更新为要设置的值,如果不相等,就不修改
两者的区别悲观锁更加重量级,占用资源更多,应用线程竞争比较频繁的情况,多写少读的场景
乐观锁更加轻量级,性能更高,应用于线程竞争比较少的情况,多读少写的场景
ABA问题举个简单的例子,理发店对本店会员有一个老会员返利活动,凡是卡内金额低于20元的,会往卡内充值20元,当店家(初始进程)充值完成后,该会员卡内有(假设某一会员卡内有15元)35元,此时该会员将在店内消费了18元,卡内金额变成17元,店家检查(新的进程)会员返利情况,发现该会员还没有进行返利,但是这个会员是已经进行了返利的,这个会员再次消费到卡内金额低于20元,店家(新的进程)检查,以此类推,程序就会进入一个死循环
ABA问题解决
线程对公共变量的修改,给公共变量加一个版本号,每修改前先取到公共变量的版本号,对公共变量进行修改时,先比较当前的版本号是否和之前的公共变量版本号一致,如果一致,则进行修改,并将版本号进行自增
懒汉式单例模式编写懒汉式的单例模式,创建100个线程,每个线程获得一个单例对象,看是否存在问题(打印对象的hashCode,看是否相同)
package com.hopu.Thread;
public class MySingleton {
private static MySingleton instance = null;
// private MySingleton() {
// }
public static MySingleton getInstance(){
if (instance == null){
instance = new MySingleton();
}
return instance;
}
}
package com.hopu.Thread;
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
MySingleton instance = MySingleton.getInstance();
System.out.println(instance.hashCode());
}).start();
}
}
}
输出前两次单例模式对象hashCode值不一样,主要原因是第一个线程获取单例对象时,第二个线程抢占CPU时间片,第一个和第二个线程获取的单例对象instance都为空,所有输出的单例对象hashCode值都为空,后面的线程在去获取单例对象时,都为原本创建的单例对象,则输出的hashCode值则相同
解决办法对线程进程加锁,乐观锁,悲观锁都可以
总结


