目录
5.4 synchronized可见性,有序性,可重入性怎么实现?
一.线程状态
状态 | 说明 |
NEW | 初始状态:线程被创建,但是还没有启动线程。 |
RUNNABLE | 运行状态:java线程将操作系统中就绪和运行状态统称“运行“” |
BLOCKED | 阻塞状态:表示线程阻塞于锁 |
WAITING | 等待状态:线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些动作(通知notify或者中断sleep) |
TIME_WAITING | 超时等待状态:与WAITING不同,是带指定时间的等待线程所处在的状态。主要差异在时限(timeout)参数上。 |
TERMNATED | 终止状态:表示当前线程已经执行完毕 |
二.线程上下文切换
使用多线程是为了充分利用CPU,但是,并发实际上是一个CPU来应付多个线程。
为了让用户感觉是多个线程在同时进行,CPU资源分配采用时间片轮转发,即给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换。
三.线程间通信方式
1.volatile和synchronized关键字
关键字volatile可以⽤来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,⽽对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰⽅法或者以同步块的形式来进⾏使⽤,它主要确保多个线程在同 ⼀个时刻,只能有⼀个线程处于⽅法或者同步块中,它保证了线程对变量访问的可见性和排他性。
2.等待、通知机制
可以通过Java内置的等待/通知机制(wait()/notify())实现⼀个线程修改⼀个对象的值,⽽另⼀ 个线程感知到了变化,然后进⾏相应的操作。
3.管道输⼊/输出流
3.1概念
管道输⼊/输出流和普通的⽂件输⼊/输出流或者⽹络输⼊/输出流不同之处在于,它主要⽤于线程之间的数据传输,⽽传输的媒介为内存。
3.2基本流程
1)创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis
2)将pos和pis匹配,pos.connect(pis);
3)将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了
3.3管道流缺点
3.3.1管道流只能在两个线程之间传递数据
线程consumer1和consumer2同时从pis中read数据,当线程producer往管道流中写入一段数据后,每一个时刻只有一个线程能获取到数据,并不是两个线程都能获取到producer发送来的数据,因此一个管道流只能用于两个线程间的通讯。不仅仅是管道流,其他IO方式都是一对一传输。
3.3.2管道流只能实现单向发送,如果要两个线程之间相互通信,则需要两个管道流
4.使用Thread.join()
如果⼀个线程A执⾏了thread.join()语句,其含义是:当前线程A等待thread线程终⽌之后才从 thread.join()返回。线程Thread除了提供join()⽅法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的⽅法。
5.使用ThreadLocal
ThreadLocal,即本地线程变量,是⼀个以ThreadLocal对象为键、任意对象为值的存储结构。这个 结构被附带在线程上,也就是说⼀个线程可以根据⼀个ThreadLocal对象查询到绑定在这个线程上的⼀个值。
可以通过set(T)⽅法来设置⼀个值,在当前线程下再通过get()⽅法获取到原先设置的值。
(ThreadLocal更多内容学习见主页)
四.线程安全
1.线程安全概念
如果多线程环境下代码运行的结果是100%符合我们的预期的,即在单线程环境下应该的结果,则说明这个线程是安全的。
2.线程不安全的原因
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。(粘贴自阿里云的解释)
3.并发编程三大特性
3.1原子性
原⼦性指的是⼀个操作是不可分割、不可中断的,要么全部执⾏并且执⾏的过程不会被任何因素打断,要么就全不执⾏。
如:我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
再如:火车卖票系统 如果A在自己的客户端买当前位置的票,B也在买当前位置的票,A买到票后,还没有执行更新数据库时,B检查当前位置还没有卖出,重复下单,当前位置又被卖了一次,就没有保证原子性。
3.2可见性
可见性指的是⼀个线程修改了某⼀个共享变量的值时,其它线程能够⽴即知道这个修改。
java内存模型(JMM):规定了如何和何时可以看到其他线程修改过后的共享变量的值,以及在必要时如何同步访问共享变量。
(JMM更多内容见主页)
3.3有序性
有序性指的是对于⼀个线程的执⾏代码,从前往后依次执⾏,单线程下可以认为程序是有序的,但是并发时有可能会发⽣指令重排。
Q:原子性、可见性、有序性都应该怎么保证呢?
原⼦性:JMM只能保证基本的原⼦性,如果要保证⼀个代码块的原⼦性,需要使⽤ synchronized
可见性:Java是利⽤ volatile 关键字来保证可见性的,除此之外, final 和 synchronized 也能保证可见性。
有序性: synchronized 或者 volatile 都可以保证多线程之间操作的有序性。
4. 保证线程安全方式
- 使用Synchronized关键字
- 使用
volatile
关键字,保证变量可见性(直接从内存读,而不是从线程cache读) - 使用
java.util.concurrent.atomic
包中的原子类,例如AtomicInteger
- 使用
java.util.concurrent.locks
包中的锁 - 使用线程安全的集合
ConcurrentHashMap
这个包里面提供了一组原子变量类。
五. synchronized关键字
5.1 synchronized用法
5.1.1 synchronized修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method(){
//业务代码
}
5.1.2 synchronized修饰静态方法
是给当前类加锁,作用于当前类内的所有对象实例,进入同步代码前要获得当前类的锁,因为静态成员不属于任何一个实例的对象,是类成员(static表明这是该类的一个静态资源,不管new了多少对象,只有一份)。
注意:如果一个线程A调用了一个实例对象的非静态synchronized方法,线程B调用实例对象所属的类的静态synchronized方法,是允许的,不会发生互斥现象,因为静态synchronized方法占用的锁是该类的锁,而非静态synchronized方法占用的是当前实力对象的锁。
synchronized void static method(){
//业务代码
}
5.1.3 synchronized修饰代码块
指定加锁对象,对给定的对象/类加锁。synchronized(this/object)表明进入同步代码块前需要获得给定对象的锁。synchronized(类.class)表示进入同步代码块前需要获得当前class的锁
synchronized (this) {
//业务代码
}
5.2 synchronized加锁原理
使用synchronized的时候发现不需要自行lock和unlock,因为JVM帮我们已经把这个事情做了
在修饰代码块时,JVM采用monitorenter、monitorexit两个指令来实现同步,字节码中添加monitorenter、monitorexit两个指令。monitorenter是指同步代码块开始的位置,monitorexit指令指的是同步代码块结束的位置。
在修饰同步方法时,JVM采用ACC_AYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。
5.3 synchronized特性
5.3.1 互斥性
当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象的synchronized,就会阻塞等待。
理解 "阻塞等待".
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有时, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统随机唤醒一个新的线程, 再来获取到这个锁.
注意:
- 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的一部分工作.
- 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
5.3.2 刷新内存
synchronized工作流程:
1.获得互斥锁
2.从主内存拷贝变量的最新副本到工作内存
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁
所以synchronized锁也能保证内存可见性
5.3.3 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 "把自己锁死" 一个线程没有释放锁, 然后又尝试再次加锁.
// 第一次加锁, 加锁成功 lock(); // 第二次加锁, 锁已经被占用, 阻塞等待. lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会死锁.
这样的锁称为 不可重入锁
可重入锁内部包含“线程持有者”和计数器两个信息。
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
5.4 synchronized可见性,有序性,可重入性怎么实现?
synchronized怎么保证可见性?
- 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要的数据是从主内存重新读取出来的最新的值。
- 线程加锁后,其他线程无法获取修改主内存中的共享变量。
- 线程解锁前,必须把工作内存中的共享变量的值刷新到主存后才能解锁。
synchronized怎么保证有序性?
- synchronized同步代码块具有排他性,一次只能被一个线程拥有,所以synchronized能保证同一时刻代码是单线程执行的。
- 因为as-if-serial语义的存在,单线程的程序能保证最终结果是有序的,但是不保证不会指令重 排。
- 所以synchronized保证的有序是执⾏结果的有序性,⽽不是防⽌指令重排的有序性。
synchronized怎么保证可重入性?
- 允许⼀个线程⼆次请求⾃⼰持有对象锁的临界资源,这种情况称为可重⼊锁。
- synchronized 锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执⾏完对应的代码 块之后,计数器就会-1,直到计数器清零,就释放锁了。
六.volatile关键字
1.概念
修饰字段(成员变量),告知程序任何对该变量的访问需要从共享内存中获取,而对它的改变必须同步刷新到共享内存中,保证所有线程对变量访问的可见性。
2.volatile如何保证可见性和有序性?
volatile如何保证可见性?
相⽐synchronized的加锁⽅式来解决共享变量的内存可见性问题,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制, 它没有上下⽂切换的额外开销成本。
volatile可以确保对某个变量的更新对其他线程马上可见,⼀个变量被声明为volatile 时,线程 在写⼊变量时不会把值缓存在寄存器或者其他地⽅,⽽是会把值刷新回主内存 当其它线程读 取该共享变量 ,会从主内存重新获取最新值,⽽不是使⽤当前线程的本地内存中的值。
例如,我们声明⼀个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷新回主内存,线程B读取x的时候,就会清空本地内存变量,然后再从主内存获取最新值。
volatile如何保证有序性?
禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
3.volatile性能
volatile读性能消耗与普通变量几乎相同,但是写稍微慢,因为他需要在本地代码中插入许多内存屏障指令来保证处理器不会发生乱序执行。
七.java标准库中的线程安全类
java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的
- String