多线程-初阶1

1. 认识线程(Thread)
1.1 概念
1) 线程是什么
一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行
着多份代码.
还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的
业务。我们进一步设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找
来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,
自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别
排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2) 为啥要有线程
首先, "并发编程" 成为 "刚需".
单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU
资源.
有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编
程.
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)

3) 进程和线程的区别
进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别
人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是
不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多
进程的最大区别。
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
4) Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使
用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

1.2 第一个多线程程序
感受多线程程序和普通程序的区别:
每个线程都是一个独立的执行流
多个线程之间是 "并发" 执行的.
import java.util.Random;
public class ThreadDemo {
  private static class MyThread extends Thread {
    @Override
    public void run() {
      Random random = new Random();
      while (true) {
        // 打印线程名称
        System.out.println(Thread.currentThread().getName());
        try {
          // 随机停止运行 0-9 秒

         Thread.sleep(random.nextInt(10));
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
   }
 }
  public static void main(String[] args) {
    MyThread t1 = new MyThread();
    MyThread t2 = new MyThread();
    MyThread t3 = new MyThread();
    t1.start();
    t2.start();
    t3.start();
    Random random = new Random();
    while (true) {
      // 打印线程名称
      System.out.println(Thread.currentThread().getName());
      try {
        Thread.sleep(random.nextInt(10));
     } catch (InterruptedException e) {
        // 随机停止运行 0-9 秒
        e.printStackTrace();
     }
   }
 }
}
Thread-0
Thread-0
Thread-2
Thread-1
Thread-2
Thread-1
Thread-0
Thread-2
main
main
Thread-2
Thread-1
Thread-0
Thread-1
main
Thread-2
Thread-2

1.3 创建线程
方法1 继承 Thread 类
1) 继承 Thread 来创建一个线程类.
2) 创建 MyThread 类的实例
3) 调用 start 方法启动线程
方法2 实现 Runnable 接口
1) 实现 Runnable 接口
class MyThread extends Thread {
  @Override
  public void run() {
    System.out.println("这里是线程运行的代码");
 }
}
MyThread t = new MyThread();
t.start(); // 线程开始运行

class MyRunnable implements Runnable {
  @Override
  public void run() {
    System.out.println("这里是线程运行的代码");
 }
}

2. Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关
联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象
就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况,下面我们会进一步说明
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了
线程的中断问题,下面我们进一步说明
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
public class ThreadDemo {
  public static void main(String[] args) {
    Thread thread = new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        try {
          System.out.println(Thread.currentThread().getName() + ": 我还
活着");
          Thread.sleep(1 * 1000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
      System.out.println(Thread.currentThread().getName() + ": 我即将死去");
   });
    System.out.println(Thread.currentThread().getName()
             + ": ID: " + thread.getId());
    System.out.println(Thread.currentThread().getName()
             + ": 名称: " + thread.getName());
    System.out.println(Thread.currentThread().getName()
             + ": 状态: " + thread.getState());
    System.out.println(Thread.currentThread().getName()
             + ": 优先级: " + thread.getPriority());

System.out.println(Thread.currentThread().getName()
             + ": 后台线程: " + thread.isDaemon());
    System.out.println(Thread.currentThread().getName()
             + ": 活着: " + thread.isAlive());
    System.out.println(Thread.currentThread().getName()
             + ": 被中断: " + thread.isInterrupted());
    thread.start();
    while (thread.isAlive()) {}
    System.out.println(Thread.currentThread().getName()
             + ": 状态: " + thread.getState());
 }
}

2.3 启动一个线程-start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程
就开始运行了。
覆写 run 方法是提供给线程要做的事情的指令清单
线程对象可以认为是把 李四、王五叫过来了
而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了

2.4 中断一个线程
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们
需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如
何通知李四停止呢?这就涉及到我们的停止线程的方式了。

目前常见的有以下两种方式:
1. 通过共享的标记来进行沟通
2. 调用 interrupt() 方法来通知
示例-1: 使用自定义的变量来作为标志位.
需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍).
示例-2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定
义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    public volatile boolean isQuit = false;
    @Override
    public void run() {
      while (!isQuit) {
        System.out.println(Thread.currentThread().getName()
            + ": 别管我,我忙着转账呢!");
        try {
          Thread.sleep(1000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
      System.out.println(Thread.currentThread().getName()
          + ": 啊!险些误了大事");
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    System.out.println(Thread.currentThread().getName()
        + ": 让李四开始转账。");
    thread.start();
    Thread.sleep(10 * 1000);
    System.out.println(Thread.currentThread().getName()
        + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
    target.isQuit = true;
 }
}public void interrupt()
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,
否则设置标志位
public static boolean
interrupted()
判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean
isInterrupted()
判断对象关联的线程的标志位是否设置,调用后不清除标志位
使用 thread 对象的 interrupted() 方法通知线程结束.
thread 收到通知的方式有两种:
1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
忽略这个异常, 也可以跳出循环结束线程.
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      // 两种方法均可以
      while (!Thread.interrupted()) {
      //while (!Thread.currentThread().isInterrupted()) {
        System.out.println(Thread.currentThread().getName()
            + ": 别管我,我忙着转账呢!");
        try {
          Thread.sleep(1000);
       } catch (InterruptedException e) {
          e.printStackTrace();
          System.out.println(Thread.currentThread().getName()
              + ": 有内鬼,终止交易!");
          // 注意此处的 break
          break;
       }
     }
      System.out.println(Thread.currentThread().getName()
          + ": 啊!险些误了大事");
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    System.out.println(Thread.currentThread().getName()
        + ": 让李四开始转账。");
    thread.start();
    Thread.sleep(10 * 1000);
    System.out.println(Thread.currentThread().getName()
        + ": 老板来电话了,得赶紧通知李四对方是个骗子!");
    thread.interrupt();
 }
}
比特就业课
2. 否则,只是内部的一个中断标志被设置,thread 可以通过
Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
示例-3 观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为
"不清除标志位".
使用 Thread.isInterrupted() , 线程中断会清除标志位.
使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        System.out.println(Thread.interrupted());
     }
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
public class ThreadDemo {
  private static class MyRunnable implements Runnable {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
     }

2.5 等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转
账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
   }
 }
  public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "李四");
    thread.start();
    thread.interrupt();
 }
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true
public class ThreadDemo {
  public static void main(String[] args) throws InterruptedException {
    Runnable target = () -> {
      for (int i = 0; i < 10; i++) {
        try {
          System.out.println(Thread.currentThread().getName()
                   + ": 我还在工作!");
          Thread.sleep(1000);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
      System.out.println(Thread.currentThread().getName() + ": 我结束了!");
   };
    Thread thread1 = new Thread(target, "李四");
    Thread thread2 = new Thread(target, "王五");
    System.out.println("先让李四开始工作");
    thread1.start();
    thread1.join();
    System.out.println("李四工作结束了,让王五开始工作");
    thread2.start();
    thread2.join();
    System.out.println("王五工作结束了");
 }
}

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 同理,但可以更高精度
方法 说明
public static Thread currentThread(); 返回当前线程对象的引用
方法 说明
public static void sleep(long millis) throws InterruptedException
休眠当前线程 millis
毫秒
public static void sleep(long millis, int nanos) throws
InterruptedException
可以更高精度的休眠
大家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?
附录
关于 join 还有一些细节内容,我们留到下面再讲解。
2.6 获取当前线程引用
这个方法我们以及非常熟悉了
2.7 休眠当前线程
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实
际休眠时间是大于等于参数设置的休眠时间的。
关于 sleep,以后我们还会有一些知识会给大家补充。
public class ThreadDemo {
  public static void main(String[] args) {
    Thread thread = Thread.currentThread();
    System.out.println(thread.getName());
 }
}
public class ThreadDemo {
  public static void main(String[] args) throws InterruptedException {
    System.out.println(System.currentTimeMillis());
    Thread.sleep(3 * 1000);
    System.out.println(System.currentTimeMillis());
 }
}

3. 线程的状态
3.1 观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了.
3.2 线程状态和状态转移的意义
大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
public class ThreadState {
  public static void main(String[] args) {
    for (Thread.State state : Thread.State.values()) {
      System.out.println(state);
   }
 }
}
比特就业课
还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工
作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入
BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着
的。
3.3 观察线程的状态和转移
比特就业课
观察 1: 关注 NEW 、 RUNNABLE 、 TERMINATED 状态的转换
使用 isAlive 方法判定线程的存活状态.
观察 2: 关注 WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换
public class ThreadStateTransfer {
  public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
      for (int i = 0; i < 1000_0000; i++) {
     }
   }, "李四");
    System.out.println(t.getName() + ": " + t.getState());;
    t.start();
    while (t.isAlive()) {
      System.out.println(t.getName() + ": " + t.getState());;
   }
    System.out.println(t.getName() + ": " + t.getState());;
 }
}
public static void main(String[] args) {
  final Object object = new Object();
  Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
      synchronized (object) {
        while (true) {
          try {
            Thread.sleep(1000);
         } catch (InterruptedException e) {
            e.printStackTrace();
比特就业课
使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
修改上面的代码, 把 t1 中的 sleep 换成 wait
使用 jconsole 可以看到 t1 的状态是 WAITING
结论:
BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
观察-3: yield() 大公无私,让出 CPU
         }
       }
     }
   }
 }, "t1");
  t1.start();
  Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
      synchronized (object) {
        System.out.println("hehe");
     }
   }
 }, "t2");
  t2.start();
}
public static void main(String[] args) {
  final Object object = new Object();
  Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
      synchronized (object) {
        try {
          // [修改这里就可以了!!!!!]
          // Thread.sleep(1000);
          object.wait();
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
     }
   }
 }, "t1");
