5.线程同步
线程同步(synchronized)
- 多个线程操作同一个对象时,如果某些线程想修改此对象,需要线程同步。
- 对比并发:同一个对象被多个线程同时操作称为并发。(如上万人同时抢100张票)
- 线程同步其实是一种等待机制,多个同时访问此对象的线程,会进入这个对象的 等待池 形成队列。等待前一个线程使用完毕,下一个线程再使用。(即线程排队等待)
- 线程同步 形成条件:队列 + 锁
- 队列:排队的线程形成队列。
- 锁:保证正在执行的线程的安全性(厕所排队,厕所里的人把门关上,门上的插销就是锁)
- 由于同一进程的多个线程共享同一块存储空间,在带来便利的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时需要加入 锁机制synchronized。当一个线程获得对象的排他锁,会独占资源,其他线程必须等待其使用完后释放锁。存在以下问题:
- 一个线程持有锁,会导致其他所有需要此锁的线程挂起,降低了性能。(性能和安全不能兼得)
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,导致了优先级倒置,引起性能问题。
1.三大不安全案例
线程不安全 例1: 不安全的买票
1 | // 不安全的买票 |
结果:
1 | aa买了票5 |
线程不安全 例2: 同一账户,两人同时取钱
1 | // 同一账户,两人同时取钱 |
结果:
1 | 农业银行余额为:-50 |
线程不安全 例3: 不安全的集合
1 | // 线程不安全的集合 |
结果:
1 | 8989 |
2.同步:
由于我们可以通过peivate
关键字来保证 数据对象 只能被方法访问,所以只需针对方法提出一套机制。这套机制就是synchronized
关键字,它包括两种方法:synchronized
方法 和 synchronized
块。
- 同步方法:public synchronized void method(int args){}
- 同步块:synchronized(obj){}
同步方法:
synchronized
方法控制对“对象”的访问,每个对象都有一把锁。每个synchronized
方法都必须获得 调用该方法的对象的锁 才能执行。否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法结束才能释放锁,后面被阻塞的线程才能获得这个锁,继续执行。- 缺陷:若一个大的方法声明为
synchronized
将会影响效率。
- 缺陷:若一个大的方法声明为
- 需要修改内容的方法才需要锁,查询操作不需要锁。(锁太多,会浪费资源)
同步块: synchronized(obj){}
Obj
称之为 同步监视器Obj
可以是任何对象,但是推荐使用共享资源作为同步监视器。- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,即锁的是当前方法所在对象,或是class。
- 锁的对象
obj
,要是一个变化的量。(被修改的量)
- 同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中的代码。
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器没有锁,然后锁定 访问。
修改不安全的代码:
1.修改买票代码: 在购票方法buy() 上加synchronized。
1 | public class UnsafeBuyTicket { |
结果:
1 | cc买了票5 |
2.银行取钱: 使用synchronized块
1 | // 同一账户,两人同时取钱 |
3.集合: 使用synchronized块
1 | public class UnsafeList { |
4.JUC安全类型集合: CopyOnWriteArrayList
1 | // 测试JUC安全类型的集合 |