《剑指Java面试-Offer直通车》--Java多线程与并发

目录

一、进程和线程的区别

进程和线程的由来

进程和线程的区别

Java进程和线程的关系

二、线程的start和run方法的区别?

三、Thread和Runnable的关系?

四、如何实现处理线程的返回值?

如何给run()方法传参?

如何实现处理线程的返回值?

五、线程的状态

sleep和wait的区别

notify和notifyall的区别

yield函数

interrupt函数

线程的状态以及状态之间的转换

六、synchronized

底层实现原理

Java对象头

Monitor

锁优化

自旋锁

自适应自旋锁

锁消除

锁粗化

偏向锁

轻量级锁

锁的内存语义

偏向锁、轻量级锁、重量级锁汇总

synchronized和ReentrantLock的区别

七、JMM的内存可见性

Java内存模型JMM

JMM中的主内存(即堆空间)

JMM中的工作内存(即本地内存,或线程栈)

JMM与Java内存区域划分(即Java内存结构)是不同的概念层次

主内存与工作内存的数据存储类型以及操作方式归纳

JMM如何解决可见性问题

指令重排序

happens-before

happens-before的八大原则

happens-before的概念

八、volatile

volatile变量为何立即可见?

volatile变量如何禁止重排序优化?

volatile和synchronized的区别

九、CAS

十、线程池

Executors类创建不同的线程池

Fork/Join框架

工作窃取算法

为什么要使用线程池?

Executor的框架

ThreadPoolExecutor

ThreadPoolExecutor的构造器参数

任务拒绝策略

线程池的状态

工作线程的生命周期 

线程池的大小如何选定?

推荐资料


一、进程和线程的区别

进程:进程百度百科

线程:线程百度百科

计算机组成结构:计算机组成及层次结构

  • 进程和线程的由来

计算机的出现是为了解决复杂的数学计算问题。

1)最初计算机只能接收一些特定的指令,用户输入一个指令,计算机就做一个操作。当用户在思考或输入数据时,计算机就等待,效率低下。

2)为了提升计算机的执行效率,不用等待用户输入,把一系列需要执行的指令预先写下来,形成一个清单,一次性交给计算机。计算机不断读取指令进行相应的操作,批处理操作系统诞生。用户可以将需要执行的多个程序写在磁带上,交由计算机读取并逐个执行这些程序,并将输出结果写到另一个磁带上(这里的磁带相当于现在的磁盘)。

批处理操作系统也遇到问题。当有两个任务A和B,任务A在执行到一半时需要读取大量的数据输入,即所谓的IO操作。此时CPU只能等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。可以让任务A读取数据时任务B去执行,任务A读取完数据后任务B暂停让任务A继续执行。这样有一个问题,原来是一个程序在计算机里运行,内存只有一个程序的运行数据,想要任务A执行IO操作时,任务B抢占CPU执行,内存中要装入多个程序.如何处理?多个程序中的数据如何辨别?当一个程序运行暂停后,如何恢复到之前所执行的状态?进程应运而生。

3)进程对应一个程序,每个进程对应一定的内存地址空间,并且只能使用自己的内存空间。各个进程互不干扰,进程保存了程序每个时刻的运行状态,为进程切换提供了可能。进程暂停时会保存当前进程的状态,比如进程标识、进程使用的资源等。在下一次等到IO设备完成作业输出结果重新切换回来时,根据之前保存的状态恢复,然后继续执行,进程让操作系统的并发成为可能。

虽然并发从宏观上看有多个任务在执行,但是事实上,对于单核CPU的机器来说,任一个具体的时刻,只有一个任务在占用CPU资源。单核情况下让用户看起来像同一时刻并发执行多个任务是CPU分配给单一任务的时间片很短,任务切换的频次高,造成所有任务都并发执行的假象。

4)一个进程在一段时间内只做一件事情,如果一个进程有多个子任务,只能逐个的执行这些子任务,往往子任务之间不存在顺序上的依赖,可以并发执行。子任务共享进程的内存资源,属于同一个进程的子任务切换不需要切换页目录以使用新的地址空间,为子任务间更快速切换提供了可能,人们便发明了线程,让一个线程去执行一个子任务,一个进程就包含多个线程,每个线程负责独立的子任务,达到提升实时性的效果,线程让进程的内部并发成为可能。

  • 进程和线程的区别

进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程是资源分配的基本单位,所有与该进程相关的资源,都被记录在进程控制块PCB中,以表示该进程拥有这些资源或者正在使用它们。


进程是抢占处理机的调度单位,线程属于某个进程,共享其资源。进程拥有一个完整的虚拟内存地址空间,当进程发生调度的时候,不同的进程拥有不同的虚拟地址空间,而同一进程内不同线程共享同一地址空间;与进程相对应,线程与资源分配无关,它属于某一个进程,并与进程内的其它线程一起共享进程的资源。

线程只由堆栈、寄存器、程序计数器和线程计数表TCB组成。

总结:

1)线程不能看做独立应用,而进程可看做独立应用。(操作系统并没有将多个线程看作多个独立的应用来实现进程的调度和管理以及资源分配)

2)进程有独立的地址空间,互不影响;线程只是一个进程中的不同执行路径。(一个进程奔溃后,在保护模式下,不会对其他进程产生影响;如果某个线程挂掉,它所在的进程也会挂掉)

3)线程有自己的堆栈和局部变量,但线程没有独立的地址空间,多进程的程序比多线程程序健壮。

4)进程的切换比线程的切换开销大。(如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程,每个独立的线程有个程序运行的入口、顺序执行序列和程序的出口。但是线程不能独立执行,必须依存于某一个应用程序当中,由应用程序提供对多个线程的执行控制)

  • Java进程和线程的关系

1)Java对操作系统提供的功能进行封装,包括进程和线程。

2)运行一个Java程序会产生一个Java进程,每个Java进程包含至少一个线程。

3)每个进程对应一个JVM实例,每个JVM实例唯一对应一个堆,多个线程共享JVM里面的堆,每一个线程都有自己私有的栈。

4)Java采用单线程编程模型,程序会自动创建主线程。(自己的程序中如果没有主动创建线程的话,程序会自动创建一个线程,这个线程就是主线程。因此在编程的时候,将耗时的操作放入子线程中进行,以避免阻塞主线程,影响用户体验)

5)主线程可以创建子线程,原则上要后于子线程完成执行。(Java程序启动时,主线程立刻运行。主线程是产生其他子线程的线程,通常是最后完成执行,因为它需要执行各种关闭动作)

//打印当前线程的名字
public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
}

运行结果为main

