Java——一篇文章了解高并发编程基础知识点

synchronized关键字

synchronized 的含义:

  • Java中每一个对象都可以成为一个监视器(Monitor), 该Monitor由一个锁(lock), 一个等待队列(waiting queue ), 一个入口队列( entry queue).
  • 对于一个对象的方法, 如果没有synchronized关键字, 该方法可以被任意数量的线程,在任意时刻调用。
  • 对于添加了synchronized关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。
  • synchronized用于实现多线程的同步操作
  • 给某个对象加锁;
  • synchronized锁定的是一个对象,而不是代码块,该对象可以由我们指定;
  • 某个方法从头到尾都需要锁定,则synchronized可以写在方法上,锁定的还是this对象;
  • synchronized如果用在一个静态方法上,相当于锁定的是T.class这个对象;
  • 被synchronized包围的代码块可以视为一个原子操作,但是其中发生未处理的异常,则锁会被自动释放,此时其他线程就能重入该方法;
  • 当可以理解锁是加给某个对象的本质就不难分析出诸如:“同步方法和非同步方法是否可以同时调用”的问题。当然可以,比如t1的m1方法是同步方法,t2的m2不是同步方法,t1的m1执行的间隙,调度器也可以调度t2执行m2,因为m1执行需要获得当前对象的锁,而m2不需要,即使锁被占用,也一样可以执行;抑或“同步非静态方法和同步静态方法是否可以同时执行?”,一样前者锁定的是this对象,后者锁定的是当前类的类对象,是两块不同的空间,所以当然可以同时执行;
  • 对业务“写方法”加锁,对“读”方法不加锁,会出现脏读的问题;
  • 同一个类中(两个方法都是加锁在this上),一个同步方法可以调用另外一个同步方法,因为调用方法获得锁之后,被调用的方法在该方法中申请该锁仍然可以获得,synchronized是可重入的
  • 调用父类的synchronized方法,锁住的是子类的对象,而不是父类的对象。所以子类同步方法可以调用父类的同步方法(有疑问?) 对于同步方法,谁调用锁定的对象就是谁,子类方法中通过super调用父类同步方法,JVM认为调用者依然是子类;
  • 同步方法调用非同步方法,非同步方法依然可以重入;

volatile关键字

1、保证内存可见性;2、防止指令重排;此外需注意volatile并不保证操作的原子性。

  • A、B线程都用到一个变量V,java默认是A、B线程中都保留一份copy,这样如果B修改了变量V,线程A未必知道。使用Volatile关键字,会让所有线程操作V前去再读一次变量V。(使用volatile关键字,会强制所有线程去堆内存中读取变量V)
  • volatile不能保证多个线程共同修改同一个变量V所带来的不一致的问题,也就是说volatile不能替代sychronized。
  • synchronized既可以保证可见性也可以保证原子性,而volatile只能保证可见性;
  • 参考

AtomicXXX类

  • 为解决多线程对一个数字变量递增递减操作的原子性和可见性可以使用AtomicXXX类。AtomicXXX类本身方法都是具有原子性的,但是不能保证多个方法连续调用时的原子性;

synchronized优化

  • synchronized优化,同步代码块语句越少越好。
  • 锁定某个对象o,如果o的属性发生改变,不影响锁的使用。但是如果o变成另一个对象,则锁定的对象发生改变锁失效;应当避免将锁定对象引用变成另外一个对象
  • 不要以字符串常量作为锁定对象;
//s1和s2其实是用一个对象,
//都在字符串常量池里
String s1 = "lock";
String s2 = "lock";
synchronized(s1)
synchronized(s2)
 //下面这俩个不是同一个对象
String s1 = new String("hello");
String s2 = new String("hello");

wait和notify/notifyAll

  • 是Object的方法
  • wait会释放锁,notify不会释放锁,只会唤醒其他挂起的线程,等待调度器调度;
  • 为什么wait()和notify()/notifyAll()需要搭配synchonized关键字使用;如果不搭配使用:
// 线程A 的代码
while(!condition){ 
// 不能使用 if ,
//因为存在一些特殊情况,
//使得线程没有收到notify
//时也能退出等待状态
    wait();
}
// do something

