Java-线程-多线程详解


Java-线程-多线程详解

线程,进程,多线程

  • 线程与线程之间互不干扰,独立执行
  • 程序运行时,没有建立自己的线程,后台也会有多个线程,比如主线程,gc线程(垃圾回收线程)
  • main() 称之为主线程,为系统入,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

法一创建线程 - 继承Thread类

三种创建方式

image-20221114092147928

  • 继承thread类
  • 重写run方法
  • 调用start()开启线程
public class TestThread1 extends Thread{

    @Override
    public void run(){
        //run方法
        for (int i=0;i<20;i++){
            System.out.println("我在看代码---"+i);
        }
    }

    public static void main(String[] args) {
        //main线程、主线程

        //创建一个线程对象
        //调用start()方法
        TestThread1 testThread1 = new TestThread1();
        testThread1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("我在学习多线程---"+i);
        }
    }

}
我在看代码---0
我在看代码---1
我在学习多线程---0
我在看代码---2
我在学习多线程---1
我在看代码---3
我在学习多线程---2
我在看代码---4
我在学习多线程---3
我在看代码---5
我在学习多线程---4
我在学习多线程---5
我在看代码---6
我在学习多线程---6
我在学习多线程---7
我在学习多线程---8
我在学习多线程---9
我在学习多线程---10
我在学习多线程---11
我在看代码---7
我在看代码---8
我在学习多线程---12
我在学习多线程---13
我在看代码---9
我在学习多线程---14
我在看代码---10
我在学习多线程---15
我在学习多线程---16
我在学习多线程---17
我在学习多线程---18
我在学习多线程---19
我在看代码---11
我在看代码---12
我在看代码---13
我在看代码---14
我在看代码---15
我在看代码---16
我在看代码---17
我在看代码---18
我在看代码---19

注意

  • 线程开启不一定立即执行,由cpu调度执行

案例:下载图片

package Demo01;


import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//练习Thread,实现多线程同步下载图片
public class TestThread2 extends Thread{

    private String url;
    private String name;

    public TestThread2(String url,String name){
        this.url = url;
        this.name = name;
    }

    //下载图片线程的执行体
    @Override
    public void run(){
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);

    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("http://static.runoob.com/images/demo/demo1.jpg","1.jpg");
        TestThread2 t2 = new TestThread2("http://static.runoob.com/images/demo/demo2.jpg","2.jpg");
        TestThread2 t3 = new TestThread2("http://static.runoob.com/images/demo/demo3.jpg","3.jpg");


        t1.start();
        t2.start();
        t3.start();
    }

}


//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("IO异常,download方法出现问题");
        }
    }
}

法二创建线程 - 实现Runnable接口

  • 实现runnable接口,
  • 重写run方法,
  • 执行
package Demo01;

//创建线程方式二,实现runnable接口,重写run方法,执行
public class TestThread3 implements Runnable{
    @Override
    public void run(){
        //run方法
        for (int i=0;i<20;i++){
            System.out.println("我在看代码---"+i);
        }
    }

    public static void main(String[] args) {
        //main线程、主线程

        //创建runnable接口的实现类对象
        TestThread3 testThread3 = new TestThread3();
        //创建线程对象,通过线程对象来开启我们的线程,代理
        Thread thread = new Thread(testThread3);

        thread.start();

        //简写
//        new Thread(testThread3).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("我在学习多线程---"+i);
        }
    }
}
我在看代码---0
我在看代码---1
我在学习多线程---0
我在看代码---2
我在看代码---3
我在学习多线程---1
我在看代码---4
我在学习多线程---2
我在看代码---5
我在学习多线程---3
我在看代码---6
我在看代码---7
我在看代码---8
我在看代码---9
我在看代码---10
我在看代码---11
我在看代码---12
我在看代码---13
我在看代码---14
我在看代码---15
我在看代码---16
我在看代码---17
我在看代码---18
我在学习多线程---4
我在看代码---19
我在学习多线程---5
我在学习多线程---6
我在学习多线程---7
我在学习多线程---8
我在学习多线程---9
我在学习多线程---10
我在学习多线程---11
我在学习多线程---12
我在学习多线程---13
我在学习多线程---14
我在学习多线程---15
我在学习多线程---16
我在学习多线程---17
我在学习多线程---18
我在学习多线程---19

小结

  • 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread()对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

初识并发问题

package Demo01;

//多个线程同时操作同一个对象
//买火车票的例子

//发现问题,多个线程操作同一个资源,线程不安全,数据紊乱
public class TestThread4 implements Runnable{

    //票数
    int ticketNums = 10;

