编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

多线程与高并发学习系列(二)(多线程高并发项目中的应用)

wxchong 2024-08-22 23:57:46 开源技术 6 ℃ 0 评论

线程的as-if-serial

单个线程,两条语句,未必是按顺序执行

单线程的重排序,必须保证最终一致性

as-if-serial:看上去像是序列化(单线程)

会产生的后果

多线程会产生不希望看到的结果

哪些指令可以互换顺序

hanppens-before原则(JVM规定重排序必须遵守的规则)

JLS17.4.5 (不需要记住)

?程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确地说是控制流顺序,因为要考虑到分支和循环结构。

?管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。

?volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。

?线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。

?线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。

?线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断

?对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。

?传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C

使用内存屏障阻止乱序执行

内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行

intel : lfence sfence mfence(CPU特有指令)

JVM中的内存屏障

所有实现JVM规范的虚拟机,必须实现四个屏障

LoadLoadBarrier LoadStore SL SS

volatile的底层实现

volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序

1: volatile i

2: ACC_VOLATILE

3: JVM的内存屏障

屏障两边的指令不可以重排!保障有序!

happends-before

as - if - serial

4:hotspot实现

bytecodeinterpreter.cpp

int field_offset = cache->f2_as_index();
          if (cache->is_volatile()) {
            if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
              OrderAccess::fence();
            }

orderaccess_linux_x86.inline.hpp

inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}

LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。

面试题

DCL单例要不要加volatile?

原子性

线程的原子性

从一个简单的小程序谈起:

package com.wchen.juc.c_001_sync_basics;

import java.util.concurrent.CountDownLatch;

public class T00_IPlusPlus {
    private static long n = 0L;

    public static void main(String[] args) throws Exception {

        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    //synchronized (T00_IPlusPlus.class) {
                    n++;
                    //}
                }
                latch.countDown();
            });
        }

        for (Thread t : threads) {
            t.start();
        }

        latch.await();

        System.out.println(n);

    }
}

一些基本概念

race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争

数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果

如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好),

monitor (管程) ---> 锁

critical section -> 临界区

如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细

具体: 保障操作的原子性(Atomicity)

  1. 悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)
  2. 乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作 CAS = Compare And Set/Swap/Exchange
  3. /**
    * 解决同样的问题的更高效的方法,使用AtomXXX类
    * AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
    *
    */
    package com.wchen.juc.c_018_00_AtomicXXX;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;


    public class T01_AtomicInteger {
    /*volatile*/ //int count1 = 0;

    AtomicInteger count = new AtomicInteger(0);

    /* synchronized */void m() {
    for (int i = 0; i < 10000; i++)
    //if count1.get() < 1000
    count.incrementAndGet(); //count1++
    }

    public static void main(String[] args) {
    T01_AtomicInteger t = new T01_AtomicInteger();

    List<Thread> threads = new ArrayList<Thread>();

    for (int i = 0; i < 100; i++) {
    threads.add(new Thread(t::m, "thread-" + i));
    }

    threads.forEach((o) -> o.start());

    threads.forEach((o) -> {
    try {
    o.join();
    }
    catch (InterruptedException e) {
    e.printStackTrace();
    }
    });

    System.out.println(t.count);

    }

    }

我们平时所说的"上锁",一般指的是悲观锁

上锁的本质

上锁的本质是把并发编程序列化

package com.wchen.juc.c_001_sync_basics;

import com.wchen.util.SleepHelper;

public class T00_01_WhatIsLock {
    private static Object o = new Object();

    public static void main(String[] args) {
        Runnable r = () -> {
            //synchronized (o) { //打开注释试试看,对比结果
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            //}
        };

        for (int i = 0; i < 3; i++) {
            new Thread(r).start();
        }
    }
}

同时保障可见性

注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)

一定是锁定同一把锁(抢一个坑位)

package com.wchen.juc.c_001_sync_basics;

import com.wchen.util.SleepHelper;

public class T00_02_SingleLockVSMultiLock {
    private static Object o1 = new Object();
    private static Object o2 = new Object();
    private static Object o3 = new Object();

    public static void main(String[] args) {
        Runnable r1 = () -> {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            }
        };

        Runnable r2 = () -> {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            }
        };

        Runnable r3 = () -> {
            synchronized (o3) {
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            }
        };

        new Thread(r1).start();
        new Thread(r2).start();
        new Thread(r3).start();
    }
}

什么样的语句(指令)具备原子性?

