多线程之间如何通讯、Synchronized与Lock、ThreadLocal分析

多线程之间如何通讯

  • 多线程之间通讯:其实就是多个线程在操作同一个资源(共享资源),但是操作的动作不同。
  • 多线程通讯场景:第一个线程写入(input)用户,另一个线程读取(out)用户。实现读一个,写一个操作。应用示例 — 消息中间件,对应生产者、消费者
发布 / 写操作
订阅 / 读操作
InputThread 生产者线程
共享资源 / 中间件
OutThread 消费者线程
/**
 * 共享资源实体类
 */
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();
    }
}

实际流程

  1. 开始 flag 是 false;读线程进入等待,写线程执行写入,写完改 flag 为 true,执行 Res 对象的 notify 方法,唤醒读线程这个正在等待的线程。
  2. 此时 flag 是 true;写线程进入等待,读线程执行读取,读完改 flag 为 false,执行 Res 对象的 notify 方法,唤醒写线程这个 正在等待的线程。

在这里插入图片描述

wait() 和 sleep() 的区别

  1. sleep是线程中的方法,但是wait是Object中的方法。
  2. sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
  4. sleep不需要被唤醒(休眠之后推出阻塞,他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态),但是wait需要(不指定时间需要被别人中断)。

详细分析: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 关键字的区别

类别synchronizedlock
存在层次一个关键字,在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();
    }
}

停止线程

  1. 使用推出标志,使线程正常退出;即当 run 方法完成后线程终止。
  2. 使用 stop 方法强行终止线程(不推荐)。
  3. 使用 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();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值