多线程基础篇

基本概念

进程:一个执行单元,PC和移动设备指一个程序或者应用
线程:操作系统能进行运算调度的最小单位。是进程的实际运作单位。
多线程:并发执行,比如单线程完成一个任务100毫秒,十个线程并发执行只需10毫秒

image.png

补充:
1、join底层是调用wait,会释放锁

2、sleep、yield不会释放锁

线程包括五种状态:

  • 新建状态(New):线程被创建
  • 就绪状态(Runnable):线程调用start方法,随时可获取CPU使用权
  • 运行状态(Running):已获得CPU使用权的线程
  • 阻塞状态(Blocked):失去CPU使用权
  • 死亡状态(Dead):线程执行完或者异常退出

1、实现线程的方式:

  • 继承Thread重写run方法
  • 实现Runnable--run方法
  • 实现Callable--call方法

2、synchrized关键字

2.1、实例锁和全局锁

实例锁:是对象锁
全局锁:是类锁,无论有多少个实例对象,都共享一个锁

2.2、当一个线程获取了锁,其他线程访问synchrized修饰的方法或者代码块将会被阻塞,但仍然可以访问非同步代码。被阻塞的线程需要占有锁线程释放锁,才有可能重新获取锁,进入就绪状态。

2.3、当占有锁的线程出现异常,JVM会自动释放锁

建议:synchrized代码块使用起来会比synchronized方法要灵活得多,更高效。因为synchronized方法是对整个方法进行synchronized同步,而synchronized代码块只需对部分代码同步

3、lock

3.1、synchrized和lock的区别

  • Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  • synchrized自动释放锁,lock手动释放锁,需要在finally块中释放锁(unlock)
  • lock可以知道有没有成功获取锁,如果没有获取锁可以执行其他代码,synchrized不行(tryLock)
  • lock可以让等待锁的线程相应中断(lockInterruptibly),而synchrized会一直等待
  • Lock可以提高多个线程进行读操作的效率。(ReadWriteLock)

3.2、lock相关方法

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock() 获取锁

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

unlock 释放锁。获取锁和释放锁是配对使用的,如果没有及时释放锁,那么线程就会发生死锁。unlock通常是放在finally中。

newCondition 创建Condition对象,Condition会在4.6讲到

ReentrantLock是lock唯一实现类
3.4、ReadWriteLock读写锁

ReadWriteLock也是一个接口

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

readLock()获取读取锁
writeLock()获取写锁

可以实现多线程进行读操作,值得注意的是

  • 如果有一个线程占用了读操作,申请写操作的线程,必须等待读线程释放锁。
  • 如果有写线程在操作,其他申请读锁或者写锁的线程必须等待写线程释放写锁
ReentrantReadWriteLock实现ReadWriteLock接口
3.5 公平锁和非公平锁

公平锁:多个线程等待一个锁,等待最久的线程先获取锁
非公平锁:跟公平锁相反,无法保证等待最久的线程先获取锁。

synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

ReentrantLock和ReentrantReadWriteLock相似,以ReentrantLock为例:

在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

可以通过以下构造函数设置锁的公平性

ReentrantLock lock = new ReentrantLock(true);

另外在ReentrantLock类中定义了很多方法,比如:

  • isFair() //判断锁是否是公平锁
  • iisLocked() //判断锁是否被任何线程获取了
  • iisHeldByCurrentThread() //判断锁是否被当前线程获取了
  • ihasQueuedThreads() //判断是否有线程在等待该锁

4、Thread

4.1 start方法

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

4.2 run方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

4.3 yield

线程让步,让出CPU权,从运行状态进入到就绪状态,不会释放锁。让其他具有相同优先级的线程获取执行权,但不保证其他具有相同优先级的线程一定能获取执行权,也可以是当前线程重新获取执行权继续运行

4.4 sleep

线程休眠,从运行状态进入进入阻塞状态,但不会释放锁,休眠结束,会由阻塞状态变成就绪状态

4.5 join

让子线程执行完,再执行父线程,由于底层是调用wait方法,所以会释放锁

4.6 wait、norify

