并发 -- 常见关键字

1. synchronized 关键字

synchronized锁住的是对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。synchronized(Sync.class)和static synchronized方法实现了全局锁的效果。作用类的所有实例

注1:synchrnoized锁是可重入的。子类也可调用父类的同步方法,每次重入,monitor中的计数器加1

注2:程序执行过程中,如果出现异常, 默认情况下锁会被释放,故当多线程访问同一个被锁的资源时,若其中一个线程异常,锁被释放,其它线程很有可能执行上一个线程处理一半的数据。

注3:不要用字符串常量作为锁定对象:两个方法锁定的一个对象,造成死锁。

public class T {	
	String s1 = "Hello";
	String s2 = "Hello"; 
	void m1() {
		synchronized(s1) {}
	}	
	void m2() {
		synchronized(s2) {}
	}
}

从字节码分析synchronized 关键字的底层原理:

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
在这里插入图片描述

synchronized 同步语句块的实现,通过对象Mintor(监视器锁)实现,每个对象头都有一个指向监视器锁的指针,同步块的入口和出口分别有monitor.enter和monitor.exit指令。当线程获取到monitor的持有权。将锁计数器加1。相应的在执行monitor.exit指令后,将锁计数器减1,当为0时表明锁被释放。监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁)实现,有用户态到内核态的相互转变,CPU状态切换非常耗时。

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁(过程不可逆):通过对象头标识

synchronized 修饰的方法并没有这两个指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

2. volatile关键字:能保证可见性,不能保证原子性。

线程可以把变量拷贝到本地内存(比如机器的寄存器)中,就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
在这里插入图片描述
需把变量声明为volatile,当变量值发生改变时,会提示该线程,到主内存去更新一下变量值,而不是每次都令线程到主存读取。 volatile的主要作用就是保证变量的可见性、防止指令重排序。

volatile底层实现原理:
通过lock前缀指令来实现。
加入volatile关键字时,会多出一个lock前缀指令,会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI)。

:volatile会把lock操作从read之前移到store之前,即在写回系统内存之前。为了防止修改的数据路过总线(CPU总线嗅探机制),但还没有经过write阶段,其他线程便已经修改,导致脏读。

3. ReentrantLock类
实现了Lock类,必须手动释放锁,常用finally结构。

ReentrantLock 在内部用了 Sync 类来管理锁,真正的获取和释放锁是由 Sync 的实现类来控制的,Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁)。

abstract static class Sync extends AbstractQueuedSynchronizer {}

构造方法

private final Sync sync;
public ReentrantLock() {
    sync = new NonfairSync();        // 默认非公平锁
}
public ReentrantLock(boolean fair) {
  	sync = fair ? new FairSync() : new NonfairSync();
}

公平锁的 lock 方法:

static final class FairSync extends Sync {
        final void lock() {
        acquire(1);
     }
}

非公平锁的 lock 方法:

static final class NonfairSync extends Sync {
    	final void lock() {
        	// 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
        	if (compareAndSetState(0, 1))
            		setExclusiveOwnerThread(Thread.currentThread());
        	else
            		acquire(1);
    }

以下是ReentrantLock的tryAcquire的实现
在这里插入图片描述
可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。该方法主要做一件事情:判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
在这里插入图片描述
总结:公平锁和非公平锁只有两处不同

  • 非公平锁在lock之后,首先会调用CSA进行一次抢锁,如果此时恰好锁没被占用,就直接获取锁返回
  • 非公平锁在CSA失败之后,和公平锁一样进入acquire方法,在acquire方法中,如果发现锁被释放了,非公平锁直接CAS抢锁,但公平锁会先判断队列中是否有线程处于等待状态,如果有,则不去抢锁,乖乖的排在后面。

获取锁的方式:
提供了lock()、tryLock()、tryLock(long timeout, TimeUnit unit)

1)lock(): 该种方式获取锁不可中断,如果获取不到则一直休眠等待。

  • 当锁可用,无论当前现成是否持有该锁,直接获取锁并把count 设置为1.
  • 当锁不可用,那么当前线程被阻塞,休眠一直到该锁可以获取,把count设置为1.

2)tryLock()

  • 当获取锁时,如果其他线程持有该锁,此时线程不用阻塞等待,先去做其他事情;
  • 即使该锁是公平锁fairLock,只要获取锁时该锁可用那么就会直接获取并返回true。

3)tryLock(long timeout, TimeUnit unit):会响应中断

  • 当获取锁时,在超时时间之内没有锁资源可用,则获取失败,不再继续等待
  • 当获取锁时,在超时等待时间之内,被中断了,那么抛出异常,不再继续等待.

4. wait和notify/notifyAll

wait会释放锁,notify和notifyAll不会释放锁。属于 Object 的一部分,并且为final方法,无法被重写。只能在同步方法或同步块中使用,否则会抛IllegalMonitorStateException。
因为调用这几个方法前必须拿到当前对象的监视器monitor对象,而synchronized关键字可以获取 monitor 。

注:wait方法会释放当前持有的监视器锁(monitor),但notify/notifyAll方法调用后,并不会马上释放,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。
  • wait()必须在同步方法或同步代码块中执行,sleep()没有这个限制。
  • wait()被调用后不会主动苏醒,需要另一个线程调用同一个对象的notify/notifyAll方法,若使用wait(Long timeout)会超时后自动苏醒,sleep()方法执行完后会自动苏醒。

Thread.sleep()和LockSupport.park()的区别
(1)从功能上来说,都阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
(2)sleep()没法从外部唤醒,只能自己醒过来;
(3)LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
(4)sleep()方法声明上抛出了InterruptedException中断异常,调用者需要捕获这个异常;LockSupport.park()方法不需要捕获中断异常;
(5)sleep()本身就是一个native方法;LockSupport.park()底层是调用的Unsafe的native方法;
:如果在park()之前执行了unpark(),线程不会被阻塞,直接跳过park(),继续执行后续内容

5. join、yield
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。

public final synchronized void join(long millis)
public final void join() throws InterruptedException

如果一个线程实例A执行了ThreadB.join(),则当前线程A会等ThreadB执行终止后执行。

yield

public static native void yield();

这是一个静态方法,一旦执行,它会让出CPU,然后通过竞争再次获得了CPU时间片当前线程才会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。

6. Condition:线程间协作
注: Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。

可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。使用 Lock 来获取一个 Condition 对象。

 ReentrantLock lock = new ReentrantLock();
 private Condition condition = lock.newCondition();

阻塞队列:用于保存等待获取锁的线程的队列。引入另一个概念,叫条件队列
在这里插入图片描述

  • 条件队列和阻塞队列的节点,都是Node的实例。
  • 每个condition有一个关联的条件队列。条件队列是一个单项链表。
  • 调用condition1.signal() 触发一次唤醒,唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行。

7. 门栓(Latch)
CountDownLatch:用来控制一个或者多个线程等待多个线程。例如在多个线程并发执行时,需要等待所有线程都完成某个任务后再汇总结果,可以使用该关键词实现。

//构造方法
CountDownLatch(int count) 

count 指闭锁需要等待的线程数量,只可被设置一次,而且CountDownLatch没有提供机制去重设该值。

维护一个计数器 cnt,每次调用 countDown() 方法计数器的值减 1,减到 0 时,那些调用 await() 在等待的线程就会被唤醒。

内部通过AQS的共享方式实现,假设任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续剩余动作。

public class CountdownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countDownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("run..");
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}

//run..run..run..run..run..run..run..run..run..run..end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值