JUC-01-简介

JUC-01-简介

1. JUC 简介

  • JUC即java.util.concurrent(java的工具包)

mark

JUC常用的几个类:

  • lock接口

mark

  • Callable接口

mark

1.1 线程和进程

进程:程序

  • 一个进程可以包含多个线程,至少包含一个线程
  • java默认有两个线程(2个 -> GC和main线程)

线程:Java是无法开启线程的,只能通过native去调用

1.2 并发和并行

  • 并发:多线程操作同一个资源(相当于一个CPU)
  • 并行:多个线程可以同时执行 (相当于多个CPU)

并发编程的本质:充分利用CPU的资源

2. 线程的状态

  • 它是一个java.lang.Thread里面的枚举类

源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {
// 线程新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 阻塞等待
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}

mark

3. wait() Sleep()的区别

  1. 来自不同的类
  • wait 来自 Object
  • sleep 来自 Thread
  1. 关于锁的释放
  • wait 会释放锁

  • sleep 不会释放 (抱着锁睡觉)

  1. 使用的范围
  • wait 必须在同步代码中

  • sleep 可以在任何地方使用

java.lang.Thread类原码解析https://blog.csdn.net/pengqiaowolf/article/details/80442071

https://www.cnblogs.com/rouqinglangzi/p/10803194.html

3. Synchronized

卖票案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.zhuuu;

// 线程就是一个单独的资源类,没有任何附属的操作
// 1. 属性,方法


// 基本的卖票例子
public class SaleTicket {
public static void main(String[] args) {
// 并发:多线程操作同一个额资源
// 把资源类丢入线程

// @FunctionalInterface:函数式接口
// lambada表达式(放参数)->{代码}
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"C").start();
}
}


// 资源类
class Ticket{
// 属性,方法
private int number = 50;

// 卖票的方式
// Sychronized本质:队列+锁
public synchronized void sale(){
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余"+number);
}
}
}

3.1 Synchronized 底层原理分析

  • JDK 1.6 之前:采用 monitorenter 和 monitorexit 来进行加锁和解锁(以下是Synchronized 的三种加锁方式)
    • 实例方法 : 锁的是当前实例对象
    • 同步方法: 锁的是class对象
    • 代码块 : 锁的是代码块Synchronized () 括号中的对象。

注意:通过javap -v来查看对应代码的字节码指令,对于同步块实现同时使用了monitorentermonitorexit指令,这样就隐式的执行了 Lock 和 unlock 的操作,用于保证原子性。

monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置

jvm 保证每一个monitorenter和都有一个对应的 monitorexit

mark

  • 线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;
  • 而执行 monitorexit就是释放monitor的所有权。

同时 那什么是monitor呢?

1.Monitor是一种用来实现同步的工具

2.与每个java对象相关联,所有的 Java 对象是天生携带 monitor

3.Monitor是实现Sychronized(内置锁)的基础

对象的监视器(monitor)由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:

1
2
3
4
5
6
7
8
9
ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
  • JDK 1.6 之后: 引入锁升级的概念(将锁放在了java对象头中,分别用00,01,10,11 代表锁的四个状态)(这里只是略带提一嘴,后面会进行详细说明)
    • 无锁状态 :
    • 偏向锁状态 :01
    • 轻量级锁状态 : 00
    • 重量级锁状态 : 10

mark

    • Mark Word(对象标记)用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
    • Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节, 也就是32bit)

3.2 锁升级的过程

mark

(上图标志位是我从别的博客找来的,图是有问题的,具体问题我会在下面解释,一边深入理解,一边带您发现图中问题在哪里。)

首先我们来了解相关锁的概念:

  • 自旋锁(CAS) : 让不满足条件的线程等待一会看看能不能获取到锁,通过占用CPU的时间来避免线程带来的上下文切换。

    • 自旋锁等待时间和次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,那么这个线程将被挂起(被阻塞)。
    • JDK 1.6 之后引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一锁上自旋的时间以及锁的拥有者状态来决定的。
    • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这个自旋很有可能再次成功,进而它将允许自旋等待持续相对更长的时间
    • 如果对于某个锁,自旋很少成功获得过锁,那么在以后尝试获得这个锁的时候可能省略掉自旋的过程,直接阻塞线程,避免处理器浪费资源。
  • 偏向锁 : 大多数情况下,锁总是由同一个线程多次获得。当一个线程访问同步块并后去锁的时候,会在对象头和栈帧中记录存储偏向锁的线程ID

    • 偏向锁是一个可重入锁。如果锁对象头的Mark Word 里面存储着当前线程ID的偏向锁,那么就不需要重新进行CAS操作来加锁和解锁。
    • 当有其他线程尝试竞争偏向锁的时候,持有偏向锁的线程(不处于活动状态)才会释放锁。
    • 偏向锁无法使用自旋锁进行优化,因为一旦有其他线程竞争锁,就破坏了偏向锁转而升级成轻量级锁。
  • 轻量级锁 : 减少无实际竞争的情况下,使用重量级锁带来的性能消耗。

    • JVM会将现在当前线程的栈帧创建用于存储锁记录的空间LockRecord,
    • 将对象头中的Mark word 复制到 lockRecord 中 并将lockRecord 中的 Owner指向锁对象。
    • 然后线程会尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,成功则当前线程获取到锁,如果失败,表示其他线程竞争锁。当前线程使用自旋的方式获取锁,自旋获取锁失败就会升级成重量级锁。
  • 重量级锁 : 通过对象内部监视器 (monitor) 实现,其中 monitor的本质是依赖于底层操作系统Mutex Lock实现。

    • 操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本十分的高。
    • 线程竞争不使用自旋,不会消耗CPU,但是线程会立马进入阻塞状态并等待被其他线程唤醒。

