Java程序员升级必备:一文详解多线程之线程同步,从基础到进阶

Java实现线程同步有如下几种方式

使用synchronized或lock锁

使用volatile修饰变量

使用ThreadLocal

使用的类库,如原子操作类、Semaphore信号量、并发集合类等

以下便一一讲解以下

一、Synchronized

synchronized又称隐式锁(不需要加锁、解锁的操作)

synchronized有两种使用方式:修饰方法、修饰代码块,特点如下:

synchronized修饰方法是类锁、修饰代码块是对象锁,

如果同步方法或代码块被static修饰时也为类锁,因为同一个对象的多个实例,在进入静态的同步方法时,一次只能有一个类实例进入,所以同步方法体效率和性能没有同步块高

synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

//加锁方法publicsynchronizedvoidsynMethod){//方法体}//加锁代码块publicvoidsynMethod(){synchronized(Object){//Object指锁对象//方法体}}

同步方法和同步代码块的区别

修饰方法,在调用该方法前,需要获得当前对象锁,即类锁,等同于synchronized(this),是对整个类对象加锁

修饰代码块,在调用该代码块时,可以指定任一对象作为锁

代码演示:

classSynObj{publicsynchronizedvoidshowA(){("showA");try{(3000);}catch(InterruptedExceptione){();}}publicvoidshowB(){synchronized(this){("showB");}}publicvoidshowC(){Strings="1";synchronized(s){("showC");}}}publicclassTest{publicstaticvoidmain(String[]args){finalSynObjsy=newSynObj();newThread(newRunnable(){publicvoidrun(){();}}).start();newThread(newRunnable(){publicvoidrun(){();}}).start();newThread(newRunnable(){publicvoidrun(){();}}).start();}}

执行以上代码,输出如下

showAshowCshowB

showB是延迟3秒才打印的,这是因为同步方法showA使用的是类锁。

二、显式锁Lock

Lock是一个接口提供了无条件、可轮询、定时、可中断的锁获取操作,加锁和解锁都是显式的

包路径是:

主要核心方法:lock()、tryLock()、unlock()、lockInterruptibly()

其实现类有:ReenTrantLock、、

1.Lock核心方法

lock()

是一个阻塞方法,调用后一直阻塞直到获得锁。阻塞过程中不会接受中断信号,忽视interrupt(),拿不到锁就一直阻塞。即:拿不到lock就不罢休,不然线程就一直block。比较无赖的做法。

tryLock

马上返回,拿到lock就返回true,不然返回false。比较潇洒的做法。带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。

lockInterruptibly

调用后如果没有获取到锁会一直阻塞,阻塞过程中会接受中断信号,即线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。

2.Lock主要方法

voidlock():执行此方法时,如果锁处于空闲状态,当前线程将获取到锁.相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁.

booleantryLock():如果锁可用,则获取锁,并立即返回true,否则返回false.该方法和lock()的区别在于,tryLock()只是"试图"获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码.而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行.

voidunlock():执行此方法时,当前线程将释放持有的锁.锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.

ConditionnewCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。

getHoldCount():查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。

getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9