CPU级别汇编,需要查询汇编手册!

Java中的8大原子操作:(了解即可,无需背过)

  1. lock:主内存,标识变量为线程独占
  2. unlock:主内存,解锁线程独占变量
  3. read:主内存,读取内存到线程缓存(工作内存)
  4. load:工作内存,read后的值放入线程本地变量副本
  5. use:工作内存,传值给执行引擎
  6. assign:工作内存,执行引擎结果赋值给线程本地变量
  7. store:工作内存,存值到主内存给write备用
  8. write:主内存,写变量值

JVM中的两种锁

重量级锁(经过操作系统的调度)synchronized早期都是这种锁(目前的实现中升级到最后也是这种锁)

轻量级锁(CAS的实现,不经过OS调度)(无锁 - 自旋锁 - 乐观锁)

CAS的深度剖析

CAS的ABA问题解决方案 - Version

CAS操作本身的原子性保障

AtomicInteger:

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Unsafe:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

运用:

package com.wchen.jol;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class T02_TestUnsafe {

    int i = 0;
    private static T02_TestUnsafe t = new T02_TestUnsafe();

    public static void main(String[] args) throws Exception {
        //Unsafe unsafe = Unsafe.getUnsafe();

        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);

        Field f = T02_TestUnsafe.class.getDeclaredField("i");
        long offset = unsafe.objectFieldOffset(f);
        System.out.println(offset);

        boolean success = unsafe.compareAndSwapInt(t, offset, 0, 1);
        System.out.println(success);
        System.out.println(t.i);
        //unsafe.compareAndSwapInt()
    }
}

jdk8u: unsafe.cpp:

cmpxchg = compare and exchange set swap

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

jdk8u: atomic_linux_x86.inline.hpp 93行

is_MP = Multi Processors

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

jdk8u: os.hpp is_MP()

  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

jdk8u: atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

最终实现:

cmpxchg = cas修改变量值

lock cmpxchg 指令

硬件:

lock指令在执行的时候视情况采用缓存锁或者总线锁

两种锁的效率

不同的场景:

临界区执行时间比较长 , 等的人很多 -> 重量级

时间短,等的人少 -> 自旋锁

synchronized如何保障可见性


JVM中的线程和OS线程对应关系

JVM 1:1 -> LOOM -> M:N (golang)

synchronized锁升级过程

链接:https://pan.baidu.com/s

JUC的五花八门的新锁(java util cocurrent)

超级面试重灾区

synchrnized wait notify (经典同步机制)

AtomicXXX -> AtomicInteger Long AtomicReference AtomicStampedReference AtomicMarkableReference AtomicFieldUpdator ...

ReentrantLock

部分场合替代synchronized

  • 手工释放锁
    • 标准写法:
    • lock.lock();
      try {
      xxxxx
      }
      finally {
      lock.unlock();
      }
  • 可以是公平锁
    • 公平锁:线程抢锁先排队
    • 非公平锁:线程到了就插队抢
  • 可被打断的上锁过程
    • tryLock()
    • lockInterruptibly()
  • 锁上面的队列可以指定任意数量
    • 区分了不同条件下的等待队列(Condition)
    • ABC ABC 问题

Latch

门闩

Semaphore

信号量(n个线程的限流)

CyclicBarrier

栅栏

Phaser

阶段同步器

LockSupport

替代wait notify

Exchanger

同步交换器

新型同步锁的原理CAS + AQS + volatile

CAS Comapre And Swap (自旋锁 乐观锁 JVM自身解决问题 不需要OS老大的调度)

AQS (Template Method) + Abtract Queued Synchronizer

//新旧版本分隔处

=========================================================

面试题

哲学家就餐问题(The Dinning Philosophers Problem)


package com.wchen.juc.c_33_TheDinningPhilosophersProblem;

public class ChopStick {
}
package com.wchen.juc.c_33_TheDinningPhilosophersProblem;


import com.wchen.util.SleepHelper;

public class T01_DeadLock {
    public static void main(String[] args) {
        ChopStick cs0 = new ChopStick();
        ChopStick cs1 = new ChopStick();
        ChopStick cs2 = new ChopStick();
        ChopStick cs3 = new ChopStick();
        ChopStick cs4 = new ChopStick();

        Philosohper p0 = new Philosohper("p0", 0, cs0, cs1);
        Philosohper p1 = new Philosohper("p1", 1, cs1, cs2);
        Philosohper p2 = new Philosohper("p2", 2, cs2, cs3);
        Philosohper p3 = new Philosohper("p3", 3, cs3, cs4);
        Philosohper p4 = new Philosohper("p4", 4, cs4, cs0);

        p0.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();

    }

