多线程从无到有


多线程(Java.Thread)

你和你的朋友为什么能一起开黑,自己操作自己的英雄,用到的就是这个技术。


01线程基本概念

进程(process) 就是程序运行的过程,王者荣耀进行一局游戏,这就是一个进程。

线程(thread) 一局游戏里的十个人能够使用不同的英雄进行游戏,这里就是十个线程。(线程就是独立运行的执行路径)。

即:一个进程可以包含多个线程,也可以只包含一个主线程main.这是面试官经常会问的基础问题。

注意事项

  1. 你就算不主动创建一个线程,后台自动就会有线程产生,如main主线程、gc线程。

  2. main是主线程,系统的入口,程序从这开始执行。

  3. 多个线程之间,线程的运行由调度器安排,调度器与操作系统紧密相关,先后顺序不能人为干预。

  4. 对同一份资源进行操作时,会存在资源抢夺的问题,因此需要加入并发控制。

    • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

    • 线程会带来额外的开销,如cpu调度时间,并发控制开销。


02线程实现(重点)

三种实现的方法:

  1. 继承Thread类。

  2. 实现Runnable接口。

  3. 实现Callable接口。

 

继承Thread类

  1. 自定义一个类继承Thread.

  2. 重写里面的run()方法。

  3. 创建对象调用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;
}
@Override
public void run() {
WebDownload weDownload=new WebDownload();
weDownload.download(url, name);
System.out.println(name+"图片已经下载成功!");
}
public static void main(String[] args) {
TestDownload testDownload1=new TestDownload("https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1033352426,95526485&fm=26&gp=0.jpg", "iuPhoto.jpg");
TestDownload testDownload2=new TestDownload("https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=4069804723,2411985716&fm=26&gp=0.jpg","iuphoto2.jpg");
TestDownload testDownload3=new TestDownload("https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=311027801,3798835252&fm=26&gp=0.jpg","iuphoto3.jpg");
testDownload1.start();
testDownload2.start();
testDownload3.start();
}
class WebDownload{
//下载器
public void download(String url,String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("Download异常!");
}
}
}
}
?

实现Runnable接口

  1. 自定义一个类继承Thread.

  2. 重写里面的run()方法.

  3. 创建线程对象,调用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;
}
@Override
public void run() {
WebDownload weDownload=new WebDownload();
weDownload.download(url, name);
System.out.println(name+"图片已经下载成功!");
}
public static void main(String[] args) {
TestDownload1 testDownload1=new TestDownload1("https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1033352426,95526485&fm=26&gp=0.jpg", "iuPhoto4.jpg");
TestDownload1 testDownload2=new TestDownload1("https://dss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=4069804723,2411985716&fm=26&gp=0.jpg","iuphoto5.jpg");
TestDownload1 testDownload3=new TestDownload1("https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=311027801,3798835252&fm=26&gp=0.jpg","iuphoto6.jpg");
new Thread(testDownload1).start();
new Thread(testDownload2).start();
new Thread(testDownload3).start();
}
class WebDownload{
//下载器
public void download(String url,String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("Download异常!");
}
}
}
}
?
?

 

两者怎么选择 因为Java单继承的局限性,最好是使用实现Runnable接口的方法。实现接口更加灵活方便,同一个对象能够被多个线程使用,只需要传进来对象即可。

 

实现Callable接口(了解即可)

有返回类型。

重写的是call()方法,需要抛出异常。

不像前两者直接对象调用start(),而是有生命周期。

创建目标对象。

创建执行服务:ExcutorService ser =Excutors.newFixedThreadPoll(1);

提交执行:Future result1=ser.submit(t1);

获取结果:boolean r1=result1.get();

关闭服务:ser.shutdownNow();

 

利用多线程实现龟兔赛跑(Race)游戏

package race;
public class Race implements Runnable{
//胜利者
private static String winner;
@Override
public void run() {
for(int i = 0; i <=100; i++) {
boolean flag=gameover(i);
if(flag) {
break;
}
System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
}
}
//判断比赛是否结束
private boolean gameover(int steps) {
//当胜利者不为空,返回true
if(winner!=null) {
return true;
}
if(steps>=100) {
winner=Thread.currentThread().getName();
System.out.println("winner is"+winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race=new Race();
new Thread(race,"乌龟").start();
new Thread(race,"兔子").start();
}
}
?

 

静态代理模式

什么叫静态代理?

比如,在你结婚的时候,你自己是一个对象,婚庆公司又是一个对象。在结婚的时候,你只需要把自己交给婚庆公司,你自己的任务就是结婚,而其他的一系列活动全都交给婚庆公司来完成就可以了(比如布置结婚场所、安排一些小活动)。

总结:

真实的对象和静态代理对象都需要实现同一个接口(结婚这件事)。

自定义的类和Thread类都实现了Runnable接口。

代理对象需要代理真实角色(你只需要把真实对象放到代理对象里面去就好了)。

 

好处

真实对象只需要关注自己需要做的事情就好,其他的都交给代理对象来做就好了。

代理对象可以做很多真实对象做不了的事情。

如:你把自定义的类对象交给Thread类代理对象里面去,由Thread对象来调用start()等其他一些方法。

 


Lamda表达式(jdk8)

为什么要使用Lamda表达式

  • 避免匿名内部类定义过多。

  • 去掉一堆没有意义的代码,只留下核心的逻辑。

 

作用于函数式接口当中。

函数式接口

定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。如Runnable接口,里面只包含一个run()方法。

对于函数式接口,我们可以通过lambda表达式来创建该接口类的对象。

代码推导lambda表达式

package lambda;
public class Lambda implements Runnable{
@Override
public void run() {
}
public static void main(String[] args) {
new Thread(()->System.out.println("这是lambda表达式!")).start();
}
}
?
package ilove;
public class Ilove {
public static void main(String[] args) {
// Wlove love=(int i)->{
// System.out.println("i love you "+i);
// };
Wlove love=i->System.out.println("i love you "+i);
love.love(520);
}
}
interface Wlove{
void love(int i);
}

 

代码推导过程

  • 定义一个函数式接口

  • 实现类

  • 静态内部类

  • 局部内部类

  • 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;
@Override
public void run() {
int i=0;
while(flag) {
System.out.println("Thread run..."+i++);
}
}
public void stop() {
this.flag=false;
}
public static void main(String[] args) {
TestStop testStop=new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main"+i);
if(i==900) {
testStop.stop();
System.out.println("线程停止了!");
}
}
}
?
}
?

 

线程休眠(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();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"停止执行");
}
}
?

 

 

线程强制执行(join)

  • join合并线程,待次线程执行完成之后,再执行其它线程,其它线程阻塞。

  • 可以想象为插队。

join代码演示

public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("线程vip正在执行!"+i);
}
}
public static void main(String[] args) throws InterruptedException {
TestJoin testJoin=new TestJoin();
Thread thread=new Thread(testJoin);
thread.start();
for (int i = 0; i < 500; i++) {
if(i==200) {
thread.join();
}
System.out.println("主线程开始执行!"+i);
}
}
}

 

 

观测线程状态

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{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行"+Thread.currentThread().getPriority());

}
public static void main(String[] args) {
TestPriority testPriority=new TestPriority();
Thread t1=new Thread(testPriority);
Thread t2=new Thread(testPriority);
Thread t3=new Thread(testPriority);
Thread t4=new Thread(testPriority);
Thread t5=new Thread(testPriority);
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(8);
t3.setPriority(Thread.NORM_PRIORITY);
t4.setPriority(4);
t5.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}

 

 

