一:什么是线程同步

线程同步是指两个或多个线程协同步调,按预期的顺序执行代码。

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();

}

}