Java采用单线程编程模型,虽然只有一个线程执行任务,JVM中并不是只有一个线程,JVM实例在创建的时候,同时会创建许多其他的线程(比如垃圾收集器线程),所以JVM是多线程的。

 

二、线程的start和run方法的区别?

调用start()方法会创建一个新的子线程并启动;run()方法只是Thread的一个普通方法的调用(还是在主线程里执行)。

eg:

public class MyThread {
    public static void attack(){
        System.out.println("attack");
        System.out.println("current thread is:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t=new Thread(){
            public void run(){
                attack();
            }
        };
        System.out.println("current main thread is:"+Thread.currentThread().getName());
        t.run();
    }
}

运行结果为:

current main thread is:main
attack
current thread is:main

public class MyThread {
    public static void attack(){
        System.out.println("attack");
        System.out.println("current thread is:"+Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t=new Thread(){
            public void run(){
                attack();
            }
        };
        System.out.println("current main thread is:"+Thread.currentThread().getName());
        t.start();
    }
}

运行结果:

current main thread is:main
attack
current thread is:Thread-0

调用run的时候会引用主线程来执行方法,start的时候会用非main的线程执行方法。

 

三、Thread和Runnable的关系?

Thread是一个类,Runnable是一个接口,Thread类实现了Runnable接口。

eg:

Thread实现多线程

public class ThreadDemo extends Thread{
    private String name;
    public ThreadDemo(String name){
        this.name=name;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("Thread start "+this.name+",i"+i);
        }
    }
}
public class ThreadDemoTest {
    public static void main(String[] args) {
        ThreadDemo t1=new ThreadDemo("Thread1");
        ThreadDemo t2=new ThreadDemo("Thread2");
        ThreadDemo t3=new ThreadDemo("Thread3");
        t1.start();
        t2.start();
        t3.start();
    }
}

 运行结果:

Thread start Thread3,i0
Thread start Thread1,i0
Thread start Thread2,i0
Thread start Thread1,i1
Thread start Thread3,i1
Thread start Thread1,i2
Thread start Thread1,i3
Thread start Thread1,i4
Thread start Thread1,i5
Thread start Thread1,i6
Thread start Thread1,i7
Thread start Thread1,i8
Thread start Thread1,i9
Thread start Thread3,i2
Thread start Thread3,i3
Thread start Thread3,i4
Thread start Thread3,i5
Thread start Thread3,i6
Thread start Thread3,i7
Thread start Thread3,i8
Thread start Thread3,i9
Thread start Thread2,i1
Thread start Thread2,i2
Thread start Thread2,i3
Thread start Thread2,i4
Thread start Thread2,i5
Thread start Thread2,i6
Thread start Thread2,i7
Thread start Thread2,i8
Thread start Thread2,i9

Runnable实现多线程

public class RunnableDemo implements Runnable {
    private String name;
    public RunnableDemo(String name){
        this.name=name;
    }
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("Thread start"+this.name+",i"+i);
        }
    }
}
public class RunnableDemoTest {
    public static void main(String[] args) {
        RunnableDemo r1=new RunnableDemo("Runnable1");
        RunnableDemo r2=new RunnableDemo("Runnable2");
        RunnableDemo r3=new RunnableDemo("Runnable3");
        Thread t1=new Thread(r1);
        Thread t2=new Thread(r2);
        Thread t3=new Thread(r3);
        //Runnable没有start方法,先创建线程,把Runnable子类的实例传进去
        t1.start();
        t2.start();
        t3.start();
    }
}

 运行结果:

Thread start Runnable1,i0
Thread start Runnable3,i0
Thread start Runnable2,i0
Thread start Runnable2,i1
Thread start Runnable2,i2
Thread start Runnable2,i3
Thread start Runnable2,i4
Thread start Runnable2,i5
Thread start Runnable2,i6
Thread start Runnable2,i7
Thread start Runnable2,i8
Thread start Runnable2,i9
Thread start Runnable3,i1
Thread start Runnable3,i2
Thread start Runnable3,i3
Thread start Runnable3,i4
Thread start Runnable3,i5
Thread start Runnable3,i6
Thread start Runnable3,i7
Thread start Runnable3,i8
Thread start Runnable3,i9
Thread start Runnable1,i1
Thread start Runnable1,i2
Thread start Runnable1,i3
Thread start Runnable1,i4
Thread start Runnable1,i5
Thread start Runnable1,i6
Thread start Runnable1,i7
Thread start Runnable1,i8
Thread start Runnable1,i9

Thread是实现了Runnable接口的类,通过start给Runnable的run方法赋值上多线程特性。因为Java类的单一继承原则,为了提升系统可扩展性,推荐通过使业务类实现Runnable接口将业务逻辑封装在run方法里。

Java创建线程的方式

 

四、如何实现处理线程的返回值?

  • 如何给run()方法传参?

和线程相关的业务逻辑需要放入到run()方法里面,但是run方法是没有参数的,并且也没有返回值的。给run()方法传参实现方式主要有三种:

1)构造函数传参

2)成员变量传参,通过set方法进行传参

3)回调函数传参

  • 如何实现处理线程的返回值?

有的程序的执行是依赖于子任务的返回值进行的,当子任务交给子线程去完成的时候,是需要获取到它们的返回值。获取子线程返回值的方式主要有三种:

1)主线程等待法,即让主线程循环等待,直到目标子线程返回值为止。主线程等待实现简单,缺点是需要自己实现循环等待的逻辑,但是如果等待的变量一多,代码就会显得异常的臃肿;而且需要循环多久是不确定的,无法做到精准的控制。

public class CycleWait implements Runnable{
    private String value;
    public void run(){
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        value="we have data now";
    }

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw=new CycleWait();
        Thread t=new Thread(cw);
        t.start();
        while(cw.value==null){
            Thread.currentThread().sleep(100);
        }
        System.out.println("value:"+cw.value);
    }
}

运行结果:value:we have data now (主线程没有休眠则运行结果为value:null)

2)使用Thread类的join()阻塞当前线程以等待子线程处理完毕。join方法可以阻塞调用此方法的线程即这里可以阻塞主线程,直到join方法所在的线程执行完毕为止。此方法比主线程等待法做到更精准的控制,实现起来简单,缺点是粒度不够细。

    public static void main(String[] args) throws InterruptedException {
        CycleWait cw=new CycleWait();
        Thread t=new Thread(cw);
        t.start();
//        while(cw.value==null){
//            Thread.currentThread().sleep(100);
//        }
        t.join();
        System.out.println("value:"+cw.value);
    }

