多线程与高并发(一)-- 基础概念

1、什么是线程

推荐一个不错的进程、线程关系连接

Java monitor机制,写的很详细,本文不介绍了https://www.cnblogs.com/qingshan-tang/p/12698705.html

com.lang.Thread类,感兴趣同学自行阅读该源码

1.1 进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程参考资料

1.1.1 进程切换

进程从硬盘读取我们的程序代码,这个时候是比较费时的,CPU不会阻塞在这里等着,而是切换到其他进程,当数据加载完成是,CPU会收到个中断,继续执行这个请求。

对于单核CPU而言,短时间内会执行多少进程,造成并行的错觉,实际可以理解为是并发的操作。

1.1.2 内核态与用户态

进程分为用户进程和内核进程两种。为了安全,用户进程是受限的,它不能随意访问资源、获取资源。所以,由内核进程负责管理和分配资源,它具有最高权限,而用户进程使用被分配的资源。且,操作系统必须能够在有需要的时候能立即切换回内核进程(通过中断),只有这样,操作系统才能有安全感。

1.1.3 用户态向内核态切换

1、发生系统调用时

2、产生异常时

3、外设产生中断时

这里不做过多解释了,感兴趣同学自行学习linux原理。

1.3 线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。百度百科

1.3.1 进程与线程的关系

1、一个进程中可以同时存在多个线程;

2、各个线程之间可以并发执行;

3、各个线程之间可以共享当前线程中的地址空间和文件等资源;

4、线程是调度的基本单位,而进程则是资源拥有的基本单位;

5、当进程只有一个线程时,可以认为进程就等于线程;

1.3.2 线程内存模型

(1) 在看线程内存模型之前,我们首先看下计算机的硬件内存模型。


硬件内存模型

寄存器:寄存器部件,包括通用寄存器、专用寄存器和控制寄存器。通用寄存器又可分定点数和浮点数两类,它们用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。 通用寄存器是中央处理器的重要部件之一。

高速多级缓存:用于解决CPU核心与内存的速度差异问题,CPU核心速度快,内存相比要慢很多,而缓存要比内存速度快。

缓存协议:多级缓存的引入在多核CPU时代导致了缓存不一致的问题,这里需要引入MESI协议缓存解决这一问题,保证缓存中的信息与内存中的信息一致。MESI协议缓存这里不做讲解。

(2)下一步我们看下java内存模型与线程模型的关系
java程序的一次编写到处运行如何体现的?jvm内存模型屏蔽了不同硬件的内存模型。jvm内存模型规定,所有的变量都存储在jvm主内存中,即堆内存,这里指可共享的变量(new出来的实例化对象,数组等)。这里提出主内存与工作内存的概念,每个线程有自己的工作内存,保存自己私有的变量,线程从主内存获取变量的副本作为自己的私有变量,不允许直接操作主内存的变量。线程的工作内存也是独立的,无法操作其他线程的变量。


jvm线程内存关系图

(3)硬件内存模型与jvm内存模型的关系
如下图所示,jvm内存模型和硬件内存模型相似,但并不完全相同。但是jvm的数据大部分会存储到硬件主内存中,部分会存储到寄存器或缓存中,这里只做简单说明。


硬件内存模型与jvm内存模型关系

2、线程实现

2.1 继承Thread类

static class ExtendThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread类方式");
    }
}

public static void main(String[] args) {
    //实例化一个对象
    ExtendThread extendThread = new ExtendThread();
    //start()执行
    extendThread.start();
}

结果:

继承Thread类方式

2.2 实现runnable接口

static class ImplRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口方式");
    }
}

public static void main(String[] args) {
    new Thread(new ImplRunnable()).start();
}

结果:

实现Runnable接口方式

2.3 实现callable接口

static class ImplCallable implements Callable {

    private String str;

    public ImplCallable(String str) {
        this.str = str;
    }

    @Override
    public Object call() throws Exception {
        System.out.println("实现callable接口方式");
        return str;
    }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //创建目标对象
    ImplCallable mc = new ImplCallable("实现callable接口方式");
    //创建执行服务
    ExecutorService ser = Executors.newFixedThreadPool(2);
    //提交执行
    Future<String> result = ser.submit(mc);
    //获取结果
    String msg = result.get();
    //关闭服务
    ser.shutdownNow();
    System.out.println("线程执行结果:" + msg);
}

