计算机系统应用教程网站

网站首页 > 技术文章 正文

你不知道的java的锁机制 描述java锁机制和原理

btikc 2024-10-08 01:16:36 技术文章 9 ℃ 0 评论

java的线程锁有哪些,各自的优劣势。

谈到java的锁,必须提到多线程编程模型:

1.多线程如何进行通信?

1.谈到这个不得不提起JMM(java Memory Model)

堆区以及方法区的内存元素是整个线程共享的。

共享内存机制 (隐式通信,JVM控制的)

2.消息通信机制,wait /notify /notifyall 消息通知 (显式调用)

2.多线程的生命周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

执行流程分析:

就绪状态(Runnable):使用start()方法启动一个线程后,系统为该线程分配了除CPU外 的所需资源,使该线程处于就绪状态。此外,如果某个线程执行了yield()方法,那么该线程会被暂时剥夺CPU资源,重新进入就绪状态。

运行状态(Running):Java运行系统通过调度选中一个处于就绪状态的线程,使其占有CPU并转为运行状态。此时,系统真正执行线程的run()方法,也只有在运行状态中会去调用java的各种行业去阻塞,锁定,和让出cpu时间片段等操作(yield)

阻塞和唤醒线程

阻塞状态(Blocked):一个正在运行的线程因某些原因不能继续运行时,就进入阻塞 状态。这些原因包括:

a) 当执行了某个线程对象的sleep()等阻塞类型的方法时,该线程对象会被置入一个阻塞集内,等待超时而自动苏醒。

b) 当多个线程试图进入某个同步区域时,没能进入该同步区域的线程会被置入锁定集,直到获得该同步区域的锁,进入就绪状态。

c) 当线程执行了某个对象的wait()方法时,线程会被置入该对象的等待集中,知道执行了该对象的notify()方法wait()/notify()方法的执行要求线程首先获得该对象的锁。

死亡状态(Dead):线程在run()方法执行结束后进入死亡状态,或者执行过程出现异常。

由上图可知,在默认的多线程并发编程模型中,我们分析如果是单核的服务器,多线程其实则会频繁的上下文切换,反而耗时,在多核的开发系统中多线程就是可以在不同核中运行,当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象,操作系统都采用抢占式调度策略,对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。我们也可以显式的调用java的函数去控制线程的执行顺序。

3.线程操作函数说明:

sleep 和 join 都是可以是线程陷入阻塞状态

控制的都是当前的线程,Thread.sleep() 让当前的线程睡眠

join是让主线程执行子线程有序,join就是主线程必须等待所有子线程执行完,控制顺序一定时

其他线程没有进入就绪状态时候设置。

Thread.yeild方法其实只是给线程调度机制一个暗示:我的任务处理的差不多了,可以让给相同优先级的线程CPU资源了;不过确实只是一个暗示,没有任何机制保证它的建议将被采纳;由于不确定性故也不建议使用.

wait() 与 notify/notifyAll() 是Object类的方法,wait会使线程中断,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程.

synchronized 关键字底层的原理:

通过javap -c 类名称 (cmd 下查看字节码文件) 可以发现,加了synchronize则会调用过程中有一个监视器monitorenter 去获得锁,结束之后monitorexit去释放锁。

多线程并发带来线程安全问题,故引入了锁机制:

锁的分类:

公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。

对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

synchronized void setA() throws Exception{

Thread.sleep(1000);

setB();

}

synchronized void setB() throws Exception{

Thread.sleep(1000);

}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。

共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

对于Synchronized而言,当然是独享锁。一个类中的多个synchronized关键字,他们之间是同步的,因为他们锁的对象是一致的,所有方法之间是竞争关系。他们之间是互斥的。

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。

乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁和互斥锁比较来理解自旋锁,

自旋锁和互斥锁功在使用时差不多,每一时刻只能有一个执行单元占有锁,而占有锁的单元才能获得临界资源的使用权,从而达到了互斥的目的。自旋锁与互斥锁的区别在于:自旋锁在执行单元在获取锁之前,如果发现有其他执行单元正在占用锁,则会不停的循环判断锁状态,直到锁被释放,期间并不会阻塞自己。由于在等待时不断的"自旋",这也是它为什么叫做自旋锁。所以自旋锁使用时,是非常消耗CPU资源的。而互斥锁在执行单元等待锁释放时,会把自己阻塞并放入到队列中。当锁被释放时,会唤醒队列上执行单元把其放入就绪队列中,并由调度算法进行调度并执行。所以互斥锁使用时会有进程的上下文切换,这可能是非长耗时的一个操作,但是等待锁期间不会浪费CPU资源。所以对两种锁的使用必须要酌情处理。

CAS算法:

java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。

并发编程中的三个概念:

1.原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.可见性

  可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

3.有序性

  有序性:即程序执行的顺序按照代码的先后顺序执行

JVM在执行多线程任务时,共享数据保存在主内存中,每一个线程(执行再不同的处理器)有自己的高速缓存,线程对共享数据进行修改的时候,首先是从主内存拷贝到线程的高速缓存,修改之后,然后从高速缓存再拷贝到主内存。当有多个线程执行这样的操作的时候,会导致共享数据出现不可预期的错误。

解决缓存不一致的问题,有两种解决方案:

  • 在总线加锁,即同时只有一个线程能执行i++操作(包括读取、修改等)。
  • 通过缓存一致性协议

第一种方法就是类型synchronize,加锁。

第二种就是缓存一致性协议,比如Intel 的MESI协议,它的核心思想就是当某个处理器写变量的数据,如果其他处理器也存在这个变量,会发出信号量通知该处理器高速缓存的数据设置为无效状态。当其他处理需要读取该变量的时候,会让其重新从主内存中读,然后再复制到高速缓存区,volatile 修饰的共享变量是遵循数据一致性原则的。

java.util.concurrent 包是专为 Java并发编程而设计的包的需要研究的东西:

CountDownLatch 部分API

1.CountDownLatch(int count).用给定的count值构造一个CountDownLatch().

2.countDown()方法. 每调用一次该方法,count的计数就会减少1。

3.await()方法。阻塞等待,直到count至0。

使用场景:

1.某一线程在开始运行前等待n个线程执行完毕

2.实现多个线程开始执行任务的最大并行性

多进程之间的锁机制:

多进程之间的锁主要是用中间件来实现分布式锁。

1.数据库唯一索引(使用的是mysql的行级锁)

表级,直接锁定整张表,在你锁定期间,其它进程无法对该表进行写操作。如果你是写锁,则其它进程则读也不允许

行级,,仅对指定的记录进行加锁,这样其它进程还是可以对同一个表中的其它记录进行操作。

页级,表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

insert和update操作:

InnoDB默认采用行锁,在未使用索引字段查询时升级为表锁

2.redis (使用setnx方法,带返回值的赋值方法,使用redis单线程的特征)

3.zookeeper 使用临时有序节点,保证数据的一致。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表