9.5、线程同步
使用线程就免不了要考虑线程安全的问题,尤其是银行财务等等系统,如果多个线程之间的共享数据一旦出现问题,那么就会造成大乱子。(比如两个人同时向一张卡中存钱,如果在操作中这两个线程同时获取到了卡中的余额,那么在存钱后,就会出现两个不同的返回值)
所以说在多线程的情况下,靠根据实际需求考虑线程的同步问题,线程同步就是当有一个线程在对某个内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,此时其他线程需要处于等待状态。
9.5.1、synchronized
Java中有一个关键字 synchronized ,它就是同步的意思,它就像是一把锁,它将两个任务访问相同的资源加上锁,第一个访问某项资源的任务必须锁定这项资源,其他任务在资源被解锁之前,是无法访问该资源的,只能进行等待,而在其被解锁之时,后续等待的任务才能继续访问。
在synchronized中,任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器),我们必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全:
-
当时用synchronized 修饰方法时,该方法就是一个同步方法(它的锁不需要显示的声明:静态方法的锁(类名.class)、非静态方法的锁(this))
上面的例子只需要在共享的银行卡中存款的方法上添加 synchronized 即可实现同步。
-
修饰代码块时,就是一个同步代码块。(锁:可自己创建对象指定,也是指定为this或类名.class)
需要注意的是对代码块或方法加上锁后,相当于这块代码从线程的并行变成了串行(相当于加锁的地方是单线程运行的),这样虽然解决了线程同步的问题,但同时也降低了运行的效率。所以需要注意加锁的范围(范围太小:没锁住所有有安全问题的代码。范围太大:会浪费多线程优势),所以我们在使用同步时需要:
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中。
会释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait() 方法,当前线程暂停,并释放锁。
不会释放锁的操作:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁()。出现死锁后,程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行。
比如线程1占用了资源A后,在运行时又需要资源B,但是此时资源B已经被线程2锁持有,所以线程1只能等待资源B被释放,但是占用资源B的线程2在运行中又需要资源A,所以两个线程就形成了一个环,但是两个线程又不放弃自身所占用的资源,这样就形成了一个死锁,两个线程一种处于等待资源的状态,但是又获取不到资源,所以程序就会卡在这里无法执行。
产生死锁的必要条件:
- 互斥条件:线程对所分配到的资源不允许其他线程进行访问,若其他线程访问该资源,只能等待,直至占有该资源的线程使用完成后释放该资源
- 请求和保持条件:线程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他线程占有,此事请求阻塞,但又对自己获得的资源保持不放
- 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
9.5.2、Lock
JDK 5.0后,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当,Java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock 接口,它是是可重入的互斥锁,虽然拥有与 synchronized 相同的并发性和内存语义,但是会比 synchronized更加灵活,可以手动开启、关闭锁。
ReentrantLock底层基于AbstractQueuedSynchronizer实现。AbstractQueuedSynchronizer抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,AbstractQueuedSynchronizer为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。
Sync继承了AbstractQueuedSynchronizer,是ReentrantLock的核心。
public class LockTest{
//true是公平锁。默认为false
ReentrantLock lock = new ReentrantLock(true);
public void f() {
//使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
//说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
//说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
//说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
9.6、线程锁
多个线程同时运行一个数据时,往往因为抢占资源而产生脏数据,所以就用到了加锁机制。
- 公平锁/非公平锁
- 公平锁是指多个线程按照申请锁的顺序来获取锁。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
- 可重入锁:递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 独享锁/共享锁
- 独享锁是指该锁一次只能被一个线程所持有。
- 共享锁是指该锁可被多个线程所持有。
- 互斥锁/读写锁
- 互斥锁在Java中的具体实现就是ReentrantLock
- 读写锁在Java中的具体实现就是ReadWriteLock
- 乐观锁/悲观锁
- 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
- 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
- 分段锁:是一种锁的设计,并不是具体的一种锁(ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。)
- 偏向锁/轻量级锁/重量级锁
- 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
- 自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
9.7、通信方式
-
同步:多个线程通过synchronized关键字这种方式来实现线程间的通信。本质上就是“共享内存”式的通信,多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
-
while轮询的方式:通过一个条件,线程A不断地改变条件,线程B不停地通过while语句检测这个条件是否成立 ,从而实现了线程间的通信。
-
wait/notify 机制:在synchronized方法或synchronized代码块中才能使用,通过一个条件,当条件未满足时,线程A调用wait() 放弃CPU,并进入阻塞状态。当条件满足时,线程B调用 notify()唤醒线程A,并让它进入可运行状态。、
-
wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
-
notify():唤醒正在排队等待同步资源的线程中优先级最高者
-
notifyAll ():唤醒正在排队等待资源的所有线程.
@Override public void run() { while (true){ synchronized (this){ notify(); //将等待线程唤醒 if(num < 20 ){ System.out.println(Thread.currentThread().getName()+"打印:"+num); num++; }else { break; } try { //线程进入等待 wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
-
-
管道通信:使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信