    public static class Philosohper extends Thread {

        private ChopStick left, right;
        private int index;

        public Philosohper(String name, int index, ChopStick left, ChopStick right) {
            this.setName(name);
            this.index = index;
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
                synchronized (left) {
                    SleepHelper.sleepSeconds(1+index);
                    synchronized (right) {
                        SleepHelper.sleepSeconds(1);
                    }
                }

        }

    }
}

解决方案:

package com.wchen.juc.c_33_TheDinningPhilosophersProblem;


import com.wchen.util.SleepHelper;

public class T02_DeadLockOpen {
    public static void main(String[] args) {
        ChopStick cs0 = new ChopStick();
        ChopStick cs1 = new ChopStick();
        ChopStick cs2 = new ChopStick();
        ChopStick cs3 = new ChopStick();
        ChopStick cs4 = new ChopStick();

        Philosohper p0 = new Philosohper("p0", 0, cs0, cs1);
        Philosohper p1 = new Philosohper("p1", 1, cs1, cs2);
        Philosohper p2 = new Philosohper("p2", 2, cs2, cs3);
        Philosohper p3 = new Philosohper("p3", 3, cs3, cs4);
        Philosohper p4 = new Philosohper("p4", 4, cs4, cs0);

        p0.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();

    }

    public static class Philosohper extends Thread {

        private ChopStick left, right;
        private int index;

        public Philosohper(String name, int index, ChopStick left, ChopStick right) {
            this.setName(name);
            this.index = index;
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            if (index == 0) {
                synchronized (left) {
                    SleepHelper.sleepSeconds(1+index);
                    synchronized (right) {
                        SleepHelper.sleepSeconds(1);
                        System.out.println(index + " 吃完了!");
                    }
                }
            } else {
                synchronized (right) {
                    SleepHelper.sleepSeconds(1+index);
                    synchronized (left) {
                        SleepHelper.sleepSeconds(1);
                        System.out.println(index + " 吃完了!");
                    }
                }
            }
        }

    }
}

第一节:线程的基本概念

首先给大家交代一下我们2019年这个版本给大家讲哪些内容,这个版本主要之中在多线程和高并发这两大块,这两大块儿是现在面试问的越来越多,也是相对一个初级的程序员向中高级迈进的必须要踏过的一个坎儿。

多线程与高并发大概讲六大块,

  • 第一:基本的概念,从什么是线程开始
  • 第二:JUC同步工具,就是各种同步锁
  • 第三:同步容器
  • 第四:线程池
  • 第五:高频面试加分项的一些面试用的东西,包括纤程
  • 第六:Disruptor,不知道有多少同学听说过这个框架的,这个框架它也是一个MQ框架(Message Queue)叫做消息队列,消息队列非常多,后面还会给大家讲Kafka、RabbitMQ,Redis等这些都是消息队列。Disruptor是目前大家公认的在单机环境上效率最高的、性能最快的MQ。

基本概念

我们先从线程的基本概念开始,给大家复习一下,不知道有多少同学是基础不太好,说什么是线程都不知道的,如果这样的话,花时间去补初级内容的课。

什么是叫一个进程? 什么叫一个线程?

  • Program app ->QQ.exe
  • 进程:做一个简单的解释,你的硬盘上有一个简单的程序,这个程序叫QQ.exe,这是一个程序,这个程序是一个静态的概念,它被扔在硬盘上也没人理他,但是当你双击它,弹出一个界面输入账号密码登录进去了,OK,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念
  • 线程:作为一个进程里面最小的执行单元它就叫一个线程,用简单的话讲一个程序里不同的执行路径就叫做一个线程

与线程中断相关的方法


线程中断与InterruptedException

(TODO)

线程状态


常见的线程状态有六种:

当我们new一个线程时,还没有调用start()该线程处于新建状态

线程对象调用 start()方法时候,他会被线程调度器来执行,也就是交给操作系统来执行了,那么操作系统来执行的时候,这整个的状态叫Runnable,Runnable内部有两个状态(1)Ready就绪状态/(2)Running运行状态。就绪状态是说扔到CPU的等待队列里面去排队等待CPU运行,等真正扔到CPU上去运行的时候才叫Running运行状态。(调用yiled时候会从Running状态跑到Ready状态去,线程配调度器选中执行的时候又从Ready状态跑到Running状态去)

