一:什么是线程同步
线程同步是指两个或多个线程协同步调,按预期的顺序执行代码。
1:若两个或多个线程同时访问同一个共享资源时,需要让多个线程之间按照顺序访问。
2:若线程A的执行依赖线程B的结果,需要依赖线程同步来保证两个线程的执行的顺序。
二:实现线程同步的几种方式
(一):synchronized
1:synchronized作用有三:
(1):保证程序执行的原子性
在多线程环境下,线程是CPU调度的基本单位,CPU根据不同的调度算法进行线程换。当一个线程获得时间片后开始执行,在时间片耗尽之后,就会失去CPU使用权。因此,在多线程场景下,由于时间片切换的原因,原子性问题可能会出现。
例如,线程1获得时间片开始执行,但在执行过程中,CPU时间片耗尽,线程1需要让出CPU。这时线程2获得了时间片开始执行。然而,对于线程1而言,它的操作可能并没有完全执行完成,也没有完全不执行,这就是原子性问题的产生。因此,保证原子性是非常重要的。
synchronized是如何保证程序执行的原子性呢?
public class SynchronizedDemo {
public void methodA(){
synchronized (this){
System.out.println("synchronized -----");
}
}
}
通过 JDK 自带的 javap
命令查看 SynchronizedDemo
类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java
命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class
。
从图中可以看出:
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 monitorexit
指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
在锁未释放之前,其他线程无法再次获取锁,因此通过monitorenter和monitorexit指令可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,其他线程在锁未释放之前无法访问该代码块。这样,synchronized可以保证方法和代码块内的操作是原子性的。
当线程执行monitorenter指令时,会对Monitor进行加锁,其他线程无法获取锁,除非线程主动解锁。即使在执行过程中,例如CPU时间片用完,线程放弃了CPU,但是并没有进行解锁。由于synchronized的锁是可重入的,线程在下一个时间片中仍然能够获取到锁,并继续执行代码,直到所有代码执行完毕。这样就保证了原子性。
(2):保证可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
在Java内存模型中,所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递需要通过主内存进行数据同步。因此,就可能出现一个线程修改了某个变量的值,但是其他线程无法立即看到修改的值的情况。
为了保证可见性,使用synchronized关键字修饰的代码会在开始执行时加锁,在执行完成后解锁。根据可见性的规则,对一个变量解锁之前,必须先把此变量的值同步回主内存中。这样解锁后,后续的线程就可以访问到被修改后的值。因此,通过synchronized关键字锁住的对象,其值具有可见性。
(3):保证有序性
有性问题可以理解为在多线程环境下,一个线程中的操作可能会被重排或者乱序执行,而在另一个线程中观察这些操作的顺序可能是无法确定的。
Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
2:synchronized作为java的关键字,可以作用在代码块、实例方法、静态方法、和类。
(1):修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void methodA(){
System.out.println("synchronized method-----");
}
synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法。JVM 通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
(2):修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
synchronized static void methodB(){
System.out.println("synchronized static -----");
}
(3):修饰代码块
指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
public void methodC(){
synchronized (this){
System.out.println("synchronized -----");
}
}
(二):ReentrantLock
ReentranLock是一个支持重入的独占锁,在JUC(java.util.concurrent)包中,底层就是基于AQS实现的。
ReentranLock类本身并没有直接继承AQS(AbstractQueuedSynchronizer),而是创建了一个内部类Sync来继承了AQS,而ReentrantLock类本身的那些方法都是调用Sync里面的方法来实现,而Sync本身自己也是一个抽象类,它还有两个子类,分别是NonfairSync和FairSync,对锁各种实际的实现其实在这两个类中实现,顾名思义,这两个类分别实现了非公平锁和公平锁,在创建ReentrantLock时可以进行选择。
/**
*默认创建一个公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 根据参数创建一个公平锁或者非公平锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1:lock()
//会调用Sync类中的lock()方法,所以需要看创建的是公平锁还是非公平锁
public void lock() {
sync.lock();
}
当定义为非公平锁时,先试用CAS
的方式更新AQS
中的state
的状态,默认是0代表没有被获取,当前线程就可以获取锁,然后把state
改为1,接着把当前线程标记为持有锁的线程,如果if
中的操作失败就表示锁已经被持有了,就会调用acquire()
方法
final void lock() {
/**
使用CAS更细state的值,默认是0代表没有被获取,当前线程就可以获取锁,然后把state改为
1,接着把当前线程标记为持有锁的线程
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//如果if中的操作失败就表示锁已经被持有了,就会调用acquire()方法
acquire(1);
}
/**
acquire(1)方法会调用abs的方法,这里面调用子类实现的tryAcquire()方法,最终是调用到Sync类中的
nonfairTryAcquire()方法,可以看到先判断state是不是0,也就是能不能获取锁,如果不能则判断请求
锁的线程和持有锁的是不是同一个,如果是的话就把state的值加1,也就是实现了重入锁
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
当定义为公平锁,在子类FairSync中重写了tryAcquire方法,注意if(c==0)
判断中的代码,也就是线程抢夺锁的时候会调用hasQueuedPredecessors()
方法,这个方法会判断队列中有没有已经先等待的线程了,如果有则当前线程不会抢到锁,这就实现了公平性,上面nonfairTryAcquire()
方法则没有这种判断,所以后来的线程可能会比先等待的线程先拿到锁。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判断是否存在等待队列
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2:tryLock()
可以看到tryLock实际上是非公平锁的实现,不能保证正在排队的线程能拿到锁,因为可能被新来的线程抢走。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
3:unlock()
这个方法是释放锁,最终会调用到Sync
类中的tryRelease()
方法。在这个方法里面会对state
减1,如果减1之后为0就表示当前线程持有次数彻底清空了,需要释放锁。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
(三):CountDownLatch
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。
CountDownLatch的作用也是如此,在构造CountDownLatch(int count):的时候需要传入一个整数count,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。
总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
1:CountDownLatch(int count):构造方法,创建一个新的 CountDownLatch 实例,用给定的计数初始化。参数 count 表示线程需要等待的任务数量。
2:void await():使当前线程等待,直到计数器值变为0,除非线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。在实际应用中,通常在主线程中调用此方法,等待其他子线程完成任务。
3:boolean await(long timeout, TimeUnit unit):使当前线程等待,直到计数器值变为0,或者指定的等待时间已到,或者线程被 interrupted。如果计数器的值已经为0,则此方法立即返回。
参数 timeout 是指定的等待时间,
参数 unit 是 timeout 的单位(如秒、毫秒等)。
此方法返回一个布尔值,表示在等待时间内计数器是否变为0。
4:void countDown():递减计数器的值。如果计数器的结果为0, 则释放所有等待的线程。在实际应用中,通常在线程完成任务后调用此方法。
5:long getCount():获取当前计数的值。返回当前 CountDownLatch 实例内部的计数值。
6:利用CountDownLatch实现ABC的的顺序打印
public class CountDownLatchB {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatchA = new CountDownLatch(1);
CountDownLatch countDownLatchB = new CountDownLatch(1);
CountDownLatch countDownLatchC = new CountDownLatch(1);
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatchA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A");
countDownLatchB.countDown();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatchB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
countDownLatchC.countDown();
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatchC.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
});
threadA.start();
threadB.start();
threadC.start();
countDownLatchA.countDown();
}
}