结果:

实现callable接口方式
线程执行结果:实现callable接口方式

2.4 lambda表达式简化实现

提供一个jdk8的方式, lambda表达式。

public static void main(String[] args) {
    new Thread(() -> {
        System.out.println("lambda表达式简化实现");
    }).start();
}

结果:

lambda表达式简化实现

3、常用方法

3.1 start()

用于启动线程,具体请看以下源码内容

public synchronized void start() {
    //判断线程状态是否为0,默认初始化为0
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    //通知组此线程即将启动,以便可以将其添加到组的线程列表中,修改未启动和已启动数量
    group.add(this);
    //启动状态默认为false
    boolean started = false;
    try {
        //调用native方法,底层jvm开启异步线程,并调用run方法
        start0();
        //线程已启动
        started = true;
    } finally {
        try {
            if (!started) {
                //移除线程组中的该线程,并修改未启动线程数
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
           //将start0方法抛出的异常加入堆栈
        }
    }
}

3.2 run()

通常需要重写该方法,线程需要执行的具体内容。

public void run() {
    if (target != null) {
        //这里执行我们自己重写的run方法
        target.run();
    }
}

3.3 currentThread()

返回当前执行线程的Thread对象

public static native Thread currentThread();

3.4 getName()

获取当前线程名称

public final String getName() {
    return name;
}

3.5 setName()

给线程设置自定义名称,一定要在start方法前执行。

public final synchronized void setName(String name) {
    //健康检查,这不做过多研究
    checkAccess();
    //非空验证
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    //当状态不为0,表示线程已启动,不能设置名字
    if (threadStatus != 0) {
        //调用底层方法,不做研究
        setNativeName(name);
    }
}

3.6 yield()

放弃当前处理器的占用

public static native void yield()

3.7 join()

在当前执行线程a中,另一个线程b调用该方法,则线程a进入阻塞状态,直到线程b执行完毕,线程a继续执行。参照下面示例:

public class Join extends Thread {

public static void main(String[] args) throws InterruptedException {
    Join join = new Join();
    join.setName("我是join线程");
    join.start();
    //调用join方法
    join.join();

    System.out.println("我是main线程");
}

@Override
public void run() {
    for (int i=0; i<3; i++) {
        System.out.println(Thread.currentThread().getName() + "i=" + i) ;
    }
}

}
执行结果:

我是join线程i=0
我是join线程i=1
我是join线程i=2
我是main线程

3.8 sleep()

阻塞当前线程指定的毫秒数

public static native void sleep(long millis) throws InterruptedException;

3.9 isAlive()

判断当前线程是否存活

public final native boolean isAlive();

4、线程状态

Thread类中定义了state枚举,可以通过getState()获取

public enum State {
    /**
     * 初始
     */
    NEW,
    /**
     * 可运行
     */
    RUNNABLE,
    /**
     * 阻塞
     */
    BLOCKED,
    /**
     * 等待
     */
    WAITING,
    /**
     * 超时等待
     */
    TIMED_WAITING,
    /**
     * 终止
     */
    TERMINATED;
}

下图是通过idea集成plantuml插件画的,无法控制布局,请各位忍受,后面的图使用visio。


线程状态流转图

5、线程同步

多个线程同时调用同一个对象,或者修改同一个变量,为了保证安全性与准确性,引入了同步的概念。关于线程同步大概归纳整理以下几种方式。

举个例子:
有一个库存类,原本100个库存,现在启动多线程增加100个库存,在没有线程同步的情况下。

/**
 * 库存
 */
static class Inventory {

    //库存数量
    private int num = 100;

    //增加库存
    public void add(int n) {
        num += n;
        System.out.println("增加库存后的数量=" + num);
    }
}

public static void main(String[] args) {
    //初始化库存
    Inventory inventory = new Inventory();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
}

取最后几条结果,发现库存最终并没有达到预想的200个:

增加库存后的数量=179
增加库存后的数量=177
增加库存后的数量=177
增加库存后的数量=175
增加库存后的数量=174
增加库存后的数量=174
增加库存后的数量=171
增加库存后的数量=169
增加库存后的数量=167

Process finished with exit code 0

5.1 synchronized同步方法

使用同步方法的方式看看能否解决这个问题,在add方法增加synchronized,锁住该方法。

/**
 * 库存
 */
static class Inventory {

    //库存数量
    private int num = 100;

    //增加库存
    public synchronized void add(int n) {
        num += n;
        System.out.println("增加库存后的数量=" + num);
    }
}

public static void main(String[] args) {
    //初始化库存
    Inventory inventory = new Inventory();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
}

查看结果,发现此时结果按照顺序展示,达到预期。

...
增加库存后的数量=192
增加库存后的数量=193
增加库存后的数量=194
增加库存后的数量=195
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200

Process finished with exit code 0

5.2 synchronized同步代码块

使用同步代码块的方式解决该问题,在add方法内部加入同步代码块,如下代码所以

/**
 * 库存
 */
static class Inventory {

    //库存数量
    private int num = 100;

    //增加库存
    public void add(int n) {

        synchronized (this) {
            num += n;
            System.out.println("增加库存后的数量=" + num);
        }
    }
}

public static void main(String[] args) {
    //初始化库存
    Inventory inventory = new Inventory();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
}

取部分结果,能够得到预期结果

...
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200

5.3 volatile关键字

网上很多说通过volatile关键字解决线程同步的问题,但是我想说,这个关键字虽说能够阻止指令重排序,让变量线程间可见,但并不能完全实现线程同步,代码来看下。

/**
 * 库存
 */
static class Inventory {

    //库存数量
    private volatile int num = 100;

    //增加库存
    public void add(int n) {
            num += n;
            System.out.println("增加库存后的数量=" + num);
    }

    //减少库存
    public void sub(int n) {
            num -= n;
            System.out.println("减少库存后的数量=" + num);
        }
}

public static void main(String[] args) {
    Inventory inventory = new Inventory();
    for(int i = 0;i<100;i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
    for(int i = 0;i<100;i++) {
        new Thread(() -> {
            inventory.sub(1);
        }).start();
    }
}

结果,后面会专门针对volatile进行讲解:

...
减少库存后的数量=110
减少库存后的数量=111
减少库存后的数量=105
减少库存后的数量=103
减少库存后的数量=101
减少库存后的数量=100
减少库存后的数量=102

5.4 重入锁

/**
 * 库存
 */
static class Inventory {

    //初始化ReentrantLock实例
    Lock lock = new ReentrantLock();

    //库存数量
    private int num = 100;

    //增加库存
    public void add(int n) {
        //加锁
        lock.lock();
        try {
            num += n;
            System.out.println("增加库存后的数量=" + num);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    //减少库存
    public void sub(int n) {
        //加锁
        lock.lock();
        try {
            num -= n;
            System.out.println("减少库存后的数量=" + num);
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

public static void main(String[] args) {
    Inventory inventory = new Inventory();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.sub(1);
        }).start();
    }
}

5.5 阻塞队列

定义LinkedBlockingQueue阻塞队列,简单实现增加一个,消费一个的线程同步功能

/**
 * 定义一个元素的队列,通过阻塞达到新增一个消费一个的效果
 * 数组元素定义多个时,因消费线程是100个独立线程,执行时间顺序存在差异,不能保证完全顺序执行
 */
final static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(1);

/**
 * 库存
 */
static class Inventory {

    //增加库存
    public void add(int n) {
        try {
            System.out.println("开始增加第" + n + "库存。。。。。");
            //压入队列
            queue.put("第" + n + "库存。。。。。");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //减少库存
    public void sub() {
        try {
            //取出数据
            System.out.println("[取出]====" + queue.take());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

public static void main(String[] args) {
    Inventory inventory = new Inventory();

    new Thread(() -> {
        for (int i = 1; i <= 100; i++) {
            inventory.add(i);
        }
    }).start();

    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.sub();
        }).start();
    }
}

结果:

...
增加库存后的数量=101
取出队列中的数量=101
减少库存后的数量=100
增加库存后的数量=101
取出队列中的数量=101
减少库存后的数量=100
增加库存后的数量=101

5.6 原子类

使用原子类的形式实现线程同步,内部实现使用了CAS(compareAndSwapInt自旋锁)机制。

/**
 * 库存
 */
static class Inventory {

    private AtomicInteger num = new AtomicInteger(100);
    //增加库存
    public void add(int n) {
        //增加库存
        num.incrementAndGet();
        System.out.println("增加库存后的数量=" + num);
    }
}

public static void main(String[] args) {
    Inventory inventory = new Inventory();
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            inventory.add(1);
        }).start();
    }
}

结果:

...
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200

Process finished with exit code 0

5.7 局部变量

使用局部限量形式,模拟两个线程分别处理,相互之间不进行影响。

/**
 * 库存
 */
static class Inventory {

    private ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 100);

    //增加库存
    public void add(int n, String threadName) {
        //增加库存
        num.set(num.get() + n);
        System.out.println("线程:" + threadName + ",增加库存后的数量=" + num.get());
    }
}

public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
        new Thread(() -> {
            Inventory inventory = new Inventory();
            for (int j = 0; j < 10; j++) {
                inventory.add(1,Thread.currentThread().getName());
            }
        }).start();
    }
}