    @Override
    public void run(){
        while (true){
            if (ticketNums<=0){
                break;
            }
            //模拟延时
            try{
                Thread.sleep(200);
            } catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        new Thread(ticket,"小明").start();
        new Thread(ticket,"老师").start();
        new Thread(ticket,"黄牛").start();
    }

}
黄牛拿到了第9票
老师拿到了第10票
小明拿到了第8票
老师拿到了第7票
黄牛拿到了第7票
小明拿到了第6票
老师拿到了第5票
小明拿到了第3票
黄牛拿到了第4票
小明拿到了第2票
黄牛拿到了第0票
老师拿到了第1票
小明拿到了第-1

龟兔赛跑

package Demo01;

public class Race implements Runnable{

    //胜利者
    private static String winner;

    @Override
    public void run(){
        for (int i = 0; i<=100;i++){

            //模拟兔子休息
            if(Thread.currentThread().getName().equals("兔子") && i%10==0){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            boolean flag = gameover(i);
            //比赛结束就停止程序
            if(flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
        }
    }
    //判断是否完成比赛
    private boolean gameover(int steps){
        //判断是否有胜利者
        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();
    }
}

法三创建线程 - 实现Callable接口

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:
  5. 提交执行:
  6. 获取结果:
  7. 关闭服务:
package Demo02;

import Demo01.TestThread2;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//线程创建方式三:实现Callable接口
public class TestCallable implements Callable<Boolean> {
    private String url;
    private String name;

    public TestCallable(String url, String name){
        this.url = url;
        this.name = name;
    }

    //下载图片线程的执行体
    @Override
    public Boolean call(){
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("http://static.runoob.com/images/demo/demo1.jpg","1.jpg");
        TestCallable t2 = new TestCallable("http://static.runoob.com/images/demo/demo2.jpg","2.jpg");
        TestCallable t3 = new TestCallable("http://static.runoob.com/images/demo/demo3.jpg","3.jpg");

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);

        //获取结果:
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);

        //关闭服务
        ser.shutdownNow();

    }
}


//下载器
class WebDownloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("IO异常,download方法出现问题");
        }
    }
}
下载了文件名为:1.jpg
下载了文件名为:3.jpg
下载了文件名为:2.jpg
true
true
true

静态代理

  • 真实对象与代理对象实现同一个接口
  • 代理对象要代理真实角色
  • 好处:
    • 代理对象可以做真实对象做不了的事情
    • 真实对象专注做自己的事情
package proxystatic;

public class StaticProxy {

    public static void main(String[] args) {
//        new Thread(()-> System.out.println("I Love You")).start();
//        new WeddingCompany(new You()).HappyMarry();
        You you = new You();
        
        WeddingCompany weddingCompany = new WeddingCompany(you);

        weddingCompany.HappyMarry();
    }

}

interface Marry{
    void HappyMarry();
}

class You implements Marry{
    @Override
    public void HappyMarry(){
        System.out.println("秦老师要结婚了,超开心");
    }
}

class WeddingCompany implements Marry{
    private Marry target;

    public WeddingCompany(Marry target){
        this.target = target;
    }

    @Override
    public void HappyMarry(){
        before();
        this.target.HappyMarry();
        after();
    }

    private void before(){
        System.out.println("结婚之前,布置现场");
    }

    private void after(){
        System.out.println("结婚之后,收尾款");
    }
}
结婚之前,布置现场
秦老师要结婚了,超开心
结婚之后,收尾款
  • Thread类是一个代理,实现了Runnable接口
  • 里面真实的类也实现了Runnable接口
  • 所以Thread类可以做真实类的代理

Lambda表达式

目的是简化程序

image-20221116151439125

image-20221116151521313

image-20221116151645947

各种类与lambda表达式

越来越方便书写

package lambdaShow;
/*
* 推导lambda表达式
* */
public class TestLambda1 {

    //3.静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda(){
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4.局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda(){
                System.out.println("i like lambda3");
            }
        }

        like = new Like3();
        like.lambda();


        //5.匿名内部类,没有类的名称,必须借助接口或者父类
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();


        //6.用lambda简化
        like = () -> {
            System.out.println("i like lambda5");
        };
        like.lambda();


    }

}

//1.定义一个函数式接口
interface ILike{
    void lambda();
}

//2.实现类
class Like implements ILike{
    @Override
    public void lambda(){
        System.out.println("i like lambda");
    }
}
i like lambda
i like lambda2
i like lambda3
i like lambda4
i like lambda5

带参数的lambda表达式

package lambdaShow;

public class TestLambda2 {


    public static void main(String[] args) {
        ILove love = (int a)->{
            System.out.println("i love you-->"+a);
        };
        love.love(2);
    }



}

interface ILove{
    void love(int a);
}
i love you-->2

微小简化lambda表达式

package lambdaShow;

public class TestLambda2 {


