当前位置:   article > 正文

线程的锁机制和解决线程同步问题的方法_同步线程解决的问题 (synchronized、同步锁)

同步线程解决的问题 (synchronized、同步锁)

线程的安全问题

多个线程同时访问一个资源(变量、代码块、数据等),可能出现线程同步问题。

多个线程的执行是抢占式的,一个线程在执行一系列程序指令中途时,就被其它线程抢占,可能导致数据状态出现问题。

解决方式:锁机制

锁机制

synchronized 关键字,实现锁机制,可以用于修饰方法或代码块,提升数据的安全性,降低性能。

synchronized的原理:

通过监视器(monitor)完成

当对方法或一段代码上锁后,会启动监视器对这段代码监控,监视器中有计数器,当计数器为0时,允许线程进入,线程进入后,计数器加1,其它线程访问时,计数器不为0,不允许线程进入,线程执行完代码块后,计数器减1为0 ,监视器允许其它线程进入。

同步方法

当第一个线程进入方法后,自动上锁,其它线程访问不了,前面线程执行完方法后,自动释放锁,其它线程就可以访问。

  1. public synchronized 修饰符 方法名(...){
  2. ...
  3. }

同步代码块

  1. synchronized(锁对象){
  2. ....
  3. ....
  4. }

锁对象的要求:

        1.任意的Java对象都可以作为锁

        2. 不能是局部变量

疑问:同步方法和同步代码块的区别?

解答:

1.语法不同,一个写在方法上,一个写在方法内部

2.锁粒度不同,同步方法作用于整个方法,同步代码块作用于一段代码

3.性能不同,同步方法低于同步块

4.锁对象不同,同步方法是固定的,静态就是类.class,非静态的就是this

同步代码块可以指定锁对象。

