java多线程基础总结

一、什么是线程?

线程是操作系统能够进行运算调度的最小单位,线程是进程中的一个执行单元,被包含在进程之中,是进程中的实际运作单位。一个进程(进程就是应用程序比如qq、微信等)中可以有多个线程,且至少有一个线程。

二、java中线程的三种创建方式

  1. 继承Thread,重写run方法
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            System.out.println("线程启动方法执行:"+i);
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run();// 直接调用run,就相当于调了一个普通方法,那么代码会按顺序执行,走完run方法再走下面的代码

        // start()是开辟了一个线程,开辟后下面的代码会继续执行
        //也就相当于有两个线程在同时执行,一个刚开辟的这个,一个main方法线程
        myThread.start();


        for (int i = 0; i < 200; i++) {
            System.out.println("main方法执行:"+i);
        }
    }
}
  1. 实现Runnable接口,重写run方法 (此创建方式可实现多个线程资源共享)
class MyRunnableImpl implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
           System.out.println("MyRunnableImpl的run方法执行了");
        }
    }

    public static void main(String[] args) {

        // 先创建一个runnable接口实现类对象
        MyRunnableImpl m1 = new MyRunnableImpl();

        //然后new一个Thread类并将runnable接口实现类对象传入,开启线程
        new Thread(m1,"线程1").start();

        // 也可以使用Lambda表达式直接创建线程,省去编写Runnable实现类的步骤
        new Thread(()->{
            for (int i = 0; i < 20; i++) {
               System.out.println("Lambda表达式的run方法执行了");
            }
        },"线程2").start();

    }
}
  1. 实现Callable接口,重写call方法
// 3. 实现callable接口,重写call方法
// 此种方式的好处是: a.可以获取返回值 b.可以抛出异常
class MyCallable implements Callable<Boolean>{

    @Override
    public Boolean call(){
        for (int i = 0; i < 20; i++) {
           System.out.println(Thread.currentThread().getName()+":MyCallable的call方法执行了");
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        MyCallable myCallable = new MyCallable();

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

        // 提交执行(也就是启动线程)
        Future<Boolean> f1 = ser.submit(myCallable);

        // 获取线程call方法返回结果
        Boolean b1 = f1.get();
        System.out.println(b1);

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


    }
}

三、线程的几种状态

线程状态图:
在这里插入图片描述

由上图可以看到,线程共经历的大致有6种状态:

  1. 初始(NEW): 初始创建状态,也就是线程刚被new出来,还没调用start()方法开启线程
  2. 运行(RUNNABLE):java中将就绪状态(ready)和运行中状态(running)统称为RUNNABLE运行状态,线程调用start()方法开启线程后,线程进入就绪状态,此时等待CPU进行调度,线程被CPU选中获取到CPU的使用权后进入运行状态
  3. 阻塞(BLOCKED):阻塞状态,表示线程被锁住了,也就是同步锁那里,需要等待锁被释放后才能继续运行。
  4. 等待(WAITING):当调用wait方法,该线程进入等待状态
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。比如sleep方法。
  6. 终止(TERMINATED):线程执行完毕,终止。

BLOCKED、WAITING、TIMED_WAITING其实也统称为阻塞状态,进入这些状态的线程,当解除阻塞状态后,会重新进入就绪状态等待CPU调度,然后获得CPU时间片后进入运行中状态。

以下是关于线程调度的几个常用的API,比如休眠、礼让、优先级等:

public class ThreadStatus {

    public static void main(String[] args) throws Exception{
        //sleep();
        yield();
    }


    private static boolean flag = true;
    // 停止线程,不推荐使用jdk的stop()方法,而是建议建立一个flag标识,到达某一条件自动终止线程
    private static void stopThread() {
        flag = false;
    }

    private void stop(){
        // 线程停止
        Thread t1 = new Thread(() -> {

            while (flag) {
                System.out.println(Thread.currentThread().getName() + "执行中...");
            }

        }, "线程1");

        t1.start();
        System.out.println(flag);
        for (int i = 0; i < 1000; i++) {
            System.out.println("main方法执行" + i);
            if (i == 900) {
                System.out.println("线程准备停止...");
                stopThread();
            }
        }
    }

