九、多线程
1.基本概念
程序:按照一定的逻辑编写的代码,存储到文件中,文件存放到磁盘---静态状态
进程:字面理解-正在进行中的程序。
资源:内存 + CPU
程序从启动到结束的过程叫做进程。
我们看到多个进程在同时执行的效果,实际上cpu在某一时刻只能执行其中一个进程
是因为cpu在做着高速的切换动作。
每个进程执行的时间也是不确定的,多个进程需要抢夺cpu的执行权。
线程:属于进程,是进程中一个可以独立运行执行单元(执行顺序,执行路径、代码片段)。
一个进程最少有一个线程。一个进程有多个可以独立运行的执行单元---多线程。
我们之前所写的代码就是单线程进程,因为只有一个执行单元。
这个执行单元中的代码都被放在main方法中----主线程。
实现效果:一个进程中,出现多个执行路径,各自执行各自的。
cpu进行切换:实际切换的各个进程中的线程。
在main方法中创建其他线程----子线程
子线程要运行的代码封装到一个叫run方法中。
多线程的目的:让CPU的使用率提高,多个程序都能执行,提高程序执行效率。
注意:cpu也是有极限的,不能无限开启线程。
并行:
前提:多个cpu。
多个任务同时发起,在某一时刻,每个任务都在执行。
并发:
单个cpu。
多个任务同时发起,在某一时刻,只有一个任务在执行,其他任务等待cpu。
2.创建线程
java是面向对象思想,任何事物都可以叫做对象,因此线程也是一类事物,就会有对应的对象来进行描述,java.lang.Thread类。
1.继承Thread类
步骤:
1.定义类继承Thread,子类就是一个线程类
2.重写run方法
3.创建子类对象
4.启动线程,调用start方法
//定义类继承Thread,这个类就是线程类 class MyThread extends Thread { @Override public void run(){ //重写Run方法 for (int i = 0 ; i<1000;i++) System.out.print("☆☆☆☆☆"+i); } } //创建子类对象 MyThread1 myThread = new MyThread1(); myThread1.start(); //启动线程
//使用匿名内部类 new Thread(){ @Override public void run(){ for (int i = 0; i <10 ; i++) { System.out.println("i==="+i); } } }.start();
注意:
-
为什么要重写Run方法?
因为Run方法封装的是被线程执行的代码,因为自定义类中不是所有的内容都是要被线程执行的,所以用Run封装区分一下。
2.Run和Start方法有什么区别?
Run 封装线程执行的代码,直接调用的话相当于普通的方法
Start 启动线程,然后由Jvm调用此线程的Run方法
2.实现接口Runnable
步骤:
1.定义类实现Runnable接口,重写接口中的run方法 (run方法中就是线程任务)
2.创建实现类对象
3.创建Thread对象,将实现类对象作为参数传给Thread的构造方法
4.调用start方法,启动线程
//定义类并且实现Runable接口 class myRun implements Runnable{ @Override public void run() { //重写方法 for (int i = 0;i <= 20000;i++) System.out.println("i="+i); } } //创建实现类对象 myRun m = new myRun(); Thread t = new Thread(m); //创建Theread 对象,并将实现类对象作为参数传入 t.start(); //启动线程
//使用匿名块创建 new Thread(new Runnable() { @Override public void run() { for (int i = 0; i <10 ; i++) { System.out.println("i==="+i); } } }).start();
3.实现接口java.util.concurrent.Callable
步骤:
1.定义类实现Callable<V> ,重写call方法,方法的返回值是V类型 (线程任务)
2.创建实现类对象
3.创建FutureTask对象,并将实现类对象作为参数传递给构造方法
4.创建线程对象,并将FutureTask对象作为参数传递给构造方法
5.调用start方法,启动线程
FutureTask实现了Runnable接口,类中提供get方法,可以获取call方法的返回值,即获取线程执行后返回的结果,还又判断线程任务状态的方法,如:isDone()...
//定义类实现Callable,重写Call方法 class call implements Callable<String>{ @Override public String call() throws Exception { for (int i = 0; i < 10; i++) { System.out.println("i++++++"+i); } return "null"; } } //2.创建类对象 call c1 = new call(); //3.创建Futuretask 对象 FutureTask<String> futureTask = new FutureTask<>(c1); //4.创建线程对象 Thread t = new Thread(futureTask); //5.运行线程 t.start(); System.out.println(futureTask.get());
//使用匿名块创建 new Thread(new FutureTask<String>(new Callable<String>() { @Override public String call() throws Exception { System.out.println("线程任务"); return ""; } })).start();
三种方式区别:
继承:代码最为简单。继承了这个类就不能继承其他类,因为是单继承。想要使用继承需要是is-->a关系,如果子类和Thread没有这种关系,使用继承不是很合适。线程任务和线程对象邦定在一起,耦合度增高了。
原理:start-->start0--->run 由于子类重写了run方法,因此创建子类对象调用run
方法,运行时子类的内容(方法重写)
实现Runnable: 还可以去继承其他类。线程任务和线程对象是分离的,耦合度降低。
实际开发建议使用这种方式。
原理:start-->start0-->run(Thread类中的run),方法中:target是Runnable类型,
创建Thread对象时,给target变量赋值了,是Runnable的实现类对象,
因此方法中的判断是true,多态形式调用方法,编译看父类运行看子类。
实现Callable:好处:任务有返回结果,可以抛出异常。基本与Runnable类似。
原理:start-->start0-->run(Thread类中的run),方法中:target是Runnable类型,
创建Thread对象时,给target变量赋值了,是Runnable的实现类对象,
是FutureTask,因此方法中的判断是true,多态形式调用方法,编译看
父类运行看子类,执行的是FutureTask中的run方法。在该方法中。
常用方法:
\1. String getName(): 获取线程名
线程名的默认格式:Thread-编号,编号是从0开始
主线程默认的名称: main
\2. Thread static currentThread(): 放回当前正在执行的线程对象的引用
\3. void setName(String name): 将线程名称设置为name
线程启动前或启动后去设置都可以。
构造方法也可以给线程设置名称:
new Thread(String name)
new Thread(Runnable run, String name)
\4. static sleep(long millis) : 当前线程休眠millis时间 1000毫秒=1秒
主动放弃执行资格,cpu不在调度
时间到,线程醒过来,恢复执行资格,等待cpu调度
\5. int getPriority():获取线程的优先级
每个线程在创建时都有一个优先级,默认是5。一共有10个级别:1-10
1最小,10最大;通常优先级高的线程被执行的概率更高一些。
void setPriority(int ) :设置线程优先级
\6. isDaemon(): 判断线程是否是守护线程
守护线程也叫做后台线程,是为前台线程(非守护线程)提供服务
守护线程是依赖于前台线程而存在的,前台线程结束守护线程也会随之结束
默认线程是非守护线程
setDaemon(true) :设置线程为守护线程。在线程启动前去做设置。
\7. interrupt(): 中断线程执行,想要中断哪个线程,必须是在另一个线程中去调用。
\8. static void yield():让步的意思。某个线程指向到该方法,意思是把cpu的执行权让出来,让其他线程或自己优先执行。注意:调用后有可能该线程会得到cpu的执行权也能得不到。
3.线程间安全问题产生
代码演示:
//创建类Sell实现Callable接口,模拟卖票功能 class Sell implements Callable<String>{ int count = 100 ; String s ; @Override public String call() throws Exception { while (true){ if (count>0){ System.out.println(Thread.currentThread().getName()+"卖了一张"+count); count--; }else if (count == 0){ String s = "票卖完了"; System.out.println(s); break; } } return null; } } //创建两个线程,模拟两个窗口同时卖票 public static void main(String[] args) { Sell s1 = new Sell(); FutureTask<String> f1 = new FutureTask<String>(s1); //创建1线程 Thread t1 = new Thread(f1); FutureTask<String> f2 = new FutureTask<String>(s1); //创建2线程 Thread t2 = new Thread(f2); t1.start(); t2.start(); } //发现问题:执行后发现,出现了错误的票数,两个窗口卖了重复的票号,出现了0号票...这是为什么呢?
原因:
多线程执行时,共同操作同一个资源数据(ticket), 其中一个线程对共享资源数据的一次计算还没有执行完(一次if执行)还没有完成,另一个线程参与进来执行,从而导致错误数据的产生。
t1刚执行完判断和打印,没有执行减减计算,t2获取cpu执行权执行if和打印...
多线程安全问题前提:
1.有多线程环境
2.有共享数据
3.有多条语句操作共享数据 破坏这个环境
解决:
对ticket的一次计算某个线程必须执行完,没有计算结束前另一个线程不能参与执行,哪怕获取了执行权也不能进入执行。
线程安全问题解决:
1.同步代码块
synchronized (锁对象){ //synchronized 同步的,同时发生 //共享资源操作的代码,即不可分割的代码 }
//代码改进 public String call() throws Exception { while (true){ synchronized (this){ //使用synchronized把不能分割的代码包起来,作用就是一个线程抢到cpu执行后 if (count>0){ //立即锁住,使得下一个线程不能进来,直到执行完synchronized大括号内的内容后解锁 System.out.println(Thread.currentThread().getName()+"卖了一张"+count); count--; }else if (count == 0){ String s = "票卖完了"; System.out.println(s); break; } } } return null; }
执行流程:
lock是一个对象,这个对象有两种状态:开和关,默认状态是开。
某个线程获取到执行权,执行到synchronized该语句,会先判断lock的状态,如果是关,则不能进入同步块中;如果是开,立即获取lock对象(线程持有该锁对象),将lock的状态由开改为关,线程执行到sleep后不会释放持有的锁。持有锁的线程一旦出了同步块,会立即释放持有的锁,将锁的状态由关改为开。
加锁的利弊:
好处:解决了安全问题
弊端:线程执行需要判断锁,只有持有了锁,才能执行,因此执行效率降低了。
前提:
1.需要保证多线程看到的是同一个锁对象 2.至少两个或两个以上的线程 3.不可分割的代码是否都在同步中
2. 同步方法
使用关键字synchronized修饰的方法,就叫做同步方法。
同步方法会有特点,就会有锁对象,锁对象是固定的,不需要手动指定。
方法分为:
同步方法: 锁对象是this
同步静态方法: 锁对象是静态方法所属的类的字节码对象, 类名.class
class MyRun1 implements Runnable{ private static int ticket = 100; @Override public void run() { while(true){ if("线程A".equals(Thread.currentThread().getName())) show(); if("线程B".equals(Thread.currentThread().getName())) synchronized (MyRun1.class){ if(ticket > 0){ try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"..."+ticket); ticket--; } } } } //将不可分割的代码封装到一个方法中,通过调用方法 //静态同步方法 ---this synchronized static public void show(){ if(ticket > 0){ try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"..."+ticket); ticket--; } } } //启动两个线程 public static void main(String[] args) { MyRun1 s1 = new MyRun1(); Thread t1 = new Thread(s1,"线程A"); Thread t2 = new Thread(s1,"线程B"); t1.start(); t2.start(); }
注意:run方法也是可以使用同步关键字进行修饰的。
3. 显示锁
jdk5.0有一个接口,叫做Lock,可以显示加锁和显示释放锁,java.util.concurrent.locks;
方法分别是:lock()添加锁 , unLock()释放锁。
使用其子类 :ReentrantLock
class Myrun2 implements Runnable{ private int ticket = 100; ReentrantLock lock = new ReentrantLock(); @Override public void run(){ while(true){ //共享资源操作的代码,即不可分割的代码 //加锁 lock.lock(); //luck 和 unluck不能包含整个方法,不然跟单线程没区别 try { if(ticket > 0){ try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"---"+ticket); ticket--; }else break; }finally { //释放锁 lock.unlock(); } } } } public static void main(String[] args) { Myrun2 m2 = new Myrun2(); //创建类对象切记不要创建两个,不然就是两个对象两个锁。 Thread t1 = new Thread(m2,"线程A"); Thread t2 = new Thread(m2,"线程B"); t1.start(); t2.start(); }
4.死锁
多线程并发执行时,各自持有资源,都想要对方的资源,但是持有资源的线程又不释放资源,
导致程序不能向下执行,就会出现所谓的”卡”的现象,叫做死锁。
注意:实际开发中千万不要写出死锁。
模拟实现死锁: 同步嵌套
//自定义类实现Runable接口 class Myrun implements Runnable{ //做标记 private boolean flag; public Myrun(boolean flag){ this.flag = flag; } @Override public void run() { //锁嵌套 //线程1 运行 if (flag){ synchronized (locks.A){ System.out.println(Thread.currentThread().getName()+"==运行了AA"); synchronized (locks.B){ System.out.println(Thread.currentThread().getName()+"==运行了BB"); } } //线程2 运行 }else{ synchronized (locks.B){ System.out.println(Thread.currentThread().getName()+"==运行了BB"); synchronized (locks.A){ System.out.println(Thread.currentThread().getName()+"==运行了AA"); } } } } } //创建锁对象 class locks { static Object A = new Object(); static Object B = new Object(); } //创建对象,运行线程 public static void main(String[] args) { Thread t1 = new Thread(new Myrun(true)); Thread t2 = new Thread(new Myrun(false)); t1.start(); t2.start(); }
总结:实际开发中尽量不要使用同步的嵌套。
工具:
可以检测是否有死锁。
jstack + pid编号
打开命令行窗口,cmd --->tasklist, java/javaw
5.线程池
引导:在我们使用线程的时候,使用一次创建一次,使用过后就会删除,我们需要花费大量的时间在创建线程上。就比如我们需要短暂的出行,我们没有必要造一个自行车,因为我们每个人都造一个自行车的话,在不使用的时候就会造成资源的浪费,我们可以骑共享单车,如果需要了就使用,如果不需要的话就放着让其他需要的人先使用,可以发挥资源的最大利用。线程池就是这么一个存在,里边提供了一定量的准备好的线程,谁需要谁就用,不需要的话就在线程池里放着。
使用Executors中的工具包创建:
//创建指定大小线程池 ExecutorService es = Executors.newFixedThreadPool(2); //开始执行 es.execute() //适用于Runable es.submit() //适用于Callable //关闭线程池 es.shutdown
//ExecutorService 是一个接口,缺少好多功能,因此我们可以研究他的子类ThreadPoolExecutor //创建指定大小线程池 ExecutorService es = Executors.newFixedThreadPool(2); //创建默线程池 ExecutorService es = Executors.newCachedThreadPool(); //向下转型 ThreadPoolExecutor tpe = (ThreadPoolExecutor)es; //获取线程池中线程的个数 System.out.println(tpe.getPoolSize()); //获取线程池的最大线程个数 System.out.println(tpe.getMaximumPoolSize()); //关闭线程池,试图停止正在执行的任务,并返回到列表中 List<Runnable> runnables = tpe.shutdownNow();
通过Executors创建的线程还是有些使用的灵活度问题,建议使用ThreadPoolExecutor对象。
注意: corePoolSize <0 maximumPoolSize <= 0 corePoolSize > maximumPoolSize
keepAliveTime < 0 这样设置不可以,如果这样设置会出现非法参数异常。
workQueue、threaDFactory、handler 都不能为null,如果为null这出异常。
TimeUnit 是时间单位,是一个枚举类,直接类名.枚举值访问。
workQueue:
提交队列:,默认工作模式,任务执行阻塞式的,
该任务执行完,才会执行下一个。
无界队列:,maximumPoolSize 设置无效,任务处理
依赖于corePoolSize 。
有界队列:,最多能处理的线程数:
maximumPoolSize + 容量(手动指定的)。
handler:任务超出范围时,采用的拒绝模式
6.线程生命周期
生命周期:是一个过程,从创建到终止的过程,在过程中可以有不同的阶段。
线程生命周期:线程从创建到消亡的过程,在这个过程中,也会有不同的阶段,这些阶段可以进行转换。别名:线程的状态图、线程的声明状态图。
新建状态:线程对象被创建,执行了new Thread()后。
就绪状态(可运行):调用start方法后的状态,有运行资格,等待cpu
运行状态:执行run方法的状态
消亡状态: run方法结束,出现异常、调用stop/destroy方法
阻塞状态:主动放弃执行资格,CPU不在调度,sleep方法
wait和sleep方法的区别:
sleep方法不会释放锁对象,时间到回到就绪态
wait方法会释放锁对象,需要通过notify或notifyAll来唤醒才能回到就绪态
以上描述的线程各个状态都是理论上的状态,实际上线程某一个时刻只能处于一种状态,这个状态可以通过方法getState获取到,方法的返回值是Thread.State,State是一个枚举类。
7.枚举
引用类型: 数组 类 接口 枚举
枚举类型:也是一个类类型,是一个特殊的类,jdk5.0
不能new对象,不能实现也不能继承
格式:
修饰符 class 类名{}
修饰符 interface 接口名{}
修饰符 enum 枚举类名{}
枚举类中的每个枚举值其实就是该类的一个实例对象,枚举值之前使用逗号进行分隔,最后一个后边可以加分号也可以不加分号,但是如果枚举值后边还有其他内容,必须加。
当某个类型的对象个数是有限个,可数的过来的,这时候就使用枚举来定义:
如:季节、月份、星期、性别...
枚举类中成员:
1.枚举值,该类对象
2.成员变量,要在枚举值的下边
3.构造方法,要私有化,可以重载,调用重载需在枚举值后边传参
4.成员方法
5.抽象方法,一定要重写(每个枚举值都要进行重写,重写格式有点类似于匿名内部类)
enum Level{ //实例对象 A,B,C,D //如果有抽象方法的话实例对象 A(100){ @Override public void method() { } },B(80){ @Override public void method() { } },C(70){ @Override public void method() { } }; //变量 int score; //构造方法 private Level(){ System.out.println("执行了"); }; private Level(int score){ System.out.println("执行了===="+score); } //方法 public void show(){ System.out.println("show"); } //抽象方法 abstract public void method();
switch语句:表达式结果是byte short int String char 枚举
注意:枚举值名按照常量名的命名规范编写,通常都是大写字母或单词表示。
枚举类的继承体系:
我们定义的枚举都是java.lang.Enum的子类,Enum是Object的子类。
//常用方法 Level a = Level.A; Level b = Level.B; //打印枚举值名 System.out.println(a.toString()); //打印枚举值名 System.out.println(a.name()); //返回序号 System.out.println(a.ordinal()); //序号差值比较 System.out.println(a.compareTo(b)); // Level[] values = Level.values(); System.out.println(Arrays.toString(values));
###