阿嘉的Java日志 [第九章:多线程基础]

151 阅读8分钟

[内容时间:2021.09.05]

进程的几种状态

创建、就绪、运行、阻塞、死亡

进程和线程的关系

多线程的三大特征

原子性、可见性、有序性

  • 进程里包含(一个或多个)线程,一个进程可以启动多个线程
  • 进程与进程之间内存不共享,但一个进程下的线程与线程 共享堆内存和方法区内存
  • 每个线程有一个栈独立运作,叫多线程并发
  • 多线程并发是为了提高程序的执行效率

实现线程有两 (三) 种方式

Java支持多线程机制,并且Java已经将多线程实现了,我们只需要继承就好了

(第三种后面讲)

第一种:分支线程直接继承 java.lang.Thread ,重写run方法

第二种:分支线程实现 java.lang.Runnable 接口 ,重写run方法(这种比较常用,因为实现接口还能继承别的类,而且接口支持多继承)

采用匿名内部类也行,但是不常用

第三种:FutureTask方式,实现Callable接口(JDK8新特性)

优点:可以获取到线程的执行结果,有返回类型,其他两种都是空类型

缺点:效率表弟,在获取 t 线程的执行结果时,当前线程受阻塞,效率较低。

重点:run( ) 方法的异常不能throws,只能try—catch,因为子类重写父类方法不能抛出更多(宽泛)的异常

控制线程的方法( )

sleep方法

interrupt方法

这个方法会让指定对象的sleep( )方法报异常,这就是终止睡眠的原理,所以sleep需要try—catch

stop方法

用法是,引用 . stop()

此方法已过时,因为强行终止线程可能会导致数据的损失,线程没有保存的数据会丢失,所以不建议使用。

如何合理终止一个线程的执行

关于线程的调度

常见的调度模型

  • 抢占式调度模型

哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些(多一些)。Java采用的就是抢占式调度模型

  • 均分式调度模型

    平均分配CPU时间片,每个线程占有的CPU时间长度一样

    有一些语言采用的调度模型就是这种方式

Java中提供了哪些方法是和线程调度有关?

  • 实例方法

  • void setPriority(int newPrioirty) 设置线程的优先级

    • int getPriority() 获取线程优先级
    • 最低优先级1

      默认优先级5

      最高优先级10

  • 静态方法

    static void yield() 让位方法

    暂停当前正在执行的线程对象,并执行其他线程

    yield方法不是阻塞方法,让当前线程让位,让给其他线程使用

    yield方法的执行会让当前线程从“运行状态”回到“就绪状态”。(就绪状态会继续抢夺……相当于给别人一个再抢的机会……给机会了,但没完全给……)

  • join方法,合并线程(不是栈合并)

    栈不会合并不会消失,只是栈直接发生了等待关系,栈之间协调了

class Mythread extands Thread{
    public void doSome(){
        MyThread2 t = new MyThread2();
​
        //当前线程进入阻塞,t线程执行,直到t线程结束(意思就是t加入进来执行,顺序是从上往下)
        //记得处理异常
        t.join();   
        System.out.println("Hello World !"); //t执行完才最后到这个。
    }
}
​
class MyThread 2 extends Thread{
​
}

线程安全

  • 为什么这个是重点?

    以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将建成的定义、线程对象的创建、线程的启动,都已经实现完了。这些代码我们都不需要编写

    最重要的是:要知道,我们编写的程序需要放到一个多线程的环境下运行,更要关注的是这些数据在多线程并发的环境下是否是安全的。重点( * * * * * )

  • 什么时候数据在多线程并发的环境下会存在安全问题呢?

    三个条件

    • 多线程并发
    • 有共享数据
    • 共享数据有修改行为
  • 怎么解决线程安全问题呢?

    当多线程并发的环境下,有共享数据,而且这个数据还会被修改,此时就存在线程安全问题

    如何解决?

    线程排队执行。(不能并发)。用排队执行解决线程安全问题。这种机制被称为:线程同步机制。

    • 怎么解决线程安全问题?

      使用“线程同步机制”

      线程同步就是线程排队了,排队就会牺牲一部分效率。没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

    • 两个专业术语

      异步编程模型

      线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步变成模型

      其实就是多线程并发(效率高)

      异步就是并发

      同步编程模型

      线程t1和线程t2,在线程t1执行的时候,必须等待t2执行结束,反之亦然。

      两个线程之间发生了等待关系,这就是同步编程模型(效率低,线程排队执行)

      同步就是排队

