当前位置: 首页> 教育> 高考 > day11-多线程

day11-多线程

时间:2025/7/9 5:04:37来源:https://blog.csdn.net/qq_58478983/article/details/142186657 浏览次数:0次

一、线程安全问题

线程安全问题出现的原因?存在多个线程在同时执行同时访问一个共享资源存在修改该共享资源
线程安全:多个线程同时修改同一个资源

取钱案例 小明和小红是一对夫妻,他们有一个共同的账户,余额是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;}
​//调用取钱方法@Overridepublic 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;}
​@Overridepublic 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;}
​@Overridepublic 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;}@Overridepublic 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起提供了代表线程池的接口:ExecutorServiceExecutorService接口---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() {@Overridepublic 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*/@Overridepublic 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进入计时等待,时间到进入等待状态。

关键字:day11-多线程

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: