多线程
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?
要解决上述问题,咱们得使用多进程或者多线程来解决.
多线程代码运行的特点:
-
每一条线程,如果抢不到CPU的执行权,就执行不了代码
-
只有抢到了CPU的执行权,才能执行run方法里面的代码
-
在执行代码的过程中,随时都有可能被别人抢走CPU的执行权
-
如果我们同时开启了多条线程,在执行的时候,结果有可能每次都不一样,是随机的。
因为在任意时刻,CPU到底执行哪条线程,我们是不确定的。
Thread类
线程开启我们需要用到了java.lang.Thread
类,API中该类中定义了有关线程的一些方法,具体如下:
构造方法:
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法:
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。
创建线程方式一_继承方式
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
代码如下:
测试类:
public class Test {
public static void main(String[] args) {
//1.创建线程的对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//2.开启线程,让线程跑起来
t1.start();
t2.start();
//并没有开启线程
//普通的方法调用
//是运行在虚拟机中的main线程中的。
//t1.run();
//特点:
//在Java中,线程启动之后。
//在每一个时刻,到底是那条线程执行,我们是无法控制的
//随机性、
//当线程启动之后,多条线程,都在抢夺CPU的执行权
//谁抢到了CPU,那么就执行哪条线程。
}
}
自定义线程类:
public class MyThread extends Thread{
//因为run方法里面的代码,就是线程开启之后,要运行的代码
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
创建线程的方式二_实现方式
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
代码如下:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(i);
}
}
}
public class Test {
public static void main(String[] args) {
//1.创建的是线程要执行的参数对象
//创建了一个参数对象
MyRunnable mr1 = new MyRunnable();
//又创建了一个参数对象
MyRunnable mr2 = new MyRunnable();
//2.创建线程对象
//两条线程,各自跑各自的
//线程一,跑参数一
Thread t1 = new Thread(mr1);
//线程二,跑参数二
Thread t2 = new Thread(mr2);
t1.start();
t2.start();
}
}
通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
Thread和Runnable的区别
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
创建线程方式三-callable方式
前两种方式,线程运行完毕之后,没有一个运行结果。
如果我们想要有一个结果,可以用第三种方式。
实现过程:
① 得到任务对象
1.定义类实现Callable接口,重写call方法,封装要做的事情。
2.用FutureTask把Callable对象封装成线程任务对象。
② 把线程任务对象交给Thread处理。
③ 调用Thread的start方法启动线程,执行任务
④ 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
代码示例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//1.开启一条线程,求1~100之间的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
return sum;
}
}
public class Test {
public static void main(String[] args) {
//多线程:
//当我们在开发中,要花大量的时间去处理一个单独问题的时候
//可以用多线程去处理。
//如果在处理完毕之后,需要有一个结果返回,可以用第三种方式
//如果不需要有结果返回,可以用第一种或第二种方式
//1.参数对象
MyCallable mc = new MyCallable();
//2.创建线程任务对象
FutureTask<Integer> ft = new FutureTask<>(mc);
//3.创建线程对象
Thread t1 = new Thread(ft);
//4.开启线程
t1.start();
//5.获取线程的处理结果
Integer result = ft.get();
//6.打印结果
System.out.println(result);
}
}
匿名内部类方式
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// }; //---这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
线程的姓名
扩展点:
线程有默认名称:格式Thread-序号。
在创建线程对象的时候,系统底层自动设置的。
需要掌握的知识点:
第一种创建多线程的方式中。
如果利用set方法设置姓名
如果利用构造方法设置姓名
如果利用get方法获取姓名
代码示例:
public class Test {
public static void main(String[] args) {
//子类不能继承父类的构造方法
//但是可以通过super进行调用
//1.创建线程的对象
MyThread t1 = new MyThread("火车");
MyThread t2 = new MyThread("飞机");
//细节
//起名字,要在创建对象之后,开启线程之前
//t1.setName("火车");
//t2.setName("飞机");
//2.开启线程,让线程跑起来
t1.start();
t2.start();
}
}
public class MyThread extends Thread{
//此时我们没有写构造,jvm会加一个无参构造
public MyThread() {
}
public MyThread(String name) {
super(name);
}
//因为run方法里面的代码,就是线程开启之后,要运行的代码
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//此时getName是获取当前线程的姓名
System.out.println(getName() + "@" + i);
}
}
}
第二种创建多线程的方式中。
如果利用set方法设置姓名
如果利用构造方法设置姓名
如果利用get方法获取姓名
代码示例:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
//Thread.currentThread(); 哪条线程执行到这行代码,此时获取的就是当前线程的对象
System.out.println(Thread.currentThread().getName() + "@" +i);
}
}
}
public class Test {
public static void main(String[] args) {
//1.创建的是线程要执行的参数对象
//创建了一个参数对象
MyRunnable mr1 = new MyRunnable();
//又创建了一个参数对象
MyRunnable mr2 = new MyRunnable();
//2.创建线程对象
//两条线程,各自跑各自的
//线程一,跑参数一
Thread t1 = new Thread(mr1,"线程A");
//线程二,跑参数二
Thread t2 = new Thread(mr2,"线程B");
// t1.setName("郭琴");
// t2.setName("杨康");
t1.start();
t2.start();
}
}
线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。
模拟票:
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket <= 0) {
//无票 可以结束
break;
}else{
//有票 可以卖
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
发现程序出现了两个问题:
- 相同的票数,比如5这张票被卖了两回。
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步
线程同步是为了解决线程安全问题。
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制。
同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块解决代码:
public class MyTicket extends Thread{
static int ticket = 100;
//锁对象,可以是任意对象
static Object obj = new Object();
//关于锁的细节:
//1.锁对象默认是打开的。
//2.当有线程进入之后,锁自动关闭
//3.当线程出去之后,锁再次自动打开
//套路:
//1.while(true) 死循环
//2.synchronized同步代码块 多个线程一定要共享同一把锁
//3.判断,共享数据是否到了结尾。(到结尾)
//4.判断,共享工具是否到了结尾。(还没有到结尾)
@Override
public void run() {
while(true){//t1
synchronized (obj){//t2 //t3
if(ticket < 0){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
//创建三个线程对象
//当多线操作共享数据的时候
//1,重复票号
//2,负数票号
MyTicket t1 = new MyTicket();
MyTicket t2 = new MyTicket();
MyTicket t3 = new MyTicket();
t1.setName("窗口A");
t2.setName("窗口B");
t3.setName("窗口C");
t1.start();
t2.start();
t3.start();
}
}
当使用了同步代码块后,上述的线程的安全问题,解决了。
同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
使用同步方法代码如下:
public class Ticket implements Runnable{
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
sellTicket();
}
}
/*
* 锁对象 是 谁调用这个方法 就是谁
* 隐含 锁对象 就是 this
*
*/
public synchronized void sellTicket(){
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
}
}
Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
使用如下:
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name+"正在卖:"+ticket--);
}
lock.unlock();
}
}
}
第三章 死锁
什么是死锁
因为由于锁的嵌套,就有可能导致死锁现象。
以后我们怎么办?不要写锁的嵌套。
产生死锁的条件
1.有多把锁
2.有多个线程
3.有同步代码块嵌套
死锁代码
public class MyThread extends Thread {
//创建两把锁对象
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
while (true) {
if("小黑".equals(getName())){
//当前线程是第一条小黑线程
synchronized (objA){
//小黑
synchronized (objB){
System.out.println("小黑在执行了");
}
}
}else{
//当前线程是第二条小美线程
synchronized (objB){
//小美
synchronized (objA){
System.out.println("小美在执行了~~~~~~~~~");
}
}
}
}
}
}
public class Test {
public static void main(String[] args) {
//创建两个线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//给两条线程起名字
t1.setName("小黑");
t2.setName("小美");
t1.start();
t2.start();
}
}
注意:我们应该尽量避免死锁
线程间的通信
睡眠sleep方法
我们看到状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示.
public static void sleep(long time)
让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行
public class Test{
public static void main(String[] args){
for(int i = 1;i<=5;i++){
Thread.sleep(1000);
System.out.println(i)
}
}
}
这时我们发现主线程执行到sleep方法会休眠1秒后再继续执行。
等待和唤醒
Object类的方法
public void wait()
: 让当前线程进入到等待状态 此方法必须锁对象调用.
public void notify()
: 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
等待唤醒案例(包子铺卖包子)
代码示例:
生成包子类:
public class ChuShi extends Thread{
//套路:
//1.while(true)
//2.同步代码块
//3.判断共享数据是否到末位-- 到了末尾
//4.判断共享数据是否到末位-- 没有到末尾
@Override
public void run() {
while(true){
synchronized (Desk.obj){
if(Desk.count == 0){
break;
}else{
if(Desk.flag == 1){
try {
//当厨师线程执行到这行代码的时候
//就会等待,并且跟前面锁对象绑定在一起了。
Desk.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//表示桌子上,没有汉堡包,厨师可以做
System.out.println("厨师正在做汉堡包");
//此时不需要让汉堡包的总数量-1,因为只有等吃货吃完了才需要减一
//修改桌子的状态
Desk.flag = 1;
//唤醒等待的吃货开吃
Desk.obj.notifyAll();
}
}
}
}
}
}
消费包子类:
package com.itheima.a07waitnotifydemo;
public class ChiHuo extends Thread{
//套路:
//1.while(true)
//2.同步代码块
//3.判断共享数据是否到末位-- 到了末尾
//4.判断共享数据是否到末位-- 没有到末尾
@Override
public void run() {
while(true){
synchronized (Desk.obj){
if(Desk.count == 0){
//此时是控制线程是否停止的判断
break;
}else{
//表示汉堡包没有吃光.需要继续吃
//判断flag
if(Desk.flag == 0){
//等待 wait
//细节:需要用锁对象去调用wait方法。
//相当于,拿着锁对象,跟当前线程绑定在一起了。
try {
Desk.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//汉堡包的总数量-1
Desk.count--;
//表示桌子上有汉堡包
System.out.println("吃货在吃,今天还能再吃" + Desk.count + "个汉堡包");
//修改桌子上汉堡包的状态
Desk.flag = 0;
//唤醒等待的厨师继续做汉堡包
Desk.obj.notifyAll();//唤醒跟锁对象绑定的所有线程。
//细节1:必须用wait方法,跟锁进行绑定,此时锁才知道要唤醒哪些线程
//细节2:如果没有线程在等待,那么也可以唤醒,没有任何问题。
}
}
}
}
}
}
测试类:
package com.itheima.a07waitnotifydemo;
public class Demo {
public static void main(String[] args) {
//创建吃货的线程
ChiHuo ch = new ChiHuo();
//创建厨师的线程
ChuShi cs = new ChuShi();
ch.setName("吃货");
cs.setName("厨师");
//开启线程
ch.start();
cs.start();
}
}
桌子类:
package com.itheima.a07waitnotifydemo;
public class Desk {
//1.汉堡包的数量
//决定了线程循环的总次数
public static int count = 10;
//2.定义汉堡包的状态
//0 没有 1 有
//控制线程是否执行
//0 没有汉堡包 --- 吃货等待 厨师执行
//1 有汉堡包 --- 吃货执行 厨师等待
public static int flag = 0;
//3.锁对象
//锁对象可以定义,也可以不定义
//如果没有定义,我们可以找一个类的字节码文件对象,作为锁即可
public static Object obj = new Object();
}
线程池
线程池方式
线程池的思想
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。
线程池概念
- **线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。
我们通过一张图来了解线程池的工作原理:
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
获取线程池的方式有两种:
- 利用工具类Executors里面的静态方法
- 自己new对象
利用工具类获取
创建池子
Executors类中有个创建线程池的方法如下:
-
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有上限的线程池,也就是池中的线程个数可以指定最大数量)
-
public static ExecutorService newCachedThreadPool()
:返回线程池对象。(很多资料说没有上限,其实不对,上限很大,为int的最大值)
提交任务
-
public Future<?> submit(Runnable task)
:将任务提交给线程池Future接口:用来记录线程任务执行完毕后产生的结果。
如果是用多线程的第三种方式提交任务,那么可以获取线程运行的结果。
提交后的情况
-
有空闲线程就执行
-
没有空闲线程,但是线程池中的线程还没有到达上限,则创建新的线程执行提交的任务
-
没有空闲线程,但是线程池中的线程已经到达上限,则排队等待
-
铁子们想想,排队的线程有没有数量限制呢?如果排队的任务数量到上限了怎么办?
此时就触发任务拒绝策略,具体的任务拒绝策略请参加下一小节。
使用线程池中线程的步骤
- 创建线程池对象。
- 将要执行的任务交给池子
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2);//包含2个线程对象
// 将要执行的任务提交给线程池
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// 注意:submit方法调用结束后,将使用完的线程又归还到了线程池中
// 销毁线程池,相当于把池子给砸了,在开发中一般不砸
//了解一下,知道有这个方法即可。
//service.shutdown();
}
}
Callable测试代码:
-
<T> Future<T> submit(Callable<T> task)
: 获取线程池中的某一个线程对象,并执行.Future : 表示计算的结果.
-
V get()
: 获取计算完成的结果。
public class ThreadPoolDemo2 {
public static void main(String[] args) throws Exception {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
Callable<Double> c = new Callable<Double>() {
@Override
public Double call() throws Exception {
return Math.random();
}
};
// 从线程池中获取线程对象,然后调用Callable中的call()
Future<Double> f1 = service.submit(c);
System.out.println(f1.get());
Future<Double> f2 = service.submit(c);
System.out.println(f2.get());
Future<Double> f3 = service.submit(c);
System.out.println(f3.get());
}
}
自己创建线程池的对象
直接创建ThreadPoolExecutor的对象即可。
参数分析:
- 参数一:核心线程数量(不能小于0)
- 参数二:最大线程数(不能小于等于0,最大数量 >= 核心线程数量)
- 参数三:空闲线程最大存活时间(不能小于0)
- 参数四:时间单位(时间单位)
- 参数五:任务队列(不能为null)
- 参数六:创建线程工厂(不能为null)
- 参数七:任务的拒绝策略(不能为null)
代码示例:
package com.itheima.a02threadpooldemo;
import java.util.concurrent.*;
public class ThreadPoolDemo1 {
public static void main(String[] args) {
/*//创建了一个上限为10的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
//给线程池提交任务
//pool.submit();
//把池子给砸了
pool.shutdown();*/
//如果我们自己想要创建线程池对象,就自己new ThreadPoolExecutor他的对象即可
//ThreadPoolExecutor pool = new ThreadPoolExecutor();
//扩展:
//任务的队列有两种:
//ArrayBlockingQueue:有界队列,我们在创建对象的时候可以指定上限
//LinkedBlockingQueue:无界队列,但是事实上还是有界的,只不过队伍可以很长,int的最大值21亿多
//以后这些参数,以后不要自己随便写,需要跟公司的组长或者项目经理去沟通
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//提交任务
while (true) {
pool.submit(new MyRunnable());
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("线程执行了~~~~");
while(true){}
}
}
扩展点1:队列的种类
队列分为两种,有界和无界
- ArrayBlockingQueue:有界队列,我们在创建对象的时候可以指定上限,也必须指定上限
- LinkedBlockingQueue:无界队列,但是事实上还是有界的,只不过队伍可以很长,int的最大值21亿多
扩展点2:任务的拒绝策略
分为四种:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,是默认的策略
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
- ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中
- ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
扩展点3:临时线程创建时机
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
注意:核心线程在忙,再有任务提交,先排队,队伍满了,再开临时线程。
扩展点4:触发拒绝策略时机
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
简单理解:所有的都满了,才会拒绝。
定时器
Timer
到了某一个时间就直接一段指定的代码,底层就是单独开启了一条线程去执行这个操作。
- Timer
- ScheduledExecutorService
代码示例:
//1.创建一个定时器对象
//在底层,实际上就是创建了一条线程。
//并给这条线程起了个名字:小黑定时器
Timer timer = new Timer();
//获取当前时间
Date date = new Date();
System.out.println(date);
//2.给定时器提交任务
//参数一:其实就是Runnable的实现类,就是线程执行的参数对象
//参数二:以当前时间为基准,进行了时间的偏移
//参数三:间隔时间
//其实就是把第一个参数,交给了Timer这条线程去执行。
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(2/0);
System.out.println(Thread.currentThread().getName() + "在执行了~~~" + new Date());
}
},1000,2000);
扩展点:Timer的源码
-
在底层Timer就是创建了一条线程,并给线程起了个名字,名字就是小括号中的参数,如果我们没有给线程起名字也会有默认的名字。
-
schedule中第一个参数就是Runnable的实现类
所以,此时我们相当于就是创建了一个线程要执行的参数对象,交给了Timer这条线程去执行了。
-
如果Timer中的线程,执行的代码出现了异常,会导致Timer这条线程直接停止。所有的任务都无法执行了。
ScheduledExecutorService
获取定时器对象:
Executors里面的静态方法 | 说明 |
---|---|
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 得到定时器对象 |
提交任务:
ScheduledExecutorService的方法 | 说明 |
---|---|
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 提交任务 |
代码示例:
public class ScheduledExecutorServiceDemo1 {
public static void main(String[] args) {
Date date = new Date();
System.out.println("当前时间为:" + date);
//创建一个定时器对象
//但是从源码的角度来看,这个定时器,实际上在底层就是一个线程池
//参数表示:线程池中的线程数量
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
//提交任务
//pool.schedule(new MyRunnable(),2,TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new MyRunnable(),0,2,TimeUnit.SECONDS);
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("线程在运行了~~~~" + new Date());
}
}
扩展点1:ScheduledExecutorService 的源码
ScheduledExecutorService在底层就相当于是一个线程池。
提交任务有两种方式:
- schedule提交一次,不会周期性执行
- scheduleAtFixedRate提交一次,可以周期性执行
扩展点2:两种定时器有什么区别呢?
第一种定时器:Timer,单线程的。
如果我们提交了两个任务,按照顺序依次执行的。
如果其中有一个任务中出现了异常,会导致所有的任务都无法执行。
第二种定时器:ScheduledExecutorService,在本质上是一个线程池。
所以,在底层是多线程。
如果我们提交了多个,是同时执行的。
而且,如果一个任务出现了异常,不会影响其他任务的执行。
结论:
以后如果用到了,建议使用第二种。
扩展知识点1:
并发和并行:
- 并发:CPU在多个线程之间进行切换。
- 并行:真·同时执行。
线程的生命周期:
- 新建
- 就绪
- 结束
- 阻塞
- 等待
- 计时等待
**注意点:**其中就绪包含:就绪 + 运行
新建:创建对象的时候就是新建状态
就绪:调用start方法进入就绪状态,现在可以开始抢CPU,但是还无法执行代码
运行:此时是跟本地CPU产生交互,所以虚拟机没有定义运行状态
虚拟机中定义的就绪状态包含了运行状态
结束:代码全部执行完毕
阻塞:执行的过程中遇到锁了。
再次获得到锁,会到就绪状态开始抢CPU的执行权
等待:遇到wait方法进入等待状态
其他线程调用nofify,来唤醒等待的线程回到就绪状态
计时等待:遇到sleep方法进入计时等待状态
当时间到了之后,会到就绪状态
线程小练习:
练习1
package thread;
public class Test104 {
public static void main(String[] args) {
Thread main = Thread.currentThread();
Thread th1 = new MyThread3();
System.out.println("=======默认优先级=======");
System.out.println("主线程名:" + main.getName() + "优先级:" + main.getPriority());
System.out.println("子线程名:" + th1.getName() + "优先级:" + th1.getPriority());
System.out.println("======修改后优先级======");
main.setPriority(Thread.MAX_PRIORITY);
th1.setPriority(Thread.MIN_PRIORITY);
System.out.println("主线程名:" + main.getName() + "优先级:" + main.getPriority());
System.out.println("子线程名" + th1.getName() + "子线程优先级" + th1.getPriority());
}
}
class MyThread3 extends Thread {
@Override
public void run() {
}
}
练习2
package thread;
public class Test105 {
public static void main(String[] args) {
Thread h = Thread.currentThread();
vip v = new vip();
//设置进程名
h.setName("普通号");
v.setName("特需号");
//run方法
v.start(); //特需号
for (int i = 1; i <= 30; i++) { //普通号
System.out.println("●" + h.getName() + ":" + i + "号病人正在看病");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当普通号等于10
if (i == 10) {
try {
v.join(); //当普通号等于10,等待特需号终止,再执行
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
}
//特需号病人
class vip extends Thread {
@Override
public void run() {
String str = Thread.currentThread().getName();
for (int i = 1; i <= 10; i++) {
System.out.println("○" + str + ":" + i + "号病人正在看病");
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
练习3
package thread;
public class Test106 {
public static void main(String[] args) throws InterruptedException {
MyRunnable m = new MyRunnable();
Thread thread1 = new Thread(m, "1号选手");
Thread thread2 = new Thread(m, "2号选手");
Thread thread3 = new Thread(m, "3号选手");
Thread thread4 = new Thread(m, "4号选手");
Thread thread5 = new Thread(m, "5号选手");
Thread thread6 = new Thread(m, "6号选手");
Thread thread7 = new Thread(m, "7号选手");
Thread thread8 = new Thread(m, "8号选手");
Thread thread9 = new Thread(m, "9号选手");
Thread thread10 = new Thread(m, "10号选手");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
thread7.start();
thread8.start();
thread9.start();
thread10.start();
}
}
class MyRunnable implements Runnable {
static int num = 10;
private String thread;
Object lock = new Object();
public void run() {
while (true) {
synchronized (lock) {
if (num == 0) {
System.out.println("比赛结束!");
break;
}
thread = Thread.currentThread().getName();
System.out.println(thread + "拿到了接力棒!");
num--;
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread + "跑了" + (i * 10));
}
return;
}
}
}
}
练习4
package thread;
public class Test107 {
public static void main(String[] args) {
System.out.println("**************开始抢票**************");
Ticket ticket = new Ticket();
Thread th1 = new Thread(ticket);
th1.setName("桃跑跑");
th1.start();
Thread th2 = new Thread(ticket);
th2.setName("张票票");
th2.start();
Thread th3 = new Thread(ticket);
th3.setName("黄牛党");
th3.start();
}
}
class Ticket implements Runnable {
private int count = 10;// 记录剩余票数
private int num = 0;//记录买到第几张票
@Override
public void run() {
while (true) {
if (!sale()) {
break;
}
}
}
// 同步方法
public synchronized boolean sale() {
if (count <= 0) {
return false;
}
num++;
count--;
try {
Thread.sleep(500); // 模拟网络延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到第" + num
+ "张票,剩余" + count + "张票!");
if (Thread.currentThread().getName().equals("黄牛党")) {
return false;
}
return true;
}
}
总结:
1.什么是多线程?
如果没有多线程,我们的程序,同时只能做一件事情。
如果有了多线程,我们同时做多件事情。
具体的体现:
就是我们的计算机,如果没有多线程,我们只能运行一个软件。
如果有了多线程,我们就可以同时运行多个软件。
2.开启线程的方式?
方式一:继承Thread类(掌握)
方式二:实现Runnable接口(掌握)
方式三:实现callable接口(了解)
方式四:匿名内部类/lambda表达式(了解)
3.Thread类中的常见方法
setName、getName、sleep、获取当前线程的对象
利用构造方法,给线程起名字
4.多线程操作共享数据
弊端1:有可能出现重复数据。
弊端2:有可能出现超出范围的数据。
细节1:如果我们使用第一种方式创建线程对象,共享数据一定要用static修饰。
细节2:如果我们使用第二种方式创建线程对象,共享数据可以加static也可以不加static。
但是,多个线程一定要跑同一个参数对象。
5,死锁
知道死锁出现的原因
我们以后如何避免
6,等待唤醒机制
7,线程池
1.线程池的作用
以前写法弊端:用到的时候就创建,不用的时候 就销毁,浪费时间,浪费资源
线程池,就是把线程临时存储。
用到的时候,不需要创建,直接到池子里面拿
用完,不会销毁,还会池子,以备下次再用
2.如何获取一个线程池
Executors 可以调用里面的两个方法:
newCachedThreadPool:没有上线的池子。真正的上限其实是有的,是int的最大值。
newFixedThreadPool:指定一个有上线的线程池。
3.如何把任务交给池子submit
4.把任务交给池子的时候,会有两种情况。
情况一:池子中没有空闲线程,已有线程也达到了上限。任务就等待。
情况二:池子中没有空闲线程,但是线程池没有到达上限。线程池自己会创建一个线程。
情况三:池子有有空闲线程,直接拿过来用。