Java多线程(了解多线程涉及的所有细节内容)

文章目录

一、多线程介绍

1 线程与进程

1.1 线程
  1. 线程是组成进程的基本单位,可以完成特定的功能,一个进程可以有一个或者多个线程组成;
1.2 进程
  1. 计算机正在执行的一个独立的应用程序
  2. 进程是一个动态的;
  3. 必须是运行状态;
  4. 如果一个应用程序没有启动,就不是进程
1.3 进程与线程的区别
  • 内存空间的区别
    • 进程:
      • 有独立的内存空间;
      • 每个进程之间相互独立,互补干扰;
    • 线程:
      • 有共享的内存空间;
  • 安全性
    • 进程
      • 进程是相互独立的,
      • 一个进程的奔溃不会影响到其他的进程;
    • 线程
      • 内存共享的,一个线程的的奔溃可能会影响到其他的线程的执行,线程的安全性不如进程;
  • 线程与进程的关系
    • 进程是相互独立的,一个进程可以包含一个或者多个线程;

二、线程的创建

1. Java多线程的创建需要调用start方法启动新线程;
2. start方法底层调用的是native修饰的start方法,native需要借助于操作系统来执行,也就是Java创建多线程的方式需要借助于底层操作系统的线程创建,
3. 线程的创建交给底层的c++的线程创建方式,java是对底层线程创建的封装。

2.1 实现Runnable接口

  • Runnable接口的定义
public interface Runnable{
	//抽象的run方法
	public abstract void run();
}
  • 通过接口定义,Runnable接口来实现多线程就需要实现该接口中的run()方法
public class Test implements Runnable{
	//重写run()方法
	public void run(){
		Thread.currentThread().getName();
	}
}
//执行的任务体
	Test runnable=new Test();
	//将任务体提交给线程,获取线程实例
	Thread thread=new Thread(runnable);
	//启动多线程
	thread.start();
	Thread.currentThread().getName();

  • start()方法在启动多线程之后,会自动调用run()方法,子线程的所有逻辑都是在run()方法中进行实现;
2.1.1 实现Runnable接口创建线程的步骤
  1. 创建一个特定的类,必须实现Runnable接口,并实现run()方法
  2. 实例化特定类的对象;
  3. 创建Thread类的对象,将特定类对象作为参数进行传递
  4. 启动子线程,调用Thread的类的start()方法;

2.2 继承Thread类

  • Thread类的定义
public class Thread implements Runnable{
	private Runnable target;
	public void run(){
	if(target!=null){
	target.run();
	   }
	}
}

  • Thread类本身是实现了Runnable接口;
  • Thread类是Runnable接口的实现类;
  • Thread类也有run()方法,run()方法类执行任务体target,需要来重写run()方法;
//继承字Thread类
public class ThreadTest extends Thread{
//重写run()方法
public void run(){
Thread.currentThread().getName();
	}
}
//继承字Thread类
TreadTest threadTest=new ThreadTest();
threadTest.start();

2.2.2 继承自Thread类创建线程的方式
  1. 创建类,继承自Thread类,重写run()方法;
  2. 实例化创建类对象;
  3. 调用对象的start()方法启动子线程;

2.3 实现Callable接口

  • Callable接口声明:
public interface Callable<V>{
V call()throws Exception‘’
  • Callable接口提供了call()方法,具有返回值,通过泛型定义,给接口可以抛出异常;
  • Callable接口不能直接使用,需要借助于FutureTast类,FutureTest类可以接受Callable接口的实现类;
  • FutureTast类的定义
public class FutureTast<V> implements RunnableFuture<V>{
	public FutureTast(Callable<V> callable){
	if(callable==null)
	throw new NullPointerException();
	this.callable=callable;
	this.state=NEW;
	}
	}
	public interface RunnableFuture<V> extends Runnable,Future<V>;

