Java多线程笔记总结
1.线程的三种创建方式
对比三种方式:
- 通过继承Thread类实现
- 通过实现Runnable接口
- 实现Callable接口
第1种方式无法继承其他类,第2,3种可以继承其他类;
第2,3种方式多线程可以共享同一个target对象,多个线程处理同一个资源;
一般使用第2,3种方式创建线程。
2.线程的生命周期
1.新建(new) 2.就绪(start) 3.运行(获得cpu资源) 4.阻塞(sleep,IO阻塞等)4.死亡(执行完成,Exception/Error)
3.线程常用方法
-
join() :Thread对象调用,线程A调用join(),其他线程被阻塞,直到线程A执行完为止。
-
setDaemon(true) : Thread对象调用,设置成后台线程; 当所有前台线程都死亡,后台线程自动死亡。
- sleep(): 静态方法,让正在执行的线程暂停,进入阻塞状态。
- yield(): 静态方法,让正在进行的线程暂停,进入就绪状态,系统的线程调度器重新调度一次;只有优先级比当前调用yield()的线程高或相同,并且处于就绪状态的线程才能获得执行。
- setPriority(int newPriority), getPriority(): 设置和获取线程的优先级;newPriority范围1-10;Thread三个静态常量,MAX_PRIORITY, 10; NORM_PRIORITY, 5; MIN_PRIORITY, 1;子线程和父线程具有相同的优先级,优先级高的线程比优先级低的线程执行机会更多。
4.线程同步
多个线程访问同一个数据时(线程调度的不确定性),很容易出现线程安全问题。解决办法:引入同步监视器,任意时刻只能有一个线程获得对同步监视器的锁定,当同步代码块执行结束后,该线程释放对该同步监视器的锁定。
“加锁”—>”修改共享资源”->”释放锁”
同步代码块
同步方法
使用synchronized修饰某个方法,则该方法称为同步方法,同步方法的同步监视器是this;
只需对会改变竞争资源的方法进行同步。
任何线程在进入同步代码块或同步方法前,必须获得同步监视器的锁定。
释放同步监视器的锁定的时机:
- 当前线程的同步代码块,同步方法执行结束
- 当前线程在同步代码块,同步方法中遇到break,return终止了执行
- 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception
- 执行了同步监视器对象的wait()方法
同步锁(Lock)
Lock对共享资源的独占访问,每次只能一个线程对Lock对象加锁。
Lock,ReadWriteLock接口,对应的实现类ReentrantLock(可重入锁),ReentrantReadWriteLock。
死锁
两个线程互相等待对方释放同步监视器,就发生了死锁。
线程池
在系统启动时,创建大量空闲的线程,将一个Runnable对象或Callable对象传给线程池,线程池会启动一个线程执行对应的run()或call(),当run()或call()执行完成后,该线程返回到线程池成为空闲状态,等待下个Runnable对象或Callable对象。
创建线程池
Executors工厂类,提供如下静态方法创建不同的线程池:
步骤:
- Executors静态工厂类创建ExecutorService
- 创建Runnable或Callable对象,作为线程执行体
- 调用ExecutorService实例的submit()方法提交Runnable或Callable
- 线程池关闭,调用ExecutorService实例的shutdown()方法
ThreadLocal
线程局部变量,把数据放在ThreadLocal中,就可以为每个线程创建一个该变量的副本,避免并发访问的线程安全问题。
与同步机制的区别:
同步机制是为了同步多个线程对共享资源的并发访问,是多线程间通信的有限方式;
ThreadLocal隔离多个线程的数据共享,避免多个线程间对共享资源的竞争,不需要对多个线程进行同步。
线程安全集合
通过Collections包装成线程安全集合:
ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap等都是线程不安全;如果多个线程对这些集合读,写时,会破坏这些集合数据的完整性。
Collections提供静态方法将这些集合包装成线程安全的集合。
----------------------------------------------------------------------------------------
线程安全集合:
ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet, ConcurrentLinkedQueue, ConcurrentLinkedDeque
CopyOnWriteArrayList
CopyOnWriteArraySet
Java线程初体验
Java.lang
class Thread
interface Runnable
public void run()
线程的常用方法
类别 |
方法签名 |
简介 |
线程的创建 |
Thread() |
|
Thread(String name) |
|
|
Thread(Runnable target) |
|
|
Thread(Runnable target,String name) |
|
|
线程的方法 |
void start() |
启动线程 |
static void sleep(long millis) |
线程休眠 |
|
static void sleep(long millis,int nanos) |
||
void join() |
使其他线程等待当前线程终止 |
|
void join(long millis) |
||
void join(long millis,int nanos) |
||
static void yield() |
当前运行线程释放处理器资源 |
|
获取线程引用 |
static Thread currentThread() |
返回当前运行的线程引用 |
两条线程不做任何处理的时候,会交替运行。
当使用boolean类型控制线程的循环时,要在变量前加上volatile关键字,volatile保证了线程可以正确的读取其他线程写入的值。
注意点:
sleep()方法的作用:使线程休眠指定的时间。
join()方法的作用: //使其他线程等待当前线程执行完毕。
线程的正确停止
如何正确的停止Java中的线程:
可以通过boolean类型来控制循环的退出。
not stop方法
stop()方法会让线程戛然而止。
stop()方法停止线程是错误的方法。
线程的交互
争用条件Race Condition
当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏(corrupted),这种现象称为争用条件。
什么是互斥?
互斥是怎么实现的?
只能被一个线程所访问,互斥。
互斥的实现:synchronized(intrinsic lock)加锁。
同步的实现:wait()/notify()/notifyAll()。
如何扩展Java并发的知识
Java Memory Mode
JMM描述了Java线程如何通过内存进行交互
Happens-before
Synchronized,volatile&final
Locks&Condition
Java锁机制和等待条件的高层实现
Java.util.concurrent.locks
线程安全性
原子性与可见性
Java.util.concurrent.atomic
Synchronized&volatile
DeadLocks
多线程编程常用的交互模型
Producer-Consumer模型
Read-Write Lock模型
Future模型
Worker Thread模型
Java5中并发编程工具
Java.util.concurrent
线程池ExecutorService
Callable&Future
BlockingQueue
推荐两本书:
Core Java
Java concurrency in practice
线程创建的两种方式比较
Thread:
① 继承Thread类;
Runnable:
① Runnable方式可以避免Thread方式由于Java单继承特性带来的缺陷。
② Runnable的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况。
使用jstack生成线程快照
jstack
作用:生成jvm当前时刻线程的快照(threaddump,即当前进程中所有线程的信息)
目的:帮助定位程序问题出现的原因,如长时间停顿、cpu占用率过高等。
如何使用jstack
jstack –l pid
内存可见性
可见性介绍
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。
Java内存模型(JMM)
Java内存模型(Java Memory Model)描述了程序中各种变量(线程共享变量)的访问规则,以及在Java中将变量存储到内存和从内存中读取出变量这样的底层细节。
所有变量都存储在主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)。
线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
共享变量可见性实现的原理
线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤:
把工作内存1中更新过的共享变量刷新到主内存中。
将主内存中最新的共享变量的值更新到工作内存2中。
可见性
要实现共享变量的可见性,必须保证两点:
线程修改后的共享变量值能够及时从工作内存刷新到主内存中。
其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中。
可见性的实现方式
Java语言层面支持的可见性实现方式:
Synchronized
Volatile
Synchronized实现可见性
Synchronized能够实现:
原子性(同步);
可见性
JMM关于Synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中;
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)
线程解锁前对共享变量得修改在下次加锁时对其他线程可见。
线程执行互斥代码的过程:
- 获得互斥锁
- 清空工作内存
- 从主内存中拷贝变量的最新副本到工作内存
- 执行代码。
- 将更改后的共享变量的值刷新到主内存。
- 释放互斥锁。
重排序
重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。
- 编译器优化的重排序(编译器优化)。
- 指令级并行重排序(处理器优化)。
- 内存系统的重排序(处理器优化)。
as-if-serial
as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)。
例子:
Int num = 1;
Int num2 = 2;
Int sum = num+num2;
单线程:第1、2行的顺序可以重排,但第3行不能
重排序不会给单线程带来内存可见性问题
多线程中程序交错执行时,重排序可能会造成内存可见性问题。
Volatile实现可见性
Volatile关键字:
能够保证volatile变量的可见性
不能保证volatile变量复合操作的原子性
Volatile如何实现内存可见性:
深入来说:通过加入内存屏障和禁止重排序优化来实现的。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
通俗的讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同线程总能看到该变量的最新值。
线程写volatile变量的过程:
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本。
Volatile适用场合
要在多线程中安全的使用volatile变量,必须同时满足:
1. 对变量的写入操作不依赖其当前值
2. 布尔,用来记录温度
3.该变量没有包含在具有其他变量的不变式中。