目录

一、多线程的创建方式

二、线程生命周期

三、多线程同步方式

四、原子类atomic包

五、volatile关键字原理

六、单例模式的两种实现

七、乐观锁和悲观锁

八、CountDownLatch、Semaphore和CyclicBarrier

九、死锁及其避免方式

十、HashMap和Hashtable


一、多线程的创建方式

1. 继承Thread类

  • 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
  • 创建Thread子类的实例,也就是创建了线程对象
  • 启动线程,即调用线程的start()方法
public class TestThread {
    public static void main(String[] args) {
        new MyThread().start();
    }
}
class MyThread extends Thread {
    int total;
    public void run() {
        synchronized (this) {
            System.out.println("MyThread is running..");
            for (int i = 0; i < 100; i++) {
                total += i;
                System.out.println("total is " + total);
            }
            notify();// 因为synchronized (this),为了保证其他线程有wait可以被重新唤醒
        }
    }
}

2. 实现Runnable接口

  • 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
  • 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
  • 第三部依然是通过调用线程对象的start()方法来启动线程
public class TestThread2 implements Runnable {//实现Runnable接口
    public static void main(String[] args) {
        // 创建TestThread2后,将对象放入线程找那个,然后执行线程
        new Thread(new TestThread2()).start();
    }
    @Override
    public void run() {
        System.out.println("------------线程被创建");
    }
}

 3.使用Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  • 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class CallableAndFuture {
    //Callable  :一个产生结果
    //Future    :一个拿到结果
    public static void main(String[] args) {
        //1.创建Callable的对象返回值
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        };
        //2.FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        //3.作为Runnable被线程执行
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            //4.作为Future得到Callable的返回值
            System.out.println(future.get());// 拿到结果
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

二、线程生命周期

1. 生命周期

参考资料:面试官:谈谈线程的生命周期

Java的线程生命周期有六种状态:

  • New(初始化状态)
  • Runnable(可运行/运行状态)
  • Blocked(阻塞状态)
  • Waiting(无时间限制的等待状态)
  • Timed_Waiting(有时间限制的等待状态)
  • Terminated(终止状态)

java 多线程并发有序执行 java的并发,多线程,线程模型_java 多线程并发有序执行

 

1.New(初始化状态):指的是在高级语言,比如Java。在Java层面的线程被创建了,而在操作系统中的线程其实是还没被创建的,所以这个时候是不可能分配CPU执行这个线程的!所以这个状态是高级语言独有的,操作系统的线程没这个状态。我们New了一个线程,那时候它就是这个状态。

2.Runnable(可运行/运行状态):这个状态下是可以分配CPU执行的,在New状态时候我们调用start()方法后线程就处于这个状态。

3.Blocked(阻塞状态):这个状态下是不能分配CPU执行的,只有一种情况会导致线程阻塞,就是synchronized!我们知道被synchronized修饰的方法或者代码块同一时刻只能有一个线程执行,而其他竞争锁的线程就从Runnable到了Blocked状态!当某个线程竞争到锁了它就变成了Runnable状态。注意并发包中的Lock,是会让线程属于等待状态而不是阻塞,只有synchronized是阻塞。(感觉是历史遗留问题,没必要多一个阻塞状态和等待没差啊)。

4.Waiting(无时间限制的等待状态):这个状态下也是不能分配CPU执行的。有三种情况会使得Runnable状态到waiting状态

调用无参的Object.wait()方法。等到notifyAll()或者notify()唤醒就会回到Runnable状态。调用无参的Thread.join()方法。也就是比如你在主线程里面建立了一个线程A,调用A.join(),那么你的主线程是得等A执行完了才会继续执行,这是你的主线程就是等待状态。调用LockSupport.park()方法。LockSupport是Java6引入的一个工具类Java并发包中的锁都是基于它实现的,再调用LocakSupport.unpark(Thread thread),就会回到Runnable状态。

5.Timed_Waiting(有时间限制的等待状态):其实这个状态和Waiting就是有没有超时时间的差别,这个状态下也是不能分配CPU执行的。有五种情况会使得Runnable状态到waiting状态

Object.wait(long timeout)。Thread.join(long millis)。Thread.sleep(long millis)。注意 Thread.sleep(long millis, int nanos) 内部调用的其实也是Thread.sleep(long millis)。LockSupport.parkNanos(Object blocked,long deadline)。LockSupport.parkUntil(long deadline)。6.Terminated(终止状态):在我们的线程正常run结束之后或者run一半异常了就是终止状态!注意有个方法Thread.stop()是让线程终止的,但是这个方法已经被废弃了,不推荐使用,因为比如你这个线程得到了锁,你stop了之后这个锁也随着没了,其它线程就都拿不到这个锁了!这不玩完了么!所以推荐使用interrupt()方法。

interrupt()会使得线程Waiting和Timed_Waiting状态的线程抛出 interruptedException异常,使得Runnabled状态的线程如果是在I/O操作会抛出其它异常。

如果Runnabled状态的线程没有阻塞在I/O状态的话,那只能主动检测自己是不是被中断了,使用isInterrupted()。

2. 线程管理方法

sleep、await、wait、join等方法

三、多线程同步方式

1. synchronized关键字

https://baijiahao.baidu.com/s?id=1612142459503895416&wfr=spider&for=pc

2. ReentrantLock类

1)可重入:

        线程可以重复的获得已经持有的锁。所保持一个持有计数来跟踪对lock方法的嵌套调用。

