Java学习笔记(一):多线程编程与volatile、synchronized关键字

最近在学习李刚老师的 Java 疯狂讲义,以下是我的学习笔记。
部分代码片段有修改。

文章目录

  • § 1. 进程与线程
    • 1.1 进程的概念
    • 1.2 线程的概念
    • 1.3 创建线程的三种方法
      • 1.3.1继承 Thread类
      • 1.3.2 实现 Runable接口
      • 1.3.3 使用 Callable和 Future创建
      • 1.3.4 三种创建线程方法的优缺点
  • § volatile关键字
  • synchronized关键字
  • 学习资料

§ 1. 进程与线程

1.1 进程的概念

进程: 当一个程序进入内存运行时,就会变成一个进程,进程是处于运行过程中的程序,是系统进行资源分配和调度的一个独立单位。
特点:

  • 独立性:系统中独立存在的实体,一个用户进程不能访问其他进程的地址空间。
  • 动态性:进程具有自己的生命周期和各种不同的状态,是一个再系统中活动的指令集合。而程序则只是静态的指令集合。
  • 并发性: 多个进程可以在单个处理器上并发执行,多个进程不会互相影响。

并发(cocurrency)与并行(parallel)的区别:
并发concurrency: 同一时刻只能有一条指令执行,多个进程指令会快速轮换执行,使宏观上具有多个进程同时执行的效果。
并行 parallel*: 同一时刻,有多条指令在处理器上同时执行。

1.2 线程的概念

线程: 线程是进程的执行单元,当进程被初始化后,主线程就被创建。一个进程可以拥有多个线程,他们共享父进程里的全部资源,也可以相互之间通信。
特点:

  • 效率高:系统创建进程时需要为该进程重新分配独立的内存空间;Java内置多线程支持,多个线程共享内存,创建多线程则代价会小很多。

1.3 创建线程的三种方法

1.3.1继承 Thread类

步骤:

  1. 定义 Thread类的子类,并重写该类的 run()方法,方法体的内容代表线程将要完成的任务,因此把 run方法称为线程执行体;
  2. 创建 Thread 子类的实例, 即创建线程对象;
  3. 调用线程对象的 start()方法启动线程。

使用继承 Thread类的方法创建线程类的时候,多个线程之间无法共享线程类的实例变量。

示例代码:

public class FirstThread extends Thread {
		
  public void run() {
    //Thread 对象的 getName()直接返回当前线程的名字
    System.out.println(getName());
  }
  public static void main(String[] args) {
    new FirstThread().start();
    new FirstThread().start();
  }
}

输出:
Java学习笔记(一):多线程编程与volatile、synchronized关键字_第1张图片

1.3.2 实现 Runable接口

步骤:

  1. 定义 Runnable接口的实现类,并重写该接口的 run()方法;
  2. 创建 Runnable实现类的实例,以此实例作为一个新 Thread的 target再创建一个 Thread 对象,新 Thread对象才是线程对象;
  3. 调用线程对象的 start()方法启动线程。

Runnable 对象仅仅作为 Thread对象的 target, 该对象实现的 run()方法仅作为线程执行体,而实际的线程对象仍然时 Thread 对象,只是该 Threadd对象负责执行 target的 run()方法。

示例代码

public class SecondThread implements Runnable {

    public void run() {
        // 线程类实现 Runnable接口时想要获取当前线程只能使用 currentThread().getName()
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        SecondThread st = new SecondThread();
        new Thread(st, " 新线程 1").start();
        new Thread(st, " 新线程 2").start();
    }   
}

输出:
Java学习笔记(一):多线程编程与volatile、synchronized关键字_第2张图片

1.3.3 使用 Callable和 Future创建

步骤:

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call方法作为线程的执行体,并且该方法有返回值,再创建 Callable类的实例。
  2. 使用 FutureTask 来包装 Callable 对象,该 FutureTask 对象封装了该 Callable对象的 call方法返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值。

实现 Callable 接口和实现 Runnable 接口并没有太大差别,只是 Callable的 call() 方法允许抛出异常,并且允许带返回值。

示例代码:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThirdThread implements Callable<Integer> {

    public Integer call() { 
        System.out.println(Thread.currentThread().getName());
        return 1;
    }
    
    public static void main (String[] args) {

        //创建 Callable对象
        ThirdThread tt = new ThirdThread();
        //使用 futureTask对象包装 Callable对象 
        FutureTask<Integer> task = new FutureTask<>(tt);
        //以FutureTask对象为 target创建Thread并启动线程
        new Thread(task, "有返回值的线程启动!").start();

        System.out.println("当前线程名称:"+Thread.currentThread().getName());

        try {
            //获取线程返回值
            System.out.println("子线程的返回值:" + task.get());

        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }
}

输出:
Java学习笔记(一):多线程编程与volatile、synchronized关键字_第3张图片

1.3.4 三种创建线程方法的优缺点

  1. 线程类实现了Runnable、Callable,还能继承其他类;线程类已经继承了其他 Thread 类,所以不能继承其他类。
  2. Runnable,Callable 多个线程共享一个 target, 非常适合多个相同线程处理同一份资源的情况。

§ volatile关键字

volatile字面意思为易变的/不稳定的,事实上也正是如此,这个关键字的作用就是告诉编译器,只要是被此关键字修饰的变量都是易变的,不稳定的。

主要是volatile所修饰的变量是直接存在于主内存中,线程对变量的操作也是直接反映在主内存中,所以说其是易变的。

Java的内存模型(Java Memory Model,JMM)中的内存分为主内存和工作内存,其中主内存是所有线程共享的,而工作内存是每个线程独立分配的,各个线程的工作内存之间相互独立、互不可见。在线程启动的时候,虚拟机为每个内存分配了一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要的共享变量的副本。

对于volatile修饰的变量来说,在工作内存发生了变化后,必须要马上写到主内存中,而线程读取到是volatile修饰的变量时,必须去主内存中去获取最新的值,而不是读工作内存中主内存的副本,这样就保证了线程之间的变量的可见性。

synchronized关键字

字面意思同步。

用法: synchronized修饰方法和synchronized修饰代码块,保证同一时刻最多只有一个线程执行该段代码。
场景:

  1. 当两个线程访问同一个对象中的这个synchronized(this)同步代码块时,一个时间内只有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块才能执行该代码块。
  2. 一个线程访问对象中的synchronized同步代码时,另一个线程仍可以访问该object中非synchronized同步代码块。
  3. 当一个线程访问object的一个synchronized同步代码块时,其他线程对object中所有其他的synchronized同步代码块的访问将被阻塞。

即,当一个线程访问object的一个synchronized同步代码块,它就获得了这个object的对象锁。其他线程对该object对象所有同步代码部分的访问都被暂时阻塞。

学习资料

《疯狂 Java讲义(第 5 版)》李刚 著
浅谈volatile关键字
java synchronized详解

你可能感兴趣的:(Java,java,jvm,面试)