  • FutureTast类是继承Runnable接口,即FutureTast是Runnable接口的实现类;
  • FutureTast类提供了构造函数FatureTast callable,可以接受Callable类型的任务;
  • 可以实现Callable类型实例转化为Runnable类型实例,交给Thread类处理;
  • Callable接口的实现类CallableTast
public class CallableDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+":子线程正在执行...");
        return null;
    }
}

//实例化Callable接口实现类
        CallableDemo callableDemo = new CallableDemo();
        //FutureTask类实例
        FutureTask <String> futureTask = new FutureTask <>(callableDemo);
        Thread thread1 = new Thread(futureTask);
        thread1.start();

2.3.1 实现Callable接口创建线程的步骤
  1. 创建特定类,实现了Callable接口,实现该接口的call方法;
  2. 创建特定类的实例;
  3. 创建FutureTask实例,将Callable实例作为参数传入;
  4. 创建Thread对象实例,将FutureTask实例作为参数传递(当做Runable实例);
  5. 启动子线程,调用Thread类对象的start方法;
  //匿名内部类形式实现
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":内名内部类实现线程");
            }
        }).start();
  • 线程的实现也可以是匿名内部类方式,可以归结为实现Runable接口;
2.3.2 Runable接口和Thread类区别
  1. 线程类继承自Thread则不能继承其他类(单继承),而Runable接口是可以;
  2. 继承自Thread类相对于Runable实现,使用线程的方法更简单;
2.3.3 Callable接口和Runable接口的区别
  1. Callable接口方法为call(),而Runable接口方法为run();
  2. Callable接口是有返回值,而Runable的接口的方法是不能返回值;
  3. call()方法是可以抛出异常的,run()是不能抛出异常;
  4. 运行Callable任务可以拿到FutureTask的对象,通过该对象可以对子线程进行操作,获取结果.;

三、线程状态及其转换

3.1 线程状态

  • 在JDK中在Thread类中有一个子枚举类State:定义线程存在状态;
public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

3.2 线程的状态

3.2.1 新建状态(NEW)
  • 用new语句创建的线程就处于新建状态,和Java对象一样,仅在堆中分配了内存,在调用start之前,就处于新建状态;
3.2.2 就绪状态(Runable)
  • 在一个线程对象创建之后,调用它的start方法,该线程就进入就绪状态,JVM为线程创建栈和程序计数器。处于这个状态的线程位于可运行池中,等待获取CPU的执行权;
3.2.3 运行状态(Running)
  • 处于该状态的线程获取了CPU的执行权,执行程序代码,只有处于就绪状态的线程才有机会到运行状态;
3.2.4 阻塞状态(BLOCKED)
  • 在线程期望进入同步代码块或者同步方法中时会进入该状态(Synchronized或者Lock锁),等待资源(非CPU资源),等待到了资源线程就会进入就绪状态等待CPU调度;
3.2.5 等待状态(WAITING)
  • 一般是调用wait方法会进入等待状态,当线程被其他的调用notifyAll或者notify的线程通知唤醒,才会从等待状态进入阻塞状态(还需要获取锁);
3.2.6 超时等待(TIMED_WAITING)
  • 如果线程执行了sleep(long time),wait(long time),join(long time),设置等待时间,当设定的等待时间到了,就会脱离阻塞状态;
3.2.7 终止状态(TERMINATED)
  • 线程退出run()方法是,就进入终止状态,该线程结束生命周期;

3.3 线程状态转换

在这里插入图片描述

  • 一个线程的生命周期中需要状态:New、Runable、Running和Rerminated四个状态。当线程需要响应资源时,进入到阻塞状态,阻塞状态包含Blocked、Waiting和Timed_Waiting状态;

四、线程方法

4.1 start():启动线程

  1. start():作用是启动一个新线程,需要首先调用,start方法是不能重复调用的;
  2. start方法启动子线程是通过调用native的系统提供的方式来启动线程
public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

       //线程组概念。已经被线程池替代
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }

    private native void start0();

4.2 run():子线程执行体

  1. 将所有的子线程业务逻辑在run方法中实现,在调用start方法时,会启动子线程并主动调用到run方法。不需要显性调用。
  2. 单独调用run()方法,会在当前线程中执行run()方法,并不会启动新线程;