3)通过Callable接口实现,通过FutureTask或者线程池获取。JDK5之后新增了Callable接口,执行了Callable任务之后,可以获取一个Future的对象,在该对象上调用get方法就可以获取到Callable任务返回的对象。关于通过Callable接口实现的方式获取线程的返回值有两种方式来实现,第一种是通过FutureTask类获取,第二种是通过线程池获取。

Future Task

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception{
        String value="test";
        System.out.println("Ready to work");
        Thread.currentThread().sleep(5000);
        System.out.println("task done.");
        return value;
    }
}
public class FutureTaskDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //重点关注构造函数、isDone()、get()、有参get()方法
        FutureTask<String> task=new FutureTask<String>(new MyCallable());
        //FutureTask实现RunnableFuture接口,RunnableFuture接口继承Runnable接口
        new Thread(task).start();
        if(!task.isDone()){
            System.out.println("task has not finished,please wait!");
        }
        System.out.println("task return:"+task.get());
    }
}

运行结果:

task has not finished,please wait!
Ready to work

(间隔5秒)
task done.
task return:test

线程池

MyCallable类同上

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService newCachedThreadPool= Executors.newCachedThreadPool();
        Future<String> future=newCachedThreadPool.submit(new MyCallable());
        if(!future.isDone()){
            System.out.println("task has not finished,please wait!");
        }
        try {
            System.out.println("task return:"+future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            newCachedThreadPool.shutdown();
        }
    }
}

运行结果:

task has not finished,please wait!
Ready to work

(间隔5秒)
task done.
task return:test

 

五、线程的状态

Thread源码枚举类型State中,包括六种状态New、Runnable、Waiting、Timed Waiting、 Blocked、Terminated。

1)新建New状态,创建后尚未启动的线程的状态,即新创建了一个线程,但是还没有调用start方法。

2)运行Runnable状态,包括操作系统线程状态中的Running和Ready。处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。比如线程对象创建后,调用了该对象的start方法之后,这个时候线程处于Runnable状态。由于该状态分为两个子状态Running和Ready,处于Running的线程位于可运行线程之中,等待被线程调度选中,获取CPU的使用权;处于Ready状态的线程位于线程池中,等待被线程调度选中,获取CPU的使用权。处于Ready状态的线程在获得CPU时间后,就变为Running状态的线程。

3)无限期等待Waiting,不会被分配CPU执行时间,需要其它线程显式的唤醒。以下方法会让线程陷入无限期等待中:

    a)没有设置Timeout参数的Object.wait()方法

    b)没有设置Timeout参数的Thread.join()方法

    c)LockSupport.park()方法

4)限期等待Timed Waiting,处于这种状态的线程,不会被分配CPU执行时间,不过无需等待其它线程显式唤醒,在一定时间后会由系统自动唤醒。以下方法会让线程进入限期等待中:

    a)Thread.sleep()方法

    b)设置Timeout参数的Object.wait()方法

    c)设置Timeout参数的Thread.join()方法

    d)LockSupport.parkNanos()方法

    e)LockSupport.parkUntil()方法

5)阻塞 Blocked,等待获取排它锁。阻塞状态和等待状态的区别是,阻塞状态在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间或者有唤醒动作的时候发生,在程序等待进入同步区域的时候,线程将进入Blocked状态。比如,当某个线程进入synchronized关键字修饰的方法或者代码块的时候,即获取锁执行的时候,其它想进入此方法或者代码块的线程就只能等着,它们的状态便是Blocked。

6)结束Terminated状态,已终止线程的状态,线程已经结束执行。当线程的run方法完成时,或者主线程的main方法完成时,我们就认为它终止了,这个线程对象也许是活的,但是它已经不是一个单独执行的线程,线程一旦终止了,就不能再复生,在一个终止的线程调用start方法会抛出异常。

  • sleep和wait的区别

基本的差别:

sleep()方法是Thread类的方法,wait()方法是Object类中定义的方法。

sleep()方法可以在任何地方使用。wait()方法只能在synchronized方法或者synchronized块中使用(已经获取锁才能释放锁)。

最本质的差别:

Thread.sleep()方法只会让出CPU,不会导致锁行为的变化。(如果当前线程是拥有锁的,那么Thread.sleep()方法不会让线程释放锁,而只会主动让出CPU。让出CPU之后,CPU就可以去执行其它任务了)

Object.wait()方法不仅让出CPU,还会释放已经占有的同步资源锁,以便其它在等待该资源的线程得到该资源进而去运行。

