1. synchronized关键字
synchronized关键字保证在同一时刻,只有一个线程可以执行某个对象内某一个方法或某一段代码块。
重量级锁。包含两个特征:互斥性和可见性。
synchronized可以解决一个线程看到对象处于不一致的状态,可以保证进入同步方法或者同步代码块的每个线程都可以看到由同一个锁保护之前所有的修改效果。
实现同步的基础:Java中每个对象都可作为锁。
方法 | static方法 | 锁的是当前类的class对象,该类的所有对象访问这个static方法都被阻塞。 |
非static方法 | 锁的是当前实例对象,必须持有该对象锁才能访问对象内的同步方法。 | |
代码块 | this对象 | 与锁非static方法一样,都是给对象上锁。 |
非this对象x | 给对象x上锁,想访问这个代码段必须持有这个对象x的锁才可以。 | |
class对象 | 与给static方法加同步关键字一样,锁的都是class对象。 |
注意:class锁和对象锁不是同一种锁。
- 持有class锁则可以访问同步的static方法和锁定class对象的代码段;
- 持有对象锁则可以访问同步的非static方法和锁定this对象的代码段。
- 只有多个线程持有同一个对象的锁时,访问该对象内的同步方法才会被阻塞,如果持有的不是同一个对象的锁则异步执行。
2. wait()和notify()
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
3. 使用重入锁
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
注:关于Lock对象和synchronized关键字的选择:
- 最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
- 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码
- 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁
4. await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在Condition上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象
5. 使用局部变量
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal 类的常用方法:
- ThreadLocal() : 创建一个线程本地变量
- get() : 返回此线程局部变量的当前线程副本中的值
- initialValue() : 返回此线程局部变量的当前线程的"初始值"
- set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
6. 使用阻塞队列
本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步。
LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),
LinkedBlockingQueue 类常用方法:
- LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
- put(E e) : 在队尾添加一个元素,如果队列满则阻塞
- size() : 返回队列中的元素个数
- take() : 移除并返回队头元素,如果队列空则阻塞
7. 使用原子变量
需要使用线程同步的根本原因在于对普通变量的操作不是原子的
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作即-这几种行为要么同时完成,要么都不完成。
在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。
其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer;
可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。
AtomicInteger类常用方法:
- AtomicInteger(int initialValue) : 创建具有给定初始值的新的
- AtomicIntegeraddAddGet(int dalta) : 以原子方式将给定值与当前值相加
- get() : 获取当前值
注:
--volatile关键字
轻量级synchronized,使用成本低,不会引起上下文切换和调度。
volatile变量在线程内存中被修改之后要立即同步回主内存中,以保证其他线程使用该volatile关键字修饰的变量时获取到的是最新的变量值。
volatile关键字保证的是变量在不同线程之间的可见性,但是无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。
- 比如自增操作!!!
---
synchronized与Lock的区别
两者区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
参考:https://blog.youkuaiyun.com/scgyus/article/details/79499650