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方法上的锁,实际是对实例对象加锁。

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

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

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

synchronized 修饰静态类或静态方法

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

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

synchronized修饰一个类

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

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

synchronized总结:

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

    Lock

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

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

先通过代码了解 Lock的使用

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
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
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

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

修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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

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

文章目录
  1. 1. 引言
  2. 2. 线程同步
    1. 2.1. synchronized
      1. 2.1.1. synchronized 同步方法
      2. 2.1.2. synchronized 同步代码块
      3. 2.1.3. synchronized 修饰静态类或静态方法
      4. 2.1.4. synchronized修饰一个类
    2. 2.2. Lock
    3. 2.3. Lock与synchronized同步方式优缺点
    4. 2.4. 感谢
|