public class WaitSleepDemo {
    public static void main(String[] args) {
        // 创建一个不可变的Object对象
        final Object lock = new Object();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 线程A等待获取锁lock,获取同步锁才可以执行代码块里面的逻辑
                System.out.println("thread A is waiting to get lock.");
                synchronized (lock) {
                    try {
                        // 获取到了锁lock
                        System.out.println("thread A get lock.");
                        // 模拟程序执行
                        Thread.sleep(20);
                        System.out.println("Thead A do wait method.");
                        // 调用wait方法如果不传入参数就进入无限期等待
                        // 如果传入1000就等待一秒自动被唤醒,进入限期等待状态。
                        lock.wait(1000);
                        System.out.println("Thread A is done.");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        //两个线程执行的顺序不一定,为了使第一个线程先执行,此处休眠10秒钟。
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                // 线程B等待获取锁lock
                System.out.println("thread B is waiting to get lock.");
                synchronized (lock) {
                    try {
                        // 获取到了锁lock
                        System.out.println("thread B get lock.");
                        System.out.println("Thead B is sleeping 10 ms.");
                        // 模拟程序执行
                        Thread.sleep(10);
                        System.out.println("Thread B is done.");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

运行结果:

thread A is waiting to get lock.
thread A get lock.
thread B is waiting to get lock.

(线程A没有释放lock,线程B进入阻塞状态,不能执行里面的逻辑)
Thead A do wait method.
thread B get lock.
Thead B is sleeping 10 ms.
Thread B is done.
Thread A is done.

public class WaitSleepDemo {
        public static void main(String[] args) {
            final Object lock = new Object();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread A is waiting to get lock.");
                    synchronized (lock) {
                        try {
                            System.out.println("thread A get lock.");
                            Thread.sleep(20);
                            System.out.println("Thead A do wait method.");
                            //由上面代码的lock.wait改为Thread.sleep
                            Thread.sleep(1000);
                            System.out.println("Thread A is done.");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread B is waiting to get lock.");
                    synchronized (lock) {
                        try {
                            System.out.println("thread B get lock.");
                            System.out.println("Thead B is sleeping 10 ms.");
                            //由上面代码的Thread.sleep改为lock.wait
                            lock.wait(10);
                            System.out.println("Thread B is done.");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
}

运行结果:

thread A is waiting to get lock.
thread A get lock.
thread B is waiting to get lock.
Thead A do wait method.
Thread A is done.
thread B get lock.
Thead B is sleeping 10 ms.
Thread B is done.

  • notify和notifyall的区别

wait可以通过notify和notifyall去唤醒。

了解两者区别先了解两个概念:锁池EntryList和等待池WaitSet。对于Java虚拟机中运行程序的每个对象而言都有两个概念,锁池和等待池,这两个池与Object类的wait、notify、notifyAll三个方法以及synchronized相关。

锁池:假设线程A已经拥有了某个对象(注意不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法(或者synchronized块)之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就会被阻塞,进入了该对象的锁池中等待锁的释放。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A进入到该对象的等待池中,进入等待池的中的线程不会去竞争该对象的锁。

区别

notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁已经呆在锁池中的线程只能等待其他机会去获取锁,不能主动回到等待池中;notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。

public class NotificationDemo {
    // volatile修饰的成员变量,表示的是多个线程对其进行修改的时候,一旦线程A对其进行修改,其它线程都可以立即看到线程A对它的改动。
    private volatile boolean go = false;

    public static void main(String args[]) throws InterruptedException {
        // 创建一个不可变的对象实例
        final NotificationDemo notificationDemo = new NotificationDemo();

        // 等待线程,使线程进入等待状态
        Runnable waitTask = new Runnable() {
            @Override
            public void run() {
                try {
                    notificationDemo.shouldGo();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        // 唤醒线程的线程
        Runnable notifyTask = new Runnable() {
            @Override
            public void run() {
                notificationDemo.go();
                System.out.println(Thread.currentThread().getName() + " finished Execution");
            }
        };

        // 创建四个线程
        Thread t1 = new Thread(waitTask, "WT1"); //wait等待线程
        Thread t2 = new Thread(waitTask, "WT2"); //wait等待线程
        Thread t3 = new Thread(waitTask, "WT3"); //wait等待线程
        Thread t4 = new Thread(notifyTask, "NT1"); //notify唤醒线程

        t1.start();
        t2.start();
        t3.start();

        // 确保等待线程能够顺利的开启,程序休眠200毫秒
        Thread.sleep(200);

        t4.start();
    }

    /*
     * wait and notify can only be called from synchronized method or bock
     *
     * synchronized方法,需要获取到同步锁才可以执行里面的逻辑
     */
    private synchronized void shouldGo() throws InterruptedException {
        // go默认是false
        while (go != true) {
            System.out.println(Thread.currentThread()  + " is going to wait on this object");
            wait(); 
            System.out.println(Thread.currentThread() + " is woken up");
        }
        go = false; //resetting condition
    }

    /*
     * both shouldGo() and go() are locked on current object referenced by "this" keyword
     */
    private synchronized void go() {
        while (go == false) {
            System.out.println(Thread.currentThread()  + " is going to notify all or one thread waiting on this object");
            go = true;
            //notify(); // only one out of three waiting thread WT1, WT2,WT3 will woke up
            notifyAll(); // all waiting thread  WT1, WT2,WT3 will woke up
        }
    }
}

notify执行结果:

(3个waitTask线程以此获取到锁,多线程执行不分先后顺序,进入wait状态,进入等待池中)

Thread[WT1,5,main] is going to wait on this object
Thread[WT2,5,main] is going to wait on this object
Thread[WT3,5,main] is going to wait on this object

(t4唤醒线程,notify只有其中一个线程被唤醒)
Thread[NT1,5,main] is going to notify all or one thread waiting on this object
Thread[WT1,5,main] is woken up
WT1 finished Execution
NT1 finished Execution

notifyall执行结果:

Thread[WT1,5,main] is going to wait on this object
Thread[WT2,5,main] is going to wait on this object
Thread[WT3,5,main] is going to wait on this object
Thread[NT1,5,main] is going to notify all or one thread waiting on this object
Thread[WT3,5,main] is woken up

(只有1个线程完成执行,线程执行后go的值改为false,其他两个线程进入无限等待状态,notifyTask执行完成,没有线程去唤醒它们)
WT3 finished Execution
Thread[WT2,5,main] is woken up
Thread[WT2,5,main] is going to wait on this object
Thread[WT1,5,main] is woken up
Thread[WT1,5,main] is going to wait on this object
NT1 finished Execution

  • yield函数

当调用Thread.yield()方法的时候,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。

yield方法对锁的行为不会有影响的,不会让当前线程让出锁。

public class YieldDemo {
    public static void main(String[] args) {
        Runnable yieldTask = new Runnable() {

            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                    if (i == 5) {
                        Thread.yield();
                    }
                }
            }
        };

        Thread t1 = new Thread(yieldTask, "A");
        Thread t2 = new Thread(yieldTask, "B");
        t1.start();
        t2.start();
    }
}

执行结果可能类似A1~A10 B1~B10,也可能类似B1~B5 A1~A5...

  • interrupt函数

已经被抛弃的方法:

1)通过调用stop()方法停止线程。可以通过一个线程停止另外一个线程,这种方法太过暴力,也不安全。比如线程A调用线程B的stop方法,去停止线程B,调用这个方法的时候,线程A其实并不知道线程B执行的具体情况,这种突然间的停止会导致线程B的一些清理工作无法完成。还有一个情况就是执行stop方法后,线程B会马上释放锁,有可能会引发数据不同步的问题。

2)通过调用suspend()方法和resume()方法。

目前使用的方法:

调用Interrupt()方法,通知线程应该中断了。含义是通知线程应该中断了,该线程到底是中断还是继续执行,应该由这个线程自己去处理。

        a)如果线程处于被阻塞状态,例如sleep、wait、join状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

        b)如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响。

Interrupt()方法并不能真正的中断线程,需要被调用的线程配合中断。在正常运行任务的时候,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。在调用阻塞方法的时候,正确去处理InterruptedException异常,例如,在catch异常后就结束线程。

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable interruptTask=new Runnable() {
            @Override
            public void run() {
                int i=0;
                try {
                    //在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志位就自行停止线程
                    while(!Thread.currentThread().isInterrupted()){
                        Thread.sleep(100);
                        i++;
                        System.out.println(Thread.currentThread().getName()+"("+Thread.currentThread().getState()+") loop"+i);
                    }
                } catch (InterruptedException e) {
                    //在调用阻塞方法时正确处理InterruptedException异常(例如Catch异常后结束线程)
                    System.out.println(Thread.currentThread().getName()+"("+Thread.currentThread().getState()+") catch InterruptedException");
                }
            }
        };
        Thread t1=new Thread(interruptTask,"t1");
        System.out.println(t1.getName()+"("+t1.getState()+")"+"is new.");

        t1.start();
        System.out.println(t1.getName()+"("+t1.getState()+")"+"is start.");

        //主线程休眠300ms,主线程给t1发中断指令,查看t1的状态
        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName()+"("+t1.getState()+")"+"is interrupted.");

        //主线程休眠300ms,再次查看t1的状态
        Thread.sleep(300);
        System.out.println(t1.getName()+"("+t1.getState()+")"+"is interrupted now.");
    }
}

t1(NEW)is new.
t1(RUNNABLE)is start.
t1(RUNNABLE) loop1
t1(RUNNABLE) loop2
t1(TIMED_WAITING)is interrupted.
t1(RUNNABLE) catch InterruptedException
t1(TERMINATED)is interrupted now.

  • 线程的状态以及状态之间的转换

1)新建:通过实现Runnable接口或者继承Thread类可以得到一个线程类,通过new一个线程实例就进入了new即新建状态。

2)可运行:调用线程start()方法,就进入到了可运行runnable状态。若此时处于runnable状态的线程被OS选中,并获得了时间片之后就会进入running状态,runnable和running状态仅仅是逻辑上的划分。

3)运行中:如果running状态的线程调用了yield()方法可能会让出CPU回到runnable状态,这取决于操作系统的调度,yield只是起到了一个建议的作用。如果时间片用完了,线程还没有结束的话也会进入到runnable状态。

4)阻塞:如果处于running状态的线程又等待用户输入或者调用sleep()方法则会进入阻塞状态。此时只会让出CPU,如果当前线程已经获得锁的话,是不会对锁的占用有任何影响,即不会释放已经获得的锁。

5)锁池:此外,处于running状态、runnable状态的线程执行synchronized方法或者方法块的时候,发现并未获取到相应的锁,也会进入到阻塞的状态,同时会被放入到锁对象的锁池当中。

6)等待队列:如果处于running状态运行中的线程,调用了wait方法,就会进入到限期或者非限期的等待状态,同时会被放入到锁对象的等待队列当中。