4.3 yield():线程让步

  1. 作用:是暂停当前的线程的执行,并让步于其他同优先级或更高线程,让其他线程先执行;
4.3.1 yield()方法的特点
  1. yield方法是Thread类的静态方法,通过Thread.yield()调用;
  2. 能够让正在执行的线程由“运行状态”进入到“就绪状态”,等待CPU调度;
  3. 正在执行线程让步与相同优先级或更高优先级的线程来获取CPU执行权,当同级优先级或者更高优先级没有对应线程时,当前线程又可以继续执行;

4.4 sleep():线程休眠

  1. 作用:让线程休眠,而且是哪个线程调用sleep方法,哪个线程休眠。
  2. sleep方法有两个,分别是sleep(long millis)和sleep(long millis, int nanos),两个方法功能一样,第二个方法提供了纳秒级的时间控制;
public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos) throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
        sleep(millis);
    }

4.4.1 Sleep()方法的特点
  1. sleep方法是静态的native方法,调用Thread.sleep()调用;
  2. sleep方法调用会使当前线程从“运行状态”进入到“睡眠状态”,直到给定的时间millis,当前线程才会被唤醒;
  3. sleep方法会使线程休眠到指定的时间,如果要提前终止休眠,可以通过Interrupt方法调用,会使当前的sleep方法抛出InterruptedException异常,结束掉休眠;

4.5 join():线程合并

  1. 作用:暂停当前线程,等待子线程执行结束当前线程才能执行。join方法让并行的线程合并为串行的线程执行;
//指定合并等待时间
public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

 //指定毫秒、纳秒界别的合并时间
    public final synchronized void join(long millis, int nanos) throws InterruptedException {

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        join(millis);
    }

    //会一直等待直到子线程执行结束
    public final void join() throws InterruptedException {
        join(0);
    }
4.5.1 join()方法的特点
  1. 线程方法提供了三种方法join(long millis)、join(long millis, int nanos)和join()方法,都是调用到join(long millis)的实现;
  2. join方法定义在Thread类中,可以抛出InterruptedException异常,即join方法是可以被中断合并;
  3. join方法调用会使线程状态从“运行状态”进入到“阻塞状态”;
  4. join方法可以使线程按照顺序串行执行

4.6 interrupt():中断线程

  1. 作用:中断当前线程,中断处于阻塞状态的线程;
boolean isInterrupted() 返回值:true:当前线程被中断  false:当前线程未被中断
void interrupt();发起中断操作 是Thread类中的普通方法,由对象调用该方法
public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
private native void interrupt0(); //底层操作系统提供中断业务
4.6.1 interrupt():方法说明
  1. 中断操作interrupt()方法底层调用的是native的interrupt0()方法,发起中断操作仅仅修改中断标志位;
  2. 、如果当前的线程处于阻塞状态(sleep、join、wait等方法会导致线程进入阻塞状态),在任意的其他线程调用Interrupt()方法,那么线程会立即抛出一个InterruptedException退出则塞状态;
  3. 如果当前的线程处于运行状态,。其他线程调用Interrupt方法,当前线程继续运行,直到发生了阻塞之后随后会立即抛出异常跳出则塞;

4.7 deamon():守护线程

  1. 介绍
void setDaemon(boolean on)  设置守护线程, 参数Boolean类型 true:设置为守护线程 false:非守护线程 默认是false
  boolean isDaemon()  判断当前线程是否为守护线程  
  1. 守护线程
  • Java中有两种线程,用户线程和守护线程,通过isDaemon方法进行区分,如果返回是false:说明线程是用户线程,否则是”守护线程“;
  1. 用户线程一般用户执行用户级的任务
  • 守护线程也称之为”后台线程“,服务于用户线程,一般执行后台任务,例如:垃圾回收的线程是单独线程来处理的,负责垃圾回收的线程就是守护线程;
  1. 线程生命周期
  • 守护线程是依赖于用户线程,当用户线程存在时,守护线程就会存活,当没有了用户线程时,那么守护线程也就会随之消亡;

