美文网首页
(五):并发编程进阶

(五):并发编程进阶

作者: JBryan | 来源:发表于2020-03-13 16:46 被阅读0次

1.并发编程三要素是否知道,能否分别解释下?

原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read,load,assign,use,store和write,我们大致可以认为,基本数据类型的访问读写是具备原子性的。
如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这些需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反映到Java代码就是同步代码块——synchronized关键字,因此在synchronized块之间的操作,也具备原子性。

可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后,将新值同步回主内存,在变量读取前,从主内存刷新刷新变量值,这种依赖主内存作为传递媒介的方式来实现的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了,新值能立即同步回主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时,变量的可见性。
除了volatile之外,synchronized和final也能实现可见性,同步块的可见性是由“对一个变量执行unlock之前,必须先把此命令同步回主内存中”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那在其他线程中就能看见final的值。

有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻,只允许一条线程对其进行lock操作”这条规则获得的。

2.说下你知道的调度算法,比如进程间的调度。

先来先服务调度算法:按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业。排在长进程后的短进程的等待时间长,不利于短作业/进程。

短作业优先调度算法:短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行,对长作业不友好。

高响应比优先调度算法:在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销。

时间片轮转调度算法:轮流的为各个进程服务,让每个进程在⼀定时间间隔内都可以得到响应。由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度。

优先级调度算法:根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理;如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理。

3.常见的线程间的调度算法是怎样的,java是哪种?

线程调度是指系统为线程分配CPU使用权的过程,主要分两种
协同式线程调度(分时调度模式):线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外⼀个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果⼀个线程有问题,可能⼀直阻塞在那里。

抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有⼀
个线程导致整个进程阻塞。

Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择⼀个线程。所以我们如果希望某些线程多分配⼀些时间,给⼀些线程少分配⼀些时间,可以通过设置线程优先级来完成。
JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,VM⼀般会运行最高优先级的线程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)在两线程同时处于就绪runnable状态时,优先级越高的线程越容易被系统选择执行。但是优先级并不是100%可以获得,只不过是机会更大而已。

4.你日常开发里面过java哪些锁?

悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized。
乐观锁:每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响。
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多

公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果⼀个线程组里,能保证每个线程都能拿到锁比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)。
非公平锁:获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,⼀直拿不到锁,比如synchronized、ReentrantLock。
小结:非公平锁性能高于公平锁,更能重复利用CPU的时间。

可重入锁:也叫递归锁,在外层使⽤锁之后,在内层仍然可以使用,并且不发⽣死锁
不可重入锁:若当前线程执⾏某个⽅法已经获取了该锁,那么在⽅法中尝试再次获取锁时,就会获取不到被阻塞。
小结:可重入锁能⼀定程度的避免死锁。synchronized、ReentrantLock 都是重入锁

自旋锁:⼀个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有⼀个执行单元获得
锁.
小结:不会发生线程状态的切换,⼀直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU。常见的自旋锁:TicketLock,CLHLock,MSCLock。

共享锁:也叫S锁/读锁,能查看但无法修改和删除的⼀种数据锁,加锁后其它⽤户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享。
互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每⼀次只能被⼀个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据。
死锁:两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去。

下面三种是Jvm为了提高锁的获取与释放效率而做的优化针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程
偏向锁:⼀段同步代码⼀直被⼀个线程所访问,那么该线程会自动获取锁,获取锁的代价更低,
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点。
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会⼀直循环下去,当自旋⼀定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低。

5.写个多线程死锁的例子。

public class DeadLockDemo {
    public static String lockA = "LockA";
    public static String lockB = "LockB";

    private void methodA(){
        synchronized (lockA){
            try {
                System.out.println("methodA持有锁A");
                Thread.sleep(1000);
                synchronized (lockB){
                    System.out.println("methodA持有锁B");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void methodB(){
        synchronized (lockB){
            try {
                System.out.println("methodB持有锁B");
                Thread.sleep(1000);
                synchronized (lockA){
                    System.out.println("methodB持有锁A");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        DeadLockDemo demo = new DeadLockDemo();
        new Thread(()-> {
            demo.methodA();
        }).start();

        new Thread(()-> {
            demo.methodB();
        }).start();
    }

}

6.实现一个简单的重入锁

public class MyLock implements Lock{
    
    //锁有没有被持有开关变量
    private boolean isHoldLock = false;
    
    //重入锁的次数
    private int reentryCount = 0;
    
    //持有锁的线程
    private Thread holdLockThread = null;

    /**
     * 同一时刻,有且仅能一个线程获得锁,其他线程,只能等待该线程释放锁之后,才能获得锁
     */
    @Override
    public synchronized void lock() {
        //如果已经被持有了且不是当前线程,则等待
        while(isHoldLock && Thread.currentThread() != holdLockThread) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        holdLockThread = Thread.currentThread();
        //被唤醒之后
        isHoldLock = true;
        reentryCount++;
    }
    
    @Override
    public synchronized void unlock() {
        //当前线程是持有锁的线程,重入锁--
        if(Thread.currentThread() == holdLockThread) {
            reentryCount--;
            if(reentryCount == 0) {
                notify();
                isHoldLock = false;
            }
        }       
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub  
    }
    @Override
    public boolean tryLock() {
        // TODO Auto-generated method stub
        return false;
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }
    @Override
    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }
}

public class ReentryDemo {
    
    private Lock lock = new MyLock();
    
    public void methodA() {
        lock.lock();
        System.out.println("进入方法A");
        methodB();
        lock.unlock();
    }
    
    public void methodB() {
        lock.lock();
        System.out.println("进入方法B");
        lock.unlock();
    }

    
    public static void main(String[] args) {
        ReentryDemo demo = new ReentryDemo();
        demo.methodA();
    }

}

7.能否介绍下你对synchronized的理解。

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。

synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

8.了解CAS不,能否解释下什么是CAS?

全称是Compare And Swap,即比较再交换,是实现并发应用到的一种技术。
底层通过Unsafe类实现原子性操作操作包含三个操作数 —— 内存地址(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 ,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环,重新从内存地址中获取变量的值,然后更新新值。
CAS这个是属于乐观锁,性能较悲观锁有很大的提高。
AtomicXXX 等原⼦类底层就是CAS实现,⼀定程度比synchonized好,因为后者是悲观锁。

9.CAS会存在什么比较严重的问题?能否解释下什么是ABA问题,怎么避免这个问题呢?

自旋时间长CPU利用率增加,CAS里面是⼀个循环判断的过程,如果线程⼀直没有获取到状态,
cpu资源会⼀直被占用。
ABA问题:如果⼀个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从而赋值给V。
给变量加⼀个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值。

10.Java 并发包提供了哪些并发工具类?

我们通常所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:
1.提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。并发工具类参考:并发工具类

2.各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。

3.各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。

相关文章

网友评论

      本文标题:(五):并发编程进阶

      本文链接:https://www.haomeiwen.com/subject/nmukshtx.html