Java多线程 -- 04 线程同步

导读目录
  • 同步代码块
  • 同步方法
  • 释放同步监视器的锁定(仔细看)
  • 同步锁(Lock)
  • Lock和synchronized的选择
  • 锁的相关概念介绍
  • 死锁

多线程情况下出现的错误往往是因为线程调度(该调度具有一定的随机性)引起的,不过这种错误是可以从程序编写上来避免的

1.同步代码块

为了解决代码这些问题,Java多线程支持引入了同步监视器,使用同步监视器的通用方法就是同步代码块,格式如下:

//该段代码块往往被放置在方法体内,且是在run()或call()方法体中
synchronized(obj) {
    //此处的代码就是同步代码块
    ...
}

其中的obj是这段同步代码块的同步监视器,这段代码的含义是:线程开始执行同步代码块之前,必须先获得同步监视器的锁定

这段代码执行的过程:加锁 -> 修改 -> 释放所

注:
1.虽然Java程序允许使用任何对象作为同步监视器,但推荐使用可能被并发访问的共享资源充当同步监视器。例如银行取钱例子中,选择将使用账户(account)作为同步监视器。
2.共享资源的代码区也被称为临界区,如上面的同步代码块
3.synchronized关键字可以修饰代码块和方法,但不能修饰构造器和成员变量

2.同步方法

用synchronized来修饰某个方法,则该方法就被称为同步方法,synchronized修饰的实例方法(即非静态方法)而言,其监视器无需显示指定,是this,即方法调用者

使用同步方法可以很方便的实现线程安全的类,(加了同步方法的类变成了线程安全的类 )该类具有的特点:
1.该类的对象可以被多个线程安全的访问
2.每个线程调用该对象的任意的方法之后都能得到正确的结果
3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

//银行取钱的例子:账户类
public class Account {
    ...
    //同步方法: 取钱的操作, 
    public synchronized void draw(double drawAmount) {
        ...
    }
}

注意:
1.不要对线程安全类的所有方法都同步(为了尽量保证程序的效率),只对那些共享资源加同步
2.如果可变类有两种运行环境:单线程、多线程环境,则应该为该可变类提供两个版本,即线程不安全版本和线程安全版本。在单线程中使用线程不安全的版本,以保证性能。在多线程环境下使用多线程版本,以保证安全
3.不可变类总是线程安全的,而可变类往往是线程不安全的。将可变类设置成线程安全的是以牺牲其运行效率为代价的

3.释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,都必须先获得对同步监视器的锁定,处理完资源后,又得释放对同步监视器的锁定,而程序是无法显式释放这个锁定,那么线程在什么情况在会释放对同步监视器的锁定?
1.当前线程的同步方法、同步代码块正常执行结束
2.当前线程在同步代码块、同步方法中遇到break,return终止了该代码块、方法的继续执行
3.当前线程在同步代码块、同步方法中出现了未处理的Error, Exception, 导致了该代码块、方法异常结束
4.当前线程在执行同步代码块、同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

请注意:下面的情况并不会导致线程释放同步监视器:
1.当前线程在执行同步代码块、同步方法时,程序调用了Thread.sleep(), Thread.yield()方法来暂停当前线程的执行,当前线程不是释放同步监视器
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法(即在一个线程中让其他的线程执行suspend()方法)将该线程挂起,则该线程是不会释放同步监视器。当然了,要尽量避免使用suspend()方法和resume()方法来控制线程

4.同步锁(Lock)

这种情况下是不存在同步监视器的,该Lock对象被称为同步锁。
前面讲的同步代码块和同步方法中,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,直到等待到线程释放锁,那么如果这个得到锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断)

再比如当有多个线程读写文件时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,但是读操作和读操作不会发生冲突现象。

Java5以后,提供了一种功能更强大的线程同步机制:通过显式定义同步锁对象来实现同步,该所对象由Lock对象来充当,就可以满足上面说的要求