在Object.java中,定义了wait(), notify()和notifyAll()等接口

  • notify()//唤醒在此对象监视器上等待的单个线程。
  • notifyAll() //唤醒在此对象监视器上等待的所有线程。
  • wait() // 让当前线程处于“等待(阻塞)状态”(当前线程指正在cpu上运行的线程),“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
  • wait(long timeout) // 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

注意:wait、notify调用的前提是当前线程是synchrized锁的拥有者

为什么notify(), wait()等函数定义在Object中,而不是Thread中?

notify和wait是通过“同步锁进行关联的”,只有notify才能唤醒wait线程,但是等待线程还不能执行,必须等到唤醒线程释放锁,才可能重新获取锁从而继续运行。

补充学习condition

condition和wait、notify区别

  • 1、Condition的await()、signal()、signalAll()对应Object的wait(), notify()和notifyAll()
  • 2、wait、norify是和“同步锁”synchronized关键字捆绑使用,condition是和"互斥锁"/"共享锁"ReentrantLock捆绑使用。
  • 3、Condition依赖于Lock接口,通过lock.newCondition获取,Condition可以有多个。(例如生产消费者问题的仓库空消费者线程等待,仓库满生产者线程等待)

Condition能够更强大的控制多线程的休眠和唤醒,同一个锁,可以创建多个Condition,在不同情况下使用不同Condition

4.7 interrupt 线程中断和线程终止方式

interrupt方法可以中断阻塞线程,不能中断运行线程

4.7.1 终止处于阻塞状态的线程
@Override
public void run() {
    try {
        while (true) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
    }
}
4.7.2 终止处于运行状态的线程

interrupt方法不能中断运行线程,所以如果我们需要中断运行线程,需要额外增加标记(可以借助isInterrupted方法)

@Override
public void run() {
    while (!isInterrupted()) {
        // 执行任务...
    }
}
4.7.3 interrupted() 和 isInterrupted()的区别

interrupted() 和 isInterrupted()都能够用于检测对象的“中断标记”。区别是interrupted()还会清楚中断标记,isInterrupted()不会

4.8 线程属性方法

4.8.3 getId

用来得到线程ID

4.8.3 getName和setName

用来得到或者设置线程名称。

4.8.3 getPriority和setPriority

获取优先级和设置优先级

线程优先级范围是0~10,默认优先级是5,“高优先级线程”会优先于“低优先级线程”执行。

4.8.4 setDaemon和isDaemon

设置守护线程和判断是否是守护线程

用户线程和守护线程(也就是后台线程),垃圾收集器线程就是守护线程,当只有守护线程在运行或者调用exit方法,jvm会自动退出。

5、volatile

由于volatile跟java内存模型有关,先了解内存模型相关概念

内存模式:程序运行过程中的临时变量存储在主存里(物理内存),而变量CPU执行速度远大于从内存中存取数据,如果每次操作数据都要和内存交互,会大大降低指令执行速度。于是就有了高速缓存。当程序运行时,会复制一份到高速缓存中,CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

JAVA内存模式:每个线程都有自己的工作内存(类似高速缓存),运行时会复制一份到工作内存中,线程对变量的操作必须在工作内存中进行,不能直接对主存进行操作。

5.1 并发的三个概念

想要并发正确执行必须保证其原子性、可见性、有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

5.1.1 原子性

一个操作要么都执行,要么都不执行。

x = x+1;

举个例子,这里就有三个操作,读取x的值,x+1,写入新值

原子性操作有:读取、赋值、synchrized或者lock实现、JUC原子类

5.1.2 可见性

多个线程共享一个变量,当一个线程改变了这个变量的值,其他线程能立即看到修改的值

volatile具备可见性

可见性操作有:volatile、synchrized或者lock实现

5.1.3 有序性

处理器为了提升运行效率,可能会对执行顺序进行重排序,会保证程序最终结果会和代码顺序执行结果相同。

原因在于,处理器在进行重排序时,会考虑到指令之间的依赖性。如果指令2必须用到指令1的结果,那么处理器会保证指令1在指令2之前执行。

volatile具备有序性

有序性操作有:volatile、synchrized或者lock实现

5.2 volatile

特点:可见性,有序性(禁止指令重排序)

原理和实现机制

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。其他线程再次读取共享变量会从内存读取

参考:
Java多线程系列目录(共43篇)
随笔分类 - Java并发编程

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

推荐阅读更多精彩内容