    private static void sleep(){
        // 线程休眠 sleep,sleep也就是让线程进入阻塞状态,但是sleep并不会释放锁,也就是还占着这把锁,其他同步线程不能执行
        new Thread(()->{

            int num = 10;
            while (true){
                try {
                    Thread.sleep(1000);
                    System.out.println(num--);
                    if(num<=0){
                        break;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        },"线程2").start();

    }

    // 线程礼让,让正在运行状态的线程暂停,但不阻塞,会让线程从运行状态转为就绪状态,然后CPU重新调度。
    // 礼让不一定成功,因为CPU调度是随机性的
    private static void yield(){

        Runnable r1 = ()->{
            System.out.println(Thread.currentThread().getName()+"线程开始执行");
            Thread.yield();// 线程礼让
            System.out.println(Thread.currentThread().getName()+"线程执行结束");
        };

        new Thread(r1,"a").start();
        new Thread(r1,"b").start();

    }

}

// join 线程强制执行,想象为插队
class join{

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("线程执行中"+i);
            }
        });
        thread.start();


        for (int i = 0; i < 100 ; i++) {
            if(i==50){
                thread.join();// thread线程插队,此时只有当thread执行完毕后,main线程才会继续执行
            }
            System.out.println("main线程执行中"+i);
        }

    }

}

// 线程优先级 1-10 , 越大表示优先级越高,获得cpu调度的概率就越大,但并不是低就一定最后调用,这只是个概率高低的问题。
class Priority{

    public static void main(String[] args) {

        System.out.println("main线程优先级"+Thread.currentThread().getPriority());

        Runnable r1 = ()->{
            System.out.println(Thread.currentThread().getName()+"优先级:"+Thread.currentThread().getPriority());
        };

        Thread t1 = new Thread(r1,"线程1");
        Thread t2 = new Thread(r1,"线程2");
        Thread t3 = new Thread(r1,"线程3");
        Thread t4 = new Thread(r1,"线程4");
        Thread t5 = new Thread(r1,"线程5");

        t1.setPriority(1);
        t1.start();

        t2.setPriority(5);
        t2.start();

        t5.setPriority(10);
        t5.start();

        // 不设置优先级,默认就是5
        t3.start();
        t4.start();

    }

}

// 守护线程:线程分为 用户线程和守护线程,
// 用户线程就是我们写的线程,虚拟机必须确保我们的用户线程执行完毕
// 守护线程:虚拟机不用等待守护线程执行完毕,如垃圾回收GC, 后台操作日志记录、监控内存等
class Daemon{

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("用户线程执行..." + i);
            }

        });

        Thread t2 = new Thread(() -> {
            while (true){ // 这里设置为true表示一直执行
                System.out.println("守护线程执行...");
            }

        });
        t2.setDaemon(true);// true表示设置该线程为守护线程,默认false


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

}

四、静态代理

代理模式是23中设计模式其中之一,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类

在线程的创建中,通过创建Runnable接口实现类,并让线程Thread类代理该实现类来创建线程,此种方式就用到了代理模式。因为我们创建的类实现了Runnable接口,Thread同样也实现了Runnable接口。

Spring中的AOP用到的就是动态代理,动态代理和静态代理都是代理模式的实现,只是区别在于动态代理是根据反射动态的创建代理类,静态代理则是已经写好的代理类。这里重点先介绍静态代理,后面有机会再详细介绍动态代理的实现。

package com.hhl.demo1;

/**
 *  静态代理:就是下面的代码,简单来说就是,这些代码是我们写死的,所以叫静态代理
 *  (比如说婚庆公司结婚前就只有布置现场吗?我还想让他帮做点其他的,比如我定制自己的婚礼呢?
 *  难道我的需求每变一次都要重新写一个代理类(婚庆公司类)吗?当然不用,不过这就要用到后面的动态代理了)
 *
 */
public class StaticProxy {

    public static void main(String[] args) {

        //普通的结婚,不用代理
        You you = new You();
        you.marryMethod();

        System.out.println("==========================");

        //使用婚庆公司结婚,用代理模式
        WenddingCorp wenddingCorp = new WenddingCorp(you);
        wenddingCorp.marryMethod();

        // 经过结果分析可以发现,目的都是结婚,普通的结婚就是单纯的结婚
        // 而使用婚庆公司的话,你也可以结婚,并且在结婚前后婚庆公司还可以帮你干很多事,这也就是代理模式


    }
}

// 接口,结婚
interface Marry{
    void marryMethod();
}

// 人,实现了结婚接口,那么人就可以结婚了
class You implements Marry{

    @Override
    public void marryMethod() {
        System.out.println("我要结婚了,我很开心");
    }
}

// 婚庆公司,也实现了结婚接口,那婚庆公司也可以结婚
class WenddingCorp implements Marry{

    // 重点在这里,婚庆公司的类要传入一个结婚Marry接口
    private Marry marry;

    // 写有参构造
    public WenddingCorp(Marry marry) {
        this.marry = marry;
    }

