多线程基础

线程的基本概念

线程的使用

创建线程的两种方式:

  • 继承Thread类,重写run()方法
  • 实现Runnable接口,重写run()方法

第一种情况继承Thread创建线程

public class Thread1 {
    public static void main(String[] args) throws InterruptedException {
        //在主线程中创建对象,并开启一个子线程
        Cat cat = new Cat();
        cat.start();//开启线程
        System.out.println("主线程继续执行");
        System.out.println("主线程名字: " + Thread.currentThread().getName());//main
        //打印60次i
        for(int i = 0; i < 60; i++){
            System.out.println("主线程" + i);
            Thread.sleep(1000);
        }
    }
}
//通过继承Thread类来实现线程
class Cat extends Thread{
    @Override
    public void run() {
        //打印当前线程名字
        System.out.println("当前线程名字: " + Thread.currentThread().getName());//Thread0
        int count = 0;
        //打印,猫喵喵,每个1秒打印一次
        while(true){
            System.out.println("猫喵喵" + (++count));
            //让线程睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当打印801次后退出循环,线程结束
            if(count == 80){
                break;
            }
        }
    }
}

当我们的主线程执行到cat.start()就会创建一个新的线程,执行Cat类中重写的run方法内容,主方法后面代码不阻塞继续执行,两个线程并发的执行,当两个线程都执行完成退出以后,整个进程就执行完成退出。默认的主方法线程名为:main,新线程的名字是Thread0。我们可以使用Jconsole监控线程。

为什么我们创建线程不是执行run方法而是执行start方法

如果我们直接调用run,那么并不会开启一个新的线程,只会想普通方法一样执行,在start方法里面,通过调用start0方法来真正的执行多线程,这个start0是一个本地的方法,由JVM来调用,当我们执行start方法以后,线程并不是立即执行,而是变成可运行的状态,等待CPU调度,只有线程获得了CPU调度的时候,线程才会真正执行。

第二种情况实现Runnable接口重写run方法来实现多线程

在Java中只能单继承,如果一个类继承了一个父类也想实现多线程,就不能通过继承Thread来线程,只能通过实现Runnable接口来实现多线程。

public class Thread2 {
    public static void main(String[] args) {
        Tiger tiger = new Tiger();
        //使用静态代理模式开启线程
        Thread thread = new Thread(tiger);
        thread.start();//开启线程
    }
}
class Tiger implements Runnable{//实现Runnable接口来实现线程

    @Override
    public void run() {
        int count = 0;
        while(true){
            System.out.println("老虎嗷嗷叫:" + (++count) + "线程名:" + Thread.currentThread().getName());
            //线程休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(count == 10){
                break;//执行10次就停止
            }
        }
    }
}

在实现Runnable接口中,由于Runnable接口中并没有start方法,所以我们想要启动多线程,必须借助Thread类来实现,这里使用到静态代理模式。在Thread类内部会有一个Runnable属性,通过创建Thread线程的时候我们可以将其传入赋值,我们可以看Thread源码:

当我们调用start执行多线程的时候执行的run会通过动态绑定机制,执行我们传入的对象的run方法。在这里就是由Thread类帮我们的对象去实现多线程。

Thread和Runnable实现线程方式区别

本质上面并没有什么区别,只不过Java不支持多继承,使用实现Runnable接口更适合多线程资源共享。
创建线程实现Callable接口,这种方式可以有返回值,也可以抛出异常

class AAA implements Callable<Boolean>{

    @Override
    public Boolean call() throws Exception {
        for(int i = 0; i < 10; i++){
            System.out.println("hello");
        }
        return true;
    }
}
//1.创建对象
AAA aaa = new AAA();
//开启服务,线程池大小为1
ExecutorService executorService = Executors.newFixedThreadPool(1);
//提交执行
Future<Boolean> r1 = executorService.submit(aaa);
//获取结果
Boolean rs1 = r1.get();
//关闭服务
executorService.shutdownNow();

线程中止

  1. 当线程完成任务以后后,会自动退出
  2. 还可以使用变量来控制run方法退出的方式来让线程终止,即通知方式。
public class Thread5 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        //主线程10秒后结束T线程运行
        System.out.println("主线程在10秒后结束T线程运行");
        Thread.sleep(1000*10);
        t.setFlag(false);
    }
}
class T extends Thread{
    private boolean flag = true;
    @Override
    public void run() {
        int count = 0;
        while(flag){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 线程在运行...." + (++count));
        }
    }

    public void setFlag(boolean flag){
        this.flag = flag;
    }
}

线程常用方法

注意事项:

  1. start底层会创建新线程,调用run(),run()就是一个简单的方法,不会启动新的线程。

  2. 线程优先级范围 1-10

  3. interrupt,中断线程,并没有结束线程,一般用于结束正在休眠的线程

  4. sleep,是一个静态方法,使当前线程睡眠