synchronize代码块

    // 取钱的方法
    public void withdraw(int money) {
        /**
         * 这里是线程不安全的情况:
         *         设置一个网络延迟必出bug
         *         try {
         *             Thread.sleep(1000);
         *         } catch (InterruptedException e) {
         *             e.printStackTrace();
         *         }
         *         //延迟在保存之前,两个线程都共用了account的数据
         *         //保存之前两个线程都进来了,那就出问题了
         *         //余额(这里是保存操作了)
         *         this.setBalance(this.getBalance() - money);
         */
        //================================================================
        /**
         * 以下是线程安全:
         *      线程同步机制的语法是:
         *      synchronize(){
         *          //线程同步代码块
         *      }
         *      ()中写什么?
         *      主要是看需要将那些线程同步
         *      假设t1,t2,t3,t4,t5,五个线程,但是我只希望t1,t2,t3排队,怎么办?
         *      则需要在()中写t1,t2,t3共享的对象,而这个对象对应t4,t5不是共享的。
         *
         *      而这里的共享对象很显然是账户对象,所以this就是账户对象
         *      不一定是this,只要是线程共享的对象就行。
         *
         *      在Java语言中,任何一个对象都有一把“锁”,其实这把锁就是标记(只是把它叫做锁)
         *      100个对象,100把锁。
         *
         *      以下代码执行原理:
         *          1、假设t1和t2线程并发,开始执行以下代码的时候肯定有一个先一个后
         *          2、假设t1先执行了,遇到了synchronize,这个时候自动找“后面共享对象”的对象锁
         *          找到之后,占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。
         *          直到同步代码块及诶书,这把锁才会释放。
         *          3、假设t1已经占有了这把锁,此时t2只能在同步代码块外面等待t1的结束,直到t1吧同步代码块执行结束了,
         *          t1会归还这把锁,此时t2终于等到这把锁,然后占有,进入代码块执行。
         *
         *      共享对象一定要选好
         */
        // synchronized (obj)也行
        // Object object2 = new Object();
        // synchronized (onject2)不行,因为是局部变量,不是共享对象。
        // synchronized ("abc")也行,因为abc在字符串常量池内
        synchronized (this) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(this.getBalance() - money);
        }
    }

三大变量

  • 实例变量(在堆区)
  • 静态变量(在方法区)
  • 局部变量(在栈区)

在栈内存的变量就不存在线程安全问题了,因为多线程不共享栈内存。

方法区中的静态变量中的常量也不存在安全问题

注意一下synchronize扩大范围的问题,其实在安全的情况下,范围越小效率越高

synchronize的使用方法

上述是第一种,包装在实例变量上

说说第二种方式:写在方法体上。

public synchronize void withdraw( ){
​    xxxxxxxxxxxx
}

这种方式,如果共享对象是 this 那么推荐使用这种方式。代码简洁。

但这种方式的缺点是可能无故扩大同步范围,导致效率低下。

第三种方式:在静态方法上使用synchronize

表示找类锁。类锁永远只有一把。类锁保证了静态变量的安全

就算创建了一百个对象,那类锁也只有一把。

对象锁:100个对象100把锁

类锁:100个对象,也可能只是1把锁

死锁

synchronize在开发中最好不要嵌套使用,一不小心就可能导致死锁的现象发生,而且很难查出,因为无异常,无报错,很难调试!

守护线程

  • java语言中线程分为两大类

    一类是用户线程

    一类是守护线程(后台线程)

    其中具有代表性的就是:垃圾回收器线程(守护线程)

  • 守护线程的特点

    一般守护线程是个死循环,所有用户线程只要结束,守护线程自动结束

    注意:比如主线程main方法是一个用户线程

  • 守护线程用在什么地方呢?

    比如每天0:00的时候系统数据自动备份

    这个时候需要使用定时器,并且我们可以将定时器设置为守护线程

    一直在那里看着,每到0;00的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了

守护线程的实现

用 t.start() 启动线程之前,调用 t.setDaemon(true) 即可让 t 线程设置为守护线程。就算 t 线程的run里是死循环,用户线程结束,守护线程也会结束。

定时器

……

生产消费者模型