// 线程 B 的代码
if(!condition){ 
    // do something ...
    condition = true;
    notify();
}
  • 现在考虑, 如果wait() 和 notify() 的操作没有相应的同步机制, 则会发生如下情况:
  1. 【线程A】 进入了 while 循环后突然被挂起
  2. 【线程B】 执行完毕了 condition = true; notify(); 的操作, 此时【线程A】的 wait() 操作尚未被执行, notify() 操作没有产生任何效果
  3. 【线程A】执行wait() 操作, 进入等待状态,如果没有额外的 notify() 操作, 该线程将持续在 condition = true 的情形下, 持续处于等待状态得不到执行。
  • 永远不要在循环之外调用wait方法【《Effective Java》第二版中文版第69条244页】
public synchronized void put(T t){
        while (lists.size()==MAX){//想想为什么用while而不用if
            try{
                this.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        lists.add(t);//防止在执行这句话前别的线程向容器里put了一个,size到MAX了,此时在执行就超出容器MAX容量了,用while可以循环检查,避免这种情况
        ++count;
        this.notifyAll();//为什么要notifyAll,因为notify只会叫醒一个线程,可能还会是生产者,如果每次都叫醒生产者,就会导致程序执行不下去

    }

    public synchronized T get(){
        T t = null;
        while (lists.size() == 0){
            try{
                this.wait();//一般wait会和while联用
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        t = lists.removeFirst();
        count--;
        this.notifyAll();//effective java 永远用notifyAll不要用notify
        return t;
    }
  • 假如有两个生产者线程程序满足条件都执行完wait()之后处于挂起状态,此时有个消费者消费了一个,并notifyAll()唤起所有线程;

  • 如果是用的while,在执行完wait()挂起时还在while循环体中,被唤醒后被调度,会再次执行while判断,决定是否要执行生产;

  • 而用if,在执行完wait()挂起时还在if中,但是不会再去判断容器是否满了,会直接生产向容器中增加元素,可能导致溢出;

  • 唤醒线程一定要用notifyAll();因为notify只会叫醒一个线程,可能还会是生产者,如果每次都叫醒生产者,就会导致程序执行不下去

  • wait和notify/notifyAll整个通信过程比较复杂

Latch(门闩)

使用Latch(门闩)替代wait notify来进行通知

  • 好处是通信方式简单,同时也可以指定等待时间
  • 使用await和countdown方法代替wait和notify
  • CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行
  • 当不涉及同步,只涉及线程通信的时候,用synchronized+wait/notify就显得太重了
  • 这是就应该考虑countdownlatch/cyclicbarrier/semaphore

ReentrantLock重入锁

①Lock lock = new ②ReentrantLock();
③lock.lock();
④lock.unlock();//必须要手动释放锁
  • 可以替代synchronized,使用reentrantlock可以完成和synchronized相同的功能;
  • reentrantlock需要手动释放锁;
  • 使用synchronized锁定如果遇到异常,jvm会自动释放锁,但是reentrantlock必须手动释放锁;
  • 通常在finally中进行释放锁。
  • 使用reentrantlock可以进行尝试锁定(尝试获得锁),不管是否获得锁,方法都继续执行
  • 可以根据tryLock的返回值来判断是否锁定,然后执行不同的业务逻辑
  • tryLock可以指定时间,tryLock(time),尝试等待几秒(一段时间)去获得锁,然后继续执行。由于tryLock(time)抛出异常,所以注意unlock处理,必须放到finally里去释放锁
  • 相比之下reentrantlock比synchronized更加灵活。
  • 使用reentrantlock可以在某个线程中调用lock.lockInterruptibly()方法,这样lock会对interrupt方法做出响应,其他线程可以通知该线程中断,不要再死等了;
  • Reentrantlock 可以使用公平锁,之前的锁都是效率优先属于不公平的,由调度器选择调度
//参数为true标识为公平锁
ReentrantLock lock = new ReentrantLock(true);

Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。

  • Conditon中的await()对应Object的wait();
  • Condition中的signal()对应Object的notify();
  • Condition中的signalAll()对应Object的notifyAll()
private Condition producer = lock.newCondition();
private Condition customer = lock.newCondition();//条件
producer.await();//生产者等着
customer.signalAll();//唤醒所有消费者
  • Condition的方式可以更加精确的指定哪些线程被唤醒,效率比notifyAll好

ThreadLocal

Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

  • 在不同线程中是两个不同对象
  • 线程中使用完ThreadLocal变量后,要记得及时remove掉。ThreadLocal可能会导致内存泄漏(内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。);

你可能感兴趣的:(Java)