也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
(1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
(2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象, 注意:必须主动去释放锁,并且在发生异常时,不会自动释放锁

(1)Lock锁
Lock<>(根接口)
    |
    ReentrantLook,可重入锁, 常用

//Lock的源码

public interface Lock {
    void lock(); //用来获取锁。如果锁已被其他线程获取,则进行等待
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock(); //尝试获取锁,如果获取成功,则返回true,反之返回false, 即不会等待着获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试获取锁,拿不到锁时会等待 time 时间, 在此时间内直到获取到(并返回true),或者没有等待到(并返回false)
    void unlock(); //释放锁
    Condition newCondition(); //用于线程通信中
}

方法讲解:
**(1) void lock();**
用来获取锁。如果锁已被其他线程获取,则进行等待
银行取钱的例子, 使用lock()方法时:

public class Account {
//定义锁对象
private final ReentrantLock reLock = new ReentrantLock();
....
//取钱操作
public void draw(double drawAccount) {
//加锁
reLock.lock();
try{

        ... //取钱的逻辑代码
    }finally {

        //释放锁,放在这里是为了确保锁一定能被释放,及时在发生异常情况后
        reLock.unlock();
    }       
}

}


**(2) boolean tryLock();**
尝试获取锁,如果获取成功,则返回true, 反之返回false。该方法会立即返回结果,不会因为没有得到锁而等待着获取锁

**(3) boolean tryLock(long time, TimeUnit unit) throws InterruptedException;** 
尝试获取锁,拿不到锁时会等待time时间, 在此时间内直到获取到(并返回true),或者没有等待到(并返回false)
采用tryLock()方法时:

Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
... //处理异常
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}


**void lockInterruptibly() throws InterruptedException;**
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断(即调用该线程的interrupt()方法),即停止等待。其他的方法和synchronized修饰的代码块和方法都不会相应该中断

//注意这里要处理InterruptedException异常
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}


**注意:**当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为单独调用interrupt()方法(Thread类的方法)不能中断**正在运行**过程中的线程,只能中断**阻塞过程**中的线程。



######(2)ReadWriteLock
读写锁,允许对共享资源并发访问

ReadWriteLock<>(根接口)
|
ReentrantReadWriteLook(可重入读写锁)
StampedLock (Java8新增的)

**(1)采用ReentrantReadWriteLock锁**
在对数据进行读写的时候,为了保证数据的一致性和完整性,需要**读和写是互斥**的,**写和写是互斥**的,但是**读和读是不需要互斥**的,这样读和读不互斥性能更高些

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:**readLock()**和**writeLock()**用来获取读锁和写锁

public class Data {
//定义可读写锁
private ReadWriteLock rwl = new ReentrantReadWriteLock();
...
//写数据
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
... //处理过程
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
//读数据
public void get() {
rwl.readLock().lock();// 取到读锁
try {
... //处理过程
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}


**注意:**的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。


######5.Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断(需要使用lockInterruptibly()方法),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。**记住:写/写互拆,读/写互拆,读/读不互拆**

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。


**注意:**上面介绍的三种同步方法,都遵循了一个规则: ***加锁 -> 修改 -> 释放锁 ***


######6.锁的相关概念介绍
**1.可重入锁**
如果锁具备可重入性(即不需要重复申请锁),则称作为可重入锁。像**synchronized**和**ReentrantLock、ReentrantReadWriteLock**都是可重入锁。可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

class MyClass {
public synchronized void method1() {
method2();
}

public synchronized void method2() {
    ...
}

}

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。


**2.可中断锁**
可中断锁:就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁, 即lockInterruptibly()方法。


**3.公平锁**
**公平锁**即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

**非公平锁**即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁(无参构造器),但是可以设置为公平锁(用有参构造器)。
``
ReentrantLock lock = new ReentrantLock(); //为非公平锁
ReentrantLock lock = new ReentrantLock(true); //true为公平锁,false为非公平锁

4.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

7.死锁

当两个线程相互等待对方释放锁同步监视器时就会放生死锁现象,Java虚拟机并没有检测(而在数据库系统的设计中考虑了监测死锁以及从死锁中恢复,数据库如果监测到了一组事物发生了死锁时,将选择一个牺牲者并放弃这个事物),也没有采取措施来处理死锁,所以所线程编程时应该采取措施避免死锁出现

出现死锁,整个程序不会发生任何异常,也不会给出任何提示,只是所有线程一直处于阻塞状态,无法继续执行

死锁很容易发生,尤其是当系统中有多个同步监视器时

(1)产生死锁的案例及原因

1.最简单的死锁案例:
Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁

2.稍微复杂点的案例
多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。

产生死锁可能性的最根本原因是:
(1)锁交叉现象:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1在没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。
(2)阻塞:另一个原因是默认的锁申请操作是阻塞的

(2)如何避免产生死锁

1.避免锁交叉:避免在一个对象的同步方法中调用其它对象的同步方法(会造成锁交叉),那么就可以避免死锁产生的可能性
2.使用非阻塞式的锁:使用非阻塞式的锁,例如Lock的tryLock()锁,获取不到锁时,会释放自己已获得锁,并睡眠一小段时间,过会再重新申请。这样就会打破锁交叉现象
3.缩小锁范围:尽量避免使用静态同步方法,因为静态同步相当于全局锁, 而我们要尽量减小锁范围
4.尽量避免然一个线程执行过程中同时只需要一把锁(这个方法不太现实,但是一种方法,看看就好)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容