Java多线程总结(一)

线程 进程

进程是程序的一次执行过程,程序是代码,是静态的,而进程是执行程序的一次过程,是一个动态的概念,是操作系统分配资源的单位。
通常一个进程里面包含若干个线程,真正占用CPU的也是线程,线程是CPU调度和执行的基本单位。从JVM角度来说的话,一个进程中有 个线程,多个线程共享 进程的堆和方法区资源,但是每个线程有自己的程 计数器、虚拟机栈、本地方法栈。所以在多线程切换的时候负担要比进程小得多,也正因为如此,线程也被叫做轻量级的进程。
关于程序计数器为什么是私有的??前面说了线程是占用 CPU 执行的基本单位,而 CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 C PU 时间片用完后,要让出CPU ,等下次轮到自 己的时候再执行 那么如何知道之前程序执行到哪 了呢?其实程计数器就是为了记住 该线程让出 CPU 时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计 器指 定地址继 执行。总之,程序计数器的私有化主要是为了线程切换后能恢复到正确的执行位置。
为什么虚拟机栈和本地方法栈是私有的?栈里面存储的是局部变量等信息,这些局部变量是线程私有的,不能被其他别的线程访问。虚拟机栈和本地方法栈的区别就是,虚拟机栈为执行Java方法(也就是字节码)服务,本地方法栈为虚拟机使用的native方法服务。
堆是 一个进程中最大的 一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用 new 操作创 建的对象实例
方法区则用来存放 JVM 加载的类、常 量及静态变量等信息,也是线程共享的。
在Java中,当启动main函数时其实就是启动了JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。

并发和并行

并发:同一时间段内,多个任务在时执行,单位时间内不一定同时执行
并行:单位时间内,多个任务同时进行
只有当CPU至少有两个核心的时候才有可能并行,而能不能并发与CPU的核心数无关,单核CPU也可以实现并发。并行是一种并发的特殊情况,并行要求必须是时间上的同时,而并发只是看起来是同时,其实不是真正的同时执行。

创建线程的三种方法

  • 实现runnable接口重写run方法,多个线程执行一样的任务不需要多分代码
  • 继承thread类重写run方法,创建了线程并不是马上执行,直到调用了start方法才真正启动了线程,启动线程并没有真正的马上执行,而是处于就绪状态,这个就绪状态指的是线程已经获取了除CPU外的其他资源,等待获取CPU资源后真正执行,一旦run方法执行完毕,该线程就处于终止状态
  • futuretask方法,实现callable接口,重写call方法,有返回值且会抛出异常

线程的各种方法

  • wait(),当线程调用一个共享变量的wait方法,该线程会被阻塞挂起,直到下面这几种情况下才能返回:(1)其他线程调用了该共享变量的notify或者notifyall方法(2)其他线程调用了该线程的interrupt方法,该线程抛出interruptedexception异常
  • wait(long)该方法相比wait() 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeout ms 时间内被其他线程调用该共享变量的notify() 或者notifyA ll() 方法唤醒,那么该函数还是会因为超时而返回。
  • notify()
    一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
  • notifyall()
    不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notify A ll () 方法则会唤醒所有在该共享变量上由于调用wait 系列方法而被挂起的线程。
  • sleep()
    当一个执行中的线程调用了Thread 的s leep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU 的调度,但
    是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。
  • yield()
    当一个线程调用y ield 方法时, 当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权。
    区别:s l eep 与y i e ld 方法的区别在于,当线程调用s l巳ep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yie ld 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程上下文切换

在多线程中,线程个数一般是要大于CPU核心数,每个CPU同一时间只能被一个线程使用,为了让用户感觉是在同时进行的,CPU采用了时间片轮转的策略,也就是给每一个线程分配一个时间片,线程在时间片内占用CPU执行任务,当前线程的时间片用完后,就会处于就绪状态让出CPU让其他线程使用,这就是上下文切换,从当前线程切换到了其他线程。

线程死锁

多个线程自己手中持有对方想要争夺的资源互相僵持一直等待下去无法继续执行的状态。产生死锁的条件: 如何避免死锁:

守护线程和用户线程

Java中的线程分为守护和用户线程,JVM启动时调用main函数,main函数所在的线程就是一个用户线程,但JVM内部还启动了很多守护线程,比如垃圾回收线程,他们之间的区别就是只用有一个用户线程没结束,JVM就不会退出,而守护线程不影响JVM的退出,创建一个守护线程就setdaemon(true)