如果你线程顺利的执行完了就会进去(3)Teminated结束状态,(需要注意Teminated完了之后还可不可以回到new状态再调用start?这是不行的,完了这就是结束了)

在Runnable这个状态里头还有其他一些状态的变迁(4)TimedWaiting等待(5)Waiting等待(6)Blocked阻塞,在同步代码块的情况就下没得到锁就会阻塞状态,获得锁的时候是就绪状态运行。在运行的时候如果调用了o.wait()、t.join()、LockSupport.park()进入Waiting状态,调用o.notify()、o.notifiAll()、LockSupport.unpark()就又回到Running状态。TimedWaiting按照时间等待,等时间结束自己就回去了,Thread.sleep(time)、o.wait(time)、t.jion(time)、LockSupport.parkNanos()、LockSupport.parkUntil()这些都是关于时间等待的方法。

问题1:哪些是JVM管理的?哪些是操作系统管理的?

上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,那个是操作系统和那个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序

问题2:线程什么状态时候会被挂起?挂起是否也是一个状态?

Running的时候,在一个cpu上会跑很多个线程,cpu会隔一段时间执行这个线程一下,在隔一段时间执行那个线程一下,这个是cpu内部的一个调度,把这个状态线程扔出去,从running扔回去就叫线程被挂起,cpu控制它。

来看一下ThreadState这段代码

package com.wchen.juc.c_000;

public class T04_ThreadState {

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(this.getState());

            for(int i=0; i<10; i++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        Thread t = new MyThread();
                //怎么样得到这个线程的状态呢?就是通过getState()这个方法
        System.out.println(t.getState());//他是一个new状态

        t.start();//到这start完了之后呢是Runnable的状态

        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
                    //然后join之后,结束了是一个Timenated状态
        System.out.println(t.getState());

    }
}

synchronized

下面我们来讲synchronized关键字,有不少同学已经耳熟能详了,不过作为复习还是要复习一下。第一个是多个线程去访问同一个资源的时候对这个资源上锁。

为什么要上锁呢?访问某一段代码或者某临界资源的时候是需要有一把锁的概念在这儿的。


比如:我们对一个数字做递增,两个程序对它一块儿来做递增,递增就是把一个程序往上加1啊,如果两个线程共同访问的时候,第一个线程一读它是0,然后把它加1,在自己线程内部内存里面算还没有写回去的时候而第二个线程读到了它还是0,加1在写回去,本来加了两次,但还是1,那么我们在对这个数字递增的过程当中就上把锁,就是说第一个线程对这个数字访问的时候是独占的,不允许别的线程来访问,不允许别的线程来对它计算,我必须加完1收释放锁,其他线程才能对它继续加。

实质上,这把锁并不是对数字进行锁定的, 你可以任意指定,想锁谁就锁谁。

我第一个小程序是这么写的 ,如果说你想上了把锁之后才能对count进行减减访问,你可以new一个Object,所以这里锁定就是o,当我拿到这把锁的时候才能执行这段代码。是锁定的某一个对象,synchronized有一个锁升级的概念,我们一会儿会讲到

/**
*synchronized关键字
*对某个对象加锁
*@author wchen
*/
package com.wchen.juc.c_001;

public class T {
    
    private int count = 10;
    private Object o = new Object();
    
