【多线程笔记】

一、多线程的相关概念和意义

1.1相关概念

  1. 进程和线程——进程是相对于操作系统而言,相当于在内存中运行的一个应用程序,每个进程拥有彼此独立的内存空间;线程是相对于进程内而言,相当于进程内的程序的一条执行路径,其实质是进程基础上的进一步划分。同一个进程的不同线程共享内存空间。例如:在电脑上浏览器和QQ音乐等软件属于不同的进程,也可以同时执行,属于多进程的体现;QQ音乐在播放音乐的同时也可以实时显示歌词,属于同一应用的两条路径,是多线程的体现。
  2. 线程的调度方式——通常调度方式包括分时调度和抢占式调度,分时调度是指各个线程轮流使用CPU,其时间平均分配,用完切换至下一进程。抢占式调度是按照优先级分配。优先级高的线程优先使用CPU,如果优先级相同,则随机选择各个线程执行。Java虚拟机采用的是抢占式调度。
  3. 同步和异步——同步:即排队执行任务,效率低但数据安全。异步:任务同时执行,效率高但数据上不安全。
  4. 并发和并行——并发:在同一个时间段内发生多个事件。并行:同一时刻发生多个事件(在多核CPU的支持下,即使是微观层面也可以实现并行执行程序)
  5. 守护线程(Dameon Thread)——线程可分为守护线程和用户线程,用户线程由代码决定任务及生命周期,守护线程在所有用户线程结束后自动消亡

1.2 多线程的意义
       可以划分程序任务,提高程序运行效率(仅指效率,而不能提高微观上的执行速度),例如:浏览器能操作网页的同时执行多个下载任务、Java虚拟机在执行程序时能时刻运行gc进行内存回收。


二、 Java中多线程的实现方式

       在Java中实现多线程的方式总共有3种,大部分情况下通过构建Thread类执行其start()方法实现新线程的开辟。3种方式如下:

2.1 继承Thread类
       通过构建Thread类的子类,并重写其run方法:

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        System.out.println("开启线程");
        t1.start();
        t2.start();
        t3.start();
        System.out.println("main方法结束");
    }
}

class MyThread extends  Thread{
    @Override
    public void run() {
        for(int i = 0; i < 3; i++){
            String s = this.getName();
            System.out.println(s + "输出了" + i);
        }
    }
}


       其运行结果如下图所示:
在这里插入图片描述
2.2 实现Runnable接口
       除无参构造方法外,Thread类还提供了Thread(Runnable target)的构造方法。从源码上来说,即把参数传给了内部的Runnable接口并将其执行,其构造时调用方法的部分截图如图所示:(Thread在调用run()方法时即调用内部Runnable接口的run方法)
在这里插入图片描述
       因此可以自行创建类实现Runnable接口,重写run()方法并赋给Thread实现多线程,实现方式也分为三种:
       方式一,创建类实现接口:

public class Demo1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());
        Thread t3 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
        t3.start();
    }
}

class MyRunnable implements  Runnable{
    public void run(){
        for(int i = 0; i < 3; i++){
            System.out.println(Thread.currentThread().getName()+"输出了"+i);
        }
    }
}

       运行结果如图:
在这里插入图片描述
       方式二,匿名内部类继承或实现接口:

public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++){
                    System.out.println(Thread.currentThread().getName()+"输出了"+i);
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 3; i++){
                    System.out.println(Thread.currentThread().getName()+"输出了"+i);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

       其运行结果:
在这里插入图片描述
       方式三,使用lamda表达式隐去内部类的方式:

public class Demo4 {
    public static void main(String[] args) {
        new Thread( () ->{
            for(int i = 0; i < 3; i++){
                System.out.println(Thread.currentThread().getName()+"输出了"+i);
            }
        }).start();
        new Thread( () ->{
            for(int i = 0; i < 3; i++){
                System.out.println(Thread.currentThread().getName()+"输出了"+i);
            }
        }).start();
    }
}

       运行结果:
在这里插入图片描述
2.3 实现Callable接口
       使用较少,与实现Runnable接口的主要区别在于可以有返回值、抛出异常,不做过多展开。


三、 Thread类的常用方法与机制

       Thread类是实现多线程的关键,其常用方法包括:

  1. void start()——该线程开始运行。
  2. String getName()——返回该线程的名称。void setName(String name)——改变线程名称,使之与参数 name 相同。Thread类中包含String name,如果构造时不给定,会默认赋值(如上例所示的Thread-0)
  3. int getPriority()——返回线程的优先级。void setPriority(int newPriority)更改线程的优先级。优先级的可设置从1-10,数字越大表示优先级越高,默认值为5
  4. boolean isDaemon()——测试该线程是否为守护线程。void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
  5. static void sleep(long millis)——当前线程休眠多少毫秒。休眠后进入Runnable状态,调用后继续执行。此方法不会释放锁。
  6. static void yield()——当前线程暂定,转而执行同优先级或更高优先级的线程。如果只存在优先级更低的线程,则当前线程仍会继续执行。此方法不会释放锁
  7. void join()——等待该线程调用完毕后继续执行下面内容。
  8. 继承自Object的方法,void wait()、void notify()、void notifyAll(),分别表示该对象的当前线程休眠、随机唤醒调用该对象的单个线程、唤醒调用该对象的所有线程。wait()方法可以释放锁。

四、 多线程的问题(线程不安全、线程死锁)

       线程不安全和线程死锁是多线程过程中容易出现的两个基本问题。线程死锁放在实现线程安全的方式后。
       线程不安全通常是指多个线程访问同一个变量、进行读写操作时,由于执行时间差异造成结果与预期结果不一样的情况。下面以通常的取款案例为例:

public class Demo5 {
    public static void main(String[] args) {
        //初始余额有200块
        BankAccount bankAccount = new BankAccount(250);
        Thread t1 = new Thread(bankAccount);
        Thread t2 = new Thread(bankAccount);
        Thread t3 = new Thread(bankAccount);
        Thread t4 = new Thread(bankAccount);
        t1.start();t2.start();t3.start();t4.start();
    }
}


class BankAccount implements Runnable{
    //用于表示账户上的余额
    private double balance;
    public BankAccount(double balance){
        this.balance = balance;
    }

    public void run(){
        //每次调用run方法取100块钱
        if(balance < 100){
            System.out.println("余额不足,取款不成功");
        }else{
            try {
                //表示一次取款需要随机0-0.2秒,用于扩大线程不安全出现的概率
                Thread.sleep((int)Math.random()*200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= 100;
            System.out.println("取款成功,余额:"+balance);
        }

    }
}

       运行结果如下图所示(结果具有随机性):
在这里插入图片描述
       从图中可以看出,余额只有300的账户在近乎同时执行4次取款100的操作时,不仅出现了取款成功的异常现象,还出现了账户余额的显示异常。
       在A线程判断能扣款,但未执行扣款前,B线程进行了判断扣款的操作,从而出现了上述例子的情况。这种问题称之为线程不安全。


五、 维持线程安全的方式

       为了避免出现(四)中的情况,需要保证不同线程访问某个数据时,从随机分配执行转换成排队执行——读取操作完毕后才能让下一个线程能继续读取。这种机制则称为“加锁”,用于保护数据安全。
       加锁的方式包括:
       1,synchronized代码块,其测试案例如下:

public class Demo6 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket(2);
        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        t1.start();t2.start();t3.start();
    }
}

class Ticket implements  Runnable{
    //使用引用类型,之后synchronized仅对该对象加锁以观察锁释放和加上的过程
    Integer tickets;
    public Ticket(int tickets){
        this.tickets = tickets;
    }
    public void run(){
        System.out.println(Thread.currentThread().getName()+"线程开始工作,未进入synchronized代码块");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (tickets){
            System.out.println(Thread.currentThread().getName()+"线程进入synchronized代码块");
            System.out.println(Thread.currentThread().getName() +"准备休眠第1次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //用作区分,以此观察获得锁的时机
            if(tickets > 0) {
                tickets--;
                System.out.println(Thread.currentThread().getName() + "减少了tickets");
            }else{
                System.out.println(Thread.currentThread().getName() +"余票不足");
            }
            //sleep()方法不会释放锁,以此观察锁释放的时机
            System.out.println(Thread.currentThread().getName() +"准备休眠第2次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"进入synchronized代码块结尾");
        }
        System.out.println(Thread.currentThread().getName()+"synchronized代码块运行结束");
    }
}

        其运行结果如图所示:
在这里插入图片描述

       该结果反复执行后,仍可见每个线程进入synchronzied代码块时存在别的线程的干扰,但开始使用tickets对象后至代码块结尾不存在其他线程干扰。因此可见锁加上的时机由代码块开始使用synchronized括号内的对象开始,由代码块运行完毕释放。

       2, synchronized关键字修饰方法

public class Demo7 {
    public static void main(String[] args) {
        SynMethods test = new SynMethods();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();t2.start();
    }

}

class SynMethods implements Runnable{
    public synchronized void Method1(){
        System.out.println(Thread.currentThread().getName()+"正在执行Method1");
    }

    public synchronized void Method2(){
        System.out.println(Thread.currentThread().getName()+"正在执行Method2");
    }

    public void run(){
        Method1();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Method2();
    }
}

       其运行结果如图所示:
在这里插入图片描述
       synchronized修饰的方法本质等同于synchronized(this)+{方法内容}。由于括号内是该对象本身,因此在执行某一方法时,该对象的其他方法也不能执行。
       3, Lock接口的实现对象

public class Demo8 {
    public static void main(String[] args) {
        Ticket2 ticket2 = new Ticket2(2);
        Thread t1 = new Thread(ticket2);
        Thread t2 = new Thread(ticket2);
        Thread t3 = new Thread(ticket2);
        t1.start();
        t2.start();
        t3.start();
    }
}

class Ticket2 implements Runnable {
    //使用引用类型,之后synchronized仅对该对象加锁以观察锁释放和加上的过程
    Integer tickets;
    Lock lock = new ReentrantLock();

    public Ticket2(int tickets) {
        this.tickets = tickets;
    }

    public void run() {
        //lock接口加锁方法
        lock.lock();
        if (tickets > 0) {
            tickets--;
            System.out.println(Thread.currentThread().getName() + "减少了tickets");
        } else {
            System.out.println(Thread.currentThread().getName() + "余票不足");
        }
        //lock接口释放锁方法
        lock.unlock();
    }
}

       其运行结果:
在这里插入图片描述
       在这过程中可以手动操作加锁和释放锁的时机。lock接口的已有实现类包括ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock

       线程死锁:
       在接触加锁后,多线程的并发执行还会导致新的线程死锁问题——即多个线程互相等待对方释放锁后再释放自身拥有的锁,因此陷入无限等待的过程。下面举例代码实现过程如下:

public class Demo9 {
    public static void main(String[] args) {
       DeadLock deadLock = new DeadLock();
       new Thread(()->{
           while(true) {
               System.out.println("调用method1");
               deadLock.method1();
               System.out.println("结束调用method1");
           }
       }).start();
        new Thread(()->{
            while(true) {
                System.out.println("调用method2");
                deadLock.method2();
                System.out.println("结束调用method2");
            }
        }).start();
    }
}

class DeadLock {
    String s1 = "s1";
    String s2 = "s2";
    public void method1(){
        synchronized (s1){
            System.out.println("锁住"+s1);
            synchronized (s2){
                System.out.println("使用"+s2);
            }
        }
    }
    public void method2(){
        synchronized (s2){
            System.out.println("锁住"+s2);
            synchronized (s1){
                System.out.println("使用"+s1);
            }
        }
    }
}

       运行结果如图所示:

       程序运行过程锁死在两个方法互相等待对方释放锁的这一刻,该现象则称为线程死锁。


六、 线程的生命周期

       线程的状态可大致包括:就绪态、运行态、阻塞态、等待态和等待锁的状态,其线程的生命周期可用下图来描述:
手绘轻喷


       以上就是学习多线程过程中的笔记理解,有不正确的地方希望大家多多指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值