六种状态

  • new状态,当new一个线程的时候,此时线程处于新建状态,此时代码还没执行
  • runnable就绪状态,新创建的线程调用start方法启动线程,此时线程处于就绪状态,处于就绪态的线程不一定立即运行run方法,线程必须和其他就绪线程竞争CPU,只有获得CPU的使用权才能运行线程,runnable状态包含ready和running状态。当在就绪队列中等待的ready状态线程被线程调度器选中后,此时从ready状态变为running状态

Synchronized

  • 偏向锁
    偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。
    也就是说:
    在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
    Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
    如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
    如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
    如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
    如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
    即偏向锁是针对于一个线程而言的,线程获得锁之后就不会进行解锁操作,节省了很多开销。为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。
  • 轻量级锁
    当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。轻量级锁主要是自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。自旋锁有一些问题:
    (1)如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu。
    (2)本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。
    基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
  • 重量级锁
    轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
    主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
    这就是说为什么重量级线程开销很大的。互斥锁(重量级锁)也称为阻塞同步、悲观锁
  • 可重入锁&&不可重入锁

volatile

轻量级的同步机制,有三个特性,第一个是保证可见性,第二不保证原子性,第三是禁止指令重排序。
详细说明:

  • 保证线程可见性,Java中是有堆内存,堆内存所有线程共享的内存,所有县城都要访问这个值得时候,他们会把这个值copy一份到自己的工作线程,然后对这个值得任何改变实在自己的工作空间里进行的,但是改变后的值什么时候被另外的线程从共享内存里面读不好控制,就是并不能即时反映到另外一个线程里面,产生了线程之间不可见的问题,加了volatile的变量保证了一个线程对他进行了改变过后另外的线程马上就能看到,本质上采用了CPU的缓存一致性协议,MESI,归根结底是要靠硬件来帮助实现。
  • 禁止指令重排序,
    计算机执行程序为了提高效率,编译器和处理器会对指令做重排序,在单线程环境中可以确保结果的一致性,但在多线程环境中线程是在交替执行的,多个线程中使用的变形量的一致性无法保证,可能导致结果出现问题。底层实现是内存屏障,能够保证编译器或者处理器不发生指令重排序。
    单例的发展,双重检查要不要加volatile?new一个对象的时候会进行三步,首先分配内存,然后赋值,然后将栈变量instance指向这个对象的地址,如果不加volatile,会发生指令重排序现象,导致初始化到一半的时候将赋值给栈空间的变量instance,另一个线程进来判空的时候因为已经初始化到一半所以不为空,导致另外的线程拿到这个对象直接使用,导致拿到的值会不正确,而加了volatile,指令重排序不允许存在,这个时候一定保证初始化完成之后赋值给这个instance,保证拿到的值是正确的。
  • 不保证原子性:
    首先原子性的意思是,某个线程在执行操作的时候,中间不能被打断,保证执行的完整性和不可分割。比如多个线程执行++操作的时候,其实底层这个操作可能有个三四步,不能保证原子性,会发生写覆盖的问题。导致最后加出来的数比以前小了。如何解决这种原子性问题:第一加synchronized,第二使用 atomicInteger
  • 在什么地方用到了volatile
    首先第一在单例模式DCL代码中,第二读写锁手写一个缓存的时候也会用到volatile,第三cas底层juc包里面大量使用了volatile

synchronized不能保证禁止指令重排序,volatile不能替代sychronized,因为他不能保证原子性,只能保证内存可见性。
锁的粗化和细化
对象作为锁的时候一般价格final,因为一旦对象被改变,则锁机制会出现问题。

CAS(无锁优化,自旋 乐观锁)

比较并且设定,CPU原语支持,也就是说cas的操作是CPU指令级别的支持,中间不能被打断
凡是atomic开头的都是用CAS操作来保证线程安全的类
ABA问题,另外的线程改动了值,但值得大小和原来保持不变,加版本号,做任何值的修改后版本号加一,后面再检查的时候连着版本号一块检查。当然如果是int类型或者是long类型这些基础数据类型都不会出现问题,但如果是一个引用,一个对象,就不好说了,可能会出现问题。
unsafe这个类

