文章目录

  • 线程的三种创建方式
  • 对于FutureTask用在Thread中的理解
  • 那么FutureTask的构造类接受参数?
  • FutureTask是如何保存值的
  • FutureTask获取值
  • 更多
  • wait方法得用在同步代码块中
  • wait释放锁的问题
  • notify的使用介绍
  • join方法的中断异常
  • sleep方法介绍
  • sleep的中断异常
  • sleep方法和yield方法的区别
  • interrupt 和 interrupted 和 isInterrupted
  • 什么是线程的上下文切换
  • 死锁的产生条件
  • 避免死锁
  • 守护线程和用户线程
  • ThreadLocal的介绍
  • ThreadLocal类的简单分析
  • ThreadLocal的继承性
  • 弱引用和软引用的区别
  • java的内存模型
  • 简单描述一下内存不可见
  • synchronized的内存语义
  • volatile的介绍
  • synchronized和volatile的比较
  • 总线锁定和缓存一致性
  • MESI协议提供的四个状态
  • MESI协议约定的监听
  • ++操作在底层的一个具体过程
  • CAS介绍
  • Unsafe类的认识
  • 伪共享(缓存行)


线程的三种创建方式

  • 继承Thread类,创建线程,但由于java类继承的单一性,所以该类就无法再继承其它类。
  • 一个类实现Runnable 接口,将该类作为实例传入Thread
  • 一个类实现Callable接口,有返回值。将该类作为实例传入FutureTask,再将FutureTask的实例传入Thread。
public class demo1 {