    @Override
    public void marryMethod() {
        // target结婚方法执行之前要调用的方法
        before();

        // 在婚庆公司类重写的结婚方法中,调用传入的target的结婚方法,这样也就相当于婚庆公司的结婚方法,代理了人的结婚方法
        marry.marryMethod();

        // target结婚方法之后要调用的方法(这样其实就相当于对人结婚的方法做了增强)
        after();
    }

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

五、线程同步

当有多个线程要同时访问一个变量或者对象时,若只是读还好,但涉及到写操作时,就会出现变量值或对象的状态混乱,从而发生一些异常。

这种情况在生活中很常见,比如电影院卖票,票的数量是一定的,但是很多人都在买这个电影院的票,每个人就是一个线程,若不对这些线程进行同步管理,那很可能就会出现100张票被多于100个人买到这种问题的发生。
再比如,一个银行账户,账户的钱是一定的有100,此账户同时被两个人在操作,A要取100,B要取50,此时若不对AB两个线程进行同步管理,那很可能出现A、B同时取到钱这种问题的发生。

java中对于多线程同步解决的办法就是:加锁。就是多个线程在操作同一变量或者对象时,对此变量或对象加一把锁,此时其他所有线程不能访问,必须等该线程释放锁以后,才能继续访问操作这一变量或对象。synchronized 关键字就是java中的锁,这个关键字可用来直接修饰方法,或修饰代码块。

以下是通过时间Runnable接口创建多线程,synchronized直接修饰了方法,那么锁对象就是this,也就是该Runnable实现类,而多个线程创建时都是使用的这一个实现类,也就是说锁对象是唯一的,所以synchronized起到了作用,线程是安全的

/**
 * synchronized 是关键字,是一种同步锁。
 * 1. 当修饰方法时,synchronized 里的这把锁就是该方法所在的类对象,也就是this
 * 2. 当修饰代码块时, synchronized(lock){...} ,这里的lock是一个锁对象,该对象可以是任意类型,但是多个对象要使用同一把锁才能起到效果
 * <p>
 * 注意: 其实两种修饰的方式都一样,重点是这把锁,一定要是同一把锁,才能起到同步的效果。
 * 就是多个线程运行时,这把锁一定要是同一个对象,若是不同的对象那根本就起不到同步的效果。
 */
public class ThreadSynchronized {
    public static void main(String[] args) {

        BuyTicket b1 = new BuyTicket();

        new Thread(b1, "用户A").start();
        new Thread(b1, "用户B").start();
        new Thread(b1, "用户C").start();

    }
}


class BuyTicket implements Runnable {

    private int tickets = 10;
    boolean flag = true;
    Object obj = new Object();

    @Override
    public void run() {

        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // synchronized 可以直接锁方法,那锁的对象其实就是this, 这个方法所在的类就是那把锁
    // 因为我们这里的线程是实现Runnable方法,多个线程操作的都是一个BuyTicket对象,所以这把锁是该类不会有问题
    private synchronized void buy() throws InterruptedException {
        if (tickets <= 0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "买到了第" + tickets-- + "张票");
    }
}

以下是通过继承Thread类来创建线程,因为不同于实现Runnable的方式,此种方式需要一个外部的对象来当这把锁,且必须多个线程的这把锁是唯一的。虽然锁对象没有要求,只需多个线程用同一个即可,但一般来说锁对象就是那个需要共享的变量或对象

public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");

        // 多个线程用的是同一个Account对象,所以锁是同一把,那么就可以实现同步
        Drawing you = new Drawing(account, 50, "你");
        Drawing girlFriend = new Drawing(account, 100, "你老婆");

        you.start();
        girlFriend.start();
    }
}

class Account {
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

// 注意 这里是继承的Thread
class Drawing extends Thread {

    Account account;
    int drawingMoney;
    int nowMoney;

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {

        // 注意:由于这里的线程是继承Thread类,所以每一个线程就是一个单独的线程,但是多个线程操作的是一个Account对象,所以这里的锁是account对象也不会有问题
        // 但是,如果将synchronized写到方法上,就表示锁对象是该类,而每个线程又是单独的类,(因为是通过继承Thread创建的线程),所以就不会起到同步的作用了
        synchronized (account) {
            if (account.money - drawingMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "来取钱,要取"+drawingMoney+",钱不够了,取不了那么多了");
                return;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            account.money = account.money - drawingMoney;
            nowMoney = nowMoney + drawingMoney;

            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(this.getName() + "手里目前有:" + nowMoney);
        }

    }
}

jdk5推出了JUC包,也就是java.util.concurrent包,此包专门用来解决多线程问题,里面提供了一个lock锁,这个锁和synchronized关键字的作用一致,都是锁,用来解决多线程同步问题的,两者相比,synchronized是一个内置锁,就是把锁封装了,自动帮我们加锁和释放锁,这个过程我们看不到;lock锁是一个显示锁,需要显示的加锁和释放锁,相对于synchronized来说,
lock锁对于锁的操作具有更强的可操作性可控制性以及提供可中断操作和超时获取锁等机制。

public class LockThread {
    public static void main(String[] args) {

        BuyTicket2 r1 = new BuyTicket2();

        new Thread(r1, "用户A").start();
        new Thread(r1, "用户B").start();
        new Thread(r1, "用户C").start();
    }

}

class BuyTicket2 implements Runnable {