递增操作

  • sync
  • atomicXXX
  • LongAdder(分段锁

可重入锁reen

sync本身就是可重入锁的一种,sync方法可以调用另外一个sync方法,也就是说锁是可以重入得
reentrantlock使用trylock进行尝试锁定,不管锁定与否,方法都将继续执行,可以根据trylock的返回值判断锁定与否,也可以指定trylock的时间
参数为true表示公平锁
和sync的对比

  • cas vs sync
  • trylock
  • lockinterupter
  • reen可以公平锁和非公平锁之间切换,sync本身是非公平锁

原生lock多数用的是cas,除了sync

countdownlatch

readwritelock

  • 共享锁 读锁
  • 排他锁 写锁
    一块读,读完在写,提高效率,如果是互斥线程一个线程读的时候其他线程不能读效率会很低,虽然写线程是排他的,但是写线程比较少,效率没多少影响

Semaphore(限流)

最多的时候允许多少个线程同时运行

threadlocal

  • 多个线程对一个变量进行操作时,容易出现线程安全问题,所以一般操作共享变量时需要进行适量的同步操作。如果创建了一个threadlocal变量,访问这个变量的每一个线程都会有一个这个变量的本地副本,操作的其实就是自己本地内存里面的变量,从而避免了线程安全问题
  • 底层实现原理:每个线程内部有一个threadlocals成员变量,类型为hashmap,key为threadlocal的实例引用,value是通过set方法设置的value,因此通过threadlocal产生的本地变量其实不放在threadlocal里面,而是存放在对线程threadlocals变量里面,也就是说明ThreadLocal 就是一个工具壳,它通过set 方法把value 值放
    入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get 方法时,再从当前线程的threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals 变量里面,所以当不需要使用本地变量时可以

    通过调用ThreadLocal 变量的remove 方法,从当前线程的threadLocals 里面删除该本地变量。另外, Thread 里面的threadLocals 为何被设计为map 结构?很明显是因为每个线程可以关联多个ThreadLocal 变量。
    image.png
  • 出现问题:同一个threadlocal变量在主线程中被设置的值,在子线程中获取不到。为什么?因为在子线程中get到的是当钱线程的value,,而设置的set给的是主线程,两者不是同一个线程,自然子线程访问不到主线程中设置的值,返回的应该就是默认情况下的null。解决方案就是一个类:lnhe itableThreadLocal 类,作用就是让子线程可以访问父线程中设置的本地变量。

线程安全问题

多个线程对共享资源进行读写的时候没有任何同步策略则会出现线程安全问题。但如果仅仅是读数据的话可能并不会出现线程安全问题,而当至少一个线程修改数据的话才会出现线程安全问题。这就需要进行同步操作,最常见的同步操作就是使用关键字synchronized进行同步。

Java内存模型

Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存中的变量赋值到自己的工作内存中,读写操作是在自己工作内存中进行的,处理完后将变量值更新到主内存。Java内存模型是一个抽象的概念,实际上多核CPU系统中,每个核拥有自己的控制器和运算器,控制器包含寄存器和操作控制器,运算器执行算数逻辑运算。每个核心都有自己的一级缓存,在有些CPU里面还会有所有CPU都共享的二级缓存。Java模型中的工作内存其实就是对应的这里的一级缓存或者二级缓存,或者寄存器。

伪共享

  • 为了解决计算机系统中主内存和CPU之间的速度差,会在CPU和主内存之间添加一级或者多级缓存,这个缓存一般集成到了CPU内部。在缓存中的存储是按行的,一行也是缓存和主内存数据交换的单位。当CPU访问某个变量时,会先去缓存里看有没有改变量,如果有直接获取,没有就去主内存中找。然后把该变量所在内存区域的一个Cache 行大小的内存复制到Cache 中。由于存放到Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache 行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,当一个线程修改完变量后,当另外的线程想去修改时,这些变量失效了,那么这个线程只能去二级缓存或者主内存中找,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。
  • 如何解决:JDK1.8之前前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,JDK1.8提供了一个sun.misc . Contended注解,用来解决伪共享问题。

JMM

Java内存模型,并不真实存在,是抽象的概念和规范。计算机CPU的缓存机制就相当于这里的jmm机制。保证了可见性。
JMM的同步规定:

  • 线程加锁前必须把主内存的最行之读取到自己的工作内存
  • 线程解锁前,必须把共享变形量的值刷会到主内存
  • 加锁和解锁是同一把锁
    三大特性:
  • 可见性
  • 原子性
  • 有序性,禁止指令重排序
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342