前言
在高并发多线程应用场景中对于synchronized和Lock的使用是很普遍的,这篇文章我们就来进行这些知识点的学习,比如说:公平锁与非公平锁、乐观锁与悲观锁、线程间通信、读写锁、数据脏读等知识内容。
目录:
1.同步问题的产生与案例代码
2.synchronized解决同步问题
3.Lock解决同步代码问题
4.公平锁与非公平锁
5.乐观锁与悲观锁
6.synchronized与Lock比较
同步问题案例
这个问题在我们日常生活中非常常见,比如说:秒杀物品的库存数据、火车票剩余票等就是有同步问题,下面我们通过代码来解释这个问题产生的原理:
package com.ckmike.mutilthread;
import java.util.concurrent.TimeUnit;
/**
* SynchronizedQuestionDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午1:34
* @copyright ckmike
**/
public class SynchronizedQuestionDemo {
public static void main(String[] args) {
// 只有10张票
TicketService ticketService = new TicketService(10);
Thread buy1 = new Thread(ticketService);
buy1.setName("buy1");
Thread buy2 = new Thread(ticketService);
buy2.setName("buy2");
Thread buy3 = new Thread(ticketService);
buy3.setName("buy3");
Thread buy4 = new Thread(ticketService);
buy4.setName("buy4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
class TicketService implements Runnable{
private int ticket_store;
public TicketService(int ticket_store) {
this.ticket_store = ticket_store;
}
@Override
public void run() {
while (true) {
if (ticket_store > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出卖票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
}else{
break;
}
}
}
}
上面的执行结果明显是不符合我们的预期的,四个线程同时去买票,对余票数据的判断存在问题,这就是数据脏读的场景。
解决办法:在做售票这个操作时,对于ticket_store的操作同一时刻只能一个线程操作,那么我们这里就会用到锁这个概念了,对于共享数据java中解决数据脏读可以通过synchronized和Lock去解决。
现在我们带着这个问题来了解synchronized和Lock。
synchronized
JVM中每个对象都有一个监控器可以作为锁。当线程试图访问同步代码时,必须先获得对象锁(对象监视器),退出或抛出异常时必须释放锁。Synchronzied实现同步的表现形式分为:代码块同步和方法同步。
同步代码块
在编译后通过将Monitor Enter指令插入到同步代码块的开始处,将Monitor Exit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个Monitor(对象监控器)与之关联,线程执行Monitor Enter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象锁。
同步方法
从class文件结构中可知,synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的对象锁,实现方法同步。
虽然同步方法和同步代码块实现细节不同,但本质上都是对一个对象监视器(monitor)的获取(对象锁的获取)。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
synchronized解决数据脏读问题
package com.ckmike.mutilthread;
import java.util.concurrent.TimeUnit;
/**
* SynchronizedQuestionDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午1:34
* @copyright ckmike
**/
public class SynchronizedQuestionDemo {
public static void main(String[] args) {
// 只有10张票
TicketService ticketService = new TicketService(10);
Thread buy1 = new Thread(ticketService);
buy1.setName("buy1");
Thread buy2 = new Thread(ticketService);
buy2.setName("buy2");
Thread buy3 = new Thread(ticketService);
buy3.setName("buy3");
Thread buy4 = new Thread(ticketService);
buy4.setName("buy4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
class TicketService implements Runnable{
private int ticket_store = 100;
public TicketService(int ticket_store) {
this.ticket_store = ticket_store;
}
@Override
public void run() {
while (true) {
if (ticket_store > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
if(ticket_store > 0) {
// 输出卖票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
}else {
break;
}
}
}else {
break;
}
}
}
}
上面的方式是使用同步代码块实现的同步,解决了数据脏读的问题。我们也可以通过同步方法来解决这个问题,相比较同步方法,同步代码块效率要高些。
上面的代码我们是对于同一个TicketService实例进行多线程操作,所以可以达到同步效果,如果我们使用的是四个不同的实例,那么他们之间就不再是互斥的,因为Java中的锁是对象锁,不同实例对象锁是不一样的,可自行验证。
如果是静态同步方法,那么获取的应该是该类的锁,锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果。
结合上面思考:同步实例方法,同步类方法,synchronized(this),synchronized(ClassName.class)他们之间的一个关系就出来了,以及他们的应用场景也就出来了。
synchronized线程间通信问题
场景描述:现在我有三个线程分别为线程A,线程B,和线程C,三个线程之间有先后顺序的,A操作完了,B才可以操作,B操作完了,C才可以操作。那么这个时候就需要进行线程之间的通信,然线程知道什么时候该自己执行。
分析:在线程A执行期间,B线程一直等待A的通知,B执行期间,C一直等待B的通知。
package com.ckmike.mutilthread;
/**
* BackupDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-20 下午1:51
* @copyright ckmike
**/
public class BackupDemo {
public static void main(String[] args) {
DataTool dataTool = new DataTool();
BackUpA A = new BackUpA(dataTool);
BackUpB B = new BackUpB(dataTool);
BackUpC C = new BackUpC(dataTool);
A.start();
B.start();
C.start();
}
}
class BackUpA extends Thread{
private DataTool dataTool;
public BackUpA(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2A();
}
}
class BackUpB extends Thread{
private DataTool dataTool;
public BackUpB(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2B();
}
}
class BackUpC extends Thread{
private DataTool dataTool;
public BackUpC(DataTool dataTool){
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2C();
}
}
class DataTool{
volatile public String prevA = "A";
// 备份到A数据源
synchronized public void backup2A(){
try {
while ("C".equals(prevA)) {
wait();
}
for(int i=0; i<2;i++){
System.out.println("backup2A数据源");
}
prevA = "B";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
// 备份到B数据源
synchronized public void backup2B(){
try{
while ("A".equals(prevA)){
wait();
}
for (int i=0; i<2; i++){
System.out.println("backup2B数据源");
}
prevA = "C";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
// 备份到c数据源
synchronized public void backup2C(){
try{
while ("B".equals(prevA)){
wait();
}
for (int i=0; i<2; i++){
System.out.println("backup2C数据源");
}
prevA = "C";
notifyAll();
}catch (Exception e){
e.printStackTrace();
}
}
}
通过volatile和synchronized和wait()\notifyAll()结合实现线程间通信,借助标识进行线程顺序执行。
ReentrantLock
在Java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁,后面我们会分析)。在Lock接口出现之前,Java程序是靠synchronized关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了Lock接口用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
ReentrantLock解决同步问题
package com.ckmike.mutilthread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* SynchronizedQuestionDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午1:34
* @copyright ckmike
**/
public class SynchronizedQuestionDemo {
public static void main(String[] args) {
// 只有10张票
TicketService ticketService = new TicketService(10);
Thread buy1 = new Thread(ticketService);
buy1.setName("buy1");
Thread buy2 = new Thread(ticketService);
buy2.setName("buy2");
Thread buy3 = new Thread(ticketService);
buy3.setName("buy3");
Thread buy4 = new Thread(ticketService);
buy4.setName("buy4");
buy1.start();
buy2.start();
buy3.start();
buy4.start();
}
}
class TicketService implements Runnable{
private int ticket_store = 100;
// 默认是非公平锁
private Lock lock = new ReentrantLock();
public TicketService(int ticket_store) {
this.ticket_store = ticket_store;
}
@Override
public void run() {
while (true) {
if (ticket_store > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
if(ticket_store > 0) {
// 输出卖票信息
System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
}else {
break;
}
lock.unlock();
}else {
break;
}
}
}
}
使用Lock同样可以解决数据多线程同步问题。
关于ReentrantLock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。
ReentrantLock线程间通信
package com.ckmike.mutilthread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* BackupDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-20 下午1:51
* @copyright ckmike
**/
public class BackupDemo {
public static void main(String[] args) {
DataTool dataTool = new DataTool();
BackUpA A = new BackUpA(dataTool);
BackUpB B = new BackUpB(dataTool);
BackUpC C = new BackUpC(dataTool);
A.start();
B.start();
C.start();
}
}
class BackUpA extends Thread{
private DataTool dataTool;
public BackUpA(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2A();
}
}
class BackUpB extends Thread{
private DataTool dataTool;
public BackUpB(DataTool dataTool) {
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2B();
}
}
class BackUpC extends Thread{
private DataTool dataTool;
public BackUpC(DataTool dataTool){
this.dataTool = dataTool;
}
@Override
public void run() {
super.run();
dataTool.backup2C();
}
}
class DataTool{
volatile public String prevA = "A";
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 备份到A数据源
public void backup2A(){
try {
lock.lock();
while ("C".equals(prevA)) {
condition.await();
}
for(int i=0; i<2;i++){
System.out.println("backup2A数据源");
}
prevA = "B";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 备份到B数据源
public void backup2B(){
try{
lock.lock();
while ("A".equals(prevA)){
condition.await();
}
for (int i=0; i<2; i++){
System.out.println("backup2B数据源");
}
prevA = "C";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 备份到c数据源
public void backup2C(){
try{
lock.lock();
while ("B".equals(prevA)){
condition.await();
}
for (int i=0; i<2; i++){
System.out.println("backup2C数据源");
}
prevA = "C";
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
利用Condition可以实现与wait()\notifyAll()一样的功能。Condition.await()等同于wait(),condition.signalAll()等同与notifyAll()。
公平锁与非公平锁
公平锁:按照线程获取锁的顺序来分配,FIFO。
非公平锁:是一种获取锁的抢占机制,是随机获取锁。可能造成某些线程一直拿不到锁。
synchronized是非公平锁,ReentrantLock可以通过isFair设置为公平锁,默认是非公平锁。
乐观锁与悲观锁
乐观锁与悲观锁的概念不是JAVA的概念,而是针对关系型数据库数据更新时的一种解决方案。
乐观锁:就是认为数据冲突的可能性比较小,只有当事物提交时才回去判断是否在读取数据后,是否有其他事务修改了该数据,如果有,则当前事务进行回滚。可以这样简单理解:一条数据其中有一个字段是version,每次的更新操作都会自动+1,当你读出这条数据后,如果有其他事务修改了,那么version就与你提交的version不相等,那么这个事务就不会被提交。
悲观锁:则认为数据冲突是大概率事件,所以每次进行修改之前都会先获取该数据的锁,类似于synchronized,所以花费时间较多,效率就会比较低。悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。而悲观锁又分为共享锁和排他锁。
共享锁:就是对于多个不同的事务,对同一个资源共享同一个锁,类似于一个门多把钥匙。
排它锁:排它锁与共享锁相对应,类似于一个门只有一把钥匙。
行锁:这个就是字面上的意思,给数据行加上锁。比如:SELECT * from user where id = 1 lock in share mode; 就是对id=1的数据行加了锁。这个锁就是行锁。
表锁:给表加上锁。
这一部分应该是数据库中的概念,我放到这里就是因为曾经因为面试问得我一脸懵逼,所以就在这里简单的介绍一下,我后面还会写关于数据库关于锁,索引、事务隔离等相关的文章。
ReentrantReadWriteLock
关于ReentrantLock进行更细粒度的锁,就是这个ReentrantReadWriteLock读写锁,可以针对读和写进行加锁。特别要注意:只有读读是不用加锁,属于读读共享;但是只要有写就一定要加锁互斥,比如读写互斥,写读互斥,写写互斥。
读写案例:
package com.ckmike.mutilthread;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* ReadWriteDemo 简要描述
* TODO:描述该类职责
*
* @author ckmike
* @version 1.0
* @date 18-12-21 下午4:48
* @copyright ckmike
**/
public class ReadWriteDemo {
public static void main(String[] args) {
ReadWriteService readWriteService = new ReadWriteService();
ReadThread read1 = new ReadThread(readWriteService);
ReadThread read2 = new ReadThread(readWriteService);
read1.setName("A");
read2.setName("B");
read1.start();
read2.start();
WriteThread write1 = new WriteThread(readWriteService);
WriteThread write2 = new WriteThread(readWriteService);
write1.setName("C");
write2.setName("D");
write1.start();
write2.start();
}
}
class ReadThread extends Thread{
private ReadWriteService readWriteService;
public ReadThread(ReadWriteService readWriteService) {
this.readWriteService = readWriteService;
}
@Override
public void run() {
super.run();
readWriteService.read();
}
}
class WriteThread extends Thread{
private ReadWriteService readWriteService;
public WriteThread(ReadWriteService readWriteService) {
this.readWriteService = readWriteService;
}
@Override
public void run() {
super.run();
readWriteService.write();
}
}
class ReadWriteService{
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
try {
lock.readLock().lock();
System.out.println("获取读锁:"+Thread.currentThread().getName()+" time:" +new Date().getTime());
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("获取写锁:"+Thread.currentThread().getName()+" time:" +new Date().getTime());
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
重入锁
当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。
具体概念就是:自己可以再次获取自己的内部锁。
Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。
public class SynchronizedTest {
public void method1() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1获得ReentrantTest的锁运行了");
method2();
}
}
public void method2() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1里面调用的方法2重入锁,也正常运行了");
}
}
public static void main(String[] args) {
new SynchronizedTest().method1();
}
}
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("方法1获得ReentrantLock锁运行了");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockTest().method1();
}
}
synchronized与ReentrantLock比较
1.区别:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
总结:ReentrantLock相比synchronized,增加了一些高级的功能。但也有一定缺陷。
在ReentrantLock类中定义了很多方法,比如:
getHoldCount():查询当前线程保持此锁定的个数。
getQueueLength(): 返回正等待获取锁定的线程估计数。
getWaitQueueLength(): 返回等待与此锁相关的给定条件Condition的线程估计数。
hasQueuedThread(Thread thread): 查询指定线程正在等待获取此锁定。
hasQueuedThreads(): 获取是否有线程在等待此锁定。
hasWaiters():查询是否有线程在等待与此锁定有关的Condition条件。
isFair():判断是否为公平锁。
isHeldByCurrentThread(): 当前线程是否保持此锁定。
isLocked(): 查询此锁定是否有任意线程保持。
lockInterruptibly(): 当前线程未被中断,则获取锁定。
tryLock():仅在调用时锁定未被另一个线程保持的情况下获取该锁定。
tryLock(long timeout,TimeUnit unit): 如果锁定在给定等待时间内没有被另外一个线程保持,且当前线程未被中断,则获取该锁定。
性能:
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
特别是你去看看ConcurrentHashMap的锁实现,在jdk1.7使用的就是segment分段锁(ReentrantLock实现),但到了jdk1.8就抛弃了segment分段锁,直接使用synchronized+CAS实现锁机制。