...
}
Thread t1 = new Thread(new Runnable() {
  @Override
  public void run() {

可以看到:
1. 不使用 yield 的时候, 张三李四大概五五开
2. 使用 yield 时, 张三的数量远远少于李四
结论:
yield 不改变线程的状态, 但是会重新去排队.
4. 多线程带来的的风险-线程安全 (重点)
4.1 观察线程不安全
    while (true) {
      System.out.println("张三");
      // 先注释掉, 再放开
      // Thread.yield();
   }
 }
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
  @Override
  public void run() {
    while (true) {
      System.out.println("李四");
   }
 }
}, "t2");
t2.start();
static class Counter {
  public int count = 0;
  void increase() {
    count++;
 }
}
public static void main(String[] args) throws InterruptedException {
  final Counter counter = new Counter();
  Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
      counter.increase();
   }
 });
  Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
      counter.increase();
   }
 });
  t1.start();
  t2.start();
比特就业课
大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢?
4.2 线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线
程安全的。
4.3 线程不安全的原因
修改共享数据
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.
此时这个 counter.count 是一个多个线程都能访问到的 "共享数据"
counter.count 这个变量就是在堆上. 因此可以被多个线程共享访问.
原子性
  t1.join();
  t2.join();
  System.out.println(counter.count);
}

什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入
房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性
的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进
不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 n++,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是
错误的。
这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.
可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并
发效果.
比特就业课
线程之间的共享变量存在 主内存 (Main Memory).
每一个线程都有自己的 "工作内存" (Working Memory) .
当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程
1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.
1) 初始情况下, 两个线程的工作内存内容一致.
2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定
能及时同步.

这个时候代码中就容易出现问题.
此时引入了两个问题:
为啥要整这么多内存?
为啥要这么麻烦的拷来拷去?
1) 为啥整这么多内存?
实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.
所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.
2) 为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也
就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果
只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问
内存了. 效率就大大提高了.
那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??
答案就是一个字: 贵

值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远
远快于硬盘.
对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.
代码顺序性
什么是代码重排序
一段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问
题,可以少跑一次前台。这种叫做指令重排序
编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 但
是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代
码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论
4.4 解决之前的线程不安全问题
这里用到的机制,我们马上会给大家解释。
static class Counter {
  public int count = 0;
  synchronized void increase() {
    count++;
 }
}
public static void main(String[] args) throws InterruptedException {

5. synchronized 关键字-监视器锁monitor lock
5.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
synchronized用的锁是存在Java对象头里的。
  final Counter counter = new Counter();
  Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
      counter.increase();
   }
 });
  Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
      counter.increase();
   }
 });
  t1.start();
  t2.start();
  t1.join();
  t2.join();
  System.out.println(counter.count);
}

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕
所的 "有人/无人").
如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.
如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队
理解 "阻塞等待".
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝
试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的
线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使用操作系统的mutex lock实现的.
2) 刷新内存
synchronized 的工作过程:
比特就业课
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.
3) 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 "把自己锁死"
一个线程没有释放锁, 然后又尝试再次加锁.
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第
二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无
法进行解锁操作. 这时候就会 死锁.
这样的锁称为 不可重入锁.
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.
代码示例
在下面的代码中,
increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前
对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释
放, 相当于连续加两次锁)
这个代码是完全没问题的. 因为 synchronized 是可重入锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
5.2 synchronized 使用示例
synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具
体的对象来使用.
1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
2) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
3) 修饰代码块: 明确指定锁哪个对象.
锁当前对象
锁类对象
static class Counter {
  public int count = 0;
  synchronized void increase() {
    count++;
 }
  synchronized void increase2() {
    increase();
 }
}
public class SynchronizedDemo {
  public synchronized void methond() {
 }
}
public class SynchronizedDemo {
  public synchronized static void method() {
 }
}
public class SynchronizedDemo {
  public void method() {
    synchronized (this) {
     
   }
 }
}

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.
5.3 Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer
StringBuffer 的核心方法都带有 synchronized .
public class SynchronizedDemo {
  public void method() {
    synchronized (SynchronizedDemo.class) {
   }
 }
}
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值