多线程从无到有
你和你的朋友为什么能一起开黑,自己操作自己的英雄,用到的就是这个技术。
01线程基本概念
进程(process) 就是程序运行的过程,王者荣耀进行一局游戏,这就是一个进程。
线程(thread) 一局游戏里的十个人能够使用不同的英雄进行游戏,这里就是十个线程。(线程就是独立运行的执行路径)。
即:一个进程可以包含多个线程,也可以只包含一个主线程main.这是面试官经常会问的基础问题。
注意事项
-
你就算不主动创建一个线程,后台自动就会有线程产生,如main主线程、gc线程。
-
main是主线程,系统的入口,程序从这开始执行。
-
多个线程之间,线程的运行由调度器安排,调度器与操作系统紧密相关,先后顺序不能人为干预。
-
对同一份资源进行操作时,会存在资源抢夺的问题,因此需要加入并发控制。
-
-
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
-
线程会带来额外的开销,如cpu调度时间,并发控制开销。
-
02线程实现(重点)
三种实现的方法:
-
继承Thread类。
-
实现Runnable接口。
-
实现Callable接口。
继承Thread类
-
自定义一个类继承Thread.
-
重写里面的run()方法。
-
创建对象调用start()方法。注意调用的是start()而不是run(),两者不一样。
注意 线程开启不一定会立即执行,它是由cpu控制的。因此,你每次运行的结果可能不一样。
代码格式如下
//1.自定义一个类继承Thread
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
//2.重写里面的run()方法
public void run() {
// compute primes larger than minPrime
. . .
}
}
//3.创建线程对象调用start()方法执行线程体
PrimeThread p = new PrimeThread(143);
p.start();
?
利用多线程实现网图下载 继承Thread形式(代码示例)
package testDownloadPhoto;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import org.apache.commons.io.FileUtils;
public class TestDownload extends Thread{
private String url;
private String name;
public TestDownload(String url,String name) {
this.url=url;
this.name=name;
}
实现Runnable接口
-
自定义一个类继承Thread.
-
重写里面的run()方法.
-
创建线程对象,调用start()方法.即:(静态代理模式把创建的对象扔进Thread里面,再调用start().)
代码格式如下
//上面的两步和继承Thread一致
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
?
public void run() {
// compute primes larger than minPrime
. . .
}
}
//关键是第三步,要把创建的对象放到线程里面去
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
利用多线程实现网图下载 实现Runnable()形式(代码示例)
package testDownloadPhoto1;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import org.apache.commons.io.FileUtils;
public class TestDownload1 implements Runnable{
private String url;
private String name;
public TestDownload1(String url,String name) {
this.url=url;
this.name=name;
}
两者怎么选择 因为Java单继承的局限性,最好是使用实现Runnable接口的方法。实现接口更加灵活方便,同一个对象能够被多个线程使用,只需要传进来对象即可。
实现Callable接口(了解即可)
有返回类型。
重写的是call()方法,需要抛出异常。
不像前两者直接对象调用start(),而是有生命周期。
创建目标对象。
创建执行服务:ExcutorService ser =Excutors.newFixedThreadPoll(1);
提交执行:Future
获取结果:boolean r1=result1.get();
关闭服务:ser.shutdownNow();
利用多线程实现龟兔赛跑(Race)游戏
package race;
public class Race implements Runnable{
//胜利者
private static String winner;
静态代理模式
什么叫静态代理?
比如,在你结婚的时候,你自己是一个对象,婚庆公司又是一个对象。在结婚的时候,你只需要把自己交给婚庆公司,你自己的任务就是结婚,而其他的一系列活动全都交给婚庆公司来完成就可以了(比如布置结婚场所、安排一些小活动)。
总结:
真实的对象和静态代理对象都需要实现同一个接口(结婚这件事)。
自定义的类和Thread类都实现了Runnable接口。
代理对象需要代理真实角色(你只需要把真实对象放到代理对象里面去就好了)。
好处
真实对象只需要关注自己需要做的事情就好,其他的都交给代理对象来做就好了。
代理对象可以做很多真实对象做不了的事情。
如:你把自定义的类对象交给Thread类代理对象里面去,由Thread对象来调用start()等其他一些方法。
Lamda表达式(jdk8)
为什么要使用Lamda表达式
-
避免匿名内部类定义过多。
-
去掉一堆没有意义的代码,只留下核心的逻辑。
作用于函数式接口当中。
函数式接口
定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。如Runnable接口,里面只包含一个run()方法。
对于函数式接口,我们可以通过lambda表达式来创建该接口类的对象。
代码推导lambda表达式
package lambda;
public class Lambda implements Runnable{
代码推导过程
-
定义一个函数式接口
-
实现类
-
静态内部类
-
局部内部类
-
lambda简化
总结
-
lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹。
-
前提是接口为函数式接口。
-
多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号。
03线程状态
创建状态、新生状态(new)
Thread t=new Thread();线程对象一旦创建就进入了新生状态。
就绪状态
当调用start()方法,线程立即进入就绪状态,但并不意味着立即调度执行。
运行状态
进入运行状态,线程才真正执行线程体的代码块。
阻塞状态
当调用sleep,wait或同步锁定时,线程进入阻塞状态,就是代码不往下执行,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行。
死亡状态
线程中断或结束,一旦进入死亡状态,就不能再次启动。
线程停止
-
建议线程正常停止-->利用次数,不建议死循环。
-
建议使用标志位-->设置一个标志位。
-
不要使用stop或destory等过时或者jdk不建议使用的方法。
package testState;
?
public class TestStop implements Runnable{
private static boolean flag=true;
线程休眠(sleep)
-
sleep(时间)指定当前线程阻塞的毫秒数。
-
sleep存在异常InterruptedException,需要抛出异常。
-
sleep时间达到后线程进入就绪状态。
-
sleep可以模拟网络延时,倒计时等。
-
每一个对象都有一个锁,sleep不会释放锁。
可以放大问题的发生性
sleep获取当前系统时间代码演示
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestSleep {
public static void main(String[] args) {
Date startTime=new Date(System.currentTimeMillis());//获取系统当前时间
while(true) {
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));//格式化时间输出格式
startTime=new Date(System.currentTimeMillis());//更新时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
?
线程礼让(yield)
-
礼让线程,让当前正在执行的线程暂停,但不阻塞。
-
将线程从运行状态转为就绪状态。
-
让cpu从新调度,礼让不一定成功!看cpu心情。
礼让代码演示
public class TestYield implements Runnable{
public static void main(String[] args) {
TestYield testYield=new TestYield();
new Thread(testYield,"小红").start();
new Thread(testYield,"小蓝").start();
}
线程强制执行(join)
-
join合并线程,待次线程执行完成之后,再执行其它线程,其它线程阻塞。
-
可以想象为插队。
join代码演示
public class TestJoin implements Runnable{
观测线程状态
Thread.State
-
NEW
-
RUNNABLE
-
BLOCKED
-
WAITING
-
TIMED_WAITING
-
TERMINATED
线程的优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
-
线程的优先级用数字表示,范围从1-10.
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread.NORM_PRIORITY=5;(默认)
-
使用以下方法改变或获取优先级
getPriority()
setPriority(int xxx)
优先级的设定建议在start()之前。
注意:
优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看cpu的调度。
线程优先级代码演示
public class TestPriority implements Runnable{
守护(daemon)线程
-
线程分为用户线程和守护线程
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
-
如,后台记录的操作日志,监控内存,垃圾回收等待...
public class TestDaemon {
public static void main(String[] args) {
God god=new God();
You you=new You();
Thread thread=new Thread(god);
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable{
04线程同步(重点)
线程同步机制
并发:同一进程的多个线程共享同一块存储空间。
确保安全:对列+锁(synchronized:当一个线程获得对象的排他锁,独占资源,其它线程必须等待,使用后释放锁即可)。
安全性提高的同时,性能也会降低
-
一个线程持有锁会导致其它所有需要此锁的线程挂起;
-
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导师优先级倒置,引起性能问题。
同步方法和同步块
同步方法
public synchronized void method(int args){}
synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。(相当于锁住了this)
同步块
synchronized(obj){}
obj称之为同步监视器
-
obj可以是任何对象,但是推荐使用共享资源作为同步监视器;
-
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this这个对象本身,或者是class(反射);
三大不安全案例代码演示
//同步方法
public class Tickets{
public static void main(String[] args) {
BuyTickets buyTickets=new BuyTickets();
new Thread(buyTickets,"弱小的我").start();
new Thread(buyTickets,"同样的你").start();
new Thread(buyTickets,"可恶的黄牛").start();
}
}
class BuyTickets implements Runnable{
private static int tickets=10;
private boolean flag=true;