结果:

线程:Thread-0,增加库存后的数量=101
线程:Thread-0,增加库存后的数量=102
线程:Thread-0,增加库存后的数量=103
线程:Thread-0,增加库存后的数量=104
线程:Thread-0,增加库存后的数量=105
线程:Thread-0,增加库存后的数量=106
线程:Thread-0,增加库存后的数量=107
线程:Thread-0,增加库存后的数量=108
线程:Thread-0,增加库存后的数量=109
线程:Thread-0,增加库存后的数量=110
线程:Thread-1,增加库存后的数量=101
线程:Thread-1,增加库存后的数量=102
线程:Thread-1,增加库存后的数量=103
线程:Thread-1,增加库存后的数量=104
线程:Thread-1,增加库存后的数量=105
线程:Thread-1,增加库存后的数量=106
线程:Thread-1,增加库存后的数量=107
线程:Thread-1,增加库存后的数量=108
线程:Thread-1,增加库存后的数量=109
线程:Thread-1,增加库存后的数量=110

6、synchronized锁升级

在jdk1.6之前,synchronized的锁只有一种方式,即重量级锁;在之后引入了偏向锁,轻量级锁来缓解锁竞争问题。从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

6.1 java对象

在讲锁之前,先简单介绍下java对的概念。如下图,java对象分为对象头,对象体,对齐字段。