setName() 设置线程名字
getName() 获取线程名字
start() 线程开始
run() 调用线程的run方法
setPriority() 设置线程优先级
getPriority() 获取线程优先级
sleep() 线程睡眠
interrupt() 中断线程
yield() 礼让线程,让出CPU,让其它线程执行,但礼让时间不确定,礼让也不一定成功
join() 线程插队,线程一旦插队成功,肯定先执行完插队的线程的所有任务,再执行其它线程。

public class Thread6 {
    public static void main(String[] args) throws InterruptedException {
        B b = new B();
        b.start();//启动子线程
        for(int i = 0; i < 20; i++){
            if(i == 5){
                b.join();//让b线程插队
            }
            System.out.println("hi");
            Thread.sleep(1000);
        }
    }
}
class B extends Thread{
    @Override
    public void run() {
        int count = 0;
        while(true){
            System.out.println("hello");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if((++count) == 20){
                break;
            }
        }
    }
}

用户线程和守护线程

用户线程也叫工作线程,当线程的任务执行完成或者被通知结束线程
守护线程,一般是为工作线程服务的,当所有的用户线程结束,守护线程自然结束
常见的守护线程:垃圾回收机制

setDaemon(true) 可以将一个方法设置成守护线程

线程的生命周期

线程同步

在多线程编程中,一些敏感的数据不允许被多个线程同时访问,这时就要使用线程同步访问机制,保证线程在任何时刻最多只有一个线程访问,以保证数据的完整性。也可以理解为当一个线程对某个内存进行操作的时候,其它地址都不允许对这个内存地址进行访问,直达该线程对这个内存地址访问操作完成,其它线程才可以对其进行操作。

同步具体方法加锁Synchronized,有两种方式,同步代码块,同步方法。

使用同步解决售票问题:

public class Thread4 {
    public static void main(String[] args) {
        //模式售票
        ThreadTest2 threadTest2 = new ThreadTest2();
        new Thread(threadTest2).start();
        new Thread(threadTest2).start();
        new Thread(threadTest2).start();

    }
}
//通过继承Runnable
class ThreadTest2 implements Runnable{
    private int num = 100;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            m1();
        }
    }
    public synchronized void m1(){//同步方法
        if(num <= 0){
            System.out.println("售票结束");
            flag = false;
            return;
        }
        //模拟等待
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "卖了一张票,还剩:" + (--num));
    }
}

互斥锁

  1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作完整性
  2. 每个对象都有一个称为互斥锁的标记,这个标记用于保证,在任何时刻只有一个线程能够访问该对象
  3. 关键词synchronized与对象的互斥锁联系,当某个对象使用synchronized修饰的时候,表示,在任何时刻只有一个线程能够访问该对象
  4. 同步会导致程序的性能降低
  5. 同步方法(非静态)的锁可以是this,也可以是其它对象,要求是一个对象
  6. 静态同步方法的锁是当前类本身

必须保证多线程的锁对象是同一个
Lock

class BBB implements Runnable{
    private ReentrantLock lock = new ReentrantLock();
    private int num = 10;

    @Override
    public void run() {
        while(true){
            try{
                lock.lock();//加锁
                if(num <= 0){
                    return;
                }
                //模拟延时
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(num--);

            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }
}


BBB bbb = new BBB();
new Thread(bbb,"线程1").start();
new Thread(bbb,"线程二").start();
new Thread(bbb,"线程三").start();

死锁

多个线程都想占用对方的锁资源,但不肯想让,导致死锁。

模拟死锁:

public class Thread9 {
    public static void main(String[] args) {
        AA a = new AA(true);
        a.setName("线程A");
        AA b = new AA(false);
        b.setName("线程B");
        a.start();
        b.start();


    }
}
class AA extends Thread{
    private static Object obj1 = new Object();//对象1
    private static Object obj2 = new Object();//对象2;
    private boolean flag;

    public AA(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        if(flag){
            //进行同步获取obj1的锁
            synchronized (obj1){
                System.out.println(Thread.currentThread().getName() + "获得obj1资源");
                //进行同步获取对象obj2的锁
                synchronized (obj2){
                    System.out.println(Thread.currentThread().getName() + "获取obj2资源");
                }
            }
        }else{
            //进行同步获取对象obj2的锁
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName() + "获取obj2资源");
                //进行同步获取obj1的锁
                synchronized (obj1){
                    System.out.println(Thread.currentThread().getName() + "获得obj1资源");
                }
            }
        }
    }
}

  • 互斥条件,一个资源每次只能被一个进程使用
  • 请求与保持条件,一个进程因请求资源阻塞时,对已获得的资源保持不放
  • 不可剥夺条件,进程已经获得的资源,在没有使用完之前,不能强行剥夺
  • 循环等待条件,若干进程之间形成一种头尾相接的循环等待资源关系

避免死锁

破坏上面任意一个或者多个条件即可

posted @ 2021-10-05 22:12  无涯子wyz  阅读(36)  评论(0)    收藏  举报