    public static void main(String[] args) {

        ILove love = null;
//        ILove love = (int a)->{
//            System.out.println("i love you-->"+a);
//        };
//        love.love(2);

        //简化1.去掉参数类型
//        love = (a) ->{
//            System.out.println("i love you-->"+a);
//        };
//        love.love(2);
        //简化2.去掉括号
//        love = a->{
//            System.out.println("i love you-->"+a);
//        };
//        love.love(2);
        //简化3.去掉花括号
        love = a-> System.out.println("i love you-->"+a);
        love.love(2);
        //总结:代码如果有多行,不可以简化花括号
        //多个参数传递,要带回来括号,但是可以都省略参数类型
    }



}

interface ILove{
    void love(int a);
}
i love you-->2

回顾

  • Runnable接口中,就只有一个方法,所以我们可以使用lambda表达式来实现这个接口

线程停止

  • 线程状态

image-20221116180958635

  • 线程方法

image-20221116181130439

  • 停止线程
    • 不推荐使用JDK提供的stop(), destroy()方法【已废弃】
    • 推荐线程自己停下来
    • 建议使用一个标志位进行终止变量,当flag=false,则终止线程运行

image-20221116181451681

run...Thread816
run...Thread817
main899
main900
run...Thread818
run...Thread819
run...Thread820
run...Thread821
线程停止了
main901
main902
main903

线程休眠_sleep

  • sleep指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后,线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁
// cpu太快了,没有延迟,可能看不到代码背后的问题,比如线程不安全问题
// 网络延时可以放大问题的发生性

比如之前的买票代码,没有延迟,就可能一个人买完了票

模拟倒计时

package lambdaShow;
// 模拟倒计时
public class TestSleep2 {


    public static void main(String[] args) {

        try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    public static void tenDown() throws InterruptedException {
        int num = 10;
        while(true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0){
                break;
            }
        }

    }

}
10
9
8
7
6
5
4
3
2
1
0

模拟时间

package state;

import java.text.SimpleDateFormat;
import java.util.Date;

// 模拟倒计时
public class TestSleep2 {


    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();
            }
        }

    }


    public static void tenDown() throws InterruptedException {
        int num = 10;
        while(true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0){
                break;
            }
        }

    }

}
14:54:58
14:54:59
14:55:00
14:55:01
14:55:02

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行态,转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看cpu心情
package state;

public class TestYield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }

}


class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

礼让成功

b线程开始执行
a线程开始执行
b线程停止执行
a线程停止执行

礼让失败

a
a
b
b

例子不好没试出来

线程强制执行_join

相当于插队

package state;

public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; 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();
        thread.sleep(1000);

        //主线程
        for (int i = 0; i < 1000; i++) {
            if(i==200){
                thread.join();
            }
            System.out.println("main"+i);
        }
    }
}

img

线程状态观测

  • Thread.State
    • NEW //尚未启动的线程处于此状态
    • RUNNABLE //在Java虚拟机中执行的线程处于此状态
    • BLOCKED //被阻塞等待监视器锁定的线程处于此状态
    • WAITING //正在等待另一个线程执行特定动作的线程处于此状态
    • TIMED_WAITING //正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
    • TERMINATED //已退出的线程处于此状态

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按学号优先级决定应该调度哪个线程来执行。
  • 线程优先级用数字表示,范围从1~10,在Thread中定义了几个常量来表示优先级
    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5; //默认优先级,不设置的话就是默认优先级
  • 线程优先级高不一定先执行,但是给它的资源就会多一些
  • 使用以下方式来改变或获取优先级
    • getPriority() //获取优先级
    • setPriority(int xxx) //改变优先级

守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 守护线程:后台记录操作日志,监控内存,垃圾回收等待

通过 setDaemon(true) 方法来设置守护线程,参数为true是守护线程,默认是false用户线程

线程同步

  • 并发:同一个对象被多个线程同时操作

    • 上万人同时抢100张票
    • 两个银行同时取钱
  • 在现实生活中,我们遇到“同一个对象被多个线程同时操作”这种问题,比如食堂打饭,解决办法就是排队

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程在使用。

  • 队列和锁:在线程访问对象时,加入锁机制 synchronized, 当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题

    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换,和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级导致,引起性能问题

三大不安全案例

买票案例

线程不安全,票数出现负数

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

买票时,每个线程都把票数拿到自己的内存,当票数只剩一张时,所以它们看到的都是1,然后都去拿这一张票,当第一个线程拿到这张票时,票数修改为0,第二个线程再去拿时,票数就变成了-1.

银行取钱案例

线程不安全的集合

以ArrayList 为例

我们加入了10000个元素,实际加入集合的只有7761个,原因是当这些线程在同一瞬间把集合元素添加到集合的同一位置,就覆盖掉了前面的元素,所以集合中的元素数量才会少

如何解决不安全的问题?


Author: Liang Junyi
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Liang Junyi !
  TOC