多线程的详解

多线程

多条路走、提高效率。线程是独立执行的路径

基本概念

进程:主要指运行在内存中的可执行文件,多进程可以让操作系统同时执行多个任务。进程是重量级的,会消耗cpu、内存等系统资源,进程数量比较局限。
线程:线程是进程内部的程序流,每个进程内部都支持多线程,线程是轻量的,新建线程会共享所在进程的系统资源。
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制
并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,多个人做同件事

线程实现(重点)

线程创建(重中之重)

  • java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
  • Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性

创建方式

  • 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
  • 自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。

相关方法

在这里插入图片描述

Thread类中Run方法测试

如果不重写run方法

public class ThreadTest {
    public static void main(String[] args) {
        //1.使用无参方式构造Thread类型的对象
        //由源码可知:Thread类中成员变量target的数值为null
        Thread test1 = new Thread();
        //2.调用run方法进行测试
        //由源码可知:由于成员变量target中数值为null,因此条件if(target!=null)不成立,跳过{}中代码不执行
        //而run方法除了上述代码再无代码,因此证明run方法确实啥也不敢
        test1.run();
        //3.打印一句话
        System.out.println("看看你是否啥也不干!");
    }
}

在这里插入图片描述
重写run方法

public class SubThreadRun extends Thread{
    @Override
    public void run() {
        //打印1-20
        for(int i=1;i<=200;i++){
            System.out.println("Run方法中i="+i);
        }
    }
}
public class SubThreadRunTest {
    public static void main(String[] args) {
        //1.生命Thread类型的引用指向子类型的对象
        Thread t1 = new SubThreadRun();
        //2.调用run方法测试
        //t1.run();
        t1.start();//用start可用于启动线程Java虚拟机自动调用该类线程中的run方法
        for(int i=1;i<=200;i++){
            System.out.println("----------main方法中i="+i);
        }
        //一般用start不用run,run方法相当于对普通成员方法的调用,因此执行run方法代码后才能向下执行
    }
}

结果如下
在这里插入图片描述

  • 执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
  • main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响。
  • 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束
  • 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定

线程创建方式一继承Thread类(重点)的实现

即将一个类声明为Thread的子类,重写run()方法,调用start开启线程

public class testThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 10; i++) {
            System.out.println("run方法线程输出:"+i);
        }
    }

    public static void main(String[] args) {
        //main方法线程体。主线程

        //调用一个线程对象,调用start方法
        testThread1 test = new testThread1();
        test.start();//start方法开启线程

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程正在输出:"+i);
        }
    }
}

这样run方法和主方法会一起执行

在这里插入图片描述

实现Runnable接口(重点)(核心)(比第一种用得多)

自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
另一种说法:实现Runnable接口,实现run()方法,编写线程执行体,创建线程对象调用Start()方法启动线程

public class testthread2 implements Runnable{

    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 100; i++) {
            System.out.println("run方法线程输出:"+i);
        }
    }
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        testthread2 test = new testthread2();
        //创建线程对象,通过线程对象来开启我们的线程,代理
        //Thread thread = new Thread();
        //thread.start();
        new Thread(test).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程正在输出:"+i);
        }
    }
}

启动线程:传入目标对象+Thread对象.start()

实现多个线程同时操作一个对象

public class TestThread3 implements Runnable{
    private int ticket = 10;
    @Override
    public void run() {
        while(true){
            if(ticket<=0){
                break;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread3 ticket1 = new TestThread3();

        new Thread(ticket1,"小明").start();
        new Thread(ticket1,"小红").start();
        new Thread(ticket1,"老师").start();
    }
}

结果:

在这里插入图片描述

发现问题:多个 线程同时操作一个资源的时候,线程不安全,数据紊乱

线程安全(重要)

public class testthread2 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket<=0){
                break;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
        }
    }

    public static void main(String[] args) {
        testthread2 ticket1 = new testthread2();

        new Thread(ticket1,"小明").start();
        new Thread(ticket1,"小红").start();
        new Thread(ticket1,"老师").start();
    }
}

在这里插入图片描述
原因:
在这里插入图片描述
某个线程尚未操作完成,有其他线程进来,也操作车票。其实就是共享数据了
锁:一个线程在操作票的时候,其他线程不能参与进来。这种情况就算当前线程被阻塞了也不能被改变