java对象

6.1.1 对象头

当对象为普通对象时,对象头只包含Mark Word 和Klass Word,当时数组对象时,会包含数组长度。
Mark Word:先看下面一张图,图片来源:https://www.cnblogs.com/ZoHy/p/11313155.html

Mark Word 不同锁下的信息

从上图看到,在Mark Word中,包含以下主要信息:
1.locked:两位二进制的锁状态标志位,配合biased_lock表示java对象各阶段不同锁状态。
2.biased_lock:只占一位的二进制偏向锁标记,1表示启用偏向锁,0表示未启用。
3.identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor(也可称为监视器)中。
4.thread:54位的持有偏向锁的线程id
5.epoch:偏向锁的时间戳。
6.ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
7.ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
8:age:4位的Java对象年龄。

Klass Word:存储一个地址,长度取决于系统位数,32位和64位,该地址指向方法区中类的元数据信息。
这里放个jdk1.8的jvm模型图,看一下堆与方法区的关系。
可参考后面的链接学习jvm模型知识:https://www.cnblogs.com/paddix/p/5309550.html

jvm内存模型

数组长度:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

6.1.2对象体

主要存放对象的属性值。

6.1.3对齐字节

也可称为补齐区域,如果对象总大小不是4字节的整数倍,会填充上一段内存地址使之成为整数倍。

6.3 锁升级

大概了解什么是对象头以及Mark Word后,我们来讨论下锁升级的过程。在jdk1.6之前,只有重量级锁,通过阻塞和唤醒的形式,需要cpu不停地切换状态,这个过程有可能比用户代码执行的时间还长,这即是1.6之前效率低下的原因。1.6之后引入偏向锁和轻量级锁解决效率低下这一问题。


锁升级过程及状态流转

6.3.1 四种锁Mark Word

无锁时Mark Word的内容
偏向锁时Mark Word的内容
轻量级锁时Mark Word的内容
重量级锁时Mark Word的内容

6.3.2 四种锁的优缺点对比

优缺点对比