4.8 Priority:线程优先级

  1. 就是用来指导线程的执行优先级的,指的是用来告诉CPU那些线程优先被执行;
void setPriority(int newPriority) //设置当前线程的优先级,newPriority必须是1~10之间的整数,否则会抛出异常
int getPriority() //获取当前线程的优先级
 public final static int MIN_PRIORITY = 1; //最小优先级
 public final static int NORM_PRIORITY = 5;//默认优先级
 public final static int MAX_PRIORITY = 10;//最大优先级
4.8.1 Priority特点
  • Java线程中提供的一些方法可以进行线程的调度,合理的线程方法使用,可以充分发挥系统的性能,提高程序的执行效率;
  1. 线程的优先级:高优先级线程获得较多的运行机会;
  2. 线程睡眠:sleep().让当前线程从运行状态进入到阻塞状态;
  3. 线程让步:yield(),让正在执行的线程从运行状态进入就绪状态;
  4. 线程合并:join(),当前调用join方法所在的线程进入到阻塞状态;
  5. 线程等待:wait(),就会是线程从运行状态进入到等待状态;

五、线程调度

5.1 方法级的调度

  • Java线程中提供的一些方法可以进行线程的调度,合理的线程方法使用,可以充分发挥系统的性能,提高程序的执行效率;
  1. 线程的优先级:高优先级线程获得较多的运行机会;
  2. 线程睡眠:sleep().让当前线程从运行状态进入到阻塞状态;
  3. 线程让步:yield(),让正在执行的线程从运行状态进入就绪状态;
  4. 线程合并:join(),当前调用join方法所在的线程进入到阻塞状态;
  5. 线程等待:wait(),就会是线程从运行状态进入到等待状态;

5.2 系统级调度

  • 主要指系统在特定的时机自动进行调度,主要涉及调度算法
  1. FIFO:先进先出算法;
  2. SJF:最短作业优先算法;
  3. SRTF:最短剩余时间优先算法;
  4. RR(Round Robin):时间片轮转算法;
  5. HPF(Higest Priority First):最高优先级算法
  • OS系统调度算法一般都是基于时间片的优先级算法;

六、线程并发

6.1 并发与并行的区别

  • 并发是指多个线程操作同一个资源,不是同时执行,而是交替执行;
  • 并行才是真正的同时执行,多核CPU,每个线程使用一个单独的CPU资源来运行;

6.2 临界资源与临界区

  • 临界资源
    • 一个时刻只能允许一个进程访问,一个进程正在使用的资源称之为临界资源;
  • 临界区
    • 是一种线程中访问临界资源的程序代码,注意:是程序代码,而不是内存资源或者CPU资源。关于临界区的使用原则,“空闲让进,忙着等待,优先等待,让权等待”;
    1. “空闲让进”:临界资源空闲时一定要让线程进入;
    2. “忙着等待”:临界资源正在使用时其他的线程需要等待;
    3. “有限等待”:线程等待进入临界区的时间是有限的,不能无限等待;
    4. “让权等待”,线程进入临界区是应该放弃CPU的资源;

6.3 并发编程带来的问题

6.3.1 原子性
  • 即一个操作或者多个操作,要么全部执行并且执行的过程是不会被任何元素打断。要么就全不执行;
6.3.2 可见性
  • 是指当多个线程访问同一个变量时,一个线程的修改能够被其他线程立即看到修改的结果值;
  • 导致可见性问题的原因是由于缓存的存在,线程持有的共享变量的副本的修改,无法让其他的线程能够感知到变量的修改,导致读取的值不是最新的;
6.3.3 有序性
  • 指的是程序的执行顺序按照代码的先后顺序执行;