方法一:同步代码块 synchronized

synchronized(同步监视器){
//需要被同步的代码
}

说明:操作共享数据的代码就是需要被同步的代码
共享数据:多个线程共同操作的变量,上面的ticket就是。只有共享数据的多个线程才会出现线程安全问题
同步监视器就是锁,任何一个类的对象都可以充当锁

public class testthread2 implements Runnable{
    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized(obj) {
                if (ticket <= 0) {
                    break;
                }
                //模拟延时
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
        }
    }

    public static void main(String[] args) {
        testthread2 ticket1 = new testthread2();

        new Thread(ticket1,"小明").start();
        new Thread(ticket1,"小红").start();
        new Thread(ticket1,"老师").start();
    }
}

锁的要求:多个线程必须共用同一把锁,在上面的代码中反应的就是obj要是唯一的
上面的代码就是创建一个对象ticket1,三个线程共用一个对象,在ticket1中有只有唯一的一个obj熟悉,所以就共用同一个锁
同步的方式解决了线程安全的问题,但是在操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个当线程的过程,效率会低

  • synchronized(obj)可以改为synchronized(this) this表示当前对象,因为 testthread2 ticket1 = new testthread2();只造了一个对象,所以是唯一的(testthread2对象)锁

但是当用继承自Thread类来创建多线程的时候

会创建多个对象
所以不能synchronized(this)
**解决方式:1.**用当前的类充当锁
如synchronized(类名.class) 因为类也是对象,且类这个对象只会加载一次
2. 创建一个private static obj;

方法二:同步方法解决实现Runnable接口

如果操作共享数据的代码正好都在一个方法中,那么我们可以把此方法声明为同步的

public synchronized void 方法名() {
    i++;
}

在此方法中监视器就是this

public class testthread2 implements Runnable{
    private int ticket = 100;
    Object obj = new Object();
    @Override
    public void run() {
        while(true){
            show();
        }
    }

    public synchronized void show(){
        
            if (ticket > 0) {
                //模拟延时
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
            }
    }

    public static void main(String[] args) {
        testthread2 ticket1 = new testthread2();

        new Thread(ticket1,"小明").start();
        new Thread(ticket1,"小红").start();
        new Thread(ticket1,"老师").start();
    }
}

方法二:同步方法解决继承Thread类解决线程安全问题

这里格外注意同步监视器的问题,用static关键字解决

public class testThread1 extends Thread{
    private static int ticket = 100;

    @Override
    public void run() {
        //run方法线程体
        while(true){
            show();
        }
    }

    private static synchronized void show(){
        if(ticket>0){
            //模拟延时
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
        }
    }

    public static void main(String[] args) {
        //main方法线程体。主线程

        //调用一个线程对象,调用start方法
        testThread1 test1 = new testThread1();
        testThread1 test2 = new testThread1();
        testThread1 test3 = new testThread1();

       /* test1.setName("窗口1");
        test1.setName("窗口2");
        test1.setName("窗口3");*/

        test1.start();//start方法开启线程
        test2.start();//start方法开启线程
        test3.start();//start方法开启线程


    }
}

同步方法存在的问题:效率低与死锁

效率低:在synchronized中只能有单个线程在里面
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源

使用Lock(锁)

lock本身是个接口

