高并发面试常考题目

2020-10-08  本文已影响0人  追梦人在路上不断追寻

保证线程安全的代码写法

public class ThreadDemo {

   int count = 0; // 记录方法的命中次数

   public synchronized void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
   }
}

synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字可以让我们的线程变的安全,但是我们在用的时候也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就应了一句话:占着茅坑不拉屎,属实有点浪费资源。

多个线程如何保持A1B2C3等顺序交替输出

使用LockSupport

package com.example.demo;

import java.util.concurrent.locks.LockSupport;

public class PrintA1B2 {
    static Thread t1 = null;
    static Thread t2 = null;

    public static void main(String[] args) {
        char[] num = "1234567".toCharArray();
        char[] str = "ABCDEFG".toCharArray();

        t1 = new Thread(()->{
            for (char n:num
                 ) {
                System.out.println(n);
                LockSupport.unpark(t2);
                LockSupport.park();
            }
        },"t1");

        t2 = new Thread(()->{
            for (char s:str) {
                LockSupport.park();
                System.out.println(s);
               LockSupport.unpark(t1);
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

使用CAS自选锁+volatile实现

package com.example.demo;

public class PrintDemoCAS {
    enum ReadyToRun {T1, T2}

    //定义T1准备运行,而且要设置volatile线程可见
    static volatile ReadyToRun r = ReadyToRun.T1;

    public static void main(String[] args) {
        char[] num = "1234567".toCharArray();
        char[] str = "ABCDEFG".toCharArray();

        new Thread(() -> {
            for (char n : num) {
                //如果不是T1线程运行就一直返回为空,直到T1运行打印,打印完之后把准备运行的变为T2
                while (r != ReadyToRun.T1) {
                }
                System.out.println(n);
                r = ReadyToRun.T2;

            }
        }, "t1").start();

        new Thread(() -> {
            for (char s : str) {
                //如果不是T1线程运行就一直返回为空,直到T1运行打印,打印完之后把准备运行的变为T2
                while (r != ReadyToRun.T2) {
                }
                System.out.println(s);
                r = ReadyToRun.T1;

            }
        }, "t1").start();
    }

}

synchronized volatile的CPU原语是如何实现的

代码片段

synchronized代码块主要是靠monitorenter和monitorexit这两个原语来实现同步的。当线程进入monitorenter获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行monitorexit释放锁后,其他线程才可以竞争获取锁。

常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。

常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。

1.每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.这里涉及重入锁,如果一个线程获得了monitor,他可以再获取无数次,进入的时候monito+1,退出-1,直到为0,开可以被其他线程获取

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

无锁、偏向锁、轻量级锁、重量级锁有什么差别?

ava中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。并且锁只能升级不能降级。

在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念。

自旋
现在假设有这么一个场景:有两个线程A,B在竞争一个锁,假设A拿到了,这个时候B被挂起阻塞,一直等待A释放了锁B才得到使用权。在操作系统中阻塞和唤醒是一个耗时操作,如果A在很短的时间内就释放了锁,当这个时间与阻塞唤醒比较起来更短的时候,我们将B挂起,其实不是一个最优的选择。
自旋是指某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。虽然CPU的时间被消耗了,但是比线程下文切换时间要少。这个时候使用自旋是划算的。

如果是单核处理器,一般建议不要使用自旋锁。因为只有单个处理器,自旋占用的时间片使得代价很高。
而偏向锁、轻量锁、重量锁也是一个锁围绕着如何使得程序运行的更加“划算”而进行改变的。

对象头
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。

在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

image

偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。
当只有一个线程去竞争锁的时候,我们不需要阻塞,也不需要自旋,因为只有一个线程在竞争,我们只要去判断该偏向锁中的ThreadID是否为当前线程即可。如果是就执行同步代码,不是就尝试使用CAS修改ThreadID,修改成功执行同步代码,不成功就将偏向锁升级成轻量锁。

轻量锁
获取轻量锁的过程与偏向锁不同,竞争锁的线程首先需要拷贝对象头中的Mark Word到帧栈的锁记录中。拷贝成功后使用CAS操作尝试将对象的Mark Word更新为指向当前线程的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。如果更新失败,那么意味着有多个线程在竞争。
当竞争线程尝试占用轻量级锁失败多次之后(使用自旋)轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。

重量锁
重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。

如何正确的启动和停止一个线程?

使用 volatile 关键字修饰 变量的方式终止

使用 interrupt() 方式终止

线程和纤程的区别的是什么?为什么纤程比较轻量级?

进程:是操作系统资源分配的基本单位,比如内存、打开文件、网络IO,分配了独立的内存空间
线程:是操作系统资源调度的基本单位,cpu分配的基本单位
纤程:是用户态的线程,是线程中的线程,切换和调度不需要经过OS(操作系统)。;轻量级的线程 - 线程

1.占有的资源少,为什么说他占有资源少呢?举例:操作系统要启一个线程前后要为这个线程配的内存数据大概有1M,而纤程大概是4K

2.由于纤程非常的轻量级,所以切换比较简单

3.可以同时被启动很多个(10万个都没问题)

上一篇下一篇

猜你喜欢

热点阅读