这个特性使得被一个锁保护的代码可以调用另一个使用相同的锁的方法。

2)原理:

    1. 说明:分为公平锁和非公平锁两个都继承内部类Sync,各自实现,而构造方法也可以指定实例化时采用公平锁还是非公平锁。

  • 公平锁:会按照请求的顺序获取锁,如果锁已经被占用,则新来的请求放到队列中。
  • 非公平锁:不是按照请求顺序获取锁,存在插队现象。

    2. 类的结构

//内部类
private final Sync sync;
//定义了内部类
abstract static class Sync extends AbstractQueuedSynchronizer
//非公平锁继承内部类
static final class NonfairSync extends Sync
//公平锁继承内部类
static final class FairSync extends Sync

 3. ReentrantLock构造方法有两个:无参和有参(指定采用公平和非公平)分别看下是如何初始化的。

public ReentrantLock() {
		//默认非公平锁
        sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
		//若fair为true,则是公平锁,否则非公平锁
        sync = fair ? new FairSync() : new NonfairSync();
}

4. 接下来看下lock()是如何实现加锁的

public void lock() {
    //根据实例化的锁走对应的子类实现
    sync.lock();
}

final void lock() {
    //采用CAS方式,若获取到则设置自己为所的持有者否则按照公平锁的方式请求。
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

3. 条件锁

用法:

public void Transfer(int from, int to, double amount) {
    ReentrantLock locker = new ReentrantLock();
    Condition sufficientFunds = locker.newCondition();//条件对象,
    lock.lock();
    try {
        while (Accounts[from] < amount) {
            //等待有足够的钱
            sufficientFunds.await();
        }
        DoTransfer(from, to, amount);
        sufficientFunds.signalAll();	//通知所有的await
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        locker.unlock();
    }
}

四、原子类atomic包

1. atomic包内容

基本数据类型:AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray

2. 用法

以AtomicInteger为例,主要方法有:

  1. addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
  2. incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
  3. getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
  4. getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;

3. 原理——CAS

在下面的乐观锁原理中。

五、volatile关键字原理

1. 作用一:保持内存可见性

内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。

Java通过几种原子操作完成工作内存主内存的交互:

  1. lock:作用于主内存,把变量标识为线程独占状态。
  2. unlock:作用于主内存,解除独占状态。
  3. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  4. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  5. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  6. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  8. write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

volatile如何保持内存可见性?

volatile的特殊规则就是:

  • read、load、use动作必须连续出现
  • assign、store、write动作必须连续出现

所以,使用volatile变量能够保证:

  • 每次读取必须先从主内存刷新最新的值
  • 每次写入必须立即同步回主内存当中

也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

2. 作用二:防止指令重排

volatile关键字通过“内存屏障”来防止指令被重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。然而,对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

六、单例模式的两种实现

1. 方式一:饿汉式

这个版本以短小精悍为特点,在程序执行的一开始,就将自身实例化。缺点是不使用的情况下就开始占用内存。优点也很明显,不需要考虑多线程下安全性。

class Singleton {
    private static Singleton singleton = new Singleton();

    public static Singleton getInstance(){
        return singleton;
    }

    private Singleton(){}

    public void display(){
        System.out.println("I am singleton.");
    }
}

2. 方式二:懒汉式

这个版本可以在需要的时候对对象进行实例化,缺点就是多线程不安全。

class SingletonLazy {
    private static SingletonLazy singleton;

    public static SingletonLazy getInstance(){
        if(singleton==null){
            singleton = new SingletonLazy();
        }
        return singleton;
    }

    private SingletonLazy(){ }

    public void display(){
        System.out.println("I am a hanger singleton.");
    }
}

3. 方式三:多线程版本

因此,在多线程环境下,需要对上面的版本进行修改。需要注意要有两次判断是否为空。

class SingletonSync {
    public static CountDownLatch latch = new CountDownLatch(1);
    private static SingletonSync singleton;

    public static SingletonSync getInstance(){
        if(singleton==null){
            synchronized (SingletonSync.class){
                if(singleton == null){
                    singleton = new SingletonSync();
                }
            }
        }
        latch.countDown();
        return singleton;
    }

    private SingletonSync(){ }
}

七、乐观锁和悲观锁

1. 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

使用场景:如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

2. 乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

使用场景:乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

~~实现原理~~

算法1:版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

 

算法2:CAS算法

CAS(Compare-And-Swap,比较和替换)。其具有三个操作数:内存地址V,旧的预期值A,新的预期值B当V中的值和A相同时,则用新值B替换V中的值,否则不执行更新。(PS:上述的操作是原子性的,因为过程是:要么执行更新,要么不更新)

  在JDK1.5新增的java.util.concurrent(J.U.C) 就是建立在CAS操作上的。CAS是一种非阻塞的实现(PS:乐观锁采用一种 “自旋锁”的技术,其原理是:如果存在竞争,则没有获得资源的线程不立即挂起,而是采用让线程执行一个忙循环(自旋)的方式,等待一段时间看是否能获得锁,如果超出规定时间再挂起),所以J.U.C在性能上有很大的提升。下面以J.U.C下的AtomicInteger的部分源码为例,看一下CAS的过程究竟如何。

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;

    public final int get() {
        return value;
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  // CAS操作
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
}

以上截取自AtomicInteger的部分源码。CAS操作的核心就在 getAndIncrement()方法中,在此方法调用的compareAndSet(int expect , int update) 的两个参数可知,当current值符合expect时,用next替换了current。如果不符合,则在for中不断尝试知道成功为止。在这里的步骤,就相当于一个原子性的 ++i 了。(PS: 单纯的 ++i 不具备原子性)

  其中,compareAndSet方法的实现得益于硬件的发展:多条步骤的操作行为可以通过一条指令完成。(在此是利用JNI来完成CPU指令的操作)

CAS存在的问题:

1. ABA问题

 ABA问题值,内存地址V的值是A,线程one从V中取出了A,此时线程two也从V中取出了A,同时将A修改为B,但是又因为一些原因修改为A。而此时线程one仍看到V中的值为A,认为没有发生变化,此为ABA问题。解决ABA问题一种方式是通过版本号(version)。每次执行数据修改时,都需要带上版本号,如:1A,2B,3A。通过比较版本号可知是否有发生过操作,也就解决了ABA问题。  

2. 未知的等待时长

  因为CAS采取失败重试的策略,所以不确定会发生多少次循环重试。如果在竞争激烈的环境下,其重试次数可能大幅增加。此时效率也就降低了。

八、CountDownLatch、Semaphore和CyclicBarrier

1. CountDownLatch

CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。

java 多线程并发有序执行 java的并发,多线程,线程模型_公平锁_02

在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作;典型的应用如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。

2. Semaphore

java 多线程并发有序执行 java的并发,多线程,线程模型_数据_03

Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:

用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。

3. CyclicBarrier

CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。类似于CountDownLatch,它也是通过计数器来实现的。当某个线程调用await方法时,该线程进入等待状态,且计数器加1,当计数器的值达到设置的初始值时,所有因调用await进入等待状态的线程被唤醒,继续执行后续操作。因为CycliBarrier在释放等待线程后可以重用,所以称为循环barrier。CycliBarrier支持一个可选的Runnable,在计数器的值到达设定值后(但在释放所有线程之前),该Runnable运行一次,注,Runnable在每个屏障点只运行一个。

java 多线程并发有序执行 java的并发,多线程,线程模型_公平锁_04

使用场景类似于CountDownLatch与CountDownLatch的区

  • CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作,描述的是1个线程或N个线程等待其他线程的关系。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作,描述的多个线程内部相互等待的关系。
  • CountDownLatch是一次性的,而CyclicBarrier则可以被重置而重复使用。

九、死锁及其避免方式

 

十、HashMap和Hashtable