6.3.3 锁升级过程分析

图片来源https://tech.souyunku.com/?p=37386
偏向锁

偏向锁升级

1.当线程访问同步块,首先检查当前对象头是否包含线程1的id,如果没有,则通过CAS替换Mark Word,替换成功则升级为偏向锁。
2.升级为偏向锁时,会将Mark Word中的内容设置为当前线程id,继续执行同步代码块。这是线程2也来访问同步块,首先检查对象头是否包含线程2的id,如果没有,则会将Mark Word的内容替换为线程2的id,失败的话则意味着存在锁竞争。
3.存在竞争意味着需要升级偏向锁为轻量级锁,在此之前需要先撤销对象的偏向锁(在安全点,没有线程操作字节码),变为无锁的状态,然后升级为轻量级锁。

轻量级锁

轻量级锁膨胀

1、在偏向锁撤销后,需要升级为轻量级锁,线程1和线程2同时访问同步代码块,通过CAS修改对象的Mark Word,将其修改成每个线程自己的LockRecord,成功则得到锁。失败则表示已经被其他线程占用,继续通过自旋获取锁。
2、当自旋满足以下两个条件之一时,发生锁膨胀,升级为重量级锁。
1)在jdk1.6前,默认10次,可通过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半。
2)jdk1.6之后,引入了自适应自旋锁,次数并非一成不变。根据获取锁的成功率来决定是否能有更长的等待时间。
升级重量级锁后,会修改对象Mark Word,同时阻塞,并等待占有锁线程释放锁并唤醒其他线程,阻塞线程被唤醒后,继续开始争夺锁访问同步块。

7、synchronized静态同步方法与非静态同步方法

1.同步锁和对象锁是否互斥
同步静态方法,是类锁
同步非静态方法,是对象所
这两种锁是不同的,所以相互之前不会产生互斥。
通过一段代码进行验证:

public class SynchronizedStaticAndNonStatic {

public static int count = 0;

public static synchronized void inc() throws InterruptedException {
    count++;
    //这里将静态方法阻塞,验证是否能访问到inc2()
    Thread.sleep(10000);
    System.out.println("结果1: " + count);
}

public synchronized void inc2() {
    count++;
    System.out.println("结果2: " + count);
}

public static void main(String[] args) throws InterruptedException {
    //调用静态同步方法
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            //同步静态方法
            try {
                SynchronizedStaticAndNonStatic.inc();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    //调用非静态同步方法前先进行阻塞一秒,确保静态同步方法在执行阻塞过程中
    Thread.sleep(1000);
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            //同步动态方法
            SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
            t.inc2();
        }).start();
    }
  }
}

结果分析:

结果2: 3
结果2: 4
结果2: 3
结果2: 5
结果2: 6
结果1: 6
结果1: 7
结果1: 8
结果1: 9
结果1: 10

当结果一进入sleep后,并没有对非静态方法inc2进行阻塞,inc2率先执行完毕。得出在静态方法加同步锁,并不会影响其他对象的非静态同步方法。类锁和对象锁并不互斥。
2.对象锁的同步方法是否互斥

public synchronized void inc3(String tName) throws InterruptedException {
    System.out.println("结果3: " + tName);
    Thread.sleep(10000);
}

public synchronized void inc4(String tName) {
    System.out.println("结果4: " + tName);
}

public static void main(String[] args) {
    SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            try {
                t.inc3(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            t.inc4(Thread.currentThread().getName());
        }).start();
    }
}

结果分析:在第一条结果3出来之前,休眠了10秒,结果4才进行打印,同一个对象在两个线程中分别访问该对象的两个同步方法是互斥的。

结果3: Thread-0
sleep 10s...
结果4: Thread-3
结果4: Thread-5
结果4: Thread-4
结果3: Thread-2
结果3: Thread-1

3.非静态锁方法,不同对象在两个线程中调用同一个同步方法

public synchronized void inc5(String tName) throws InterruptedException {
    System.out.println("结果5: " + tName);
    Thread.sleep(10000);
    System.out.println("结果5操作完成: " + tName);
}