    public void m() {
        synchronized(o) { //任何线程要想执行下面的代码,必须先拿到o的锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    
}


我们来谈一下synchronized它的一些特性。如果说你每次都定义个一个锁的对象Object o 把它new出来那加锁的时候太麻烦每次都要new一个新的对象出来,所以呢,有一个简单的方式就是synchronized(this)锁定当前对象就行

/**
 * synchronized关键字
 * 对某个对象加锁
 * @author wchen
 */
package com.wchen.juc.c_002;

public class T {
    
    private int count = 10;
    
    public void m() {
        synchronized(this) { ?//任何线程想要执行那个下面的代码,必须先要拿到this的锁
      count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
    
}

如果你要是锁定当前对象呢,你也可以写成如下方法。synchronized方法和synchronized(this)执行这段代码它是等值的

package com.wchen.juc.c_003;

public class T {

    private int count = 10;
    
    public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

我们知道静态方法static是没有this对象的,你不需要new出一个对象来就能执行这个方法,但如果这个这个上面加一个synchronized的话就代表synchronized(T.class)。这里这个synchronized(T.class)锁的就是T类的对象

package com.wchen.juc.c_004;

public class T {

    private static int count = 10;
    
    public synchronized static void m() { //这里等同于synchronized(T.class)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void mm() {
        synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?
            count --;
        }
    }
}

问题:T.class是单例的吗?

一个class load到内存它是不是单例的,想想看。一般情况下是,如果是在同一个ClassLoader空间那它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那他一定就是单例

下面程序:很有可能读不到别的线程修改过的内容,除了这点之外count减减完了之后下面的count输出和你减完的结果不对,很容易分析:如果有一个线程把它从10减到9了,然后又有一个线程在前面一个线程还没有输出呢进来了把9又减到了8,继续输出的8,而不是9。如果你想修正它,前面第一个是在上面加volatile,改了马上就能得到。

/**
 * 分析一下这个程序的输出
 * @author wchen
 */
package com.wchen.juc.c_005;

public class T implements Runnable {

    private /*volatile*/ int count = 100;
    
    public /*synchronized*/ void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void main(String[] args) {
        T t = new T();
        for(int i=0; i<100; i++) {
            new Thread(t, "THREAD" + i).start();
        }
    }
    
}

另外这个之外还可以加synchronized,加了synchronized就没有必要在加volatile了,因为synchronized既保证了原子性,又保证了可见性。

//对比上一个小程序
package com.wchen.juc.c_006;
public class T implements Runnable {

    private int count = 10;
    
    public synchronized void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    
    public static void main(String[] args) {
        
        for(int i=0; i<5; i++) {
            T t = new T();
            new Thread(t, "THREAD" + i).start();
        }
    }
    
}

如下代码:同步方法和非同步方法是否可以同时调用?就是我有一个synchronized的m1方法,我调用m1的时候能不能调用m2,拿大腿想一想这个是肯定可以的,线程里面访问m1的时候需要加锁,可是访问m2的时候我又不需要加锁,所以允许执行m2。

这些小实验的设计是比较考验功力的,学习线程的时候自己要多动手进行试验,任何一个理论,都可以进行验证。

/**
 *同步和非同步方法是否可以同时调用?
 * @author wchen
 */
package com.wchen.juc.c_007;
public class T {

    public synchronized void m1() { 
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    
    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2 ");
    }
    
    public static void main(String[] args) {
        T t = new T();
        
        /*new Thread(()->t.m1(), "t1").start();
        new Thread(()->t.m2(), "t2").start();*/
        
        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();
        
        /*
        //1.8之前的写法
        new Thread(new Runnable() {

            @Override
            public void run() {
                t.m1();
            }
        });
        */
    }
}

我们在来看一个synchronized应用的例子

我们定义了一个class账户,有名称、余额。写方法给哪个用户设置它多少余额,读方法通过这个名字得到余额值。如果我们给写方法加锁,给读方法不加锁,你的业务允许产生这种问题吗?业务说我中间读到了一些不太好的数据也没关系,如果不允许客户读到中间不好的数据那这个就有问题。正因为我们加了锁的方法和不加锁的方法可以同时运行。

问题比如说:张三,给他设置100块钱启动了,睡了1毫秒之后呢去读它的值,然后再睡2秒再去读它的值这个时候你会看到读到的值有问题,原因是在设定的过程中this.name你中间睡了一下,这个过程当中我模拟了一个线程来读,这个时候调用的是getBalance方法,而调用这个方法的时候是不用加锁的,所以说我不需要等你整个过程执行完就可以读到你中间结果产生的内存,这个现象就叫做脏读。这问题的产生就是synchronized方法和非synchronized方法是同时运行的。解决就是把getBalance加上synchronized就可以了,如果你的业务允许脏读,就可以不用加锁,加锁之后的效率低下。

/**
 * 面试题:模拟银行账户
 * 对业务写方法加锁
 * 对业务读方法不加锁
 * 这样行不行?
 *
 * 容易产生脏读问题(dirtyRead)
 */
package com.wchen.juc.c_008;

import java.util.concurrent.TimeUnit;

public class Account {
    String name;
    double balance;
    
    public synchronized void set(String name, double balance) {
        this.name = name;

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        this.balance = balance;
    }
    
    public /*synchronized*/ double getBalance(String name) {
        return this.balance;
    }
    
    public static void main(String[] args) {
        Account a = new Account();
        new Thread(()->a.set("zhangsan", 100.0)).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(a.getBalance("zhangsan"));
        
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(a.getBalance("zhangsan"));
    }
}

再来看synchronized的另外一个属性:可重入,是synchronized必须了解的一个概念。

如果是一个同步方法调用另外一个同步方法,有一个方法加了锁,另外一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。比如说是synchronized可重入的,有一个方法m1 是synchronized有一个方法m2也是synchrionzed,m1里能不能调m2。我们m1开始的时候这个线程得到了这把锁,然后在m1里面调用m2,如果说这个时候不允许任何线程再来拿这把锁的时候就死锁了。这个时候调m2它发现是同一个线程,因为你m2也需要申请这把锁,它发现是同一个线程申请的这把锁,允许,可以没问题,这就叫可重入锁。

/**
 * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
 * 也就是说synchronized获得锁是可重入的
 * synchronized
 * @author wchen
 */
package com.wchen.juc.c_009;

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end");
    }
    
    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        new T().m1();
    }
}

模拟一个父类子类的概念,父类synchronized,子类调用super.m的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1,就是这么个概念。

package com.wchen.juc.c_010;

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m() {
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m end");
    }
    
