目录
一.线程概述
1.进程
进程:在一个操作系统中,每个独立执行的程序都可以称之为一个进程。
2.线程
2.1.线程:一个进程中可以由多个执行单元同时运行,来完成一个或多个程序任务,这些执行单元就叫线程。一个进程中至少有一个线程。当Java程序启动时,就会创建一个进程,该 进程会默认创建一个线程,这个线程会去运行main()方法中的代码。正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。
【注意】CPU一次只能只能执行一个线程,多线程的本质就是CPU轮流在多个单线程之间切换,只不过CPU切换的速度极快,让人觉得多个线程在同时执行。
2.2.线程的并发
并发:CPU分时轮询的执行线程。
2.3.线程的并行
并行:同一个时刻同时在执行。如果电脑是4核8线程的,那么就是并行就是CPU最多让8个线程同时执行,而并发就是CPU在切换线程时最多8个8个地切换,因此,多个线程其实是并发与并行同时进行的。
3.进程与线程关系图:
二.线程的创建
1.继承Thread类
1.1.实现过程:
(1)定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
(2)创建MyThread类的对象
(3)调用线程对象的start()方法启动线程(启动后还是执行run方法的)
1.2.代码实现:
package cn.zcj.Thread;
public class ThreadDemo_01 extends Thread{
public static void main(String[] args) {
//3.new一个线程
MyThread myThread = new MyThread();
//4.调用start()方法启动线程,此时实际调用的依旧是run方法
myThread.start();
//4.1.run()方法
//myThread.run();
for(int i = 51;i<100;i++){
System.out.println("主线程输出:"+i);
}
}
}
/**
* 1.定义一个线程类继承Thread类
*/
class MyThread extends Thread{
//2.重写run方法,里面是定义线程以后要干什么的
@Override
public void run() {
for(int i = 0;i<50;i++){
System.out.println("子线程输出:"+i);
}
}
}
输出结果:
1.3.优缺点
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
1.4.思考两个问题:
(1)为什么代码中把开启线程的start方法放在main方法的for循环之前?
答:这样才能呈现出多线程效果,如果main方法的for循环之后再开启线程,那么整个过程就相当于main方法的for业务已经执行完了再去执行新建线程里的业务,就是一个单线程效果了。
(2)调用start方法与调用run方法的区别?
答:start方法是开启线程,run方法是相当于调用普通方法。
2.实现Runnable接口
2.1.实现过程:
(1)定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
(2)创建MyRunnable任务对象
(3)把MyRunnable任务对象交给Thread处理
(4)调用线程对象的start()方法启动线程
2.2.代码实现一:
package cn.zcj.Thread;
public class ThreadDemo_02{
public static void main(String[] args) {
//3.创建一个任务对象
MyRunnable myRunnable = new MyRunnable();
//4.把任务对象交给Thread处理
Thread thread = new Thread(myRunnable);
//5.启动对象
thread.start();
for (int i = 51;i < 100;i++){
System.out.println("主线程任务:"+i);
}
}
}
/**
* 1.定义一个线程任务,实现Runnable接口
*/
class MyRunnable implements Runnable{
//2.重写run()方法,定义线程的执行任务
@Override
public void run() {
for (int i = 0;i < 50;i++){
System.out.println("子线程任务:"+i);
}
}
}
2.3.输出结果:
2.4.优缺点:
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
2.5.另一种写法(与第一种写法效果相同):
package cn.zcj.Thread;
public class ThreadDemo_02Other {
public static void main(String[] args) {
//匿名内部类写法
Runnable myRunnable = new Runnable(){
@Override
public void run() {
for (int i = 0;i < 100;i++){
System.out.println("子线程任务:"+i);
}
}
};
//把任务对象交给Thread处理
Thread thread = new Thread(myRunnable);
//启动对象
thread.start();
for (int i = 101;i < 200;i++){
System.out.println("主线程任务:"+i);
}
}
}
3.实现Callable接口
3.1.实现过程:
(1)得到任务对象
a.定义类实现Callable接口,重写call方法,封装要做的事情。
b.用FutureTask把Callable对象封装成线程任务对象。
(2)把线程任务对象交给Thread处理。
(3)调用Thread的start方法启动线程,执行任务
(4)线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
3.2.代码实现:
package cn.zcj.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo_03 {
public static void main(String[] args) {
//3.创建Callable任务对象
MyCallable myCallable = new MyCallable(10);
//4.把Callable任务对象,交给FutureTask对象
//FutureTask对象作用一:是Runnable的对象(FutureTask实现了Runnable接口),可以交给Thread
//FutureTask对象作用二:可以在线程执行完毕之后通过其get方法得到线程执行完成的结果
FutureTask<Integer> call = new FutureTask<>(myCallable);
//5.交给线程处理
Thread thread = new Thread(call);
//6.启动线程
thread.start();
//7.打印线程返回的结果
//线程返回的结果有两种:正常执行结果和异常结果,所以这里要抓取异常
//如果线程正在执行中,get方法来拿线程的返回结果,是拿不到值的,必须等待线程执行结束
try {
System.out.println(call.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//1.定义一个任务类,实现Callable接口,应该申明线程执完毕后的结果的数据类型
class MyCallable implements Callable<Integer>{
private Integer s = 0;
public MyCallable(Integer s){
this.s = s;
}
//2.重写call方法(任务方法)
@Override
public Integer call() throws Exception {
Integer sum = new Integer(0);
for (int i = 0 ; i< s; i++){
sum += i;
}
return sum;
}
}
3.3.输出结果:
3.4.优缺点:
优点一:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
优点二:可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
3.5.三种创建线程的方式对比:
三.线程的常用方法
1.线程命名和获取线程名
1.1.方法参数
1.2.代码案例:
package cn.zcj.Thread;
public class ThreadDemo_04Name {
public static void main(String[] args) {
//线程取名方式一:
MyThread04 t1 = new MyThread04("1号");
t1.start();
//线程取名方式二:
//t1.setName("1号");
System.out.println(t1.getName());
MyThread04 t2 = new MyThread04("2号");
t2.start();
//t2.setName("2号");
System.out.println(t2.getName());
//哪个线程执行它,它就得到哪个线程的对象(即当前线程对象)
//主线程的名称就叫main
Thread tMain = Thread.currentThread();
for (int i=0;i<6;i++) {
System.out.println(tMain.getName()+"线程输出:"+i);
}
}
}
class MyThread04 extends Thread{
public MyThread04() {
}
public MyThread04(String name) {
super(name);
}
@Override
public void run() {
for (int i =0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"线程输出:"+i);
}
}
}
1.3.输出结果:
2.线程休眠
2.1.方法参数
2.2.代码实现
package cn.zcj.Thread;
public class ThreadDemo_05 {
public static void main(String[] args) throws InterruptedException {
for (int i=1;i<=5;i++){
System.out.println("线程输出:"+i);
if(3==i){
//让线程进入休眠状态
Thread.sleep(3000);
}
}
}
}
四.线程安全和多线程同步
1.线程安全
1.1.线程安全问题:多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
【补充】多个线程去读取同一个共享资源是不会存在线程安全问题的,多个线程去修改同一个共享资源才会存在线程安全问题。
1.2.线程安全问题出现的原因:多个线程同时访问同一个共享资源且存在修改该资源。
1.3.线程不安全的案例模拟
1.3.1.需求
小明和小红花同时去银行取钱,账户余额10万,小红和小明每人都取10万;不加锁,银行最终亏损10万。
1.3.2.需求分析
①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类,线程类可以处理账户对象。
③:创建2个线程对象,传入同一个账户对象。
④:启动2个线程,去同一个账户对象中取钱10万。
1.3.3.代码实现:
package cn.zcj.Thread;
public class ThreadDemo_06 {
public static void main(String[] args) {
Account account = new Account("123456", 10000d);
//创建线程并开启线程
new ThreadMoney(account,"小红").start();
new ThreadMoney(account,"小明").start();
}
}
//账户对象
class Account{
private String CardId;
private Double money;
public Account(String cardId, Double money) {
CardId = cardId;
this.money = money;
}
public String getCardId() {
return CardId;
}
public void setCardId(String cardId) {
CardId = cardId;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
public static void f(){
synchronized (Account.class){
System.out.println("静态方法加锁");
}
}
public void drawMoney(double money){
//1.获取当前线程是谁来取钱
String name = Thread.currentThread().getName();
//this == account共享账户对象
//2.判断账户余额是否充足
if(this.money >= money){
//3.取钱
System.out.println(name+"来取钱成功,取出:"+money);
//4.更新余额
this.money -= money;
System.out.println(name+"取钱后,还剩:"+this.money);
}else {
//5.余额不足
System.out.println(name+",您的余额不足!");
}
}
}
//处理业务的线程对象
class ThreadMoney extends Thread{
//接收处理的账户对象
private Account account;
public ThreadMoney(){
}
//提供可以给线程取名的构造方法
public ThreadMoney(Account account,String name){
super(name);
this.account = account;
}
@Override
public void run() {
//取钱的方法
account.drawMoney(10000);
}
}
1.3.4.输出结果:
2.多线程同步
1.案例回顾
(1)取钱案例出现问题的原因?
多个线程同时执行,发现账户都是够钱的。
(2)如何才能保证线程安全呢?
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
2.线程同步的核心思想
线程同步的核心思想就是加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
3.用synchronized关键字加锁:
3.1.同步方法----在方法上面使用synchronized加锁
略。
3.2.同步代码块----将某一段代码块包起来用synchronized加锁
synchronized(lock){
//操作共享资源代码
...
}
解释:
上述代码中,lock是一个锁对象,是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位设置为0。当一个新的线程执行到这个同步代码块时,由于锁对象标志位置的值为0,新的线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位又会被设置为1,新的线程才能进入同步代码块执行其中的代码。如此循环往复。
图解:
3.3.案例改进:在上文1.3.3的代码中,给核心代码块加锁即可
运行结果:线程安全
3.4.同步方法和同步代码块谁更好?
答:同步方法锁定的范围更大,同步代码块锁定的范围相对更小;由于同步代码块的锁定范围通常比同步方法的锁定范围小,所以执行时消耗的时间也更少,但是不利于代码的整体阅读,实际场景推荐使用同步方法。
3.5.注意事项:
【补充】被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
3.6.补充:
同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
4.用Lock对象加锁
4.1.Lock简介
同步锁:即Lock锁。同步锁最大的优势在于可以让某个线程持续获取同步锁失败后返回,不再继续等待,并且同步锁在使用时更加灵活。Lock锁比synchronized关键字锁定的代码资源更加精细,可以精确到具体哪一行。
4.2.代码案例:
4.2.1.创建账户对象
package cn.zcj.Thread.threadComunication;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String CardId;
//余额,关键信息
private Double money;
//final修饰后,锁对象唯一且不可替换,非常专业的写法
private final Lock lock = new ReentrantLock();
public Account(String cardId, Double money) {
CardId = cardId;
this.money = money;
}
public String getCardId() {
return CardId;
}
public void setCardId(String cardId) {
CardId = cardId;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
public void keepMoney(double money) {
//1.获取当前线程是谁来取钱
String name = Thread.currentThread().getName();
//2.判断账户余额是否为0
lock.lock();
if (this.money > 0) {
//3.有钱,就取钱
this.money -= money;
//4.取钱
System.out.println(name + "来取钱成功,取:" + money);
System.out.println(name + "来取钱后,还剩:" + this.money);
} else {
//5.没钱,不取钱
System.out.println(name+"账户余额不为0,暂时不能取钱!");
}
lock.unlock();
}
}
4.2.2.创建线程对象
package cn.zcj.Thread.threadComunication;
public class ThreadMoney extends Thread{
private Account account;
public ThreadMoney(Account account, String name){
super(name);
this.account = account;
}
@Override
public void run() {
//取钱的方法
account.keepMoney(10000);
}
}
4.2.3.测试
package cn.zcj.Thread.threadComunication;
public class Test {
public static void main(String[] args) {
Account account = new Account("123456", 10000d);
new ThreadMoney(account,"小红").start();
new ThreadMoney(account,"小明").start();
}
}
4.2.4.测试结果:
4.3.总结:
5.线程死锁
5.1.概念
线程死锁:两个线程在运行时都在等待对方的锁,这样就造成了程序的停滞,这种现象被称为线程死锁。
5.2.代码实现
package cn.zcj.Thread;
class DeadLockThread implements Runnable {
//定义两个不同的锁对象
static Object China = new Object();
static Object England = new Object();
private boolean flag;
public DeadLockThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (China) {
//China锁对象上的同步代码块
System.out.println(Thread.currentThread().getName() + "China对象已经被锁");
synchronized (England) {
//England锁对象上的同步代码块
System.out.println(Thread.currentThread().getName() + "England对象已经被锁");
}
}
}
} else {
while (true) {
synchronized (England) {
//England锁对象上的同步代码块
System.out.println(Thread.currentThread().getName() + "England对象已经被锁");
synchronized (China) {
//China锁对象上的同步代码块
System.out.println(Thread.currentThread().getName() + "China对象已经被锁");
}
}
}
}
}
}
public class Example {
public static void main(String[] args) throws InterruptedException {
//创建两个DeadLockThread对象
DeadLockThread thread1 = new DeadLockThread(true);
DeadLockThread thread2 = new DeadLockThread(false);
//创建线程并开启线程
new Thread(thread1, "ChinesePeople线程的").start();
new Thread(thread2, "EnglishPeople线程的").start();
}
}
代码解释:
ChinesePeople线程开始执行时,会先将China对象锁住,而EnglishPeople线程开始执行时,也会先将England对象锁住;但是ChinesePeople线程如果要执行完毕就必须获得England锁,同理EnglishPeople线程如果要执行完毕就必须获得China锁,两个线程都需要对方占用的锁,但都无法释放自己所拥有的锁,于是这两个线程都处于了挂起状态,造成死锁。
5.3.输出结果
五.线程的生命周期及状态转换
1.线程的状态
线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
理解线程的状态有利于提升并发编程的理解能力。
2.Java线程的状态
Java总共定义了6种状态
6种状态都定义在Thread类的内部枚举类中。如图:
线程的6种状态解释:
6种状态对应的线程方法
3.线程各状态间的转换
sleep苏醒前后不会改变锁状态,如果线程sleep前有(或没有)抢到锁对象,那么在线程休眠时,不会将锁对象释放,因此用sleep方法休眠的线程苏醒后由于锁对象依然存在(或不存在),因此不会参与锁竞争去抢锁。但是用wait方法使线程进入等待状态时,当前线程会将锁对象释放,若等待状态的锁对象被唤醒,那么会立即参与到锁竞争中去抢锁。一句话:sleep前后,线程的锁对象状态一致;wait前后,线程的锁对象状态不一致。
六.线程的调度
1.基本概念
线程的调度:一个程序是多线程并发执行的,但是线程不是在同一时刻执行的,某个线程如果想被执行,必须得到CPU的使用权。JVM会按照特定的机制为程序中的每一个线程分配CPU使用权,这种机制被称为线程的调度。
线程调度的两种模型:分时调度模型和抢占式调度模型(JVM默认使用抢占式调度模型)
分时调度模型:让所有线程轮流获得CPU的使用权,并且平均分配每个线程占用CPU的时间片。
抢占式调度模型:让可运行池中所有就绪态的线程争抢CPU的使用权,而优先级高的线程抢到使用权的概率大于优先级低的线程。
2.线程优先级
2.1.优先级参数:
最高优先级为10,最低优先级为1,普通优先级为5,main方法就是普通优先级,其优先级值为5。
【补充】不同的操作系统对优先级的支持是不同的,不一定能很好地和java中线程的优先级一一对应。因此,在设计程序时,不能将功能的实现依赖于线程的优先级,而是将线程的优先级作为提高程序执行效率的一种手段。
2.2.代码实现:
package cn.zcj.Thread;
public class Example {
public static void main(String[] args) {
//分别创建两个Thread线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
},"优先级较低的线程");
Thread thread2 = new Thread(() -> {
for (int i = 10; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
},"优先级较高的线程");
//分别设置优先级,1--10之间,数字越大,优先级越高,
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.MAX_PRIORITY);
//开启两个线程
thread1.start();
thread2.start();
}
}
2.3输出结果:
3.线程休眠
上文已经介绍,略。
4.线程让步
4.1.线程让步:线程运行到某一条件时,使当前线程暂停,让所有线程再次争夺CPU使用权,抢到CPU使用权的线程可以执行。
【补充】线程调度通过yield()方法实现,虽然yield()方法和sleep()方法有点类似,都是可以让当前正在运行的线程暂停,但是yield()方法不会阻塞该线程,它只是将线程转换为就绪状态,让系统的调度器重新调度一次。
4.2.yield()方法与 wait()方法比较
(1) wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”。
(2) wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁
4.3.代码实现:
package cn.zcj.Thread;
public class Example {
public static void main(String[] args) {
//创建两个线程
YieldThread thread1 = new YieldThread();
YieldThread thread2 = new YieldThread();
//开启两个线程
thread1.start();
thread2.start();
}
}
class YieldThread extends Thread{
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName()+"输出"+i);
if (2 == i){
System.out.println("线程让步");
//线程运行到这里做出让步
Thread.yield();
}
}
}
}
4.4.输出结果:
4.5.使用场景:百度
5.线程插队
5.1.线程插队:
当某个线程(main线程)中调用其他线程的join()方法时,调用的线程将被阻塞,直到join()方法加入的线程执行完后它才会继续运行。
5.2.代码实现
package cn.zcj.Thread;
public class Example {
public static void main(String[] args) throws InterruptedException {
//创建线程
EmergencyThread thread1 = new EmergencyThread();
Thread thread = new Thread(thread1, "thread");
//开启线程
thread.start();
for (int j = 6 ;j<10;j++){
System.out.println(Thread.currentThread().getName()+"线程输出"+j);
if (8 == j){
//调用join()方法
thread.join();
}
}
}
}
class EmergencyThread implements Runnable{
@Override
public void run() {
for (int i = 0;i < 5;i++){
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
}
}
5.3.输出结果:
5.4.使用场景:百度。
七.多线程通信
1.什么是线程通信、如何实现?
所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。
2.线程通信常见形式
(1)通过共享一个数据的方式实现。
(2)根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
3.线程通信实际应用场景
(1)生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
4.Object类的等待和唤醒方法:
5.案例及代码实现
案例:小明的父亲、干爹、岳父负责向一个银行账户存钱,小明和小红从银行账户里面取钱,银行账户里面如果有钱,则小明的父亲、干爹、岳父都不能继续存钱,只有等小明或小红将钱取完后才能向账户里面存钱;反之,账户里面如果没钱,小明和小红必须等他人来存钱之后才能取钱。
代码实现:
准备账户对象
package cn.zcj.Thread.lockTest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String CardId;
//余额,关键信息
private Double money;
//final修饰后,锁对象唯一且不可替换,非常专业的写法
private final Lock lock = new ReentrantLock();
public Account(String cardId, Double money) {
CardId = cardId;
this.money = money;
}
public String getCardId() {
return CardId;
}
public void setCardId(String cardId) {
CardId = cardId;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
/**
* 存钱行为
*
* @param money
*/
public synchronized void keepMoney(double money) {
try {
//1.获取当前线程是谁来存钱
String name = Thread.currentThread().getName();
//2.判断账户余额是否为0
if (this.money == 0) {
//没钱了,就存钱
this.money += money;
//4.取钱
System.out.println(name + "来存钱成功,存入:" + money);
System.out.println(name + "来存钱后,还剩:" + this.money);
//有钱了,等人来取,唤醒别人,等待自己
this.notifyAll();
this.wait();
} else {
//5.有钱,不存钱
System.out.println("账户余额不为0,暂时不能存钱!");
//唤醒其他所有线程,唤醒别人,让自己进入等待状态
this.notifyAll();
//锁对象,让当前线程进入等待
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 取钱行为
*
* @param money
*/
public synchronized void drawMoney(double money) {
try {
//1.获取当前线程是谁来取钱
String name = Thread.currentThread().getName();
//2.判断账户余额是否充足
if (this.money >= money) {
//3.线程安全时可以先更新余额,后取钱
this.money -= money;
//4.取钱
System.out.println(name + "来取钱成功,取出:" + money);
System.out.println(name + "取钱后,还剩:" + this.money);
//没钱了
this.notifyAll();
this.wait();
} else {
//5.余额不足
System.out.println(name + ",您的余额不足!");
//唤醒其他所有线程,唤醒别人,让自己进入等待状态
this.notifyAll();
//锁对象,让当前线程进入等待
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
准备存钱线程对象
package cn.zcj.Thread.lockTest;
public class KeepThread extends Thread{
private Account account;
public KeepThread(Account account, String name){
super(name);
this.account = account;
}
@Override
public void run() {
while (true){
//取钱的方法
account.keepMoney(10000);
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
准备取钱线程对象
package cn.zcj.Thread.lockTest;
public class DrawThread extends Thread{
private Account account;
public DrawThread(Account account, String name){
super(name);
this.account = account;
}
@Override
public void run() {
while (true){
//取钱的方法
account.drawMoney(10000);
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
测试
package cn.zcj.Thread.lockTest;
public class Test {
public static void main(String[] args) {
Account account = new Account("123456", 10000d);
new DrawThread(account,"小红").start();
new DrawThread(account,"小明").start();
new KeepThread(account,"亲爹").start();
new KeepThread(account,"干爹").start();
new KeepThread(account,"岳父").start();
}
}
测试结果:
八.线程池
1.线程池概述
问:不使用线程池有什么问题?
答:如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
线程池:就是一个可以复用线程的技术。
2.线程池实现API、参数说明
问:谁代表线程池?
答:JDK 5.0起提供了代表线程池的接口----ExecutorService。
2.1.如何得到线程池对象?
3.线程池处理Runnable任务
3.1.准备线程对象
package cn.zcj.Thread.ThreadPool;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
try {
System.out.println(Thread.currentThread().getName()+"本线程已经休眠");
Thread.sleep(1000000);
}catch (Exception e){
e.printStackTrace();
}
}
}
3.2.创建线程池并测试
package cn.zcj.Thread.ThreadPool;
import java.util.concurrent.*;
public class RunnablePoolTest {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,
6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
MyRunnable myRunnable = new MyRunnable();
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
pool.execute(myRunnable);
}
}
3.3.输出结果:
3.4.参数解释
3.4.1参数解释1:
3.4.2.参数解释2:
3.5.补充2个问题:
(1)问:临时线程什么时候创建?
答:新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
(2)什么时候会开始拒绝任务?
答:核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
4.线程池处理Callable任务
4.1.准备线程对象
package cn.zcj.Thread.ThreadPool02;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
private Integer number;
public MyCallable(Integer number){
this.number = number;
}
@Override
public String call() throws Exception {
Integer sum = 0;
for (int i = 0;i<number;i++){
sum += i;
}
return Thread.currentThread().getName()+"计算的1--"+number+"和为:"+sum;
}
}
4.2.创建线程池并测试
package cn.zcj.Thread.ThreadPool02;
import java.util.concurrent.*;
public class ThreadCallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5,
6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory());
Future<String> f1 = pool.submit(new MyCallable(5));
Future<String> f2 = pool.submit(new MyCallable(10));
Future<String> f3 = pool.submit(new MyCallable(15));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
}
}
4.3.测试输出:
4.4.常用方法
5.Executors工具类实现线程池
5.1.准备实现线程对象
package cn.zcj.Thread.ThreadPool03;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"输出"+i);
}
try {
System.out.println(Thread.currentThread().getName()+"本线程已经休眠");
Thread.sleep(1000000);
}catch (Exception e){
e.printStackTrace();
}
}
}
5.2.测试
package cn.zcj.Thread.ThreadPool03;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
//线程池提供固定数量的线程个数
//弊端:由于底层代码没有规定排队线程的数量,因此这种方式创建出来的线程池允许无限排队,会内存溢出
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(new MyRunnable());
pool.execute(new MyRunnable());
pool.execute(new MyRunnable());
//由于线程池里面永远只有3个线程,所以第四个线程会阻塞
pool.execute(new MyRunnable());
}
}
5.3.常用方法
5.4.安全隐患
5.5.总结
(1)Executors工具类底层是基于什么方式实现的线程池对象?
答:线程池ExecutorService的实现类:ThreadPoolExecutor
(2)Executors是否适合做大型互联网场景的线程池方案?
答:不合适。建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。由于Executors底层代码没有规定排队线程的数量,因此这种方式创建出来的线程池允许无限排队,会内存溢出
九.定时器
1.概念
定义:定时器是一种控制任务延时调用,或者周期调用的技术。
应用场景:闹钟、定时邮件发送。
2.定时器的2种实现方式
(1)方式一:Timer
(2)方式二: ScheduledExecutorService
3.Timer定时器
3.1.代码案例:
package cn.zcj.Thread.dingshiqi;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo1 {
public static void main(String[] args) {
//1.创建定时器
Timer timer = new Timer();
//2.调用方法,处理定时任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"定时器执行了。");
}
//定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
},5000,2000);
}
}
3.2.测试输出:
3.3.方法解释
3.4.Timer定时器的特点和存在的问题
(1)Timer是单线程的,一次只能执行一个定时任务,在处理多个任务时按照顺序执行,存在延时与设置定时器的时间有出入----即可能会出现延迟。案例解释:假设程序中有定时器A和定时器B,定时器A是当定时任务开启后马上执行第一次定时任务,之后每隔2秒执行一次定时任务,定时器B也是如此。如果此时定时器A中进行了线程休眠5秒钟,那么此时即使定时器B是每隔2秒执行一次,定时器B也要等定时器A执行完后才能继续执行,那么定时器B本次任务举例上传任务的时间间隔就是5秒,造成时间紊乱。
代码:
package cn.zcj.Thread.dingshiqi;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo1 {
public static void main(String[] args) {
//1.创建定时器
Timer timer = new Timer();
//2.调用方法,处理定时任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"定时器A执行了。"+new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
},0,2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"定时器B执行了。"+new Date());
}
//定时任务开启的5秒后执行第一次定时任务,之后每隔2秒执行一次定时任务
},0,2000);
}
}
输出结果:
(2)如果有多个定时器,可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行,导致所有线程挂掉。
4.ScheduledExecutorService定时器
方法介绍:
4.1.代码案例:
package cn.zcj.Thread.dingshiqi;
import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TimerDemo2 {
public static void main(String[] args) {
//1.创建ScheduledExecutorService线程池,做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//2.开启定时任务
/**
* 参数说明:
* initialDelay:第一次定时任务的执行时间
* period:定时任务执行的周期时间--每隔多长时间执行一次
* unit:时间单位
*/
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行AAA"+new Date());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行BBB"+new Date());
System.out.println(10/0);
}
},0,2, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行CCC"+new Date());
}
},0,2, TimeUnit.SECONDS);
}
}
4.2.测试输出:
4.3.ScheduledExecutorService的优点
答:基于线程池,某个任务的执行情况不会影响其他定时任务的执行。