6.4 Java内存模型

  • 主内存和工作内存
    1. 主内存:所有的变量都存储在主内存中;
    2. 工作内存:每个线程自己的内存区域;
  • 工作内存保存了该线程使用的变量的主内存的副本。线程对变量的操作(读取、复制等)都必须在功能内存中进行,而不能直接读取主内存的数据;
  • 不同的线程之间无法直接访问对象的工作内存,线程间变量的传递均需要主内存来完成;
    工作内存、主内存和线程之间交互的关系:
    在这里插入图片描述
6.4.1 主内存和工作内存主键的交互的操作
  1. lock(锁定) :作用于主内存的变量,它把一个变量标识为一条 线程独占 的状态;
  2. unlock(解锁) :作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取) :作用于主内存的变量,它把一个变量的值 从主内存传输到线程的工作内存 中,以便 随后的load动作 使用。
  4. load(载入) :作用于工作内存的变量,它把read操作 从主内存中得到的变量值放入工作内存 的变量副本中。
  5. use(使用) :作用于工作内存的变量,它把 工作内存中一个变量的值传递给执行引擎 ,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值) :作用于工作内存的变量,它把一个 从执行引擎接收的值赋给工作内存的变量 ,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储) :作用于工作内存的变量,它把 工作内存中一个变量的值传送到主内存 中,以便随后的write操作使用。
  8. write(写入) :作用于主内存的变量,它把 store操作 从工作内存中得到的变量的值放入主内存的变量中。
  • 如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。
  • Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。 也就是说 read与load之间、store与write之间是可插入其他指令的 ,如对主内存中的变量a、b进行访问时, 一种可能出现的顺序是read a、read b、load b、load a 。
  • 除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
    1. 不允许read和load、store和write操作之一单独出现 ;
    2. 不允许一个线程丢弃它最近的assign操作 ;
    3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中;
    4. 不允许在工作内存中直接使用一个未被初始化(load或assign)的变量 ;
    5. 只允许一条线程对其进行lock操作;

七、Volatile关键字

  • 使用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值;
  • 场景问题:统计1秒内count++的次数
    给定一个线程专门进行count++操作
    给定另一个线程睡眠1秒,通过两个线程共享变量来完成count++的暂停操作;
  private static  boolean flag = true;
    public static void main(String[] args) throws IOException {
        Thread thread = new Thread(new Runnable() {
            Integer count=0;
            @Override
            public void run() {
                while (flag) {//标志位置为false,++操作结束
                    count++;
                }
                System.out.println("计数线程结束啦。。。");

            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //修改标志位
                flag = false;
                System.out.println("修改标志位成功");
            }
        });

        thread.start();
        thread1.start();
    }
  1. 在当前的代码执行过程中,一个线程修改了标志位,另一个线程没有感知到修改而程序会继续执行,
    失败原因:
    1. 在JVM运行的时候,在两个线程的共享变量count运行时,每一个线程会获取当前变量的副本在自己的私有的内存区域即虚拟机栈中,当thread1线程在堆变量做修改时,在thread1的本地内存中赋值的最新值没有及时store到主内存,那么Thread线程中也就不能读取到最新的flag的变化,所有thread线程会继续执行;
    2. 在flag变量上添加volatile修饰之后,thread1线程的修改操作能够立即引起Thread线程停止循环;

7.1 volatile特征

7.1.1 禁止指令重排序
  • volatile修饰的变量的的操作顺序与程序代码中的执行顺序一致;
  • 普通的变量仅会保证方法执行的最终结果获取正确,不能保证变量的操作顺序和程序代码中的执行顺序一致;
7.1.2 保证线程间可见性
  • 保证变量对所有的线程的可见性,“可见性”是指当一个线程修改这个变量的值,新值对于其他线程可以立即感知到
  • volatile修饰变量可以保证有序性和可见性,不能保证原子性,也不能保证线程安全;

7.2 Volatile工作原理

  • “观察加入volatile关键字和没有加volatile关键字时所生成的汇编代码发现,加入了volatile关键字时,会都出一个Lock前缀指令;
  • 该内存屏障提供3个功能
    1. 它确保指令重排时不会把后面的指令重排到屏障之前,同理,也不会把前面的指定排到屏障的后面;
    2. 它会强制将对缓存的修改操作立即写回主内存;
    3. 如果是写操作,它会导致其他CPU上对应的缓存行无效(缓存一致性协议);