public class LockTest implements Runnable{
    private int ticket = 100;

    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true) {
            try{
                //调用lock
                lock.lock();

                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "拿到了第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
            }finally{
                //调用解锁方法
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        LockTest ticket1 = new LockTest();

        Thread t1 = new Thread(ticket1,"小明");
        Thread t2 = new Thread(ticket1,"小红");
        Thread t3 = new Thread(ticket1,"老师");

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

为什么会有线程安全问题:
因为进程是CPU中分配资源的基本单位,而线程共享进程中资源,如内存地址,当多个线程访问同一个内存地址,且内存地址是可变的时候,就容易发生线程安全问题
面试题1:
synchronized与lock方式的异同:
同:二者都可以解决线程安全问题
不同:

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类
  2. synchronized执行完同步代码会自动释放,Lock要手动启动同步,同时结束同步也需要手动实现
  3. synchronized有代码块锁和方法锁,lock只有代码块锁
  4. lock性能更好,多用

面试题2:如何解决线程安全问题?有几种方式
synchronized(同步代码块和同步方法)和lock

线程的通信

wait/notify机制

  • wait():让当前线程 释放对象锁并进入等待(阻塞)状态(执行wait会释放锁
  • notify():一旦执行此方法就会唤醒被wait的一个线程,多个线程被wait就唤醒优先级高的那个
  • notifyAll()——唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行

实现两个线程交替打印1到100

class Number implements Runnable{
    private int number = 1;
    @Override
    public void run() {
        while(true) {
            synchronized (this) {

                notify(); 
                if (number < 100) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用wait()的线程进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

梳理过程:首先当线程1拿到锁进入后,到了wait()状态,阻塞了并释放锁。这时候线程二拿到锁进来,notify唤醒了线程1,线程1进入就绪状态。然后线程2自身进入wait(),以此类推

注意

  1. 这三个方法都要在同步代码块或同步方法中,在lock中都不行
  2. 这三个方法都是由同步监视器发起调用
  3. 这三个方法都是定义在Object类中的,因为任何对象都要有这个方法(任何对象都可以充当同步监视器)

面试题

  • sleep()方法和wait方法的异同?
    相同点: 一旦执行方法都可以让当前线程进入阻塞状态
    不同点:1)两个方法声明的位置不同:Thread类中声明的sleep(),Object()中声明的wait()
  1. 调用范围不同:sleep()可以在任何场景下调用,wait()必须使用在同步代码块或同步方法中
    3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会

使用线程池的方式

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用

龟兔赛跑

  1. 首先要有赛道距离,离终点越来越近
  2. 判断比赛是否结束
  3. 打印出胜利者
  4. 龟兔赛跑开始
  5. 乌龟赢,兔子需要睡觉,所以我们来模拟兔子睡觉
  6. 最终乌龟赢了
public class Race implements Runnable{
    private static String winner;//胜利者
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //模拟兔子休息
            if(Thread.currentThread().getName().equals("兔子")&& i%10==0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //判断比赛是否结束
            boolean flag = gameOver(i);
            if(flag){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }
    //判断是否完成比赛
    private boolean gameOver(int step){
        if(winner!=null){
            return true;
        }if(step>=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();
    }
}

匿名内部类实现线程创建和启动

public class ThreadNoNameTest {
    public static void main(String[] args) {
        //匿名内部类语法格式:父类/接口类型 引用变量名 = new 父类/接口类型() {方法的重写};
        //1.使用继承加匿名内部类的方式创建并启动线程
        /*Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("张三说:在吗?");
            }
        };*/
        new Thread() {
            @Override
            public void run() {
                System.out.println("张三说:在吗?");
            }}.start();
        //2.使用实现接口加匿名内部类的方式创建并启动线程
        Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("李四说:不在。");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();
    }
}

线程状态

生命周期
在这里插入图片描述
线程的编号和名称
在这里插入图片描述

public class ThreadIdNameTest extends Thread{
    public ThreadIdNameTest(String name){
        super(name);//表示调用父类的构造方法
    }

    @Override
    public void run() {
        System.out.println("子线程的编号是:"+getId()+",名称是:"+getName());
        //修改名称为"张飞“
        setName("zhangfei");
        System.out.println("修改后的子线程的编号是:"+getId()+",名称是:"+getName());
    }

    public static void main(String[] args) {
        ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
        tint.start();
        //获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用
        Thread t1 = Thread.currentThread();
        System.out.println("主线程的编号是:"+t1.getId()+",名称是:"+t1.getName());
    }
}

在这里插入图片描述

终止线程的三种方式

1.使用 interrupt() 中断线程,但是 interrupt() 并不会理解结束线程,而是给在当前线程中打一个停止的标记,也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。
2. 用 stop() 终止线程,不安全,不使用它
3.使用标志位终止线程

停止线程时不推荐使用stop()、destroy()等方法,建议使用一个标志位进行中止变量,当flag=false,则中止线程运行

//设置标准位停止线程
public class TestStop implements Runnable{
    //设置标识位
    private boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while(flag){
            System.out.println("running"+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("这个线程停止,只有主线程在跑");
                System.out.println(i);
            }
        }
    }
}

常用的方法

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值