3.3 完整的锁升级过程

mark

3.4 结合Object 中 wait,notify,notifyall 来认识Synchronized

  • 前面我们在讲 synchronized 的时候,发现被阻塞的线程什 么时候被唤醒,取决于获得锁的线程什么时候执行完同步代码块并且释放锁
    • 那么怎么做到显示的控制呢? 我们就需要借助一个信号机制
    • 在Object类中 提供了 wait/nofify/notifyall 来控制线程的状态。
  1. wait()
  • 表示持有对象锁的线程A准备释放对象锁的权限,释放cpu资源并进入等待状态
  1. notify()
  • 表示持有对象锁的线程A准备释放对象锁的权限,通知 jvm 唤醒某个等待中竞争该锁的线程X.
  • 线程A 执行完 synchronized 代码块并释放了锁之后,线程X直接获取对象锁的权限,其他竞争线程继续等待(即使X同步执行完毕,释放了对象锁,其他竞争线程仍然等待,直到有新的notify和 notifyall被调用)
  1. notifyAll()
  • notifyAll 和 notify 的区别是
    • notifyall 会唤醒所有竞争等待同一个对象锁的所有线程
    • 当获取锁的线程A被释放了之后,所有被唤醒的线程都可以获得对象的锁权限。

注意:

  • 三个方法都必须在synchronized 同步关键字所限定的作用域中被调用,否则会抛出 java.lang.illegalMonitorStateException
  • 意思就是没有同步,所以线程对对象锁的状态是不确定的,不能调用这些方法。
  • 另外,同步机制确保了线程从wait方法返回时能够感知到notify线程对变量做出的修改。

这里有一个常见面试题

  • 请你说说 wait/notify/notifyall 为什么需要在synchronized里面?
  1. wait方法的语义有两个,一个是释放当前的对象锁,另一个是使得当前线程进入阻塞队列,而这些都和监视器相关,所以wait方法必须要获得一个监视器锁。
  2. 对于notify来说也一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以必须要找到这个对象并且拿到这个对象获取的锁,然后到这个对象等待队列中去唤醒另外一个线程
  3. 每个对象可能有多个线程调用wait方法,所以需要有要给等待队列存贮这些阻塞线程。这个等待队列应该和这些对象绑定,在调用wait 和 notify 方法时也会存在线程安全问题,所以需要一个锁来保证线程安全。

mark

4. Lock 接口

  • java.util.concurrent.locks 包下

mark

  • 可重入锁:(ReebtrantLock)

  • 读锁 : (ReentrantReadWtiteLock.ReadLock)

  • 写锁: (ReentrantReadWtiteLock.WriteLock)

mark

  • 公平锁 :先来后到
  • 非公平锁 : 可以插队(默认)

lock锁实现公平卖票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.zhuuu;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SaleTicketLock {
public static void main(String[] args) {
// 并发:多线程操作同一个额资源
// 把资源类丢入线程

// @FunctionalInterface:函数式接口
// lambada表达式(放参数)->{代码}
Ticket2 ticket2 = new Ticket2();

new Thread(()->{ for (int i = 0; i < 60; i++) { ticket2.sale(); } },"A").start();
new Thread(()->{ for (int i = 0; i < 60; i++) { ticket2.sale(); } },"B").start();
new Thread(()->{ for (int i = 0; i < 60; i++) { ticket2.sale(); } },"C").start();
}
}

// 资源类
// Lock锁
class Ticket2{
// 属性,方法
private int number = 50;

Lock lock = new ReentrantLock();

// 卖票的方式
public void sale(){

lock.lock(); // 加锁的操作

try {
// 业务代码
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余"+number);
}
} finally {
lock.unlock(); // 解锁
}
}
}

5. Sychronized 和 Lock区别

  • Synchronized是一个内置的关键字,lock是一个java类

  • Synchronized无法判断获取锁的状态,lock可以判断是否获取到了锁

  • Synchronized会自动释放锁,lock必须手动释放锁,如果不释放锁会造成死锁

  • Synchronized 线程1(获得锁,阻塞) 线程2 (一直等待) 而lock锁有(try-lock) 不会一直等待下去

  • Synchronized 可重入锁,不可以中断的,非公平

  • Lock可重入锁,可以中断,可以公平也可以公平

  • Synchronized适合锁方法和代码块,lock更适合所大量的代码块

6. start()和 run()方法的区别

  • 用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

private native void start0();

注意:

  • start()方法才是真是开启了一个新线程
  • Start()是启动一个新的线程,然后新的线程会调用run()方法,但是start()方法不可以重复调用,若会出现异常Exception in Thread “main” java.lang.IllegalThreadStateException.而且启动线程,会出现异步的效果,即线程创建和启动是随机的
  • run()方法类似一个一个普通方法,如果单独调用,仅仅会在当前线程启用,不会重新启动新的线程。启动线程是同步的。
  • 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

    Thread 的子类应该重写该方法。

    run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}

注意:

  • run()方法只是重写了Runnable接口的一个普通方法而已,不会开启一个新的线程,只会在main线程中执行
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信