getWaitQueueLength:(Conditioncondition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10

hasWaiters(Conditioncondition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了方法

hasQueuedThread(Threadthread):查询给定线程是否等待获取此锁

hasQueuedThreads():是否有线程等待此锁

isFair():该锁是否公平锁

isHeldByCurrentThread():当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true

isLock():此锁是否有任意线程占用

lockInterruptibly():如果当前线程未被中断,获取锁

tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁

tryLock(longtimeoutTimeUnitunit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

3.ReentrantLock

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。ReentrantLock类实现了Lock,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

换句话说,当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上。

可重入的意思是:ReentrantLock锁可以被单个线程多次获取。

ReentrantLock分为“公平锁”和“非公平锁”

公平锁的机制下,线程依次排队获取锁;

非公平锁的机制下,线程通过竞争获取锁。

ReentrantLock实现

publicclassMyService{privateLocklock=newReentrantLock();//Locklock=newReentrantLock(true);//公平锁//Locklock=newReentrantLock(false);//非公平锁privateConditioncondition=();//创建ConditionpublicvoidtestMethod(){try{();//lock加锁//1:wait方法等待://("开始wait");();//通过创建Condition对象来使线程wait,必须先执行方法获得锁//:2:signal方法唤醒();//condition对象的signal方法可以唤醒wait线程for(inti=0;i5;i++){("ThreadName="+().getName()+(""+(i+1)));}}catch(InterruptedExceptione){();}finally{();}}}

4.ReentrantReadWriteLock

为了提高性能,Java提供了读写锁ReentrantReadWriteLock,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。

读写锁具有公平选择性、可重入性,锁降级的特点

读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。

读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

总之,读的时候上读锁,写的时候上写锁!

Java中读写锁有个接口,也有具体的实现ReentrantReadWriteLock。

5.synchronized和ReentrantLock的区别

两者的共同点:

都是用来协调多线程对共享对象、变量的访问

都是可重入锁,同一线程可以多次获得同一个锁

都保证了可见性和互斥性

两者的不同点:

ReentrantLock是JDK实现的,synchronized是JVM实现的

ReentrantLock是Lock接口的实现,而synchronized是Java中的关键字

ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁

ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性

ReentrantLock可以实现公平锁

底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略

通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。

ReentrantLock通过Condition可以绑定多个条件

Condition

Condition是在中才出现的,它用来替代传统的Object的wait()、notify();使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。

Condition可以实现多路通知功能,也就是在一个Lock对象里可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活

//实例化一个ReentrantLock对象privateReentrantLocklock=newReentrantLock();//为线程A注册一个ConditionpublicConditionconditionA=();//为线程B注册一个ConditionpublicConditionconditionB=();

6.Condition类和Object类锁方法区别

Condition类的awiat方法和Object类的wait方法等效

Condition类的signal方法和Object类的notify方法等效

Condition类的signalAll方法和Object类的notifyAll方法等效

ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的

7.tryLock和lock和lockInterruptibly的区别

tryLock能获得锁就返回true,不能就立即返回false,tryLock(longtimeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回false

lock能获得锁就返回true,不能的话一直等待获得锁

lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,而lockInterruptibly会抛出异常。

三、volatile

volatile变量具备两种特性:变量可见性、禁止重排序

即volatile变量,用来确保将变量的更新操作通知到其他线程。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。比sychronized更轻量级的同步锁

并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。

java多线程中的原子性、可见性、有序性

原子性:是指线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。

可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。

有序性:有序性就是代码执行顺序及是并发执行顺序。

1.变量可见性

其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

2.禁止重排序

volatile禁止了指令重排。所以具备有序性,但不能保证原子性,所以不适用于高并发环境做安全机制

3.为什么不能保证原子性

举例说明:线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。

问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

4.适用场景

值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(booleanflag=true)。

该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。

5.volatile与synchronize的区别

都可以解决线程并发的问题,

synchronized修饰同步,用volatile修饰的成员变量

volatile:保证可见性,禁止指令重排序,不能保证原子性;synchronized:则可以保证变量的修改可见性和原子性

volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。

四、ThreadLocal

很多地方ThreadLocal叫做线程本地变量,也有些地方叫做线程本地存储;ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

使用场景

最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

示例:

//JDBC获取连接privatestaticThreadLocalConnectionthreadLocal=newThreadLocalConnection();publicstaticConnectiongetConnection(){Connectionconn=();if(conn==null){try{conn=((""),(""),(""));(conn);}catch(Exceptione){thrownewRuntimeException("建立数据库连接异常",e);}}returnconn;}
五、类库

1.原子操作类型

在Java并发比编程中,要想保证一些操作不被其他线程干扰,就需要保证原子性,在的包下JDK中提供了16(jdk8增加了4个)个原子操作类来帮助我们进行开发。可以在高并发环境下的高效程序处理,其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,由于一般CPU切换时间比CPU指令集操作更加长,所以在性能上有了很大的提升。

原子类如下:

基本类型:AtomicInteger、AtomicLong、AtomicBoolean

引用类型:AtomicReference、AtomicStampedeReference、AtomicMarkableReference

数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

JDK8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder

我们知道,在多线程程序中,诸如++i或i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用synchronized将该操作变成一个原子操作,但JVM为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是ReentantLock的好几倍。

2.ConcurrentHashMap并发

整个ConcurrentHashMap由一个个Segment组成,Segment代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁,线程安全(Segment继承ReentrantLock加锁)

如果需要在ConcurrentHashMap中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行

具体可以看一下[Java-集合篇(5)集合之Map](Java-集合篇(5)集合之Map章节.md)章节

3.CAS(比较并交换-乐观锁机制-锁自旋)

CAS(CompareAndSwap/Set)比较并交换,CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。

三种CAS方法

Atomic包里的类基本都是使用Unsafe下的CAS方法实现,Unsafe只提供了三种CAS方法:compareAndSwapObject、compareAndSwapInt和compateAndSwapLong,其他类型都是转成这三种类型再使用对应的方法去原子更新的。

4.AQS(抽象的队列同步器)

AbstractQueuedSynchronizer类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它

范例演示:

publicclassAtomicIntegerTest{privatestaticfinalintTHREADS_CONUT=20;publicstaticAtomicIntegercount=newAtomicInteger(0);publicstaticvoidincrease(){();}publicstaticvoidmain(String[]args){Thread[]threads=newThread[THREADS_CONUT];for(inti=0;iTHREADS_CONUT;i++){threads[i]=newThread(newRunnable(){@Overridepublicvoidrun(){for(inti=0;i1000;i++){increase();}}});threads[i].start();}while(()1){();}(count);}}

5.ABA问题

CAS会导致“ABA问题”。CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题

六、Semaphore信号量

信号量Semaphore是包下一个常用的同步工具类;Semaphore基于计数的信号量它可以设定一个阈值,基于此AQS的共享模式,可以控制同时访问的线程个数,通过acquire()获取一个许可,如果没有(超过阈值),线程申请许可信号将会被阻塞,而release()释放一个许可或者线程被中断

使用场景:

我们经常用信号量来管理可重复使用的资源,比如数据库连接、线程等,因为这些资源都有着可预估的上限

那么为什么Semaphore没有单独同重设信号量数量的方法呢?直接把AQS的setState方法暴露出来不就行的吗?

因为setState操作如果发生在在某些使用该Semaphore的线程还没有走完整个信号量的获取和释放的流程时,将会直接导致state值的不准确。

范例:

操场上有5个跑道,一个跑道一次只能有一个学生在上面跑步,一旦所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不跑了。

//操场publicclassPlayground{//跑道staticclassTrack{privateintnum;publicTrack(intnum){=num;}@OverridepublicStringtoString(){return"Track{num="+num+'}';}}privateTrack[]tracks={newTrack(1),newTrack(2),newTrack(3),newTrack(4),newTrack(5)};privatevolatileboolean[]used=newboolean[5];privateSemaphoresemaphore=newSemaphore(5,true);//获取一个跑道publicTrackgetTrack()throwsInterruptedException{(1);returngetNextAvailableTrack();}//遍历,找到一个没人用的跑道privateTrackgetNextAvailableTrack(){for(inti=0;;i++){if(!used[i]){used[i]=true;returntracks[i];}}returnnull;}//释放跑道publicvoidreleaseTrack(Tracktrack){if(makeAsUsed(track)){(1);}}//确认跑道使用情况privatebooleanmakeAsUsed(Tracktrack){for(inti=0;;i++){if(tracks[i]==track){if(used[i]){used[i]=false;returntrue;}else{returnfalse;}}}returnfalse;}}

测试类

publicclassSemaphoreDemo{staticclassStudentimplementsRunnable{privateintnum;privatePlaygroundplayground;publicStudent(intnum,Playgroundplayground){=num;=playground;}@Overridepublicvoidrun(){try{//获取跑道=();if(track!=null){("学生"+num+"在"+()+"上跑步");(2);("学生"+num+"释放"+());//释放跑道(track);}}catch(InterruptedExceptione){();}}}publicstaticvoidmain(String[]args){Executorexecutor=();Playgroundplayground=newPlayground();for(inti=0;i100;i++){(newStudent(i+1,playground));}}}
七、死锁

定义:两个或者多个线程互相持有对方所需要的资源,导致这些线程处于无限期等待状态,无法继续执行

例如:线程1锁住了A资源并等待读取B资源,而线程2锁住了B资源并等待A资源,这样两个线程就发生了死锁现象。

死锁发生的条件:

互斥条件:一个资源每次只能被一个进程使用。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持占有。

不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁:只要破坏产生死锁的四个条件中的其中一个就可以了。

死锁范例

privatestaticStringA="A";privatestaticStringB="B";publicstaticvoidmain(String[]argc){newThread(()-{synchronized(A){try{(2000);}catch(InterruptedExceptione){();}synchronized(B){}}}).start();newThread(()-{synchronized(B){synchronized(A){}}}).start();;}

版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。

相关推荐