7.3 如何保证可见性

7.3.1 变量被volatile关键字修饰
  1. 当一个共享变量被volatile修饰时,会保证修改时的值立即更新回主内存,当其他的线程读取该值时,也不会直接读取工作内存中的值,而是直接去主内存中读取;
  2. 被volatile修饰的变量,在每个写操作之前,会加入一条store内存屏障命令,会强制将此变量最新值从工作内存同步到主内存,在每个读操作之前,都会加入一个load内存屏障命令,会强制从主内存中将此变量的最新值加载从主内存加载到工作内存中;
7.3.2 变量未被volatile关键字修饰
  1. 普通变量不能保证可见性,因为普通共享变量被修改后,写入到工作内存中,什么时候写回主内存是不可知的,当其他线程去读取时,此时无论工作内存还是主内存,还是原来的值,因此无法保证可见性;
7.3.3 volatile关键字如何保证有序性原理
  • 通过内存屏障来解决多线程下的有序性问题
  • 编译器在生成字节码文件是,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序;
    1. 在每个volatile写操作的前面插入一个StoreStore屏障 ;
    2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
    3. 在每个volatile读操作的前面插入一个LoadLoad屏障 ;
    4. 在每个volatile读操作的后面插入一个LoadStore屏障;
  • 写操作之前后插入内存屏障后生成指令序列的示意图;
    在这里插入图片描述
  • 读操作之后插入内存屏障后生成指令序列的示意图

在这里插入图片描述

7.4 volatile应用场景

  1. Boolean的共享状态标志位;
  2. 单例模式下双重检验锁;
  3. 注意
    • volatile对于基本数据类型才用;
    • 对于对象来说,是没有作用的,volatile只能保证对象引用的可见性,对于对象内部的字段修改,他无法保证可见;

八、Synchronized关键字

  • Synchronized关键字,也提供了线程同步的方式;

8.1 Synchronized的使用

  • Synchronized关键字可以修饰方法或者是同步代码块,确保在多个线程中的同一个时刻,只能有一个线程处于方法或者同步代码块中,它保证了线程对变量访问的可见性和排他性;
  1. Synchronized加在普通方法上:
  2. Synchronized加在普通方法上,锁锁的是当前对象实例
    //修饰普通方法
    public synchronized void  test1(){
        //sometiong
    }
  1. Synchronized加在静态方法上;
  2. Synchronized加在静态方法上,锁住的是当前的class实例,因为class数据放在方法区中,相当于该类的全局锁;
    //修饰静态方法
    public synchronized static void  test2(){
        //sometiong
    }
  1. Synchronized加在代码块上;
  2. Synchronized如果锁的是o实例,锁住的是代码块,该中加锁粒度会更小一些;
    //加在代码块上
    public  void test3() {
        Object o = new Object();
        synchronized (o) {
            //业务逻辑
        }
    }

8.2 Synchronize的特点

  • Synchronized修饰的方法或者代码块,在同一时刻JVM只允许一个线程进入执行。Synchronized通过锁机制达到只允许一个线程在统一时刻进入执行的效果;
  • 在并发编程当中,Synchronized可以做到线程并发的原子性、可见性、有序性;