    private int tickets = 10;
    boolean flag = true;
    // 使用lock锁
    Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while (flag) {

            try {
                // 加锁
                lock.lock();
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 释放锁
                lock.unlock();
            }
        }
    }

    private void buy() throws InterruptedException {
        if (tickets <= 0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "买到了第" + tickets-- + "张票");
    }
}

六、线程通信

线程通信,从字面意思来看就是多个线程之间需要互相通信。也就是说线程与线程之间并不是独立的个体,线程与线程之间有时是需要互相通信和协作的。其中最典型的就是生产者和消费者问题。

  • 仓库中只能放一件产品,生产者生产,消费者消费。
  • 仓库没有产品时,生产者需要开始生成,而消费者来消费必须等待
  • 仓库有产品,生产者不能生产需要等待,消费者此时来消费,消费后生产者才能继续生产。
  • 生产者和消费者是两个不同的线程,但是又因为某些条件必须互相约束,此时就要用到线程通信机制来解决,常用的也就是线程中的等待/唤醒机制 wait()/notify()

生产者消费者问题的解决办法,这里列举两个。

解决方法1:管程法,利用缓冲区解决问题。此种方法主要就是在生产者和消费者之间建立一个缓冲区,生产者与消费者通过缓冲区进行交流和通信。

public class ThreadComm {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();

        new Producer(synContainer).start();
        new Consumer(synContainer).start();
    }
}


//生产者(KFC)
class Producer extends Thread{

    // 生产者和消费者都是根据这个容器来建立连接的,所以都需要这个参数
    SynContainer container;
    public Producer(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        // 生产者只管生产,要生产100只鸡,但是容器只有10,所以容器满了后就会等待消费者消费
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("KFC生产了第"+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;

    // 生产者放入产品容器(KFC做好炸鸡后,放到容器里)
    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(){
        // count==0 表示目前没有产品,不能消费,
        if(count==0){
            try {
                this.wait();// 需要等待生产者生产
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 容器有产品,那就消费
        count--;
        Chicken chicken = chickens[count];

        //消费过后,通知生产者进行生成
        this.notifyAll();

        return chicken;
    }



}

解决方法2:信号灯法,通过标志位解决,也就是一个布尔值flag。

/**
 * 生产者消费者问题解决方法2:
 * 信号灯法:通过标志位解决,也就是一个布尔值flag
 */
public class ThreadCommTwo {
    public static void main(String[] args) {
        TVShow tvShow = new TVShow();

        new Player(tvShow).start();
        new Watcher(tvShow).start();
    }
}


// 生产者 (演员,录节目)
class Player extends Thread {
    TVShow tvShow;
    public Player(TVShow tvShow){
        this.tvShow=tvShow;
    }

    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            if(i%2==0){
                this.tvShow.play("快乐大本营");
            }else{
                this.tvShow.play("天天向上");
            }
        }
    }
}

// 消费者 (观众,看节目)
class Watcher extends Thread {
    TVShow tvShow;
    public Watcher(TVShow tvShow){
        this.tvShow=tvShow;
    }

    @Override
    public void run() {
        for (int i = 0; i <20 ; i++) {
            tvShow.watch();
        }
    }


}

// 产品 (节目)
class TVShow {

    String voice;// 产品(节目)
    boolean flag = true; // 标志位,true表示现在没节目,需要演员录制观众等待。反之false表示现在有节目,演员休息观众观看


    // 录节目
    public synchronized void play(String voice) {

        if(!flag){// 有节目,演员等待 (等观众看)
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.voice = voice;
        this.flag = !this.flag;
        System.out.println("演员录制了:" + voice);
        this.notifyAll();//唤醒消费者(通知观众观看)
    }

    // 看节目
    public synchronized void watch(){

        if (flag){// 节目没有,观众等待 (等演员录制)
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


        System.out.println("观众观看了:"+voice);
        this.flag=!this.flag;
        this.notifyAll();// 观看完了,唤醒演员,通知演员录节目

    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值