Java SE 学习笔记-Day 07

基础概念

  • 进程是系统分配资源的最小单位。
  • 线程是实际执行的最小单位,一个进程里至少有一个线程。
  • main()称为主线程,为系统的入口,用于执行整个程序。
  • 各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。

两个关键字

transient:被修饰了的变量在序列化时,不会被序列化。

volatile:被修饰了的变量被一个线程修改(即将工作内存的数据写回主内存)时,会立即被多个线程感知,实现轻量级的线程同步方式

线程状态

广义上的五个线程状态:

image-20210312123945215

新生状态:被创建出来,Thread t = new Thread()

就绪状态:当调用了start方法,线程就就绪了

运行状态:CPU调度运行该线程

阻塞状态:线程需要等待输入或被sleep,wait等阻塞

死亡状态:线程中断或结束

Java里的六个线程状态
  • NEW : 尚未启动的线程
  • RUNNABLE : 在Java中运行的线程(就绪态和运行态 二合一)
  • BLOCKED:被阻塞
  • WAITTING:正在等待另一个线程执行特定动作的线程
  • TIMED_WATTING:正在等待另一个线程执行动作达到指定等待时间(如调用sleep)的线程
  • TERMINATED:已退出的线程

可以通过Thread.State来观察线程状态,用Stare对象接受线程对象调用getState的返回值。

创建线程的两种方法

通过Thread类
  • 创建继承了Thread的类
  • 重写run方法
  • 在main方法里,实例化自定义线程类,调用start()方法。
    • 若直接调用run()方法,相当于只是一个线程里面在调用方法而已。
通过Runnable类(推荐)
  • 实现Runnable接口

  • 重写run方法

  • 在main方法里,实例化自定义线程类,将对象传入一个Thread类的构造函数,通过Thread类对象调用start()方法。

    • 若直接调用run()方法,相当于只是一个线程里面在调用方法而已。

    实现Runnable接口更灵活,避免了Java单继承的局限,方便一个对象被多个线程使用。而一个Thread对象就代表一个对象,只能调用一次start方法,要建立新的线程,只能重新new 一个对象。而将一个Runnable对象传入多个Thread线程,即可共享一个对象。

多线程实例 - 龟兔赛跑
public class Race implements Runnable{
    // 通过winner 来实现两个线程之间判断对方是否已到达终点
    String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(Thread.currentThread().getName().equals("兔子") && i == 50)
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            if (gameOver(i))
                break;
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }

    boolean gameOver(int step){
            if(winner != null) {
                return true;
            }
            else{
                if( step >= 100){
                    winner = Thread.currentThread().getName();
                    System.out.println(winner+"赢得了比赛");
                    return true;
                }
            }
            return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"乌龟").start();
        new Thread(race,"兔子").start();
    }
}
Callable接口(了解)

好处:可以定义并获得返回值,且可以抛出异常

  1. 实现Callable接口,需要传入返回类型,如Callable<Boolean>

  2. 重载call方法

  3. 实例化对象

  4. 创建执行服务,指定线程数量

    ExecutorService ser = Executor.newFixedThreadPool();
    
  5. 提交执行,需要接受返回值

  6. 获取结果

  7. 关闭服务

静态代理模式

  • 代理对象和真实对象实现同一个接口
  • 通过代理对象来执行一个方法,就可以在真实对象的基础上做很多其他事

自定义实现了Runnable接口类,实例化对象,就是一个真实对象

同时Thread类也实现了Runnable接口,我们将自定义类的对象传入Thread类的对象,Thread类就充当了一个代理对象,可以做自定义类里面未实现的工作。

Lamda表达式

避免匿名内部类定义过多,使代码更简洁。

Functional Interface(函数式接口):只包含一个抽象方法的接口,如Runnable

public interface Runnalbe{
    public abstract void run();
}
  • lamda表达式只能用于函数式接口
  • lamda表达式重写的方法只有一行代码时,可以简化为一行,否则需要用花括号包裹
  • 由于在接口的方法里已经定义了参数类型,所以lamda表达式中只需要写出形参名称即可。单个参数时,可以不用写(),多个参数时,则需要用(),并用逗号隔开。
interface Love{
    int love (int a,int b);
}

public static void main(String[] args) {
    // 相当于实现了一个匿名内部类,重写了它的方法
    Love me = (a,b) -> {
        int c = a + b;
        return c;
    };

    int c = me.love(2,5);
    System.out.println(c);
}

线程睡眠

通过sleep方法,可以模拟网络延时和倒计时等功能

