一、线程安全问题
线程安全问题出现的原因? 存在多个线程在同时执行 同时访问一个共享资源 存在修改该共享资源 线程安全: 多个线程同时修改同一个资源
取钱案例 小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元 如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
实现步骤 1. 创建1个账户类,在类中定义账户金额和取钱方法 2. 创建1个取钱线程类,在类中调用取钱方法 3. 在主线程中创建1个账户对象代表二人的共享账户 4. 在主线程中创建2个取钱线程,分别代表小明和小红,并将共享账户对象传递给2个线程处理 5. 启动2个线程
public class Demo {
public static void main(String[] args) {
//在主线程中创建1个账户对象代表二人的共享账户
Account account = new Account();
//创建2个取钱线程
Person xiaoming = new Person(account);
xiaoming.setName("小明");//线程名称
Person xiaohong = new Person(account);
xiaohong.setName("小红");//线程名称
//启动2个线程,开始取钱
xiaoming.start();
xiaohong.start();
}
}
//账户类
class Account{
//定义账户金额和取钱方法
private Integer money = 100000;
public void drawMoney(Integer drawMoney){
//判断余额是否充足
if(drawMoney > money){
throw new RuntimeException(Thread.currentThread().getName() + "余额不足");
}
//模拟取钱
System.out.println(Thread.currentThread().getName() +"ATM吐出" + drawMoney);
//更新余额
money -= drawMoney;
System.out.println("余额是" + money);
}
}
//取钱线程类
class Person extends Thread{
//线程共享的账户对象,不能创建Account对象,要传入Account对象
private Account account;
//构造器
public Person(Account account){
this.account = account;
}
//调用取钱方法
@Override
public void run() {
account.drawMoney(10000);
}
}
运行结果:
小明100000
小红100000
小红余额为-100000
小明余额为0
二、线程同步方案
线程同步 线程同步就是让多个线程实现先后依次访问共享资源,这样就解决了安全问题,它最常见的方案就是加锁 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
2.1 同步代码块
同步代码块 把访问共享资源的核心代码给上锁,以此保证线程安全 格式 synchronized(同步锁){ 访问共享资源的核心代码 } 原理 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行 注意 1、对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug注意 2、同步锁不建议随便使用一个唯一的对象,也能锁住,但可能影响无关线程, 建议使用共享资源作为锁对象 对于实例方法建议使用this作为锁对象规范 对于静态方法建议使用字码(类名.class) 对象作为锁对象
public class Account {
//账户余额
private Integer balance = 100000;
//取钱
public void drawMoney(Integer money) {
//0.获取线程名称
String threadName = Thread.currentThread().getName();
synchronized (this){//排他互斥锁
//this: 当前对象, 当前对象就是锁对象,这里是共享资源,即账户余额
//1. 判断余额是否充足
if (money > balance) {
System.out.println(threadName + "取钱失败,余额不足");
return;//方法结束
}
//2. 如果够,出钱
System.out.println(Thread.currentThread().getName() + "取钱成功");
//3. 更新余额
balance -= money;
System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
}
}
}
//取钱人
public class Person extends Thread {
//账户
private Account account;
public Person(Account account) {
this.account = account;
}
@Override
public void run() {
//调用取钱的方法
account.drawMoney(100000);
}
}
/*
测试类
*/
public class Demo {
public static void main(String[] args) {
//1. 创建一个账户对象
Account account = new Account();
//2. 创建两个取钱的人,并把账户交给它
Person person1 = new Person(account);
person1.setName("小明");
Person person2 = new Person(account);
person2.setName("小红");
//3. 启动2个线程
person1.start();
person2.start();
}
}
2.2 同步方法
同步方法 把访问共享资源的核心方法给上锁,以此保证线程安全。 格式 修饰符 synchronized 返回值类型 方法名称(形参列表) { 操作共享资源的代码 } 原理 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。 如果方法是实例方法:同步方法默认用this作为的锁对象。 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
public class Account {
//账户余额
private Integer balance = 100000;
public Integer getBalance() {
return balance;
}
//充值
public synchronized void setBalance(Integer balance) {
this.balance = this.balance + balance;
System.out.println(Thread.currentThread().getName() + "充值之后余额为:" + balance);
}
//取钱
public synchronized void drawMoney(Integer money) {
//0.获取线程名称
String threadName = Thread.currentThread().getName();
//1. 判断余额是否充足
if (money > balance) {
System.out.println(threadName + "当前余额为:" + balance);
System.out.println(threadName + "取钱失败,余额不足");
return;//方法结束
}
//2. 如果够,出钱
System.out.println(Thread.currentThread().getName() + "取钱成功");
//3. 更新余额
balance -= money;
System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
}
}
//取钱人
public class Person extends Thread {
//账户
private Account account;
public Person(Account account) {
this.account = account;
}
@Override
public void run() {
//调用取钱的方法
account.drawMoney(100000);
//调用取钱的方法
account.setBalance(2000);
}
}
/*
测试类
*/
public class Demo {
public static void main(String[] args) {
//1. 创建一个账户对象
Account account = new Account();
//2. 创建两个取钱的人,并把账户交给它
Person person1 = new Person(account);
Person person2 = new Person(account);
//3. 启动2个线程
person1.start();
person2.start();
}
}
是同步代码块好还是同步方法好一点?
范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
可读性:同步方法更好。
2.3 Lock锁
Lock锁概述 Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大 Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建锁对象 方法 public ReentrantLock() 创建锁对象 public void lock() 上锁 public void unlock() 释放锁 Lock锁使用规范 规范1、锁对象创建在成员位置,使用final修饰 规范2、释放锁的代码写在finally块中
构造器 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
Lock常用方法名称 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
public class Account {
//账户余额
private Integer balance = 100000;
//创建锁对象
private Lock lock = new ReentrantLock();
//取钱
public void drawMoney(Integer money) {
//0.获取线程名称
String threadName = Thread.currentThread().getName();
//1.上锁
lock.lock();
try {
//2.判断余额是否充足
//2.1 判断余额是否充足
if (money > balance) {
System.out.println(threadName + "取钱失败,余额不足");
return;//方法结束
}
//2.2 如果够,出钱
System.out.println(Thread.currentThread().getName() + "取钱成功");
//2.3 更新余额
balance -= money;
System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
} catch (Exception e){
e.printStackTrace();
}finally {
//3.释放锁
lock.unlock();
}
}
}
//取钱人
public class Person extends Thread {
//账户
private Account account;
public Person(Account account) {
this.account = account;
}
@Override
public void run() {
//调用取钱的方法
account.drawMoney(100000);
}
}
/*
测试类
*/
public class Demo {
public static void main(String[] args) {
//1. 创建一个账户对象
Account account = new Account();
//2. 创建两个取钱的人,并把账户交给它
Person person1 = new Person(account);
Person person2 = new Person(account);
// 设置线程的名称
person1.setName("张三");
person2.setName("李四");
//3. 启动2个线程
person1.start();
person2.start();
}
}
三种线程同步方式的对比
同步代码块 | 同步方法 | lock | |
---|---|---|---|
语法 | synchronized (this){ } | synchronized 方法(){ } | lock.lock(); lock.unlock(); |
加锁方式 | 自动加锁、释放锁 | 自动加锁、释放锁 | 手动加锁、释放锁 |
锁粒度 | 代码行 | 方法 | 代码行 |
三、线程池
3.1 认识线程池
线程池就是一个可以复用线程的技术 它就像一个大的池子一样,里面可以放置一些线程,当需要的时候,就从里面取出来用,用完了就还回去 如此一来,就不必频繁的创建和销毁线程了,大大的提高了线程的利用率,提供系统的性能
3.2 线程池的执行流程
线程池创建后,内部没有线程,当第一个任务提交后,线程工程就创建线程,
1.判断核心线程是否已满,如果未满,则创建一个新的核心线程来执行任务
2.如果核心线程满了,则判断工作队列是否已满,如果没满,则将任务存储到这个工作队列 3.如果工作队列满了,则判断最大线程数是否已满,如果没满,则创建临时线程执行任务
4.如果最大线程已满,则执行拒绝策略
3.3 创建线程池
JDK5.0起提供了代表线程池的接口:ExecutorService ExecutorService接口---ThreadPoolExecutor实现类
任务缓冲队列
队列 | 详解 |
---|---|
ArrayBlockingQueue | 基于数组的有界缓存等待队列,可以指定缓存队列的大小 |
LinkedBlockingQueue | 基于链表的无界阻塞队列,此时最大线程数无效 |
任务拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy: | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
3.4 线程池处理Runnable任务
线程池如何处理Runnable任务: 使用ExecutorService的方法: void execute(Runnable target)
public class Demo2 {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,// 核心线程数量
5,// 最大线程数量
10,// 临时线程的存活时间
TimeUnit.SECONDS,// 存活时间单位
new ArrayBlockingQueue(5),// 等待队列
Executors.defaultThreadFactory(),// 线程工厂
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println(threadPoolExecutor);
//提交多次任务
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("执行任务");
}
});
}
System.out.println(threadPoolExecutor);
}
}
3.5 线程池处理Callable任务
线程池如何处理Callable任务,并得到任务执行完后返回的结果? 使用ExecutorService的方法: Future<T> submit(Callable<T> command)
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
3,// 核心线程数量
5,// 最大线程数量
10,// 临时线程的存活时间
TimeUnit.SECONDS,// 存活时间单位
new ArrayBlockingQueue(5),// 等待队列
Executors.defaultThreadFactory(),// 线程工厂
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println(poolExecutor);
//执行自己的任务
SumTask sumTask1 = new SumTask(5);
Future<Integer> submit1 = poolExecutor.submit(sumTask1);// 返回未来任务对象,用于获取线程返回的结果
Integer sum1 = submit1.get();// 获取线程返回的结果
System.out.println(sum1);
SumTask sumTask2 = new SumTask(10);
Future<Integer> submit2 = poolExecutor.submit(sumTask2);
Integer sum2 = submit2.get();
System.out.println(sum2);
//关闭线程池
List<Runnable> runnables = poolExecutor.shutdownNow();// 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
System.out.println(runnables);// 返回未执行的任务
}
}
//需求: 编写一个任务类, 可以通过构造器接收n, 计算并返回1~n的和
class SumTask implements Callable<Integer> {
private int n;
public SumTask(int n) {// 有参构造
this.n = n;
}
/**
* 计算1-n的和
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
}
3.6 Executors工具类实现线程池
Executors工具类底层是基于什么方式实现的线程池对象?
线程池ExecutorService的实现类:ThreadPoolExecutor
Executors是否适合做大型互联网场景的线程池方案?
不合适。Executors指定了线程的参数,不能自己设置,而且设置的上限很大,可能会导致OOM。
建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则
四、线程通信(了解)
线程通信: 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
Object类的等待和唤醒方法(这些方法应该使用当前同步锁对象进行调用)
方法名称 | 说明 |
---|---|
void wait() | 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待的单个线程 |
void notifyAll() | 唤醒正在等待的所有线程 |
4.1 进程与线程
进程:正在运行的程序(软件)就是一个独立的进程 线程:线程是属于进程的,一个进程中可以同时运行很多个线程 关系:进程=火车 线程=车厢
4.2 并发与并行
并发: 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全 部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我 们的感觉这些线程在同时执行,这就是并发 并行: 在同一个时刻上,同时有多个线程在被CPU调度执行。
4.3 线程的生命周期和状态
public class Thread{ ... public enum State { NEW, 线程刚被创建,但是并未启动 RUNNABLE, 线程已经调用了start(),等待CPU调度 BLOCKED, 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 WAITING, 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法 才能够唤醒 TIMED_WAITING, 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态 TERMINATED; 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 } ... }
新建一个线程,正常进入就绪态,获取CPU时间片就会被执行,执行完就会结束, 而在执行期间若获取锁失败就会进入阻塞态,重新获取锁成功进入就绪态, 执行期间如调用wait进入等待状态,被notify唤醒进入就绪态, 执行器若调用sleep进入计时等待,时间到进入等待状态。