    public static void main(String[] args) {
        new TT().m();
    }
}

class TT extends T {
    @Override
    synchronized void m() {
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}

下面再看,异常锁

看这个小程序,加了锁synchronized void m()while(true)不断执行,线程启动,count++ 如果等于5的时候认为的产生异常。这时候如果产生任何异常,就会出现什么情况呢? 就会被原来的那些个准备拿到这把锁的程序乱冲进来,程序乱入。这是异常的概念。

/**
 * 程序在执行过程中,如果出现异常,默认情况锁会被释放
 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
 * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
 * 因此要非常小心的处理同步业务逻辑中的异常
 * @author wchen
 */
package com.wchen.juc.c_011;

import java.util.concurrent.TimeUnit;

public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
                
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            if(count == 5) {
                int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                System.out.println(i);
            }
        }
    }
    
    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {

            @Override
            public void run() {
                t.m();
            }
            
        };
        new Thread(r, "t1").start();
        
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(r, "t2").start();
    }
}


synchronized的底层实现

  • 早期,jdk早期的时候,这个synchronized的底层实现是重量级的,重量级到这个synchronized都是要去找操作系统去申请锁的地步,这就会造成synchronized效率非常低,java后来越来越开始处理高并发的程序的时候,很多程序员都不满意,说这个synchrionized用的太重了,我没办法,就要开发新的框架,不用你原生的了
  • 改进,后来的改进才有了锁升级的概念
  • 关于这个锁升级的概念,我写过一篇文章《我就是厕所所长一、二》,大家可以去找一下,公众号里也有,专门以小说的形式讲了这个锁升级到底是怎么样的一个概念

这个锁升级的概念呢,是这样的,原来呢都要去找操作系统,要找内核去申请这把锁,到后期做了对synchronized的一些改进,他的效率比原来要改变了不少,改进的地方。当我们使用synchronized的时候HotSpot的实现是这样的:上来之后第一个去访问某把锁的线程 比如sync (Object) ,来了之后先在这个Object的头上面markword记录这个线程。(如果只有第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的ID(偏向锁))。

偏向锁如果有线程争用的话,就升级为自旋锁,概念就是(有一个哥们儿在蹲马桶 ,另外来了一个哥们,他就在旁边儿等着,他不会跑到cpu的就绪队列里去,而就在这等着占用cpu,用一个while的循环在这儿转圈玩儿, 很多圈之后不行的话就再一次进行升级)。

自旋锁转圈十次之后,升级为重量级锁,重量级锁就是去操作系统那里去申请资源。这是一个锁升级的过程。

参考:https://blog.csdn.net/baidu_38083619/article/details/82527461

需要注意并不是CAS的效率就一定比系统锁要高,这个要区分实际情况:

执行时间短(加锁代码),线程数少,用自旋 执行时间长,线程数多,用系统锁

关于效率方面的内容如果暂时不能理解的,等讲到CAS锁的时候再说。

内容回顾

  • 线程的概念、启动方式、常用方法
  • synchronized(Object) 不能用String常量 Integer Long
  • 线程同步 -synchronized
  • -锁的是对象不是代码
  • -this XX.class
  • -锁定方法 非锁定方法 同时执行
  • -锁升级
  • 偏向锁 记录这个线程的ID
  • 自旋锁 如果线层争用,就升级为自旋锁(线程数量少)
  • 重量级锁 10次(线程数量多)



Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表