线程停止

现在已经不建议使用jdk内部的stop方法使线程停止,一般设置一个标志变量,然后重写stop方法,在这个方法里面,更改标志变量的值,使死循环终止。

线程礼让

某个运行态的线程主动转到就绪态,但不进入阻塞态,但能否礼让成功,要看CPU的调度策略,有可能还是让礼让的线程继续执行,不一定礼让成功。

Java里通过yield()方法礼让。

Join

相当于插队,当一个线程主动调用该方法时,会阻塞其它所有线程,只执行该线程。

线程优先级

JVM有一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。线程优先级从1~10, 最高为10,最低为1,正常为5,一般都默认为5.

通过getPriority() 可获得优先级。

通过setPriority() 可设置优先级,主要要在启动前设置。

即使后启动的线程,但优先级高,也会先执行。

守护(Daemon)线程

线程分为用户线程守护线程。JVM必须确保用户线程执行完毕,但不用等待守护线程执行完毕。守护线程,如垃圾回收等,进程存在时会一直存在,但当只剩下守护线程的时候,JVM就会退出。

可以在线程执行前,通过setDaemon方法(传入true),设置线程为守护线程。

线程同步

线程同步就是一种等待机制,多个需要同时访问某个对象的线程,直接进入这个对象的等待池形成排队队列。

为了线程添加了锁机制(synchronized),即拥有锁的线程才能访问资源,否则要一直等待拿到锁。

  • 锁机制提高了安全性,但也会导致较多的上下文切换、优先级倒置等性能问题。

synchronized

使用synchronized修饰某个方法,某个对象对应一把锁,每个synchronized方法只有拥有这个对象的这把锁时,才能执行,一旦开始执行,就会独占该锁,直到该方法返回才释放锁,而后面阻塞的线程才能获得这个锁。
由于将某个方法都锁上, 效率比较低, 一般对数据只读的代码区没有必要锁上, 可以用synchronized修饰代码区.

  • 同步方法给调用它的对象(this)一把锁

  • 同步块可以设置同步监视器,监视任何指定对象(一般设置为会被增删查改的共享资源)

    // 给对象加一把锁,也相当于独占这个对象
    synchronized(共享资源的对象){
        // 增删查找改代码块
    }
    

    JUC 是java下的一个包Java.util.concurrent,里面有线程安全的一些类

死锁

多个线程互相拥有着对方的资源(即锁),形成环路。

  • 互斥条件:一个资源只能被一个进程使用

  • 请求与保持条件:一个进程对拥有的资源保持不放,同时请求另一个资源

    体现在代码里,即在synchronized代码块内,再嵌套一层,相当于持有的资源还未释放,又去获取新的资源

  • 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺

  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

Lock(锁)

java.util.concurrent.locks.Lock 接口提供了多个线程对共享资源访问的工具。

ReentrantLock类(可重入锁)实现了Lock接口,它拥有和synchronized相同的并发性和内存语义,可以显式地加锁,释放锁。

使用Lock锁,性能更高,更有效率。

class A{
    private final ReentrantLock lock = new ReentrantLock();
    public void m(){
        // 加锁
        lock.lock();
        try{
            // 需要保证线程同步的代码
        }finally {
            lock.unlock(); // 解锁
        }
    }
}

生产者消费者模型

假设仓库只能放一件产品,生产者将生产出来的产品放入仓库,消费者从仓库取走消费。若仓库没有产品,消费者不能进行消费,生产者可以将产品放入仓库后,停止生产,通知消费者取走,消费者取走后通知生产者又开始生产。即一个反映线程通信的模型。

  • Object里面的wait方法会使线程进入等待状态,但会释放锁,sleep则不会。

synchronized只能保护共享数据并发同步安全性,不能实现通信。为实现通信,有两种方式,管程信号量

管程法:

建立一片缓冲区,使用waitnotify来进行通信。

信号量:

用一个标志位来通信

线程池

为避免线程经常创建、销毁,可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。提高响应速度,降低资源消耗,便于管理。

  • ExecutorService类 用于管理、执行线程池中的线程。
    • submit 方法执行继承Callable接口的类,且有返回值
    • execute方法执行Runnable接口的类
    • shutdown 关闭线程池
  • Executors用于创建线程池,Executors.newFixedThreadPool(threadNum),该方法返回指定线程数目的线程池。
// 1.创建服务,创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);

// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());

// 关闭
service.shutdown();
posted @ 2021-03-27 10:21  蓬飞  阅读(49)  评论(0)    收藏  举报