7)等待队列 -> 锁池 -> 可运行: 处于等待队列中的线程如果wait时间到了或者被其它线程调用notify或者notifyall去唤醒的话,则会被放入到锁池当中。之后,位于锁池中的线程一旦获得了锁,则会再次进入可运行runnable状态当中,被OS选中之后,就会进入到running状态运行状态。

8)死亡:最后处于running状态的线程,在方法执行完毕或者异常退出,该线程就会结束,进入死亡terminated状态。

 

六、synchronized

线程安全问题的主要诱因:

1)存在共享数据(也称临界资源)

2)存在多条线程共同操作这些共享数据

解决问题的根本方法:

同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。便引入互斥锁。

互斥锁的特性

1)互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性。

2)可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

synchronized锁的不是代码,锁的是对象。JVM内存模型里的堆是线程共享的,恰当合理的给对象上锁,是解决线程安全问题的关键。

下面根据获取的锁的分类对synchronized用法进行讲解,获取的锁的分类包括获取对象锁和获取类锁。

获取对象锁的两种用法

1)同步代码块(synchronized(this),synchronized(类实例对象)),获取对象的锁去执行接下来花括号的内容,锁是小括号()中的实例对象。

2)同步非静态方法(synchronized method),锁是当前对象的实例对象。

获取类锁的两种方法

1)同步代码块(synchronized(类.class),锁是小括号()中的类对象(Class对象))

2)同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)

对象锁和类锁的总结

1)有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块。

2)若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问的对象的同步代码块的线程被阻塞

3)若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞

4)若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然

5)同一个类的不同对象的对象锁互不干扰

6)类锁,由于也是一种特殊的对象锁,因此表现的同上述1、2、3、4一致;而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁,将会是同步的

7)类锁和对象锁互不干扰

底层实现原理

实现synchronized的基础:Java对象头、Monitor

  • Java对象头

HotSpot虚拟机中Java对象在内存中的布局主要分为三块区域:对象头、实例数据、对齐填充。实例数据、对齐填充不再展开讲。下面是对象头的讲解。

synchronized使用的锁对象是存储在Java对象头里的,由Mark Word和Class Metadata Address组成,Mark Word是实现轻量级锁和偏向锁的关键。对象头结构如下:

由于对象头的信息是与对象自身定义的数据没有关系的额外成本,考虑到JVM的空间效率,Mark Word被设计成非固定的数据结构,以便存储更多有效的数据,会根据对象本身的状态,复用自己的存储空间。如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构。轻量级锁和偏向锁是Java6后对synchronized锁优化后新增加的。Mark Word结构:

  • Monitor

每个Java对象天生自带一把看不见的锁,它叫内部锁或Monitor锁,Monitor也称为管程或监视器锁。Monitor其实是个同步工具,也可以说成是一种同步机制,通常被描述为一个对象。

上图中的重量级锁也就是通常说的synchronized的对象锁,锁的标志位是10,指针指向Moinitor对象的起始地址,每个对象都存在一个Moinitor与之关联,对象与其Moinitor之间的关系存在多种实现方式,如Moinitor可以与对象一起创建销毁,或当线程试图获取对象锁时自动生成。当一个Moinitor被某个线程使用后,便处于锁定状态。在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下:

 ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

_owner:指向持有ObjectMonitor对象的线程

_WaitSet:存放处于wait状态的线程队列

_EntryList:存放处于等待锁block状态的线程队列

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor锁)并复位变量的值,以便其他线程进入获取monitor(锁)

ObjectMonitor结构:

monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。

synchronized在字节码层面的具体语义实现eg:

public class SyncBlockTest {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }
 
    public synchronized void syncTask() {
        System.out.println("Hello Baby");
    }
}

编译得字节码文件

public com.SyncBlockTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
 
  public void syncsTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 12: 12
        line 13: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/SyncBlockTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
 
  public synchronized void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String Hello Baby
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8
}

同步代码块: 

monitorenter指向同步代码块的开始位置,首先获取PrintStream类,传入hello参数,再调用println打印hello字样。monitorexit指名同步代码块的结束位置。执行monitorenter指令时,当前线程将试图获取对象锁所对应的monitor的持有权。当对象锁的进入计数器count为0时,线程可成功的获取到monitor,并将计数器设置为1,表示取锁成功。如果当前线程在之前已经拥有对象锁的持有权,可以重入monitor;如果其他线程已经先于当前线程拥有对象锁的monitor所有权,当前线程将会被阻塞。直到持有该锁的线程执行完毕,即monitorexit被执行,执行线程将释放Monitor锁,并设置计数器为0,其它线程将有机会持有Monitor。