    /**
     * 线程的三种创建方式
     */
    
    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("继承Thread并创建线程");
        }
    }
    public static class RunableTask implements Runnable{

        @Override
        public void run() {
            System.out.println("实现Runnable接口,创建线程,重写方法");
        }
    }

    public static class CallableTask implements Callable{

        @Override
        public Object call() throws Exception {
            System.out.println("实现Callable接口,重写call方法");
            return "成功";
        }
    }

    public static void main(String[] args) {
//      --------继承Thread,重写run方法--------
        MyThread myThread = new MyThread();
        myThread.start();
//      --------实现Runnable接口,重写run方法--------
//        new Thread(new RunableTask()).start();
        RunableTask task = new RunableTask();
        new Thread(task).start();
//       --------实现Callable方法,重写call方法--------
        CallableTask callableTask = new CallableTask();
        FutureTask<String> futureTask = new FutureTask<>(callableTask);
        new Thread(futureTask).start();
        try {
            System.out.println("打印返回值:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

对于FutureTask用在Thread中的理解

Thread是并不支持Callable接口的,通过下图可以发现,并没有Callable接口,那么为什么FutureTask可以传入呢?

java 并发下生成唯一的名称 java并发编程之美_java


来看一下FutureTask的结构,可以发现Future实现了RunableFuture接口,而RunableFuture接口又继承实现了Runnable和Future接口。所以FutureTask可以传入到Thread当中去。

java 并发下生成唯一的名称 java并发编程之美_多线程_02

那么FutureTask的构造类接受参数?

进入FutureTask源码,可以发现其构造类,虽然可以接受Runnable和Callable,但是最终都会被转成Callable接口,可能跟其实现了Future接口,需要有返回值有关系吧。

java 并发下生成唯一的名称 java并发编程之美_java 并发下生成唯一的名称_03

FutureTask是如何保存值的

在run方法中,通过setException和set(result)来保存值。

java 并发下生成唯一的名称 java并发编程之美_多线程_04

FutureTask获取值

通过get方法获取值,在get方法中有一个重要的方法是awaitDone

java 并发下生成唯一的名称 java并发编程之美_java_05


这个awaitDone的方法,意思就如解释的那样,等待的过程,如果成功则返回,如果中断或者超时则终止返回。它是通过判断AQS状态state,来决定的。(研究多线程,一定要注意state,AQS,CAS)

java 并发下生成唯一的名称 java并发编程之美_System_06

更多

如果想具体了解,可以看下面这篇文章
Java并发编程Future超详细教程

wait方法得用在同步代码块中

wait()方法,它是一个Object的方法。
假设有一个公共资源,和两个线程,按照下面方式启动

public class demo{
	private static List<String> list = new ArrayList<>();
	public static void main(String []args){
		Thread threadA = new Thread(()->{
			//wait,只有在当前共享变量的同步代码块中,才能用
			synchronized(list){
				System.out.println("我正在等待中");
				 try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
				System.out.println("我已经被唤醒,准备运行");
			}
		});
		Thread threadB = new Thread(()->{
			synchronized(list){
				    try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
				System.out.println("既然你偷懒了,释放锁了,那就让我来吧,等会我再叫你");
				list.notifyAll();
				System.out.println("我好了,到A你了");
			}
		});
		threadA.start();
		threadB.start();
	}
}

效果如下

java 并发下生成唯一的名称 java并发编程之美_多线程_07


如果wait,或者notify,notifyAll不放在同步代码块中,是会抛出下面这个异常的

非法监视器状态异常

java 并发下生成唯一的名称 java并发编程之美_多线程_08

wait释放锁的问题

wait方法只会释放调用wait()方法的对象的身上的锁,并不是在哪个同步代码块中调用,就释放这个同步代码块的锁。比如下面:

synchronized(resourceA){
	System.out.println("获取到资源A的锁");
	synchronized(resourceB){
		try{
			System.out.println("获取到资源B的锁");
			resourceA.wait();
			System.out.println("释放资源A的锁");
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

对于上述代码,如果启动一个线程去执行,那么这个线程在获得两个锁之后,只会释放A锁,然后就被挂起,则B锁一直被它攥在手里。别的线程将无法获得B锁。

notify的使用介绍

notify()方法也是Object的一个方法
当有线程锁的资源,调用了wait方法之后,该线程就会释放该资源的锁,并进入阻塞状态。当该资源执行notify的时候,那么其余线程将再来争夺这个资源的锁,抢到锁的才会运行,所以并不是刚刚释放资源锁的线程立马开始运行,也是要抢夺的。

join方法的中断异常

当线程A还没结束时,主线程就被结束了,在抛出异常的地方,抛出了中断异常

public class demo6 {


    public static void main(String[] args) {

        Thread threadA = new Thread(()->{
            System.out.println("线程A即将进入循环");
            for(;;){
            }
        });

        Thread mainThread = Thread.currentThread();

        Thread threadB = new Thread(()->{
            try {
                System.out.println("让主线程先往下执行到threadA.join");
                TimeUnit.SECONDS.sleep(2);
                System.out.println();
                System.out.println("现在已经2s后了,准备中断线程,即将抛出异常");
                mainThread.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        System.out.println("线程A启动");
        threadB.start();
        System.out.println("线程B启动");
        try {
            System.out.println("准备让主线程阻塞,等待threadA执行完");
            threadA.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

执行效果如下:

java 并发下生成唯一的名称 java并发编程之美_并发编程_09

sleep方法介绍

Thread中有一个静态的 sleep方法,当一个执行中的线程调用了Thread.sleep方法后,调用线程会暂时让出CPU,但是该线程所拥有的锁,仍然被其抱着。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行了。如果在睡眠期间其它线程调用了该线程的 interrupt ()方法中断了该线程,则该线程会在调用 sleep 方法的地方抛出 IntermptedException 异常而返回

public class demo7 {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(()->{
            try {
                lock.lock();
                System.out.println("threadA 获取锁,准备去睡觉");
                TimeUnit.SECONDS.sleep(2);
                System.out.println("threadA 准备醒了,.....执行完啦");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                lock.lock();
                System.out.println("threadB 获取锁,准备去睡觉");
                TimeUnit.SECONDS.sleep(2);
                System.out.println("threadB 准备醒了,.....执行完啦");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        threadA.start();
        threadB.start();

    }

}

输出效果一直如下,不会交叉改变,说明线程抱着锁去睡觉了。

java 并发下生成唯一的名称 java并发编程之美_多线程_10

sleep的中断异常

在上面的代码中,ThreadB中添加上

threadA.interrupt();

会发现报下面这个错误,在threadA睡觉的地方抛出异常

java 并发下生成唯一的名称 java并发编程之美_System_11

sleep方法和yield方法的区别

sleep方法会让当前方法阻塞,但是这个cpu并不会进行重新调度,等待睡眠时间过了之后,还是会执行当前线程。
yield方法会让出当前的cpu资源,让自己重新进入就绪状态,然后cpu就开始重新进行调度,包括让出资源的线程在内,每个线程都有机会获得cpu资源

interrupt 和 interrupted 和 isInterrupted

interrupt是Thread类中的一个普通方法,由Thread对象调用,调用后会中断调用线程。此时中断标志位为true。
interrupted是Thread类中的一个静态对象,直接由Thread类调用,调用后会返回当前线程的标志位,然后将当前中断标志位设置为false。
isInterrupted 是Thread类中的一个普通方法,用来返回当前线程的中断标志位。

可以看一个例子:

public class demo8 {
    public static void main(String[] args) throws InterruptedException {
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("start thread isInterrupted:"+Thread.currentThread().isInterrupted());
               while (!Thread.interrupted()){

               }
               System.out.println("end thread isInterrupted:"+Thread.currentThread().isInterrupted());
           }
       });
       thread.start();
       thread.interrupt();
        System.out.println("中断thread线程");
       thread.join();
        System.out.println("over");
    }
}

执行效果如下:

主线程中断thread线程后,进入,发现当前标志位是true,经过interrupted重置后改为false。

java 并发下生成唯一的名称 java并发编程之美_java_12

什么是线程的上下文切换

就是当前线程用完cpu分配的时间片后(当然也可能没用完就让出了cpu资源),需要让出cpu资源了,让出之后,cpu就要重新进行分配,将时间片分配给下一个线程。这一种时间片轮转法,让你感觉多个线程是同时执行的。这就是线程切换,从当前的线程的上下文,切换到了别的线程的上下文
但是当线程重新获取到资源时,线程是怎么知道自己之前执行到了哪里呢?
所以在切换线程的上下文时,需要保存上下文环境,等到下一次cpu资源到来时,再恢复上下文环境,继续向下执行

死锁的产生条件

  • 互斥条件:即一个资源只能由一个线程占用。
  • 请求资源和持有条件:请求着别人的资源,又不释放自己的资源
  • 不可剥夺条件:自己的资源在自己使用完前,不能被其他人抢走
  • 环路等待条件:形成A(B)《==》B(A)

避免死锁

破坏上述死锁条件中的一种,或者提前避免发生死锁。
目前只有请求并持有和环路等待条件可以破坏。
预设置好执行的顺序,可以有效的避免产生死锁。

守护线程和用户线程

java中的线程分为两种,一种是daemon线程,一种是user线程。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程。只有所有用户线程执行完的时候,JVM才能退出。守护线程,就比如JVM启动时,内部同时还启动了很多的线程,比如垃圾回收线程。守护线程并不影响JVM的退出

在java中通过如下方法创建守护线程

public static void main(String[] args){
	Thread deamon = new Threa(new Runnable(){
		@Overried
		public void run(){
		
		}
	});
	deamon.setDaemon(true);
	deamon.start();
}

ThreadLocal的介绍

在Thread类中,可以发现ThreadLocal作为ThreadLocalMap类型的变量存在着,ThreadLocalMap是一个定制化的HashMap,至于为什么用Map结构? 原因是一个线程可以关联多个ThreadLocal变量。

java 并发下生成唯一的名称 java并发编程之美_System_13


当一个线程访问ThreadLocal(执行get方法时)的时候,其实际执行的方法是,详细请看下面代码注释

public T get() {
    	//获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程持有的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //找到ThreadLocalMap中,key为当前线程所对应的的实例Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取Entry中保存着的value
                T result = (T)e.value;
                //返回该值
                return result;
            }
        }
        //如果没有ThreadLocalMap,则去初始化一个,并返回一个Value为Null的值。set方法内容跟下面这个方法几乎一样。
        //但这个方法是private的,set方法不是。
        return setInitialValue();
    }

ThreadLocal类的简单分析

  • 内部的ThreadLocalMap的key和value分别是什么?key指的是当前的线程,而value指的是当前线程对象的实例
  • 可以发现这个key用到的是WeakReference的一个方法,说明这个key是一个弱引用。之所以用弱引用的原因就是,当没有强引用指向该ThreadLocal变量时,它就可以被回收。
  • 我们调用的set方法,最终执行的是ThreadLocalMap的set方法(即创建一个Entry对象)。
  • 在ThreadLocalMap的set方法中,如果key为Null,存在一个清除key的方法 (replaceStaleEntry)。在清除key的方法中,存在一个删除key对应entry的方法 (expungeStaleEntry)
  • java 并发下生成唯一的名称 java并发编程之美_多线程_14

  • 它是作为一个变量存在线程当中的,只对当前线程可见,所以不存在多线程安全和锁的问题。

ThreadLocal的继承性

ThreadLocal不支持继承性。即你在父线程中设置的的ThreadLocal变量,在子线程中是无法访问到的。
但是你可以用InheritableThreadLocal来创建ThreadLocal,如下,这样子线程也可以访问到父线程的了。如果子线程自己有,则使用自己的。

ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

弱引用和软引用的区别

Java中4种引用的级别和强度由高到低依次为:
强引用 -> 软引用 -> 弱引用 -> 虚引用

  • 强引用,gc宁愿抛出异常,也不会去回收这些具有强引用的对象。
  • 软引用,当内存不够的时候,才会去回收这些具有软引用的对象。
  • 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

java的内存模型

在学习java内存可见性时,我们需要了解一下多线程下处理共享变量时java的内存模型。

java 并发下生成唯一的名称 java并发编程之美_System_15

java内存模型(JMM)规定,将所有的共享变量存放在主内存中,当线程需要使用这些变量时,再将这些变量拿到自己的工作内存中使用。java内存模型是一个抽象的概念。实际上应该是像下图这样子的。

java 并发下生成唯一的名称 java并发编程之美_java_16

而工作内存也就对应着图中的一二级缓存或者CPU寄存器。

简单描述一下内存不可见

针对上面这个图,进行一下描述。
最开始都是空的

  • 如果此时A线程去访问一个共享变量X,那么先去一二级缓存,都不命中,然后再去主内存中读取变量X=0,然后A对其进行修改,将X改为1,并存入一二级缓存,并将X刷回主内存,这样主内存和两级缓存中都存在X=1。
  • 此时B线程也去访问共享变量X,先去一级缓存,一级缓存不命中,那么就去二级缓存,二级缓存命中了,那么就将该共享变量改为了X=2,然后将一级缓存设置成X=2,主内存刷成X=2
  • 然后A线程又去访问了,这时A线程发现自己的一级缓存中存在X=1,那么就返回了。但是线程B明明已经把共享变量X改成了2了。所以这就是内存的不可见性。

synchronized的内存语义

synchronized也是可以保证内存可见性的,通过其语义来理解:
进入synchronized块的内存语义,其实就是把synchronized中用到的变量从工作内存中清除,重新从主内存中读取。
退出synchronized块的内存语义,就是把synchronized块中的共享变量修改后的值,刷回到主内存。
只是加锁太笨重,而且上下文的切换影响性能。
所以java提供了一种弱形式的同步,也就是使用volatile

volatile的介绍

当一个变量被声明为volatile时,在对该变量进行写的操作的时候,不会将该变量写进缓存或者寄存器,而是直接写入主内存中。当其他线程读取该变量时,不会从工作内存中读取,而是直接去主内存中读取

synchronized和volatile的比较

  • volatile不需要加锁,比synchronized轻量级,不会阻塞线程。
  • synchronized关键字既能保证原子性,又能保证可见性;而volatile关键字只能保证可见性

总线锁定和缓存一致性

对于上面的JMM内存结构,只是简单描述了在单核情况下并发的内存不可见性,如果在多核情况下,多个线程并行的情况下,同样会产生问题。

  • 所以先是提出了总线锁定的一个解决方案,意思就是锁定单个cpu,其它的cpu等待操作内存。但这个方案对于系统的开销损失较大。
  • 于是后来提出了一个缓存一致性协议MESI。下面简单描述一下MESI保证内存可见性的一个过程。

MESI协议提供的四个状态

1.Modified,修改的:只在当前cpu内有修改后的缓存数据,其它cpu中没有,且该数据还并没有写回主内存
2.Exclusive,独占的:只在当前cpu内有缓存数据,且与主内存中一致
3.Share ,共享的:在所有的cpu中都有缓存数据,且与主内存中一致
4.Invalid,无效的:本cpu中的这份缓存已经无效了

MESI协议约定的监听

  • 一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
  • 一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
  • 一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S

当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下会性能开销是相对较大的。在写入完成后,修改其缓存状态为M。

所以如果一个变量在某段时间只被一个线程频繁地修改,则使用其内部缓存就完全可以办到,不涉及到总线事务,如果缓存一会被这个CPU独占、一会被那个CPU独占,这时才会不断产生RFO指令影响到并发性能(与线程多少不一样的)。

++操作在底层的一个具体过程

假设现在我们声明了一个变量

value

并对该变量做如下操作

value++

当多个线程操作value时,就会出现问题,因为value++并不是一个原子性操作。什么是原子性操作?一组指令要么全部执行,要么全不执行,不可能执行到一半。

value++在底层的过程应该是这样的

1.获取到value的值放入栈顶
2.将常量1放入栈顶
3.将当前栈顶两个值相加,并把结果放入栈顶
4.将栈顶的值赋给value

因此,这么多步,如果不进行同步,那么是很容易出现线程不安全问题的。

CAS介绍

  • 为了保证内存可见性,可以对代码块加上synchronized。
    但由于synchronized导致的线程阻塞,切换上下文引起的性能开销。
  • 于是提供了关键字volatile
    但是volatile并不保证原子性操作,
  • 所以JDK提供了非阻塞原子性操作CAS
    compare and swap,通过硬件保证比较–更新,Unsafe类中提供了一系列的CAS操作

ABA问题的解决:
变量的状态发生了环形转换就会造成ABA,所以,JDK的AtomicStampedReference为每个变量的状态值都增加了一个时间戳,这样就避免了前后一致ABA问题

Unsafe类的认识

Unsafe类是JDK的rt.jar包下的类,里面的方法都是native方法,即都是本地方法,它们使用JNI的方式访问本地的C++库,实现对操作系统的操作。
unsafe类的介绍

伪共享(缓存行)

像我们上面说的工作内存那里,实际上是由多个缓存而组成的。我们这里以两层缓存为例,来讲一下伪共享的问题。
缓存内部其实是分为一行行的,称为缓存行,缓存行是有大小的,在向主内存取值的时候,并不是取一个变量进来,而是取了一个内存块过来,所以可能会有多个变量被取到了一个缓存行中。而缓存行只能由一个线程操作,所以当多个线程想要操作的变量在一个缓存行里,就会导致竞争缓存行,对比单线程操作,就会产生性能下降的问题。

但比如对一个线程操作一个数组来说。由于其存在内存中的变量是连续的,如果按顺序取值,那么对于一个缓存行来说,就可以取到多个数组的值,这比直接去内存中查询值来的更快。因为可以在一个缓存行中命中多个值。

如何避免伪共享。
JDK8 提供了 sun.misc Contended 注解,用来解决伪共享问题,但其只能用于Java核心类,比如rt包下的类。
对于用户路径下的类需要使用这个注解,需要添加JVM参数
-XX:-RestrictContended,填充的宽度默认为 128 。
要自定义宽度则可以-XX:ContendedPaddingWidth 参数。