• Java进阶 ——— Java多线程(三)之多线程同步问题


    引言

    接上一篇,Java进阶 ——— Java多线程(二)之如何开启多线程
    介绍了Java多线程的开启方法,但是多线程运行的安全问题,将是本篇的重点

    延伸阅读,Java多线程系列文章

    Java进阶 ——— Java多线程(一)之进程和线程
    Java进阶 ——— Java多线程(二)之如何开启多线程
    Java进阶 ——— Java多线程(三)之多线程同步问题

    在第一篇文章中,提到要实现多线程安全,就要实现线程同步,那么线程同步有哪些方法呢?

    介绍线程同步之前,先大概了解一下多线程的原理。

    线程的执行是CPU随机调度的,比如我们开启N个线程,这N个线程并不是同时执行的,而是CPU快速的在这N个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了。发生上面问题的本质就是CPU对线程执行的随机调度,比如A线程此时正在打印信息还没打印完毕此时CPU切换到B线程执行了,B线程执行完了又切换回A线程执行就会导致第一篇文章中打印错乱问题。

    线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是CPU对线程的随机调度,CPU无法保证一个线程执行完其逻辑才去调用另一个线程执行。

    线程同步

    所以解决线程同步的思路就是:保证一个线程在执行方法的时候如果没执行完那么另一个线程不能执行此方法,换句话说就是只能等待别的线程执行完毕才能执行,确保数据在任何时刻只有一个线程可以操作,保证数据完整性

    为了解决线程同步问题,引入的概念

    synchronized

    synchronized同步有两种方式,同步代码块和同步方法

    synchronized 同步方法

    在方法上加上synchronized关键字,实际上锁的是this,即当前类对象,
    如下列代码,例如外部要调用run方法,则需要创建ThreadRunnable对象实例,此时添加在run方法上的锁,实际是对实例对象加锁。

    class  ThreadRunnable implements Runnable {
    		@Override
    		public synchronized void run() {
    				age++;
    				System.out.println(Thread.currentThread().getName() + "----" + age);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    synchronized 同步代码块

    同步代码块写法:synchronized(obj){},其中obj为锁对象,此处我们传入this,同样方法的锁也为当前对象。

    	class  ThreadRunnable implements Runnable {
    		@Override
    		public void run() {
    			synchronized (this){  //对代码块添加锁,保证线程同步
    				age++;
    				System.out.println(Thread.currentThread().getName() + "----" + age);
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    synchronized 修饰静态类或静态方法

    我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象

    	private static int count = 0;
    	public synchronized static void staticMethod(){
    		for (int i = 0; i < 10; i++) {
    			count++;
    			System.out.println(count);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    synchronized修饰一个类

    synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。

    	class  ThreadRunnable implements Runnable {
    		int a = 0;
    		@Override
    		public synchronized void run() {
    			synchronized (ThreadRunnable.class){
    				a++;
    				System.out.println(Thread.currentThread().getName() + "----" + a);
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    synchronized总结:

    • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
    • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
    • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁(接下来会将到死锁的形成和解决方式),所以尽量避免无谓的同步控制。

    Lock

    Lock与synchronized有什么区别呢?Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,
    可操作性:就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。

    Lock接口的实现子类之一ReentrantLock(关于ReentrantLock,可以参考深入理解ReentrantLock),翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起lock()请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起lock()操作无关。默认情况下是不公平的锁,为什么要这样设计呢?现实生活中我们都希望公平的啊?我们想一下,现实生活中要保证公平就必须额外开销,比如地铁站保证有序公平进站就必须配备额外人员维持秩序,程序中也是一样保证公平就必须需要额外开销,这样性能就下降了,所以公平与性能是有一定矛盾的,除非公平策略对你的程序很重要,比如必须按照顺序执行线程,否则还是使用不公平锁为好。

    先通过代码了解 Lock的使用

    public class ThreadTest {
    
    	private ReentrantLock lock = new ReentrantLock();
    	public void threadTest() {
    
    		ThreadRunnable runnable = new ThreadRunnable();
    		Thread thread = new Thread();
    		Thread thread1 = new Thread(runnable);
    		Thread thread2 = new Thread(runnable);
    		Thread thread3 = new Thread(runnable);
    		Thread thread4 = new Thread(runnable);
    		Thread thread5 = new Thread(runnable);
    		Thread thread6 = new Thread(runnable);
    		Thread thread7 = new Thread(runnable);
    		thread.start();
    		thread1.start();
    		thread2.start();
    		thread3.start();
    		thread4.start();
    		thread5.start();
    		thread6.start();
    		thread7.start();
    
    	}
    
    
    
    	class  ThreadRunnable implements Runnable {
    		int a = 0;
    		@Override
    		public void run() {
    			lock.lock(); // 获取锁对象
    			try {
    				a++;
    				System.out.println(Thread.currentThread().getName() + "----" + a);
    			} finally {
            //为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的
    				lock.unlock(); //释放锁对象
    			}
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    看一下运行结果,程序执行是没有问题的

    10-18 14:40:33.985 2847-3642/com.t9.news I/System.out: Thread-14----1
    10-18 14:40:33.987 2847-3641/com.t9.news I/System.out: Thread-13----2
    10-18 14:40:33.990 2847-3643/com.t9.news I/System.out: Thread-15----3
    10-18 14:40:33.994 2847-3645/com.t9.news I/System.out: Thread-17----4
    10-18 14:40:33.995 2847-3640/com.t9.news I/System.out: Thread-12----5
    10-18 14:40:33.997 2847-3639/com.t9.news I/System.out: Thread-11----6
    10-18 14:40:33.998 2847-3644/com.t9.news I/System.out: Thread-16----7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其实在Lock还有几种获取锁的方式,我们这里再说一种就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候如果拿不到锁就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

    修改代码:

    class  ThreadRunnable implements Runnable {
    		int a = 0;
    		@Override
    		public void run() {
    			if (lock.tryLock()){ //获取锁对象
    				try {
    					a++;
    					System.out.println(Thread.currentThread().getName() + "----" + a);
    				} finally {
    					lock.unlock(); //释放锁对象
    				}
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行程序,查看结果

    10-18 14:43:21.365 3846-3867/com.t9.news I/System.out: Thread-5----1
    10-18 14:43:21.368 3846-3866/com.t9.news I/System.out: Thread-4----2
    10-18 14:43:21.370 3846-3870/com.t9.news I/System.out: Thread-8----3
    10-18 14:43:21.374 3846-3869/com.t9.news I/System.out: Thread-7----4
    10-18 14:43:21.375 3846-3871/com.t9.news I/System.out: Thread-9----5
    
    • 1
    • 2
    • 3
    • 4
    • 5

    很明显,有三个线程没有获取到锁对象,这时候就不等待了。那么这种方法肯定不完美,想让所有线程获取对象,但是线程发现获取不到就放弃了,
    其实tryLock()方法还可以设置获取的等待时长。

    	class ThreadRunnable implements Runnable {
    		int a = 0;
    
    		@Override
    		public void run() {
    			try {
    				// 如果5秒内获取不到锁对象,那就不再等待
    				if (lock.tryLock(5, TimeUnit.SECONDS)) { 
    					try {
    						a++;
    						System.out.println(Thread.currentThread().getName() + "----" + a);
    					} finally {
    						lock.unlock(); //释放锁对象
    					}
    				}
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    查看结果:所有线程都获取到锁对象

    10-18 14:50:48.466 4031-4053/com.t9.news I/System.out: Thread-5----1
    10-18 14:50:48.466 4031-4057/com.t9.news I/System.out: Thread-9----2
    10-18 14:50:48.470 4031-4056/com.t9.news I/System.out: Thread-8----3
    10-18 14:50:48.473 4031-4054/com.t9.news I/System.out: Thread-6----4
    10-18 14:50:48.476 4031-4055/com.t9.news I/System.out: Thread-7----5
    10-18 14:50:48.477 4031-4052/com.t9.news I/System.out: Thread-4----6
    10-18 14:50:48.481 4031-4051/com.t9.news I/System.out: Thread-3----7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Lock与synchronized同步方式优缺点

    • 实现
      Lock 的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的(所有对象都自动含有单一的锁。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数变为0。在线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上获得锁时,计数会递增。只有首先获得锁的线程才能继续获取该对象上的多个锁。每当线程离开一个synchronized方法,计数递减,当计数为0的时候,锁被完全释放,此时别的线程就可以使用此资源)。

    • 释放
      synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。

    • 资源
      在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronized,另外可读性非常好。在资源竞争激烈情况下,Lock同步机制性能会更好一些。

    感谢


    https://www.cnblogs.com/leipDao/p/8295766.html
    http://www.importnew.com/21866.html

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    SpringBoot加载配置文件的顺序
    动态规划与贪心算法
    病人看病模拟程序
    最优传输(Optimal Transport)
    Android的PendingIntent.getBroadcast()PendingIntent.FLAG_IMMUTABLE问题
    ts如何使用class类?与js的class类有什么区别?
    【JAVASE】String类
    IDEA 新建 JavaWeb 项目(:找不到 Web Application 解决方法)
    弱项分析与提高举措
    【Shell 脚本速成】01、编程语言与 Shell 脚本介绍
  • 原文地址:https://blog.csdn.net/m0_67401660/article/details/126107732