线程同步
-
synchronized
-
Lock
- ReentranLock
-
volatile
-
局部变量 ThreaLocal
-
阻塞队列
-
原子变量
锁 (对象监视器)
synchronized锁
- 是什么锁?
- 是一种互斥锁:一次只允许一个线程进入被锁住的代码块
- 是一种内置锁/监视器锁
- monitor lock(监视器锁) 工作原理
- 只有在获取(acquire)锁成功之后 ,才能成为锁的拥有者(owner),通过调用wait()可以进入 等待区(wait set),方法执行完毕,线程退出临界区,释放监视锁
- 用处是什么?
- 保证了线程的原子性
- 只允许一个线程访问
- 保证了线程的可见性
- 执行完sync之后,修改的变量对其他线程是可见的
- 怎么使用?
- 修饰普通方法
public class syncTest implements Runnable {
int count=0;
//加锁
@Override
public synchronized void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+":"+count++);
}
}
public static void main(String[] args) {
//创建一个对象
syncTest syncTest = new syncTest();
//两个线程 一个对象
Thread thread = new Thread(syncTest,"thread1");
Thread thread2 = new Thread(syncTest,"thread2");
thread.start();
thread2.start();
}
}
打印结果:
thread1:0
thread1:1
thread1:2
thread2:3
thread2:4
thread2:5
public static void main(String[] args) {
//同样的 如果是两个线程
syncTest syncTest = new syncTest();
syncTest syncTest2 = new syncTest();
// 两个线程 两个对象
Thread thread = new Thread(syncTest,"thread1");
Thread thread2 = new Thread(syncTest2,"thread2");
thread.start();
thread2.start();
}
result:
thread1:0
thread2:0
thread1:1
thread2:1
thread1:2
thread2:2
//不涉及到数据共享的情况 分别访问两个不同的对象
对于这个问题 我们可以用修饰动态静态方法 来解决
- 修饰静态方法
- 静态方法不属于当前实例,而是属于类。
public class syncTest implements Runnable {
//静态变量
static int count = 0;
@Override
public void run() {
count();
}
//修饰静态方法
private synchronized static void count() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
public static void main(String[] args) {
syncTest syncTest = new syncTest();
syncTest syncTest2 = new syncTest();
Thread thread = new Thread(syncTest, "thread1");
Thread thread2 = new Thread(syncTest2, "thread2");
thread.start();
thread2.start();
}
}
打印结果:
thread2:0
thread2:1
thread2:2
thread1:3
thread1:4
thread1:5
// 虽然是两个对象实例,但是是count是正确的,也就是线程安全的。 但是thread1 2 不一定哪个先运行。
- 修饰代码块
- 使用场景:方法体比较大,需要同步的代码只是一小部分,如果整个方法同步,效率太低。
其他代码同上
private void count() {
…… 省略
synchronized (this) {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
输入结果:
thread1:0
thread2:0
thread2:2
thread1:1
thread2:3
thread1:4
//是线程不安全的 因为this 指的就是调用这个对象的实例,上面两个线程调用两个不同的实例
解决上面问题的办法就是
//把this改成当前类
synchronized (syncTest.class) {
}
//或者任意一个类都可以 不建议使用
synchronized (Object.class) {
}
- this的效果和修饰方法体类似 是当前实例
- xxx.class 效果和修饰静态类类似 是类
4.释放锁
-当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
-当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
- 原理
通过javap生成的class可以看见
- Synchronized在JVM里可以通过成对的MonitorEnter和MonitorExit指令来实现
- 执行到MonitorEnter尝试获取monitor的所有权,即获取该对象锁
sync锁存放结构图:
imagemonitor运行机制图:
image- Monitor Record
- 是线程私有的数据结构,每个线程都有一个Monitor Record列表。
- Monitor Record结构包括
- Owner:存放拥有该锁的线程的唯一标识
- 其他……
- java对象头
- Synchronized使用的锁是放在java对象头里的,具体位置是对象头的MarkWord
- MarkWord中的LockWord指向monitor record的起始地址
具体参考
Lock
image- Lock是一个接口,常用方法有
- lock()
// 1.尝试获取锁,如果锁被其他线程获取,则处于等待状态
// 2.必须主动释放锁,发生异常时,也不会自动释放锁
// 3. 必须在 try catch中进行,在finally中释放锁,防止死锁发生
void lock();
可中断锁
//尝试获取锁,可以相应中断,可以中断线程的等待状态
void lockInterruptibly() throws InterruptedException;
- tryLock()
轮询锁
//1.尝试获取锁,获取锁成功则返回true,否则返回false
//2.所以拿不到锁也会立即返回 不会等待
boolean tryLock();
定时锁
//1. 可以设置等待时间
//2. 未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁 一定要在finally块中释放
void unlock();
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能
//一个锁可以有多个条件变量
Condition newCondition();
Condition中await()/signal()/signalAll()分别对应synchronize中的wait()/notify()/notifyAll()
- ReentrentLock
- 可重入锁 一个线程可以多次lock()
- 独占锁(排它锁) 只允许一个线程获取锁
- 公平/非公平锁 :只针对上锁过程
- 公平锁:直接加入同步队列
- 非公平锁(默认使用):尝试获取锁,成功则返回,失败则加入同步队列
Lock lock = new ReentrentLock();
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
synchronized与ReentrentLock对比
- 性能
- sync JDK1.5中效率较低,1.6以后有很多优化措施,加入了自适应自旋,锁消除,锁粗化,轻量级锁等
- 1.6以后 建议使用sync
- 用途
- 都是可重入锁
- ReentrantLock增加了一些高级功能:
- 等待可中断:正在等待的线程可以放弃等待
- 可实现公平锁
- 锁可以绑定多个条件:可以同时绑定多个Condition()对象
- 实现策略
-sync:阻塞(互斥)同步,是一种悲观的并发策略。- 原理是,其他线程只能依靠阻塞来等待线程释放锁,而cpu在转换线程阻塞的时候会引起上下文切换,从而导致效率低
- ree :非阻塞同步:基于冲突检测的乐观并发策略。
- 如果没有线程争用共享数据,操作成功。否则,产生冲突就再进行补偿措施(不断的重试,直到成功)
乐观的并发策略里,需要操作和冲突检测具备原子性,这里用到了CAS操作Compare and Swap ,ReentrantLock中实现非公平锁的时候用到了compareAndSetState,compareAndSet()叫做非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他的线程
3.ReadWriteLock
// 是一个接口 只定义了两个方法
//读和写分成两个锁,从而实现多个线程可以同时进行读操作
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
- ReentrenReadWriteLock
//并为实现Lock接口 只实现了ReadWriteLock接口
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
}
多个线程进行【读】操作
public class RwLockTest {
//创建锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read(Thread thread) {
//获取锁
rwLock.readLock().lock();
try {
long start = System.currentTimeMillis();
System.out.println("线程" + thread.getName() + "read start");
while (System.currentTimeMillis() - start <= 1) {
System.out.println("线程" + thread.getName() + "read ing ");
}
System.out.println("线程" + thread.getName() + "read end");
} finally {
//必须释放锁
rwLock.readLock().unlock();
}
}
public static void main(String[] args) {
//必须用final 不然下面两个线程会创建两个对象副本
final RwLockTest rwtEST = new RwLockTest();
new Thread("AAA") {
@Override
public void run() {
rwtEST.read(Thread.currentThread());
}
}.start();
new Thread("BBB") {
@Override
public void run() {
rwtEST.read(Thread.currentThread());
}
}.start();
}
}
打印结果:
线程BBB read start
线程AAA read start
线程BBB read ing
线程AAA read ing
……
线程BBB read ing
线程AAA read ing
线程BBB read ing
线程AAA read end
线程BBB read end
//可以看到两个线程是同时读的
【注意】
- 如果一个线程已经占用了读锁,其他线程要申请写锁,该线程会一直等待释放读锁
- 如果一个线程占用了写锁,此时申请读或者写的线程,都要等待释放写锁
- 锁的概念
- 可重入锁
- 如果锁具备可重入性,成为可重入锁
- 表明锁的分配粒度:基于线程的分配,而不是基于方法的分配
- 例子:
//method1、method2都用synchronized修饰,当一个线程获取到method1的锁,而需要调用method2时,不需要再次申请锁,可以直接访问。
//假设不具备可重入性,该线程获取到该对象的锁,又要申请该对象的锁,会造成死锁情况。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
网友评论