public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            //同步动态方法
            SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
            try {
                t.inc5(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

结果分析:“结果5”的提示几乎同时输出,然后sleep了10秒后,才出现操作完成,证明一个对象阻塞后,并不会与其他对象的同步方法互斥。

结果5: Thread-0
结果5: Thread-4
结果5: Thread-3
结果5: Thread-2
结果5: Thread-1
结果5操作完成: Thread-3
结果5操作完成: Thread-4
结果5操作完成: Thread-0
结果5操作完成: Thread-1
结果5操作完成: Thread-2

4.用类直接在两个线程中调用两个不同的同步方法

 public static synchronized void inc() throws InterruptedException {
    count++;
    Thread.sleep(10000);
    System.out.println("结果1: " + count);
}

public static synchronized void inc6() throws InterruptedException {
    count++;
    Thread.sleep(10000);
    System.out.println("结果6: " + count);
}

public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            try {
                SynchronizedStaticAndNonStatic.inc();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            try {
                SynchronizedStaticAndNonStatic.inc6();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

结果分析:无论结果1还是结果6,每条结果间隔都是10秒,用类直接在两个线程中调用两个不同的同步方法是互斥的。

结果1: 1
结果6: 2
结果6: 3
结果6: 4
结果6: 5
结果6: 6
结果1: 7
结果1: 8
结果1: 9
结果1: 10

8、synchronized锁重入

问题提出:当一个线程获取了一个对象的锁,在方法内部是否能够获取其他方法的锁?答案是肯定的,Synchronized是可重入锁。
实现原理:通过monitor机制,monitor通过维护一个计数器来记录锁的获取,重入,释放情况。
看个例子:

static class Inventory {

    //库存数量
    private int num = 100;

    //增加库存
    public synchronized void add(int n) {
        num += n;
        System.out.println("增加库存后的数量=" + num);
        sub(n);
    }

    //增加库存
    public synchronized void sub(int n) {
        num -= n;
        System.out.println("减少库存后的数量=" + num);
    }
}

public static void main(String[] args) {
    //初始化库存
    Inventory inventory = new Inventory();
    new Thread(() -> {
        inventory.add(1);
    }).start();
}

结果:线程调用同步方法add,其内部又调用了同步方法sub,看到结果正常返回,仍然能够获取到锁。

增加库存后的数量=101
减少库存后的数量=100

还有另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。

static class Inventory extends InventoryParent{

    //增加库存
    public synchronized void add(int n) {
        super.num += n;
        System.out.println("增加库存后的数量=" + super.num);
        super.sub(n);
    }
}

static class InventoryParent {

    //库存数量
    private int num = 100;

    //增加库存
    public synchronized void sub(int n) {
        num -= n;
        System.out.println("减少库存后的数量=" + num);
    }
}

public static void main(String[] args) {
    //初始化库存
    Inventory inventory = new Inventory();
    new Thread(() -> {
        inventory.add(1);
    }).start();
}

结果:

增加库存后的数量=101
减少库存后的数量=100

9、volatile

参考https://baijiahao.baidu.com/s?id=1663045221235771554&wfr=spider&for=pc
很详细

volatile 的特性

1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
2、禁止进行指令重排序。(实现有序性)
3、volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

10、wait notify (面试高频)

是java.lang.Object下提供的方法,其中有五个相关方法

//随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
public final native void notify();
//使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
public final native void notifyAll();
//超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
public final native void wait(long timeout) throws InterruptedException;
//对于超时时间更细力度的控制,单位为纳秒
public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }
    if (nanos > 0) {
        timeout++;
    }
    wait(timeout);
}
//使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
public final void wait() throws InterruptedException {
    wait(0);
}

使用wait和notify实现数字和英文字母的交替打印

static class Print {
    /**
     * 当值为1时打印数字,当值为2时打印字母
     */
    private int flag = 1;
    private int count = 1;

    public synchronized void printNum() {
        if (flag != 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print(count);
        flag = 2;
        notify();
    }

    public synchronized void printChar() {
        if (flag != 2) {
            //打印字母
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print((char) (count - 1 + 'A'));
        count++;
        flag = 1;
        notify();
    }
}

public static void main(String[] args) {
    Print print = new Print();
    new Thread(() -> {
        for (int i = 0; i < 26; i++) {
            print.printNum();
        }
    }).start();
    new Thread(() -> {
        for (int i = 0; i < 26; i++) {
            print.printChar();
        }
    }).start();
}

结果:

1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269