为了保证该方法异常时monitorenter和monitorexit指令也能正确配对执行,编译器会自动生成一个异常处理器,异常处理器声明可处理所有的异常,它的目的就是执行monitorexit指令。

重入:从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。在java中synchronized是基于原子性的内部锁机制,是可重入的。因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁是允许的,这就是synchronized的可重入性。

同步方法:

里面并没有monitorenter和monitorexit,且字节码较短,其实这里方法的同步是隐式的,是无需通过字节码指令控制,在上面可以看到一个“ACC_SYNCHRONIZED”这样的一个访问标志,用来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查ACC_SYNCHRONIZED是否被设置,如果被设置,当前线程将会持有monitor,然后再执行方法,最后不管方法是否正常完成都会释放monitor。

在方法执行期间,执行线程持有monitor,其他任何线程都无法获得同一个monitor,如果一个同步方法执行期间抛出异常,并且在方法内部无法处理此异常,该同步方法持有的monitor将在异常抛到同步方法之外时自动释放。

参考:Java中Synchornize关键字原理

锁优化

Java早期版本中synchronized是重量级锁,效率低下,依赖于底层操作系统的Mutex Lock实现。操作系统实现线程之间的切换时需要从用户态转换到核心态,时间较长。Hotspot尝试对synchronized做了很多优化,主要是为了减少重量级锁的使用。Java6以后从JVM层面做了较大优化。Hotspot虚拟机花费大量精力去实现锁优化技术,如自适应自旋(Adaptive spinning)、锁消除(Lock Eliminate)、锁串(Lock Coarsening)、轻量锁(Lightweight Locking)、偏向锁(Biased Locking)...

  • 自旋锁

许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。在多处理器环境下,可以让另一个没有获取到锁的线程等待一会,但不放弃CPU的执行时间。这个等一会但不放弃CPU执行时间的行为即所谓的自旋。通过让线程执行忙循环(类似while(true))等待锁的释放,不让出CPU。

缺点:若锁被其他线程长时间占用,会带来许多性能上的开销。因为线程自旋时始终会占用CPU的时间片,锁暂用的时间太长,自旋线程会白白消耗掉CPU资源。因此自旋等待的时间要有一定的限度。如果超过限定的次数仍没有获取到锁,就应该使用传统的方式去挂起线程。在JDK定义中,可以使用PreBlockSpin来更改。

  • 自适应自旋锁

由于每次线程需要等待的时间和次数是不固定的,PreBlockSpin想要设计的比较合理就有些困难。引入自适应自旋锁,提高并发性能。自旋次数不再固定。而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。在同一个锁对象上,自旋等待刚刚成功获取过锁并且持有锁的线程正在运行中,JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间;相反如果对于某个锁自旋很少获取到锁,以后获取锁时将可能省略自旋过程,以避免浪费处理器资源。

  • 锁消除

锁消除是JVM另一种锁优化,这种优化更彻底。在JIT编译时,对运行上下文进行扫描,去除不可能存在资源竞争的锁。这种方式可以消除不必要的锁,可以减少毫无意义的请求锁时间。

public class StringBufferWithoutSync {
    public void add(String str1, String str2) {
        //StringBuffer是线程安全
        //由于sb属于本地变量没有return出去,只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
 
    public static void main(String[] args) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa", "bbb");
        }
    }
  • 锁粗化

原则上在加同步锁时尽可能将同步块的作用范围限制到尽量小的范围,即只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,在存在锁同步时使得等待锁的线程尽可能拿到锁。如果存在一连串系列操作都对同一个对象反复加锁和解锁,即使没有线程竞争,频繁的进行互斥同步锁操作也会导致不必要的性能操作。通过扩大加锁的范围,以避免反复加锁和解锁操作。

public class CoarseSync {
    public static String copyString100Times(String target){
        int i = 0;
        // JVM会将锁粗化到操作外部,使得重复的加解锁操作只需要进行一次
        StringBuffer sb = new StringBuffer();
        while (i < 100){
            sb.append(target);
        }
 
        return sb.toString();
}

synchronized锁存在四种状态:无锁、偏向锁、轻量级锁、重量级锁,会随着竞争情况逐渐升级。 

锁膨胀的方向(即锁升级):无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。锁膨胀存在跨级现象,例如直接从无锁膨胀到重量级锁。

  • 偏向锁

大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,为了减少同一线程获取锁的代价,就会使用偏向锁

核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。即当一个线程访问同步块并获取锁时,会在对象头和栈中的锁记录里存储锁偏向的线程id,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。偏向锁不适用于锁竞争比较激烈的多线程场合。因为这种场合极有可能每次申请锁的线程都是不相同的。偏向锁失败后不会立即膨胀为重量级锁,而是先升级为轻量级锁。

  • 轻量级锁

轻量级锁是由偏向锁升级而来,偏向锁运行在一个线程进入同步块的情况下,当有第二个线程加入锁竞争时,偏向锁就会升级为轻量级锁

适用场景:线程交替执行同步块

若存在线程同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级锁的加锁过程:

1)代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示:

2)拷贝对象头中的Mark Word复制到锁记录中

3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5

4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00",即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示:

5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10",Mark Word中存储的就是指向重量级锁(互斥量)的指针,一旦升级为重量级锁,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的解锁过程:

1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word

2)如果替换成功,整个同步过程就完成了

3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁己膨胀),那就要在释放锁的同时,唤醒被挂起的线程

  • 锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

线程A释放一个锁实质上是线程A向接下来将要获取锁的某个线程发出消息,这个消息就是线程A对共享变量所做的修改。线程B获取一个锁是线程B接收之前某个线程发出的消息,这个消息指在释放锁之前对共享变量所做的修改。线程A释放锁线程B获取锁,线程通过主内存向线程B发送消息。Mark Word可以粗暴的认为是在主存,位于栈里的display Mark Word则是位于线程的本地内存中。线程A确保将Mark Word同步到display Mark Word,也就完成了对共享数据的操作,也就表明已经可以解锁并且解锁成功。

  • 偏向锁、轻量级锁、重量级锁汇总

synchronized和ReentrantLock的区别

Java5以后提供ReentrantLock即再入锁的实现,语义和synchronized基本相同,通过代码直接调用lock方法去获取,代码编写也更加灵活。位于java.util.concurrent.locks包;和CountDownLatch、FuturaTask、Semaphore一样基于AQS框架实现;能够实现比synchronized更细粒度的控制,如控制fairness;调用lock之后,必须调用unlock释放锁;在JDK6之后性能未必比synchronized高,并且也是可重入的。