手写一个简单的线程安全的单例模式

  1. /**
  2. * 懒汉式
  3. */
  4. public class LazySingleton {
  5. //创建一个锁对象
  6. private static Lock lock = new ReentrantLock();
  7. //2.定义静态实例 加volatile的目的是禁止指令重排,从而得到一个没有初始化的对象
  8. private static volatile LazySingleton singleton = null;
  9. //1.私有的构造方法
  10. private LazySingleton() {
  11. System.out.println("懒汉式创建对象");
  12. try {
  13. Thread.sleep(300);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. //3.静态方法创建对象并返回
  19. public synchronized static LazySingleton getInstance() {
  20. //双检索,DCL double check lock
  21. //提升性能,不为空,就不执行同步代码块
  22. if(singleton==null){
  23. //同步代码块
  24. synchronized (LazySingleton.class){
  25. //判断是否为空,再创建,整体执行
  26. if(singleton==null){
  27. singleton = new LazySingleton();
  28. }
  29. }
  30. }
  31. return singleton;
  32. }
  33. }

同步锁

在并发包 java.concurrent.lock下存在大量的锁

Lock接口

常用的方法:

  1. lock() //上锁
  2. unlock() //解锁

常见的实现类:

  1. ReentrantLock //重入锁
  2. ReadLock //读锁
  3. WriterLock //写锁

简单应用:

  1. //构造方法如果带参数,表示是否是公平锁
  2. Lock lock = new ReentrantLock(); //成员变量
  3. //方法内:
  4. try{
  5. lock.lock();
  6. 同步的代码
  7. }finally{
  8. lock.unlock();
  9. }

公平锁和非公平锁的区别?

公平锁:等待锁时间长的线程,更容器获得锁。

非公平锁:等待锁时间长和短的线程,获得锁几率相同。

synchronized和同步锁的区别?

上锁:synchronized是自动上锁和解锁,同步锁是手动完成的。

性能:同步锁的性能高于synchronized。

使用:synchronized使用简单,功能单一,同步锁提供更多方法,使用灵活。

粒度: 同步方法 > 同步块/同步锁

性能: 同步锁 > 同步块 > 同步方法

方便: 同步方法 > 同步块 > 同步锁

手写一个简单的线程死锁案例

  1. /**
  2. * 线程死锁案例
  3. * 有两个线程,两个锁,线程A持有锁B,需要锁A;线程B持有锁A,需要锁B;
  4. */
  5. public class DieLock {
  6. private static Lock a = new ReentrantLock();
  7. private static Lock b = new ReentrantLock();
  8. public void testA() throws InterruptedException {
  9. synchronized (a){
  10. System.out.println(Thread.currentThread().getName()+"有a锁,需要b锁");
  11. Thread.sleep(300);
  12. synchronized (b){
  13. System.out.println(Thread.currentThread().getName()+"有a锁,b锁");
  14. }
  15. }
  16. }
  17. public void testB() throws InterruptedException {
  18. synchronized (b){
  19. System.out.println(Thread.currentThread().getName()+"有b锁,需要a锁");
  20. Thread.sleep(300);
  21. synchronized (a){
  22. System.out.println(Thread.currentThread().getName()+"有a锁,b锁");
  23. }
  24. }
  25. }
  26. public static void main(String[] args) {
  27. DieLock dieLock = new DieLock();
  28. new Thread(()->{
  29. try {
  30. dieLock.testA();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }).start();
  35. new Thread(()->{
  36. try {
  37. dieLock.testB();
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }).start();
  42. }
  43. }

volatile关键字

线程同步有三大特性:

1.原子性(一系列指令要么全部执行,要么全部不执行)。

2.可见性(线程中的数据修改后,其它线程也可以读取最新的数据)。

3.有序性(线程中的指令按顺序执行,不会出现乱序)。

volatile用于修饰成员变量,保证变量的可见性和有序性。

原子性

一段代码作为整体执行,不会被其它线程打断,可以通过锁机制保证。

可见性

 

多线程的执行过程中,每个线程有自己的工作内存,线程会从主内存中加载数据到工作内存中,修改工作内存的数据后,其它线程是看不到最新的数据的,直到线程将新数据写回到主内存为止,中间存在线程间数据不一致的问题。

解决方法:变量前加volatile

变量前加上volatile,保证可见性,线程在修改此变量时,是在主内存中直接进行修改,其它线程马上可以看到,不存在数据不一致的情况。

有序性

cpu为了提高程序执行的效率,可能对指令顺序进行重排,volatile可以禁止指令重排。

synchronized和volatile的区别

修饰的对象:synchronized修改代码块或方法,volatile只能修饰变量。

保证的特性:synchronized保证原子性、可见性;volatile不能保证原子性,保证可见性和有序性。

性能:volatile是轻量级的线程同步方式,性能更高。

原子类

并发包中提供了一系列原子类,可以保证数据操作的原子性。这里简单举一个AtomicInteger原子整数的例子。

  1. /**
  2. * 原子类
  3. */
  4. public class AtomicDemo {
  5. private static int count =0;
  6. //原子整数
  7. private static AtomicInteger atomicInteger = new AtomicInteger(0);
  8. public static void main(String[] args) {
  9. for (int i = 0; i < 10000 ; i++) {
  10. new Thread(()->{
  11. //1.加载count值
  12. //2.计算count++
  13. //3.将新的值传给count
  14. // count++;
  15. //原子自增
  16. atomicInteger.getAndIncrement();
  17. }).start();
  18. }
  19. try {
  20. Thread.sleep(3000);
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. System.out.println(count);
  25. System.out.println(atomicInteger.intValue());
  26. }
  27. }

乐观锁和悲观锁的区别?

悲观锁:比较悲观,认为线程同步问题会经常出现,倾向于给资源上锁。

乐观锁:比较乐观,认为线程同步问题不会常出现,不给资源上锁,通过其它方式解决。

版本号机制,对数据设置版本,每次修改后版本会更新,修改后将版本和前面版本进行比较,相同就提交,不同就不提交。

CAS机制,CompareAndSwap:通过内存偏移量获得数据的原始值,通过原始值计算出预期的值,将预期的值和实际的值进行比较,相同就更新,否则就不更新进入循环等待状态,直到比较相等。

如果线程的竞争比较激烈,应该使用悲观锁。线程竞争不强的时候,使用乐观锁。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/157667
推荐阅读
相关标签
  

闽ICP备14008679号