8.3 Synchronized的原理

  • Synchronized的是如何做到线程安全的呢?研究其修饰的方法或者是代码块;
  • 通过javac将java的源代码编译成class字节码文件;
  • 通过javap 可以查看字节码文件;
  • 不管是同步代码块还是同步方法,在底层实现上同步代码块使用到monitorenter和monitorexit指令,同步方法是通过修饰符ACC_SYNCHRONIZED来完成的。
  • 无论哪一种形式,本质上是获取一个对象的监视器(monitor)进行获取,而获取监视器是一个排他的过程。也就是说同一时刻只能有一个线程来获取到Synchronized所保护的对象的监视器;
    在这里插入图片描述1. Synchronized允许任何的一个对象作为同步的内存够,任意的一个对象有拥有自己的监视器,当对象有同步块或者同步方法调用时,执行的方法的线程必须先获取到该对象的监视器才能进入同步块或同步方法,而没有获取到监视器的线程将会被阻塞在同步代码块或者是同步方法中,进入到则塞状态;
  1. 关于monitor对象的立即;
  2. monitor对象的实现细节上,记录在对象的头部mark Word区域,关于对象的内存区域细节下
    在这里插入图片描述
  3. 在对象的对象头中,可以看到,当是Synchronized修饰时,早期就直接是重量级锁,重量级锁会有一个指针指向Monitor对象;
  • Monitor类的构成。
    1. Monitor是基于C++的ObjectMonitor实现的,主要的成员;
    2. owner:执行持有objectMonitor对象的线程;
    3. WaitSet:存放处于wait状态的线程队列,即调用wait方法的线程;
    4. count:是waitset和entrylist中的节点数之和;
    5. EntrySet:存放处于等待锁block状态的线程队列;
  • Synchronize的早期版本是一个重量级锁,其加锁释放锁需要更底层的操作系统来支持,这个会对系统西能有比较大影响,在后续版本做了优化,提供了轻量级锁,偏向锁等锁优化方案;

九、线程间通信

9.1 通信方法

  1. wait():调用一个对象的wait方法,会导致当前持有该对象锁的线程等待,直到该对象的另一个持有锁线程调用notify或者notifyAll方法进行唤醒;
  2. notify():调用一个对象的notify方法,会导致当前持有该对象锁的所有线程中的某一个线程被唤醒;
  3. notifyAll();调用一个对象的notifyAll方法,会导致当前持有该对象锁的所有线程同时被唤醒;
public class Wait213Demo extends Thread {
    //线程间通信共享对象
    private Object object;
    //构造函数,在创建实例时将共享对象传递进来
    public Wait213Demo(Object obj){
        this.object = obj;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName()+"线程启动啦");
        //线程等待
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName()+"线程等待中...");
                //当前的线程会等待,直到其他线程调用notify或者notifyAll方法才会继续执行
                object.wait();
                System.out.println(Thread.currentThread().getName()+"线程被唤醒...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"线程结束啦");
    }
}
public class Notify213Demo extends Thread {
    //线程间通信共享对象
    private Object object;
    //构造函数,在创建实例时将共享对象传递进来
    public Notify213Demo(Object obj){
        this.object = obj;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName()+"线程启动啦");
        //线程等待
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName()+"线程睡眠中...");
                Thread.sleep(5000);//睡眠5秒
                //线程发起通知
                object.notify();
                System.out.println(Thread.currentThread().getName()+"线程发起通知...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"线程结束啦");
    }
}

9.2 线程间通信使用注意点

  1. 调用notify和wait方法必须是作用于同一个对象(如果不是同一个对象,发送的消息是无法接受);
  2. 对于wait、notify、notifyAll的调用, 必须是在该对象的同步方法或者是同步代码块中,锁作用的对象和wait等方法作用的是同一个对象;
  3. wait方法在调用进入阻塞之前会释放锁,而sleep和join不会释放锁;
  4. 线程状态转换是:当wait线程被唤醒或者超时或者被中断,并不是直接进入到运行或者是就绪状态而是先进入到Block状态,抢锁成功后,才能进入到课运行状态;
    在这里插入图片描述

9.3 等待池和锁池

  1. 等待池:假设线程调用了对象的wait方法,线程会释放锁之后,进入到该对象的等待池;
  2. 锁池:锁池是抢锁失败的线程进入锁池,等待持有锁的线程释放锁之后,处于锁池中的线程才能来抢锁锁;

9.4 notify和notifyAll的区别

  1. notify唤醒的是对象等待池中的一个线程,这个线程会进入到blocked状态,从等待池进入锁池,等其他持有该对象锁的线程释放锁之后才能抢锁锁;
  2. notifyAll唤醒对象等待池中所有的线程,这些线程进入到blocked状态,当前对象所有处于等待池中的线程全部进入到锁池,竞争锁资源;