守护(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{
@Override
public void run() {
while(true) {
System.out.println("上帝保佑着你");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你一生快乐地活着");
}
System.out.println("=======");
System.out.println("goodbye world!");
}
}

 


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;
@Override
public void run() {
while(flag) {
buy();
}
}
private synchronized void buy() {
if(tickets<=0) {
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}
}
?
?
//同步代码块
public class Tickets1 {
public static void main(String[] args) {
BuyTickets1 buyTickets1=new BuyTickets1();
new Thread(buyTickets1,"弱小的我").start();
new Thread(buyTickets1,"同样的你").start();
new Thread(buyTickets1,"可恶的黄牛").start();
}
}
class BuyTickets1 implements Runnable{
private static Integer tickets=10;//使用包装类
private boolean flag=true;
@Override
public void run() {
while(flag) {
buy();
}
}
private void buy() {
synchronized(tickets) {//synchronized不能锁基本类型,使用包装类,自动装箱拆箱
if(tickets<=0) {
flag=false;
return;
}
System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
}

}
}
?
?

 

 

死锁

某一个同步块同时拥有两个以上对象的锁时,就有可能会发生死锁问题。

即:多个线程互相抱着对方需要的锁,然后形成僵持。

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对方获得的资源保持不放。

  3. 不剥夺:进程已获得的资源,在未使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

只要想办法破解其中任意一个或多个条件,就可以避免死锁的发生

 

Lock锁

  • JDK5.0,开始,通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。

  • ReentrantLock(可重入锁)类实现Lock.

class A{
   //1.定义锁
   private final ReentrantLock lock=new ReentrantLock();
   public void m(){
      //2.开锁
       lock.lock();
       try{
           //保证线程安全的代码;
      }finally{
           //3.解锁
           lock.unlock();
           //如果同步代码有异常,要将unlock()写入finally语句,一定会执行
      }
  }
}
?
?
?
?
import java.util.concurrent.locks.ReentrantLock;
public class Lock {
public static void main(String[] args) {
BuyTickets2 buyTickets2=new BuyTickets2();
new Thread(buyTickets2,"弱小的我").start();
new Thread(buyTickets2,"同样的你").start();
new Thread(buyTickets2,"可恶的黄牛").start();
}
}
class BuyTickets2 implements Runnable{
private static Integer tickets=10;//使用包装类
private boolean flag=true;
private final ReentrantLock lock=new ReentrantLock();//1.使用ReentrantLock定义一把锁
@Override
public void run() {
while(flag) {
buy();
}
}
private void buy() {
lock.lock();//2.开锁,锁里面放同步代码块
try {
if (tickets <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "买到了第" + tickets-- + "张票");
} finally {
lock.unlock();//3.关锁,放在finally里面,确保会执行
}
}
}
?

synchronized与Lock的对比

  • synchronized是隐式锁,出了作用域就自动释放。Lock是显示锁(手动开启和关闭锁)。

  • synchronized有代码块和方法锁,Lock只有代码块锁。

  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

  • 使用顺序:Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)。


05 线程通信

生产者消费者问题

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

在这个问题中,仅有synchronized是不够的。

  • synchronized可阻止并发更新同一个共享资源,实现了同步。

  • 但是它不能用来实现不同线程之间的消息传递(通信)。

因此,Java提供几个方法解决线程之间的通信问题。

wait(),notify(),notifyAll().

注意:

这些都是Object类的方法,都只能在同步方法或者同步代码块中使用,否者会抛出异常。

管程法实现

ppackage pc;
public class Pcg {
public static void main(String[] args) {
SynContainer container=new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者进行生产鸡
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了第"+i+"只鸡");
}
}
}
//消费者消费鸡
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了第"+container.pop().id+"只鸡");
}
}
}
//鸡
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//管理两者的容器
class SynContainer{
//需要一个容器大小
Chicken[] chickens=new Chicken[10];
int count=0;
//生产者放入产品
public synchronized void push(Chicken chicken) {
//如果容器满了,就需要通知消费者消费
if(count==chickens.length) {
//通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果容器没满,生产者进行生产
chickens[count]=chicken;
count++;

//可以通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() {
//判断能否消费
if(count==0) {
//等待生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
Chicken chicken=chickens[count];
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}

}
?

 

信号灯法实现

 

线程池

背景:经常创建和销毁,对性能影响大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。

  • JDK5.0起提供了线程池相关的API:ExecutorService和Executor.

  • ExecutorService真正的线程池接口。常见的子类ThreadPoolExecutor.

void execute(Runnable command):执行命令,没有返回值。

Futuresubmit(Callabletask):执行命令,有返回值。

void shundown():关闭连接池。

  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

//1.创建服务,创建线程池;
ExecutorService  Service=Executors.newFixedThreadPool(int n);
//2.执行;
Service.execute(new Thread());
//3.关闭连接
Service.shutdown();
?
?
package pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Pool {
public static void main(String[] args) {
//1.定义一个线程池
ExecutorService service=Executors.newFixedThreadPool(10);
//2.执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//3.关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}

}
?

 

 

 

 

 

 

 

 

 

 

相关