多线程同步

多线程同步

synchronized

synchronized关键字

synchronized, wait, notify 是任何对象都具有的同步工具。wait/notify必须存在于synchronized块中。详情如下:
方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)
所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。(在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。)

  • 静态方法:Java类对应的Class类的对象所关联的监视器对象。
  • 实例方法:当前对象实例所关联的监视器对象。
  • 代码块:代码块声明中的对象所关联的监视器对象。

示例

代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容

  • lock 称之为同步监视器
    • lock 可以是任何对象, 但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器, 因为同步方法中的同步监视器就是 this, 就是这个对象本身, 或者是 class
  • 同步监视器的执行过程
    • 第一个线程访问, 锁定同步监视器, 执行其中的代码
    • 第二个线程访问, 发现同步监视器被锁定, 无法访问
    • 第一个线程访问完毕, 解锁同步监视器
    • 第二个线程访问, 发现同步监视器没有锁, 然后锁定并访问
public class Thread1 implements Runnable {
   Object lock;
   public void run() {  
       synchronized(lock){
         ..do something
       }
   }
}

方法: 相当于上面代码中用lock来锁定的效果,实际获取的是线程创建的示例的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。

  • synchronized 方法控制 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放,后面被阻塞的线程才能获得这个锁,继续执行。
  • 缺陷:若将一个大的方法申明为 synchronized 将会影响效率。读资源无需加锁。
public class Thread1 implements Runnable {
   public synchronized void run() {  
        ..do something
   }
}

线程通信

  • 生产者和消费者问题

    • 假设仓库中只能存放一件产品, 生产者将生产出来的产品放入仓库, 消费者将仓库产品取走消费

    • 如果仓库中没有产品, 则生产者将产品放入仓库, 否则停止生产并等待, 直到仓库中的产品被消费者取走为止

    • 如果仓库中放有产品, 则消费者可以将产品取走消费, 否则停止消费并等待, 直到仓库中再次放入产品为止

    • 在生产者消费者问题上, synchronized 可阻止并发更新同一个共享资源, 实现了同步,synchronized 不能用来实现不同线程之间消息传递 (通信)

  • wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。

  • notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。

  • notifyAll:将对象中等待池中的线程,全部放入锁池。

  • (notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
    代码范例如下:

    /**
     * 生产者生产出来的产品交给店员
     */
    public synchronized void produce()
    {
        if(this.product >= MAX_PRODUCT)
        {
            try
            {
                wait();  
                System.out.println("产品已满,请稍候再生产");
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
            return;
        }
        this.product++;
        System.out.println("生产者生产第" + this.product + "个产品.");
        notifyAll();   //通知等待区的消费者可以取出产品了
    }
    /**
     * 消费者从店员取产品
     */
    public synchronized void consume()
    {
        if(this.product <= MIN_PRODUCT)
        {
            try 
            {
                wait(); 
                System.out.println("缺货,稍候再取");
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
            return;
        }
        System.out.println("消费者取走了第" + this.product + "个产品.");
        this.product--;
        notifyAll();   //通知等待去的生产者可以生产产品了
    }
    

Lock

  • JDK1.5 开始, java 提供了更为强大的线程同步机制——通过显示定义同步锁对象来实现同步, 同步锁使用 lock 对象来充当
  • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问, 每次只能有一个线程对 Lock 对象加锁, 线程开始访问共享资源之前应先获得 Lock 对象
  • ReentrantLock(可重入锁) 类实现了 Lock, 它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是 ReentrantLock, 可以显式加锁, 释放锁
class TestLock implements Runnable {
    private final ReentrantLock lock = new ReentrantLock();//定义Lock锁
    @Override
    public void run() {
        try {
            lock.lock(); //加锁
            //需要保证线程安全的代码
        } finally {
            lock.unlock();//解锁
        }
    }
}
Lock synchronized
格式 显式锁 (手动开启和关闭, 别忘记关闭锁) 隐式锁, 出了作用域自动释放
死锁 加锁和解锁必须成对出现,否则会出现死锁的可能。 直接作用与jvm层面,跟obeject类紧密相关,不会生产死锁
中断 可以中断锁 非中断锁,必须等待线程执行完成释放锁
类型 代码块锁 代码块锁和方法锁
优点 使用 lock 锁, JVM 将花费较少的时间来调度线程, 性能更好, 并且具有更好的扩展性 (提供更多的子类);可实现公平锁,读写锁等等应用场景更灵活。 可以直接给方法加锁

volatile

volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。

用来确保对一个变量的修改被正确地传播到其他线程中。

但是注意,一般多线程出现数据共享问题一般出现在修改中,而volatile只保证数据可见,即在使用该数据时去主存中刷新最新的值,当后面需要修改的时候,并不能保证该值没有被其他线程修改,所以需要修改数据的时候,还需要其他同步手段,一般结合cas使用。

/**
 * 创建线程
 */
public class MybanRunnable implements Runnable{
    private Bank bank;
    public MybanRunnable(Bank bank) {
        this.bank = bank;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            bank.save(100);
            System.out.println("账户余额是---"+bank.getAccount());
        }
    }
}
/**
 * 银行存款实例
 */
class Bank{
    private volatile int account = 100;
    public int getAccount() {
        return account;
    }
    //由于此方法不是原子操作,并不是线程安全的,但是操作此方法时候,
    //account会强制去刷新缓存
    public  void save(int money) {
        account+=money;
    }
    public void userThread() {
        Bank bank = new Bank();
        MybanRunnable my1 = new MybanRunnable(bank);
        System.out.println("线程1");
        Thread th1 = new Thread(my1);
        th1.start();
        System.out.println("线程2");
        Thread th2 = new Thread(my1);
        th2.start();
    }
}

ThreadLocal

用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。
主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

下面看个例子:

public class Bank {  
    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){  
        @Override  
        protected Integer initialValue() {  
            // TODO Auto-generated method stub  
            return 0;  
        }  
    };  
    // 存钱  
    public void addMoney(int money) {  
        count.set(count.get()+money);  
        System.out.println(System.currentTimeMillis() + "存进:" + money);  
    }  
    // 取钱  
    public void subMoney(int money) {  
        if (count.get() - money < 0) {  
            System.out.println("余额不足");  
            return;  
        }  
        count.set(count.get()- money);  
        System.out.println(+System.currentTimeMillis() + "取出:" + money);  
    }  
    // 查询  
    public void lookMoney() {  
        System.out.println("账户余额:" + count.get());  
    }  
}
public class SyncThreadTest {  
    public static void main(String args[]){  
        final Bank bank=new Bank();  
        Thread tadd=new Thread(new Runnable() {  
            @Override  
            public void run() {  
                while(true){  
                    try {  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                    bank.addMoney(100);  
                    bank.lookMoney();  
                    System.out.println("\n");  
                }  
            }  
        });  
        Thread tsub = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                while(true){  
                    bank.subMoney(100);  
                    bank.lookMoney();  
                    System.out.println("\n");  
                    try {  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }     
                }  
            }  
        });  
        tsub.start();  
        tadd.start();  
    }  
}

发现没有,有个线程账户一直为0,是的,这就是ThreadLocal得数据隔离
ThreadLocal与同步机制
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

posted @ 2022-07-12 17:28  Faetbwac  阅读(27)  评论(0编辑  收藏  举报