多线程之间如何通讯、Synchronized与Lock、ThreadLocal分析
多线程之间如何通讯
- 多线程之间通讯:其实就是多个线程在操作同一个资源(共享资源),但是操作的动作不同。
- 多线程通讯场景:第一个线程写入(input)用户,另一个线程读取(out)用户。实现读一个,写一个操作。应用示例 — 消息中间件,对应生产者、消费者。
/**
* 共享资源实体类
*/
class Res{
public String userName;
public String sex;
}
class Out extends Thread {
Res res;
public Out(Res res) {
this.res = res;
}
@Override
public void run() {
// 写操作
int count = 0;
while(true) {
if(count == 0) {
res.userName="小红";
res.sex="女";
}else{
res.userName="小白";
res.sex="男";
}
// 计算奇数或偶数
count = (count + 1) % 2;
}
}
}
class Input extends Thread {
Res res;
public Input(Res res) {
this.res = res;
}
@Override
public void run() {
while(true) {
System.out.println(res.userName + "," + res.sex);
}
}
}
public class OutInputThread {
public static void main(String[] args){
Res res = new Res();
Out out = new Out(res);
Input input = new Input(res);
out.start();
input.start();
}
}
运行后发现,出现了如下图框出的线程问题。没有对共享资源进行控制,导致线程安全问题。
解决方法是给共享资源对象进行加锁,控制两个线程每次只能有一个线程对共享资源对象进行操作。
...
class Out extends Thread {
Res res;
public Out(Res res) {
this.res = res;
}
@Override
public void run() {
// 写操作
int count = 0;
while(true) {
synchronized (res){
if(count == 0) {
res.userName="小红";
res.sex="女";
}else{
res.userName="小白";
res.sex="男";
}
// 计算奇数或偶数
count = (count + 1) % 2;
}
}
}
}
class Input extends Thread {
Res res;
public Input(Res res) {
this.res = res;
}
@Override
public void run() {
while(true) {
synchronized (res) {
System.out.println(res.userName + "," + res.sex);
}
}
}
}
...
由下图可以看出,线程安全问题得到了解决:
wait()、notify()、notifyAll() 方法
- 问题:安全问题解决了,但是由于实现时是每写一种信息就切换另一种信息,由上图可以看出同一种信息多次输出的问题。
- 产生原因:写入线程还未做出下一次写的操作,读线程也在不断的读取;因此导致读取重复信息。
- 解决思路:生产者生产一条信息,消费者消费一条信息。生产者没有生产,消费者不能消费;消费者未消费完,生产者不能生产。
可以使用 wait()、notify()、notifyAll() 来实现(线程同步、并且是同一个锁资源的情况下使用):
- wait():对象调用该方法,使持有该对象的线程把对象的控制权交出,进行等待(释放锁资源)。
- notify():对象调用该方法,通知某个正在等待该对象的控制权的线程可以继续运行;notify唤醒的是其所在锁所阻塞的线程。
- notifyAll():对象调用该方法,通知所有正在等待该对象的控制权的线程可以继续运行。
class Res{
public String userName;
public String sex;
// false 生产不消费, true 消费不生产
public boolean flag = false;
}
class Out extends Thread {
Res res;
public Out(Res res) {
this.res = res;
}
@Override
public void run() {
// 写操作
int count = 0;
while(true) {
synchronized (res){
if(res.flag){
try {
res.wait();
}catch (Exception e){
e.printStackTrace();
}
}
if(count == 0) {
res.userName="小红";
res.sex="女";
}else{
res.userName="小白";
res.sex="男";
}
// 计算奇数或偶数
count = (count + 1) % 2;
res.flag = true;
res.notify();
}
}
}
}
class Input extends Thread {
Res res;
public Input(Res res) {
this.res = res;
}
@Override
public void run() {
while(true) {
synchronized (res) {
if(!res.flag){
try {
res.wait();
}catch (Exception e){
e.printStackTrace();
}
}
System.out.println(res.userName + "," + res.sex);
res.flag = false;
res.notify();
}
}
}
}
public class OutInputThread {
public static void main(String[] args){
Res res = new Res();
Out out = new Out(res);
Input input = new Input(res);
out.start();
input.start();
}
}
实际流程:
- 开始 flag 是 false;读线程进入等待,写线程执行写入,写完改 flag 为 true,执行 Res 对象的 notify 方法,唤醒读线程这个正在等待的线程。
- 此时 flag 是 true;写线程进入等待,读线程执行读取,读完改 flag 为 false,执行 Res 对象的 notify 方法,唤醒写线程这个 正在等待的线程。
wait() 和 sleep() 的区别
- sleep是线程中的方法,但是wait是Object中的方法。
- sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
- sleep不需要被唤醒(休眠之后推出阻塞,他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态),但是wait需要(不指定时间需要被别人中断)。
lock 锁
lock 锁是 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。
synchronized(内置锁)上锁、释放锁
- 加锁:synchronized 修饰的代码开始执行时上锁。
- 释放锁:
- 释放场景:
- 当前线程的同步方法、代码块执行结束的时候释放。
- 当前线程在同步方法、同步代码块中遇到break 、 return 终于该代码块或者方法的时候释放。
- 出现未处理的error或者exception导致异常结束的时候释放。
- 程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁。
- 不释放场景:
- 代码块中使用了 Thread.sleep() Thread.yield() 这些方法暂停线程的执行,不会释放。
- 线程执行同步代码块时,其他线程调用 suspend 方法将该线程挂起,该线程不会释放锁 ,所以我们应该避免使用 suspend 和 resume 来控制线程 。
- tip:
- 对于一个已经竞争到同步锁的线程,在还没有走出同步块的时候,即使时间片结束也不会释放锁。
- 对象锁和类锁是两个不同的锁。在同一个类的静态方法和实例方法上都加synchronized关键字,这时静态方法的synchronized对应的是 类锁,实例方法的synchronized是对象锁。这是两个不同的锁。 实例对象调用类锁方法也会同步。
- 释放场景:
Lock 接口与 Synchronized 关键字的区别
类别 | synchronized | lock |
---|---|---|
存在层次 | 一个关键字,在jvm层面上 | 一个接口 |
所得释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,否则容易造成线程死锁 |
锁的获取 | 假设A线程获取锁,B线程等待。如果A线程阻塞,B线程一致等待 | 分情况而定,Lock有多个锁的方式: lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly() |
锁的状态 | 无法判断 | 可以判断 |
锁的类型 | 可重入、不可中断、非公平 | 可重入、可中断、可公平 |
性能 | 少量同步 | 大量同步 |
Lock 接口的方法介绍
public interface Lock {
void lock();// 阻塞方法 阻塞的时间:另外一个线程释放锁
void lockInterruptibly() throws InterruptedException;//使锁具备可被中断的能力,防止死等,中断在等待中的锁
boolean tryLock();//非阻塞方法,试图获取锁的方法 (线程启动的时候 会尝试的获取锁 获取到---true 获取不到--false)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 试图获取锁的方法 (参数1:参数时间大小, 参数2:时间单位)
//阻塞方法 阻塞的时间就是参数的时间
//参数的时间内 获取到了---true 获取不到---false
void unlock();
Condition newCondition();
}
lock()
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
- Lock 写法:
Lock lock = new ReentrantLock();
lock.lock();
try{
// 可能会出现线程安全的操作
}finally{
// 在finally中释放锁
// 也不能在try里获取锁,因为有可能在获取锁的时候抛出异常
lock.ublock();
}
tryLock()
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用**threadB.interrupt()**方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
Condition 方法调用
- Condition 的功能类似 Object.wait() 和 Object.notify() 的功能
Condition condition = lock.newCondition();
condition.await(); // 类似 wait
condition.Signal(); // 类似 notify
condition.singalAll(); // 类似 notifyAll
- 示例:
class Res{
public String userName;
public String sex;
// false 生产不消费, true 消费不生产
public boolean flag = false;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); // 公共方法
}
class Out extends Thread {
Res res;
public Out(Res res) {
this.res = res;
}
@Override
public void run() {
// 写操作
int count = 0;
while(true) {
try {
res.lock.lock();
if(res.flag){
try {
res.condition.await();
}catch (Exception e){
e.printStackTrace();
}
}
if(count == 0) {
res.userName="小红";
res.sex="女";
}else{
res.userName="小白";
res.sex="男";
}
// 计算奇数或偶数
count = (count + 1) % 2;
res.flag = true;
res.condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
res.lock.unlock();
}
}
}
}
class Input extends Thread {
Res res;
public Input(Res res) {
this.res = res;
}
@Override
public void run() {
while(true) {
try{
res.lock.lock();
if(!res.flag){
try {
res.condition.await();
}catch (Exception e){
e.printStackTrace();
}
}
System.out.println(res.userName + "," + res.sex);
res.flag = false;
res.condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
res.lock.unlock();
}
}
}
}
public class OutInputThread {
public static void main(String[] args){
Res res = new Res();
Out out = new Out(res);
Input input = new Input(res);
out.start();
input.start();
}
}
停止线程
- 使用推出标志,使线程正常退出;即当 run 方法完成后线程终止。
- 使用 stop 方法强行终止线程(不推荐)。
- 使用 interrupt 方法中断线程。
ThreadLocal 原理剖析
ThreadLocal 提高一个线程的局部变量,访问某个线程拥有自己的局部变量,解决线程不安全问题。
ThreadLocal 是 JDK 包提供的,它提供线程本地变量,如果创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示
常见的ThreadLocal用法有
- 存储单个线程上下文信息。比如存储id等;
- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。
ThreadLocal 实现原理
每个Thread内部都有一个Map,我们每当定义一个ThreadLocal变量,就相当于往这个Map里放了一个key,并定义一个对应的value。每当使用ThreadLocal,就相当于get(key),寻找其对应的value。
每个Thread都有一个 {@link Thread#threadLocals}
变量,它就是放k-v的map,类型为{@link java.lang.ThreadLocal.ThreadLocalMap}
。这个map的entry是{@link java.lang.ThreadLocal.ThreadLocalMap.Entry}
,具体的key和value类型分别是{@link ThreadLocal}
(我们定义ThreadLocal变量就是在定义这个key)和 {@link Object}
(我们定义ThreadLocal变量的值就是在定义这个value)。
(注:实际上key是指向ThreadLocal类型变量的弱引用WeakReference<ThreadLocal<?>>
,但可以先简单理解为ThreadLocal。)
当设置一个ThreadLocal变量时,这个map里就多了一对ThreadLocal -> Object的映射。
详细分析:ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal 原理分析即使用
ThreadLocal 类接口有4个方法
- void set(Object value):设置当前线程的线程局部。
- public Object get():获取当前线程所对应的线程局部变量。
- pubic void remove():将当前线程的变量的值删除,目的为了减少内存的占用;属于Jdk 1.5 的新增方法,线程结束,对应的局部变量会被GC自动回收,调用该该方法非必须操作,但可以加快内存回收的速度。
- protected Object initialValue():获取当前线程局部变量的初始值。用 protected 修饰,是为让子类覆盖而设计的。该方法是一个延迟调用方法,在线程第 1 次调用 get() 或 set(Object) 时才执行,并且仅执行 1 次。ThreadLocal 中缺省实现直接返回 null。
class Res {
// 生成序列号共享变量
public static Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
}
};
public Integer getNum() {
int count threadLocal.get() +1;
threadLocal.set(count);
return count;
}
}
public class ThreadLocalDemo2 extends Thread{
private Res res;
public ThreadLocalDemo2(Res res){
this.res = res;
}
@Override
public void run(){
for(int i=0; i<3; i++){
System.out.println(Thread.currentThread().getName + "---" + "i---" + i + "--num:" + res.getNum());
}
}
public static void main(String[] args){
Res res = new Res();
ThreadLocalDemo2 = threadLocalDemo1 = new ThreadLocalDemo2(res);
ThreadLocalDemo2 = threadLocalDemo2 = new ThreadLocalDemo2(res);
ThreadLocalDemo2 = threadLocalDemo3 = new ThreadLocalDemo2(res);
threadLocalDemo1.start();
threadLocalDemo2.start();
threadLocalDemo3.start();
}
}