由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。使用多线程可能遇到的问题:
并发问题
并发:同一个对象被多个线程同时操作。
例子一买票
问题呈现:
创建类TestThread4,实现Runnable接口。同时创建三个线程对象(小明、Pete、钢铁侠)同时操作该类的实例化对象ticker。
1 |
public class TestThread4 implements Runnable{ |
运行结果:
1 |
Pete----拿到了第9张票~ |
观察程序运行结果,我们发现有同一张票被两个人抢到的情况。多个线程操作同一个资源时,线程不安全,数据紊乱。
例子二银行取钱
账户类Account,记录卡名和余额。
银行类Drawing,模拟取钱操作,操作对象为账户。
添加线程休眠Thread.sleep(1000);
,放大问题的发生性。
1 |
public class UnSafeBank { |
运行结果:
同一个资源被多个线程同时操作,导致数据紊乱。语句System.out.println("--------余额不足,取款失败!--------");
并没有正常执行。
1 |
pete取了50元 |
例子三不安全的集合
启用线程将线程名添加到到集合,操作对象为集合。for循环,建立10万条线程。
1 |
public class UnSafeList { |
运行结果:
使用10万条线程添加数据到集合中,最后结果应该是集合中有10万条数据才对。
1 |
集合的大小:99998 |
要解决这个问题,可以给使用下边的线程同步解决方法或者使用JDK中提供的线程安全类集合CopyOnWriteArrayList
(底层使用了Lock锁)。
解决方法
线程同步:线程同步就是一种等待机制,多个需要同时访问同一个对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。实现线程同步需要:队列+锁。
synchronized关键字
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时需要加入锁机制synchronized
。当一个线程获得对象的排它锁后,将独占资源,其他线程必须等待该线程使用后释放锁。但这个机制也引出了以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起。
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
实现线程同步,使用synchronized
关键字。用法:synchronized
方法与synchronized
块。
synchronized方法
synchronized
方法也可以叫做同步方法,就跟static
关键字一样加在方法的前边用来修饰方法即可。同步方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized
方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
缺点:若将一个大的方法申明为synchronized
将会影响效率。方法里面需要修改的内容才需要锁,锁的太多会浪费资源。
synchronized块
synchronized
块也可以叫做同步块,synchronized( Obj ){ }
。
Obj
称之为同步监视器
Obj
可以是任何对象,但是推荐使用共享资源作为同步监视器。(共享资源可以理解为多个线程共同操作的对象)- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class。
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码。
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
代码演示
使用synchronized
关键字对前边的银行取钱例子进行修改:
注意:synchronized
关键字需要锁的是共享资源即多个线程共同操作的对象(同步监视器),如果这里使用同步方法即关键字加在取钱的run方法上是不行的。因为同步方法的同步监视器是this即银行取钱类Drawing。
1 |
|
取钱操作是对账户中的钱进行增删改查,锁的对象应该是account,应使用同步块。
1 |
|
完整代码:
1 |
public class UnSafeBank { |
运行结果:
1 |
pete取了50元 |
LOCK锁
从JDK5.0开始,java提供了更强大的线程同步机制:通过显式定义同步锁对像来实现同步。(同步锁对像即Lock对像)(显式定义–区别于前边的synchronized关键字)
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对像加锁,线程开始访问共享资源之前应先获得Lock对像
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。(synchronized关键字都是离开synchronized块或方法的范围自动释放锁)(ReentrantLock可重入锁)
代码演示
使用ReentrantLock
对像对前边的买票例子进行修改:
创建ReentrantLock对象mylock,用于加锁和解锁。
1 |
private final ReentrantLock mylock = new ReentrantLock(); |
注意:使用try-finally结构
,加锁放在try中,解锁放在finally中。
1 |
try{ |
对前边的买票例子中的run方法中操作资源部分进行修改,放在try-finally结构
中:
1 |
try{ |
完整代码:
1 |
import java.util.concurrent.locks.ReentrantLock; |
运行结构:
1 |
小明----拿到了第10张票~ |
总结
synchronized关键字与Lock接口的对比。
- Lock锁显式锁(手动开启和关闭锁,别忘记了关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)。(建议使用synchronized,因为是隐式锁,避免出现不必要的问题。)
死锁问题
什么是死锁 ?
多个线程各自占有一些共享资源,并且互相对待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系。
如何避免死锁:上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。
代码演示
使用代码演示化妆时,多人需要使用同一个化妆品时产生的死锁问题。
创建两个资源类:口红Lipstick、镜子Mirror
一个调用资源的类:化妆Makeup(通过继承Thread类来实现多线程)
1 |
//口红 |
死锁
Makeup类具体代码如下:
1 |
//化妆,调用口红、镜子等资源 |
先模拟会出现死锁的情况。两个synchronized块嵌套在一起,模拟拿起了某样资源还未放下时又去拿另一样资源。(里头使用了线程休眠sleep,是为了放大问题的发生性。)
写一个测试类DeadLock:
两个Makeup对象模拟两个准备要化妆的人,灰姑娘与白雪公主。灰姑娘先拿口红再去拿镜子,白雪公主先拿镜子再去拿口红。(两个人取东西顺序是反过来的)
1 |
//死锁:多个线程相互抱着对方需要的资源,然后形成僵持 |
运行结果:
灰姑娘与白雪公主是拿不到她们想要的东西的,因为灰姑娘先拿口红再去拿镜子,白雪公主先拿镜子再去拿口红。她们都是拿起了某样资源还未放下时又去拿另一样资源。
1 |
灰姑娘拿了口红 |
改进
对run方法内执行的makeup方法进行改进,将嵌套的synchronized拆开,模拟需要去拿另一个资源时,先将当前使用的资源释放掉再去拿另一样资源。
1 |
if (choice == 0){ |
运行结果:
1 |
灰姑娘拿了口红 |
该代码示例完整代码:
1 |
package com.study.多线程.demo3; |
学习自B站遇见狂神说