ReentrantLock公平性的设置:

ReentrantLock fairlock=new ReentrantLock(true);参数为true时,倾向于将锁赋予等待时间最久的线程,即设置为所谓的公平锁,公平性是减少线程饥饿的一个办法

公平锁:获取锁的顺序按先后调用lock方法的顺序,公平锁需慎用,因为会影响性能

非公平锁:线程抢占锁的顺序不一定,与调用顺序无关,看运气

synchronized是非公平锁

ReentrantLock的好处在于将锁对象化了,因此可以实现synchronized难以实现的逻辑,例如:

1)判断是否有线程,或者某个特定线程,在排队等待获取锁

2)带超时的获取锁的尝试

3)感知有没有成功获取锁

如果说ReentrantLock将synchronized转变为了可控的对象,那么是否能将wait、notify及notifyall等方法对象化,答案是有的,即Condition:位于java.util.concurrent.locks包,可以通过ReentrantLock的newCondition方法获取该Condition对象实例

synchronized和ReentrantLock的区别:

1)synchronized是关键字,ReentrantLock是类

2)ReentrantLock可以对获取锁的等待时间进行设置,避免死锁

3)ReentrantLock可以获取各种锁的信息

4)ReentrantLock可以灵活地实现多路通知

5)内部机制:synchronized操作的是Mark Word,而ReentrantLock底层是调用Unsafe类的park方法来加锁

参考:synchronized和volatile的区别、synchronized和Lock的区别

 

七、JMM的内存可见性

Java内存模型JMM

Java内存模型(Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

由于JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(或称为栈空间),用于存储线程私有的数据。Java内存模型规定,所有变量都存在主存中。主内存是共享内存区域,所有内存都可以访问。但线程对变量的操作如读取赋值等必须在工作内存中进行。首先将变量从主内存拷贝到自己的工作内存空间中,然后对变量进行操作,操作完成后再将变量将写回主内存,不能直接操作主内存中的变量。工作内存中存储主内存中的变量的副本拷贝。工作内存是每个线程的私有区域,因此不同线程间无法访问对方的工作内存。线程间的通信即传值必须通过主内存来完成。

  • JMM中的主内存(即堆空间)

1)存储Java实例对象

2)包括成员变量、类信息、常量、静态变量等

3)属于数据共享的区域,多线程并发操作时会引发线程安全问题

  • JMM中的工作内存(即本地内存,或线程栈)

1)存储当前方法的所有本地变量信息,本地变量对其他线程不可见

2)字节码行号指示器、Native方法信息

3)属于线程私有数据区域,不存在线程安全问题

  • JMM与Java内存区域划分(即Java内存结构)是不同的概念层次

1)JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性及可见性展开的

2)两者相似点:存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度讲应该包括堆和方法区;私有数据区域从某个程度讲应该包括程序计数器、虚拟机栈、本地方法栈。

  • 主内存与工作内存的数据存储类型以及操作方式归纳

1)方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中

2)引用类型的本地变量,引用存储在工作内存中,对象实例存储在主内存中

3)实例对象的成员变量、static变量、类信息均会被存储在主内存中

4)主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存

  • JMM如何解决可见性问题

忽略硬件部分,可以理解为数据从内存加载到缓存、寄存器,运算结束写回主内存。线程共享变量时,处理器对某个变量做了修改,可能只是体现在该内核的缓存里,运行在其他内核的线程可能加载的是旧状态,可能导致一次性的问题。理论上多线程共享引入了复杂的数据依赖性,不管编译器处理器怎么做重排序,都必须数据依赖性的需求,否则就打破了数据的正确性,这就是JMM所要解决的问题。

  • 指令重排序

在执行程序时,为了提交性能,处理器和编译器常常会对指令进行重排序。指令重排序需要满足的条件:

1)在单线程在单线程环境下不能改变程序运行的结果

2)存在数据存在数据依赖关系的不允许重排序

以上两点可以归结为:无法通过happens-before原则推导出来的,才能进行指令的重排序

JVM内部的实现通常依赖于所谓的内存屏障,通过禁止某些重排序方式提供内存可见保证,也就是实现了各种happens-before的规则。与此同时,更高的复杂度在于需要尽量确保各种编译器各种体系结构的处理器能够提供一致的行为。

 

happens-before

在JVM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before的关系。happens-before原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。依靠这个原则能解决在并发环境下两个操作之间存在冲突的问题。

i=1;//线程A执行,它happens-before线程B的操作,线程A先于线程B发生

j=i;//线程B执行,j=1成立。如果不存在happens-before原则,则j=1不一定成立

  • happens-before的八大原则

1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。一段代码在单线程中执行的结果是有序的。虚拟机处理器会对指令进行重排序,但不会影响程序的执行结果,程序最后执行的结果与顺序执行的结果是一致的。这个规则只对单线程有效,多线程环境下无法保证准确性。

2)锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。一个锁处于被锁定状态,必须先执行unLock操作,才能进行lock操作。

3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。volatile保证了线程的可见性,线程先去写volatile的变量,再去读volatile的变量,写操作一定是happens-before读操作的。

4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

5)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。假设线程A和B,A先做了一些操作,然后调用B线程的interrupt()方法,B线程感知到自己的中断标识被设置时,操作A中的结果对B是可见的。

7)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。假定线程A在执行过程中,通过制定ThreadB.join()等待线程B终止,线程B在终止之前对共享变量的修改在线程A等待返回后是可见的。

8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。“结束”和“开始”表明在时间上,一个对象的构造函数必须在它的finalize()方法调用时执行完。根据这条原则,可以确保在对象的finalize()方法执行时该对象的所有field字段值都是可见的。

  • happens-before的概念

如果两个操作不满足上述任意一个happens-before规则,那么这连个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。

如果操作A happends-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。

 

八、volatile

JVM提供的轻量级同步机制,有如下两个作用:

1)保证被volatile修饰的共享变量对所有线程总是可见的。当一个线程修改了被volatile修饰的共享变量的值时,其他线程立刻感知到变动。

2)禁止指令的重排序优化

使用volatile不能保证线程安全,需要变量的操作满足原子性。

  • volatile变量为何立即可见?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中。当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么就需要从主内存中重新读取该变量。

  • volatile变量如何禁止重排序优化?

先了解一个概念内存屏障(Memory Barrier),它是CPU指令,其作用有两个:

1)保证特保证特定操作的执行顺序。通过插入内存屏障指令来禁止对内存屏障前后的指令执行重排序优化

2)保证某些变量的内存可见性。强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

volatile变量正是通过内存屏障实现其在内存上的语义即可见性和禁止重排优化。

单例的双重检测实现(线程安全的单例写法):

public class Singleton{
    private static  volatile Singleton instance;
    
    private Singleton(){}

    public static Singleton getInstance(){
        if(instance==null){
            sychronized (Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}

使用volatile禁止instance重排序。

  • volatile和synchronized的区别

1)volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止

2)volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别

3)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性

4)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞

5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

 

九、CAS

悲观锁始终假定会发生并发冲突,会屏蔽一切可能违反数据完整性的操作;乐观锁假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性,如果提交失败则重试。

synchronized属于悲观锁;CAS(Compare and Swap)属于乐观锁。

CAS是一种高效实现线程安全性的方法,支持原子更新操作,适用于计数器,序列发生器等场景。CAS操作失败时由开发者决定是继续尝试,还是执行别的操作,因此挣锁失败的线程不会被阻塞挂起。

CAS思想:CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B),执行CAS操作时将内存位置的值和预期原值进行比较,如果相匹配,处理器会自动将该位置的值更新为新值,否则处理器不做任何操作。这里内存位置的值V即主内存的值。

CAS操作应用场景的例子:当一个线程需要修改共享变量的值,完成这个操作先取出共享变量的值赋给A,基于A的值进行计算得到新值B,执行完毕需要更新共享变量的值时,可以调用CAS方法更新变量的值。

CAS多数情况下对开发者来说是透明的:J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工作,是很多线程安全程序的首选。Unsafe类虽然提供CAS服务,但因能够操纵任意内存地址读写而有隐患。Java9以后,可以使用Variable Handle API来代替Unsafe。

缺点:

1)若循环时间长,则开销很大

2)只能保证一个共享变量的原子操作。对多个共享变量,可以用锁保证原子性。

3)存在ABA问题,可以通过使用AtomicStampedReference来解决,但由于是通过版本标记来解决,所以存在一定程度的性能损耗。

 

十、线程池

在web开发中,服务器需要接收并处理请求,所以会为一个请求分配一个线程进行处理,如果并发的请求数量非常多,但每个线程执行的时间很短,就会频繁的创建和销毁线程,会大大降低系统的效率,可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间更多。

开发者利用利用Executors类创建不同的线程池满足不同场景的需求。

  • Executors类创建不同的线程池

1)newFixedThreadPool(int nThreads):指定工作线程数量的线程池。

每当一个任务创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。如果有工作线程退出,将会有新的工作线程创建,以补足nThreads的数目。

2)newCachedThreadPool():处理大量短时间工作任务的线程池,特点:

    a)试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程

    b)如果线程闲置的时间超过阈值(60s),则会被终止并移出缓存

    c)系统长时间闲置的时候,不会消耗什么资源

3)newSingleThreadExecutor():创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺序的执行各个任务,并且在任意给定时间不会有多个线程是活动的。

4)newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。前者和newSingleThreadExecutor()一样,如果线程异常结束,会有另一个线程取代它,保证顺序执行。

5)JDK8新增的newWorkStealingPool():内部会构建ForkJoinPool ,利用working-stealing算法,并行地处理任务,不保证处理顺序。

参考:Java线程池,你了解多少?

  • Fork/Join框架

Java7提供的用于并行执行任务的框架,是一个把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架是ExecutorService接口的具体实现,是为了更好的利用多处理器,为可以递归拆解成子任务的工作类型量身设计的,其目的在于能够使用所有可用的运算能力来提升应用的性能。跟Map-Reduce的原理是一样的。Fork/Join会把任务分发给线程池种的工作线程,使用工作窃取即working-stealing算法。

  • 工作窃取算法

  工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。假如我们需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

  • 为什么要使用线程池?

1)减低资源消耗,避免频繁地创建和销毁线程

2)提高线程的可管理性,使用线程池可以进行统一的分配、调优和监控。

  • Executor的框架

Executor的框架是根据一组执行策略调用、调度、执行和控制的异步任务的框架,提供一种将任务提交与任务运行分离开来的机制。

J.U.C的三个Executor接口

1)Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦。Executor接口中有唯一方法execute,对于不同的Executor实现,execute可能是创建一个新线程,并立即启动,也可能是使用已有的工作线程来运行传入的任务,也可能是根据设置线程池的容量和阻塞队列的容量来决定是否将传入的线程放入阻塞队列中,或者拒绝接收传入的线程。

2)ExecutorService:扩展了Executor接口,具备管理执行器和任务生命周期的方法,提交任务机制更完善。

3)ScheScheduleExecutorService:扩展了ExecutorService,支持Future和定期执行任务。

  • ThreadPoolExecutor

大多数应用场景下使用Executor提供的5类线程池就足够了,但还是有些场景需要直接利用ThreadPoolExecutor构造函数去创建。

  • ThreadPoolExecutor的构造器参数

int corePoolSize:核心线程数

int maximumPoolSize:最大线程数

workQueue:任务等待队列

long keepAliveTime:线程空闲存活时间

ThreadFactory threadFactory:创建新线程,默认使用Executors.defaultThreadFactory()创建线程,使用默认的ThreadFactory创建线程会使新创建的线程具有相同的优先级,并且是非守护线程。

TimeUnit unit:存活时间的单位

RejectedExecutionHandler handler:线程池的饱和策略

  • 任务拒绝策略

1)AbortPolicy:直接抛出异常,这是默认策略

2)CallerRunsPolicy:使用调用者所在的线程来执行任务

3)DiscardOldestPolicy:丢弃队列中最靠前的任务,并执行当前任务

4)DiscardPolicy:直接丢弃提交的任务

另外可以实现RejectedExecutionHandler接口来自定义handler

流程图

  • 线程池的状态

1)RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务

2)SHUTDOWN:不再接受新提交的任务,但可以处理存量任务(线程池处于running状态时调用shutdown方法会使线程池进入该状态)

3)STOP:不再接受新提交的任务,也不处理存量任务(线程池处于running或shutdown状态调用shutdownNow方法会使线程池进入该状态)

4)TIDYING:所有的任务都已终止

5)TERMINATED:terminated() 方法执行完后进入该状态

  • 工作线程的生命周期 

  • 线程池的大小如何选定?

CPU密集型任务:线程数 = 按照CPU核心数或者CPU核心数 + 1进行设定

I/O密集型任务:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均工作时间)

 

推荐资料

《Java并发编程实战》

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读