9.5 生产者消费者问题

  1. 生产者消费者问题,也称作有界缓冲区问题。两个线程共享一个固定大小的缓冲区;
  2. 其中一个是生产者、用于将数据放入缓冲区,另一个是消费者,用户从缓冲区取出数据;
  3. 问题出现在当缓冲区已经满了,此时生产者不能继续往缓冲区放入数据,解决方法让生产者进入休眠等待,等待消费者从缓冲区取走至少一个数据再去唤醒生产者;
  4. 当缓冲区空了,消费者就无法从缓冲区获取数据,就需要休眠等待,等待生产者至少生产一个产品消费者才能继续进行消费;
  5. 说明
    • 如果缓冲区满了,则生产者线程阻塞;
    • 如果缓冲区空了,消费者线程则塞,
    • 生产者唤醒需要消费者通知,同理,消费者线程阻塞需要生产者唤醒;
  6. 生产者、消费者模式代码实现
  7. 一个生产者、一个消费者、有界容量为3的队列
    生产者模型:
/**
 * 生产者线程
 */
public class Produce extends Thread {
    private List <Integer> cache;
    private Random random = new Random();

    public Produce(List <Integer> cache) {
        this.cache = cache;
    }

    @Override
    public void run() {
        while (true) {
            //有界队列访问是线程共享的需要加锁
            synchronized (cache) {

                try {
                    Thread.sleep(random.nextInt(1000));

                    //判断是否可以生产
                    while (cache.size() >= 3) {
                        //队列已满,需要阻塞,等待消费者通知
                        System.out.println("队列满了,等待消费");
                        cache.wait();
                    }


                    //生产数据
                    int value = random.nextInt(100) + 1;//数据范围[1,100]
                    cache.add(value);

                    System.out.println("生产者生产:"+value);

                    //通知消费者进行消费
                    cache.notifyAll();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
  • 消费者模型
/**
 * 消费者
 */
public class Comsumer extends Thread {
    private List <Integer> cache;

    public Comsumer(List <Integer> cache) {
        this.cache = cache;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (cache) {

                try {
                    //判断是否可以消费
                    while (cache.size() == 0) {
                        //队列为空,不能消费,等待生产者生产数据才能消费
                        System.out.println("队列空了,等待生产");
                        cache.wait();
                    }

                    //消费者消费
                    Integer value = cache.remove(0);
                    System.out.println("消费者消费数据:"+ value);


                    //通知生产者生产数据
                    cache.notifyAll();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
抱歉,我无法通过文字回答您所提供的多线程知识点流程图。然而,我可以为您提供一个简单的文字描述来帮助您理解Java多线程的核心概念和流程: 1. 首先,创建一个线程类。这可以通过继承Thread类或实现Runnable接口来实现。 2. 在线程类中,重写run()方法。这个方法包含了线程要执行的代码逻辑。 3. 创建线程的实例。如果使用继承Thread类的方式,直接实例化线程类即可。如果使用实现Runnable接口的方式,需要创建一个Thread对象,并将实现了Runnable接口的类的实例作为参数传递给Thread的构造方法。 4. 调用start()方法启动线程。start()方法会启动一个新的线程,并在新线程中执行run()方法中的代码。 5. 当线程开始执行时,它会进入就绪状态,等待操作系统分配CPU资源。 6. 一旦获得CPU资源,线程就会进入运行状态,并开始执行run()方法中的代码。 7. 在运行过程中,线程可能会被阻塞,例如等待IO操作完成或等待其他线程的完成。当这些条件满足时,线程将重新进入就绪状态,等待再次获得CPU资源。 8. 当线程执行完run()方法中的代码后,它将进入终止状态,表示线程的生命周期已经结束。 请注意,这只是一个简单的概述,涉及到更多的细节和概念,例如线程同步、互斥、等待和通知等。如果您需要更详细的信息,请提出您想要了解的具体问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在炮火中前进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值